Skip to content

Commit

Permalink
feat: Implement ldc based automatic contract chunking with auto split…
Browse files Browse the repository at this point in the history
… and loader generation (#6250)

## Description

This PR adds chunk deployment based on LDC. Contracts larger than 100 kb
size, is split into chunks and chunks are deployed as `blobs`. Out of
these blobs we create a loader contract, which loads all the blobs using
`LDC` opcode.

One important thing is that this feature works nicely with the proxy
feature introduced in #6069, so a large contract, with proxy can be
deployed directly. Large contract will be split into chunks, chunks will
get deployed, loader will get get generated and deployed, after all
these a proxy contract is deployed and pointed to the loader contract
deployed.

Simple chunked deploy, chunked deployment re routing the call, chunked
deployment behind a proxy re routes the call is tested.

---------

Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com>
  • Loading branch information
kayagokalp and sdankel committed Aug 17, 2024
1 parent 9c49bc9 commit d003a5f
Show file tree
Hide file tree
Showing 9 changed files with 784 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ jobs:
chmod +x fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core
mv fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core /usr/local/bin/fuel-core
- name: Run tests
run: cargo test --locked --release -p forc-client
run: cargo test --locked --release -p forc-client -- --test-threads 1
cargo-test-sway-lsp:
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 4 additions & 0 deletions docs/book/src/forc/plugins/forc_client/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,7 @@ address = "0xd8c4b07a0d1be57b228f4c18ba7bca0c8655eb6e9d695f14080f2cf4fc7cd946" #
```

If an `address` is present, `forc` calls into that contract to update its `target` instead of deploying a new contract. Since a new proxy deployment adds its own `address` into the `Forc.toml` automatically, you can simply enable the proxy once and after the initial deployment, `forc` will keep updating the target accordingly for each new deployment of the same contract.

## Large Contracts

For contracts over 100KB, `forc-deploy` will split the contract into chunks and deploy the contract with multiple transactions using the Rust SDK's [loader contract](https://github.com/FuelLabs/fuels-rs/blob/master/docs/src/deploying/large_contracts.md) functionality. Chunks that have already been deployed will be reused on subsequent deployments.
157 changes: 134 additions & 23 deletions forc-plugins/forc-client/src/op/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ use fuel_core_client::client::FuelClient;
use fuel_crypto::fuel_types::ChainId;
use fuel_tx::{Salt, Transaction};
use fuel_vm::prelude::*;
use fuels::programs::contract::{LoadConfiguration, StorageConfiguration};
use fuels::{
programs::contract::{LoadConfiguration, StorageConfiguration},
types::{bech32::Bech32ContractId, transaction_builders::Blob},
};
use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account};
use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder};
use futures::FutureExt;
Expand All @@ -38,10 +41,17 @@ use std::{
use sway_core::language::parsed::TreeType;
use sway_core::BuildTarget;

/// Maximum contract size allowed for a single contract. If the target
/// contract size is bigger than this amount, forc-deploy will automatically
/// starts dividing the contract and deploy them in chunks automatically.
/// The value is in bytes.
const MAX_CONTRACT_SIZE: usize = 100_000;

#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
pub struct DeployedContract {
pub id: fuel_tx::ContractId,
pub proxy: Option<fuel_tx::ContractId>,
pub chunked: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -122,13 +132,77 @@ fn validate_and_parse_salts<'a>(
Ok(contract_salt_map)
}

/// Depending on the cli options user passed, either returns storage slots from
/// compiled package, or the ones user provided as overrides.
fn resolve_storage_slots(
command: &cmd::Deploy,
compiled: &BuiltPackage,
) -> Result<Vec<fuel_tx::StorageSlot>> {
let mut storage_slots =
if let Some(storage_slot_override_file) = &command.override_storage_slots {
let storage_slots_file = std::fs::read_to_string(storage_slot_override_file)?;
let storage_slots: Vec<StorageSlot> = serde_json::from_str(&storage_slots_file)?;
storage_slots
} else {
compiled.storage_slots.clone()
};
storage_slots.sort();
Ok(storage_slots)
}

/// Creates blobs from the contract to deploy contracts that are larger than
/// `MAX_CONTRACT_SIZE`. Created blobs are deployed, and a loader contract is
/// generated such that it loads all the deployed blobs, and provides the user
/// a single contract (loader contract that loads the blobs) to call into.
async fn deploy_chunked(
command: &cmd::Deploy,
compiled: &BuiltPackage,
salt: Salt,
signing_key: &SecretKey,
provider: &Provider,
pkg_name: &str,
) -> anyhow::Result<ContractId> {
println_action_green("Deploying", &format!("contract {pkg_name} chunks"));

let storage_slots = resolve_storage_slots(command, compiled)?;
let chain_info = provider.chain_info().await?;
let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet());
let contract_url = match target.explorer_url() {
Some(explorer_url) => format!("{explorer_url}/contract/0x"),
None => "".to_string(),
};

let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone()));
let blobs = compiled
.bytecode
.bytes
.chunks(MAX_CONTRACT_SIZE)
.map(|chunk| Blob::new(chunk.to_vec()))
.collect();

let tx_policies = tx_policies_from_cmd(command);
let contract_id =
fuels::programs::contract::Contract::loader_from_blobs(blobs, salt, storage_slots)?
.deploy(&wallet, tx_policies)
.await?
.into();

println_action_green(
"Finished",
&format!("deploying loader contract for {pkg_name} {contract_url}{contract_id}"),
);

Ok(contract_id)
}

/// Deploys a new proxy contract for the given package.
async fn deploy_new_proxy(
command: &cmd::Deploy,
pkg_name: &str,
impl_contract: &fuel_tx::ContractId,
provider: &Provider,
signing_key: &SecretKey,
) -> Result<fuel_tx::ContractId> {
) -> Result<ContractId> {
fuels::macros::abigen!(Contract(
name = "ProxyContract",
abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json"
Expand All @@ -149,12 +223,14 @@ async fn deploy_new_proxy(
.with_storage_configuration(storage_configuration)
.with_configurables(configurables);

let proxy_contract_id = fuels::programs::contract::Contract::load_from(
let tx_policies = tx_policies_from_cmd(command);
let proxy_contract_id: ContractId = fuels::programs::contract::Contract::load_from(
proxy_dir_output.join("proxy.bin"),
configuration,
)?
.deploy(&wallet, TxPolicies::default())
.await?;
.deploy(&wallet, tx_policies)
.await?
.into();

let chain_info = provider.chain_info().await?;
let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet());
Expand All @@ -168,10 +244,11 @@ async fn deploy_new_proxy(
&format!("deploying proxy contract for {pkg_name} {contract_url}{proxy_contract_id}"),
);

let instance = ProxyContract::new(&proxy_contract_id, wallet);
let proxy_contract_bech_id: Bech32ContractId = proxy_contract_id.into();
let instance = ProxyContract::new(&proxy_contract_bech_id, wallet);
instance.methods().initialize_proxy().call().await?;
println_action_green("Initialized", &format!("proxy contract for {pkg_name}"));
Ok(proxy_contract_id.into())
Ok(proxy_contract_id)
}

/// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members
Expand Down Expand Up @@ -277,7 +354,24 @@ pub async fn deploy(command: cmd::Deploy) -> Result<Vec<DeployedContract>> {
bail!("Both `--salt` and `--default-salt` were specified: must choose one")
}
};
let deployed_contract_id = deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?;
let bytecode_size = pkg.bytecode.bytes.len();
let deployed_contract_id = if bytecode_size > MAX_CONTRACT_SIZE {
// Deploy chunked
let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?;
let provider = Provider::connect(node_url).await?;

deploy_chunked(
&command,
pkg,
salt,
&signing_key,
&provider,
&pkg.descriptor.name,
)
.await?
} else {
deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?
};

let proxy_id = match &pkg.descriptor.manifest_file.proxy {
Some(forc_pkg::manifest::Proxy {
Expand Down Expand Up @@ -306,9 +400,14 @@ pub async fn deploy(command: cmd::Deploy) -> Result<Vec<DeployedContract>> {
}) => {
let pkg_name = &pkg.descriptor.name;
// Deploy a new proxy contract.
let deployed_proxy_contract =
deploy_new_proxy(pkg_name, &deployed_contract_id, &provider, &signing_key)
.await?;
let deployed_proxy_contract = deploy_new_proxy(
&command,
pkg_name,
&deployed_contract_id,
&provider,
&signing_key,
)
.await?;

// Update manifest file such that the proxy address field points to the new proxy contract.
update_proxy_address_in_manifest(
Expand All @@ -324,6 +423,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result<Vec<DeployedContract>> {
let deployed_contract = DeployedContract {
id: deployed_contract_id,
proxy: proxy_id,
chunked: bytecode_size > MAX_CONTRACT_SIZE,
};
deployed_contracts.push(deployed_contract);
}
Expand Down Expand Up @@ -357,8 +457,17 @@ async fn confirm_transaction_details(
_ => "",
};

let pkg_bytecode_len = pkg.bytecode.bytes.len();
let blob_text = if pkg_bytecode_len > MAX_CONTRACT_SIZE {
let number_of_blobs = pkg_bytecode_len.div_ceil(MAX_CONTRACT_SIZE);
tx_count += number_of_blobs;
&format!(" + {number_of_blobs} blobs")
} else {
""
};

format!(
"deploy {}{proxy_text}",
"deploy {}{blob_text}{proxy_text}",
pkg.descriptor.manifest_file.project_name()
)
})
Expand Down Expand Up @@ -408,21 +517,12 @@ pub async fn deploy_pkg(

let bytecode = &compiled.bytecode.bytes;

let mut storage_slots =
if let Some(storage_slot_override_file) = &command.override_storage_slots {
let storage_slots_file = std::fs::read_to_string(storage_slot_override_file)?;
let storage_slots: Vec<StorageSlot> = serde_json::from_str(&storage_slots_file)?;
storage_slots
} else {
compiled.storage_slots.clone()
};
storage_slots.sort();

let storage_slots = resolve_storage_slots(command, compiled)?;
let contract = Contract::from(bytecode.clone());
let root = contract.root();
let state_root = Contract::initial_state_root(storage_slots.iter());
let contract_id = contract.id(&salt, &root, &state_root);
let tx_policies = TxPolicies::default();
let tx_policies = tx_policies_from_cmd(command);

let mut tb = CreateTransactionBuilder::prepare_contract_deployment(
bytecode.clone(),
Expand Down Expand Up @@ -523,6 +623,17 @@ pub async fn deploy_pkg(
Ok(contract_id)
}

fn tx_policies_from_cmd(cmd: &cmd::Deploy) -> TxPolicies {
let mut tx_policies = TxPolicies::default();
if let Some(max_fee) = cmd.gas.max_fee {
tx_policies = tx_policies.with_max_fee(max_fee);
}
if let Some(script_gas_limit) = cmd.gas.script_gas_limit {
tx_policies = tx_policies.with_script_gas_limit(script_gas_limit);
}
tx_policies
}

fn build_opts_from_cmd(cmd: &cmd::Deploy) -> pkg::BuildOpts {
pkg::BuildOpts {
pkg: pkg::PkgOpts {
Expand Down
6 changes: 3 additions & 3 deletions forc-plugins/forc-client/src/util/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::path::PathBuf;
use std::{collections::HashMap, path::Path, sync::Arc};

/// The name of the folder that forc generated proxy contract project will reside at.
pub const PROXY_CONTRACT_FOLDER_NAME: &str = ".generated_proxy_contracts";
pub const GENERATED_CONTRACT_FOLDER_NAME: &str = ".generated_contracts";
pub const PROXY_CONTRACT_BIN: &[u8] = include_bytes!("../../proxy_abi/proxy_contract.bin");
pub const PROXY_CONTRACT_STORAGE_SLOTS: &str =
include_str!("../../proxy_abi/proxy_contract-storage_slots.json");
Expand Down Expand Up @@ -42,8 +42,8 @@ pub(crate) fn update_proxy_address_in_manifest(
pub(crate) fn create_proxy_contract(pkg_name: &str) -> Result<PathBuf> {
// Create the proxy contract folder.
let proxy_contract_dir = user_forc_directory()
.join(PROXY_CONTRACT_FOLDER_NAME)
.join(pkg_name);
.join(GENERATED_CONTRACT_FOLDER_NAME)
.join(format!("{}-proxy", pkg_name));
std::fs::create_dir_all(&proxy_contract_dir)?;
std::fs::write(
proxy_contract_dir.join(PROXY_BIN_FILE_NAME),
Expand Down
2 changes: 2 additions & 0 deletions forc-plugins/forc-client/test/data/big_contract/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
8 changes: 8 additions & 0 deletions forc-plugins/forc-client/test/data/big_contract/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
authors = ["Fuel Labs <contact@fuel.sh>"]
entry = "main.sw"
license = "Apache-2.0"
name = "big_contract"

[dependencies]
std = { path = "../../../../../sway-lib-std/" }
Loading

0 comments on commit d003a5f

Please sign in to comment.