From 67b020de801f6500b7125972d136d40b7de298ef Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 8 Aug 2024 15:51:03 +0300 Subject: [PATCH 01/17] feat: loader contract generation --- forc-plugins/forc-client/src/util/pkg.rs | 227 ++++++++++++++++++++++- 1 file changed, 225 insertions(+), 2 deletions(-) diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 94a068a5bea..c01cb811953 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -2,19 +2,30 @@ use anyhow::Result; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; use forc_util::user_forc_directory; +use fuel_abi_types::abi::program::ProgramABI; use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; use std::{collections::HashMap, path::Path, sync::Arc}; +use sway_utils::{MAIN_ENTRY, MANIFEST_FILE_NAME, SRC_DIR}; /// 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"); pub const PROXY_BIN_FILE_NAME: &str = "proxy.bin"; pub const PROXY_STORAGE_SLOTS_FILE_NAME: &str = "proxy-storage_slots.json"; +pub const LOADER_CONTRACT_FORC_TOML: &str = r#" +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "proxy_contract" + +[dependencies] +"#; /// Updates the given package manifest file such that the address field under the proxy table updated to the given value. /// Updated manifest file is written back to the same location, without thouching anything else such as comments etc. @@ -42,7 +53,7 @@ pub(crate) fn update_proxy_address_in_manifest( pub(crate) fn create_proxy_contract(pkg_name: &str) -> Result { // Create the proxy contract folder. let proxy_contract_dir = user_forc_directory() - .join(PROXY_CONTRACT_FOLDER_NAME) + .join(GENERATED_CONTRACT_FOLDER_NAME) .join(pkg_name); std::fs::create_dir_all(&proxy_contract_dir)?; std::fs::write( @@ -85,3 +96,215 @@ pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result String { + let mut contract = String::new(); + let types = create_type_map(abi); + + // Contract header + contract.push_str("contract;\n\n"); + + // Configurables + contract.push_str("configurable {\n"); + for i in 1..=num_chunks { + contract.push_str(&format!( + " TARGET_{}: ContractId = ContractId::from({}),\n", + i, + chunk_contract_ids[i - 1] + )); + } + contract.push_str("}\n\n"); + + // ABI + contract.push_str("abi LoaderContract {\n"); + for function in &abi.functions { + let inputs = function + .inputs + .iter() + .map(|input| { + format!( + "{}: {}", + input.name, + type_id_to_string(input.type_id, &types) + ) + }) + .collect::>() + .join(", "); + let output = type_id_to_string(function.output.type_id, &types); + contract.push_str(&format!( + " fn {}({}) -> {};\n", + function.name, inputs, output + )); + } + contract.push_str("}\n\n"); + + // Implementation + contract.push_str("impl LoaderContract for Contract {\n"); + for function in &abi.functions { + let inputs = function + .inputs + .iter() + .map(|input| { + format!( + "{}: {}", + input.name, + type_id_to_string(input.type_id, &types) + ) + }) + .collect::>() + .join(", "); + let output = type_id_to_string(function.output.type_id, &types); + contract.push_str(&format!( + " fn {}({}) -> {} {{\n", + function.name, inputs, output + )); + contract.push_str(&format!(" run_external{}(", num_chunks)); + for i in 1..=num_chunks { + if i > 1 { + contract.push_str(", "); + } + contract.push_str(&format!("TARGET_{}", i)); + } + contract.push_str(")\n }\n"); + } + contract.push_str("}\n\n"); + + // run_external function + contract.push_str(&generate_run_external(num_chunks)); + + contract +} + +/// Creates a map from type id to the name of the type, so that we can do ,ook +fn create_type_map(abi: &ProgramABI) -> HashMap { + abi.types + .iter() + .map(|t| (t.type_id, t.type_field.clone())) + .collect() +} + +fn type_id_to_string(type_id: usize, types: &HashMap) -> String { + types + .get(&type_id) + .cloned() + .unwrap_or_else(|| format!("Type{}", type_id)) +} + +/// Generates an run external function given the `num_targets`. This function +/// is later used in the generated loader contract. +fn generate_run_external(num_targets: usize) -> String { + let mut func = String::new(); + + func.push_str(&format!("fn run_external{}(", num_targets)); + for i in 1..=num_targets { + if i > 1 { + func.push_str(", "); + } + func.push_str(&format!("load_target{}: ContractId", i)); + } + func.push_str(") -> ! {\n"); + + // Generate assembly + func.push_str(" asm(\n"); + for i in 1..=num_targets { + func.push_str(&format!(" load_target{}: load_target{},\n", i, i)); + } + for i in 2..=num_targets { + func.push_str(&format!(" load_target{}_heap,\n", i)); + } + func.push_str(" heap_alloc_size,\n"); + for i in 1..=num_targets { + func.push_str(&format!(" length{},\n", i)); + } + func.push_str(" ssp_saved,\n"); + func.push_str(" cur_stack_size,\n"); + func.push_str(" ) {\n"); + + // Get lengths of all chunks + for i in 1..=num_targets { + func.push_str(&format!(" csiz length{} load_target{};\n", i, i)); + } + + // Store load_target2 and onwards on the heap + for i in 2..=num_targets { + func.push_str(" addi heap_alloc_size zero i32;\n"); + func.push_str(" aloc heap_alloc_size;\n"); + func.push_str(&format!( + " mcp hp load_target{} heap_alloc_size;\n", + i + )); + func.push_str(&format!(" move load_target{}_heap hp;\n", i)); + } + + // Save the old $ssp value and shrink the stack + func.push_str(" move ssp_saved ssp;\n"); + func.push_str(" sub cur_stack_size sp ssp;\n"); + func.push_str(" cfs cur_stack_size;\n"); + + // Do the loads + func.push_str(" ldc load_target1 zero length1;\n"); + for i in 2..=num_targets { + func.push_str(&format!( + " ldc load_target{}_heap zero length{};\n", + i, i + )); + } + + // Set up jump + func.push_str(" addi heap_alloc_size zero i64;\n"); + func.push_str(" aloc heap_alloc_size;\n"); + func.push_str(" sw hp ssp_saved i0;\n"); + func.push_str(" }\n"); + func.push_str(" __jmp_mem()\n"); + func.push_str("}\n"); + + func +} + +/// Creates a loader contract project, ready to be built at the given path. +/// +/// Constructs: +/// - The project folder with a correct structure if it is not already there. +/// - Loader contract soruce code, that is created specifcally for number of +/// chunks in hand. +pub(crate) fn create_chunk_loader_contract( + abi: &ProgramABI, + chunk_contract_ids: &[String], + num_chunks: usize, + pkg_name: &str, +) -> Result { + // Create the proxy contract folder. + let proxy_contract_dir = user_forc_directory() + .join(GENERATED_CONTRACT_FOLDER_NAME) + .join(pkg_name); + std::fs::create_dir_all(&proxy_contract_dir)?; + + // Create the Forc.toml + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(proxy_contract_dir.join(MANIFEST_FILE_NAME))?; + write!(f, "{}", LOADER_CONTRACT_FORC_TOML)?; + + // Create the src folder + std::fs::create_dir_all(proxy_contract_dir.join(SRC_DIR))?; + + // Create main.sw + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(proxy_contract_dir.join(SRC_DIR).join(MAIN_ENTRY))?; + + let contract_str = + generate_proxy_contract_with_chunking_src(abi, num_chunks, chunk_contract_ids); + write!(f, "{}", contract_str)?; + Ok(proxy_contract_dir) +} From 9fed5ab6a036665c2f6bce1db9e6c35227518e06 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 8 Aug 2024 16:09:05 +0300 Subject: [PATCH 02/17] feat: implemented chunking and chunk deployment --- forc-plugins/forc-client/src/op/deploy.rs | 8 +++ forc-plugins/forc-client/src/util/pkg.rs | 83 ++++++++++++++++++++++- forc-plugins/forc-client/tests/deploy.rs | 2 + 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index f41fd80532c..46a252a1fd1 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -38,10 +38,17 @@ use std::{ use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; +/// Maximum contract size allowed to be in 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, + pub chunks: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -324,6 +331,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { let deployed_contract = DeployedContract { id: deployed_contract_id, proxy: proxy_id, + chunks: vec![], }; deployed_contracts.push(deployed_contract); } diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index c01cb811953..5602daf5e95 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,9 +1,15 @@ -use anyhow::Result; +use anyhow::{bail, Result}; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; use forc_util::user_forc_directory; use fuel_abi_types::abi::program::ProgramABI; +use fuel_crypto::SecretKey; +use fuel_tx::{ContractId, Salt, StorageSlot}; +use fuels::types::transaction::TxPolicies; +use fuels_accounts::provider::Provider; +use fuels_accounts::wallet::WalletUnlocked; use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; +use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; @@ -54,7 +60,7 @@ pub(crate) fn create_proxy_contract(pkg_name: &str) -> Result { // Create the proxy contract folder. let proxy_contract_dir = user_forc_directory() .join(GENERATED_CONTRACT_FOLDER_NAME) - .join(pkg_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), @@ -308,3 +314,76 @@ pub(crate) fn create_chunk_loader_contract( write!(f, "{}", contract_str)?; Ok(proxy_contract_dir) } + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ContractChunk { + id: usize, + size: usize, + bytecode: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DeployedContractChunk { + contract_id: ContractId, +} + +impl DeployedContractChunk { + pub fn contract_id(&self) -> &ContractId { + &self.contract_id + } +} + +impl ContractChunk { + pub fn new(id: usize, size: usize, bytecode: Vec) -> Self { + Self { id, size, bytecode } + } + + pub async fn deploy( + self, + provider: &Provider, + salt: &Salt, + signing_key: &SecretKey, + ) -> anyhow::Result { + let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); + + let contract_chunk_storage_slot = StorageSlot::default(); + let contract_chunk = fuels::programs::contract::Contract::new( + self.bytecode, + *salt, + vec![contract_chunk_storage_slot], + ); + + let policies = TxPolicies::default(); + let bech32 = contract_chunk.deploy(&wallet, policies).await?; + let contract_id = ContractId::from(bech32); + Ok(DeployedContractChunk { contract_id }) + } +} + +/// Split bytecode into chunks of a specified maximum size. Meaning that each +/// chunk up until the last one, is guaranteed to be `chunk_size`, and +/// `chunk_size` is guaranteed to be divisble by 8, and will result an error +/// otherwise. This requirement comes from VM, as LDC'ed bytecode is appended +/// to word boundary. +pub fn split_into_chunks(bytecode: Vec, chunk_size: usize) -> Result> { + // This is done so that LDC'ed bytecode aligns perfectly, as the VM appends + // them to word boundary. This should normally be the case if the bytecode + // is not modified manually. + assert!(chunk_size % 8 == 0); + if chunk_size % 8 != 0 { + bail!( + "Chunks size is not divisible by 8, chunk size: {}", + chunk_size + ); + } + let mut chunks = Vec::new(); + + for (id, chunk) in bytecode.chunks(chunk_size).enumerate() { + let chunk = chunk.to_vec(); + let size = chunk.len(); + let contract_chunk = ContractChunk::new(id, size, chunk); + chunks.push(contract_chunk); + } + + Ok(chunks) +} diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 8f45d4a3478..4fcb709ffe7 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -145,6 +145,7 @@ async fn test_simple_deploy() { ) .unwrap(), proxy: None, + chunks: vec![], }]; assert_eq!(contract_ids, expected) @@ -194,6 +195,7 @@ async fn test_deploy_fresh_proxy() { ) .unwrap(), ), + chunks: vec![], }; let expected = vec![impl_contract]; From 11e3097c7f091479129e700471b9ace155bc68c9 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 8 Aug 2024 16:12:30 +0300 Subject: [PATCH 03/17] test: contract chunking --- forc-plugins/forc-client/src/util/pkg.rs | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 5602daf5e95..1fa40f597b3 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -387,3 +387,61 @@ pub fn split_into_chunks(bytecode: Vec, chunk_size: usize) -> Result Date: Fri, 9 Aug 2024 00:16:44 +0300 Subject: [PATCH 04/17] minor ux improvements --- forc-plugins/forc-client/src/op/deploy.rs | 93 ++++++++++++++++++++++- forc-plugins/forc-client/src/util/pkg.rs | 34 +++++++-- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 46a252a1fd1..1e381cec100 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,7 +3,10 @@ use crate::{ constants::TX_SUBMIT_TIMEOUT_MS, util::{ node_url::get_node_url, - pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, + pkg::{ + build_loader_contract, built_pkgs, create_proxy_contract, split_into_chunks, + update_proxy_address_in_manifest, + }, target::Target, tx::{ bech32_from_secret, prompt_forc_wallet_password, select_secret_key, @@ -42,7 +45,7 @@ use sway_core::BuildTarget; /// 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; +const MAX_CONTRACT_SIZE: usize = 1_000; #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DeployedContract { @@ -129,6 +132,70 @@ fn validate_and_parse_salts<'a>( Ok(contract_salt_map) } +async fn deploy_chunked( + command: &cmd::Deploy, + compiled: &BuiltPackage, + salt: Salt, + signing_key: &SecretKey, + provider: &Provider, + pkg_name: &str, +) -> anyhow::Result<(ContractId, Vec)> { + println_action_green("Splitting", &format!("{pkg_name} into chunks")); + let contract_chunks = split_into_chunks(compiled.bytecode.bytes.clone(), MAX_CONTRACT_SIZE)?; + let mut deployed_contracts = vec![]; + println_action_green("Deploying", &format!("{pkg_name} chunks")); + 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(), + }; + for contract_chunk in contract_chunks { + let chunk_id = contract_chunk.id(); + let deployed_chunk = contract_chunk.deploy(provider, &salt, signing_key).await?; + let chunk_contract_id = deployed_chunk.contract_id(); + println_action_green( + "Finished", + &format!("deploying chunk {chunk_id} for {pkg_name}: {chunk_contract_id}"), + ); + deployed_contracts.push(deployed_chunk); + } + println_action_green("Deployed", &format!("{pkg_name} chunks")); + let deployed_contract_ids: Vec = deployed_contracts + .iter() + .map(|deployed_contract| format!("0x{}", deployed_contract.contract_id())) + .collect(); + + let deployed_contracts: Vec<_> = deployed_contracts + .iter() + .map(|deployed_contract| *deployed_contract.contract_id()) + .collect(); + + let program_abi = match &compiled.program_abi { + sway_core::asm_generation::ProgramABI::Fuel(abi) => abi, + _ => bail!("contract chunking is only supported with fuelVM"), + }; + + println_action_green("Building", &format!("loader contract for {pkg_name}")); + let loader_contract = build_loader_contract( + program_abi, + &deployed_contract_ids, + deployed_contracts.len(), + pkg_name, + &build_opts_from_cmd(command), + )?; + + println_action_green("Deploying", &format!("loader contract for {pkg_name}")); + let deployed_id = deploy_pkg(command, &loader_contract, salt, provider, signing_key).await?; + + println_action_green( + "Finished", + &format!("deploying loader contract for {pkg_name} {contract_url}{deployed_id}"), + ); + + Ok((deployed_id, deployed_contracts)) +} + /// Deploys a new proxy contract for the given package. async fn deploy_new_proxy( pkg_name: &str, @@ -284,7 +351,25 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { 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, chunk_ids) = 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 { + let deployed_contract_id = + deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?; + (deployed_contract_id, vec![]) + }; let proxy_id = match &pkg.descriptor.manifest_file.proxy { Some(forc_pkg::manifest::Proxy { @@ -331,7 +416,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { let deployed_contract = DeployedContract { id: deployed_contract_id, proxy: proxy_id, - chunks: vec![], + chunks: chunk_ids, }; deployed_contracts.push(deployed_contract); } diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 1fa40f597b3..5bda253c174 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -28,7 +28,7 @@ pub const LOADER_CONTRACT_FORC_TOML: &str = r#" authors = ["Fuel Labs "] entry = "main.sw" license = "Apache-2.0" -name = "proxy_contract" +name = "loader_contract" [dependencies] "#; @@ -103,7 +103,7 @@ pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result usize { + self.id + } + pub async fn deploy( self, provider: &Provider, @@ -388,12 +391,29 @@ pub fn split_into_chunks(bytecode: Vec, chunk_size: usize) -> Result Result> { + let loader_contract = + create_chunk_loader_contract(abi, chunk_contract_ids, num_chunks, pkg_name)?; + let mut build_opts = build_opts.clone(); + let proxy_contract_dir_str = format!("{}", loader_contract.clone().display()); + build_opts.pkg.path = Some(proxy_contract_dir_str); + let built_pkgs = built_pkgs(&loader_contract, &build_opts)?; + let built_pkg = built_pkgs + .first() + .cloned() + .ok_or_else(|| anyhow::anyhow!("could not get proxy contract"))?; + Ok(built_pkg) +} + #[cfg(test)] mod tests { use super::*; - use forc_pkg::BuildOpts; - use forc_util::user_forc_directory; - use std::path::PathBuf; #[test] fn test_split_into_chunks_exact_division() { From b84931215ace80eb87e594a153d408fe00ac4978 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 9 Aug 2024 01:09:46 +0300 Subject: [PATCH 05/17] test: add re route call --- forc-plugins/forc-client/src/op/deploy.rs | 6 +- forc-plugins/forc-client/src/util/pkg.rs | 6 +- .../test/data/big_contract/.gitignore | 2 + .../test/data/big_contract/Forc.toml | 8 ++ .../data/big_contract/big_contract-abi.json | 26 +++++ .../test/data/big_contract/src/main.sw | 14 +++ forc-plugins/forc-client/tests/deploy.rs | 96 ++++++++++++++++++- 7 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 forc-plugins/forc-client/test/data/big_contract/.gitignore create mode 100644 forc-plugins/forc-client/test/data/big_contract/Forc.toml create mode 100644 forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json create mode 100644 forc-plugins/forc-client/test/data/big_contract/src/main.sw diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 1e381cec100..c5b9e986134 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -45,7 +45,7 @@ use sway_core::BuildTarget; /// 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 = 1_000; +const MAX_CONTRACT_SIZE: usize = 100_000; #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DeployedContract { @@ -152,7 +152,7 @@ async fn deploy_chunked( }; for contract_chunk in contract_chunks { let chunk_id = contract_chunk.id(); - let deployed_chunk = contract_chunk.deploy(provider, &salt, signing_key).await?; + let deployed_chunk = contract_chunk.deploy(provider, signing_key).await?; let chunk_contract_id = deployed_chunk.contract_id(); println_action_green( "Finished", @@ -176,7 +176,7 @@ async fn deploy_chunked( _ => bail!("contract chunking is only supported with fuelVM"), }; - println_action_green("Building", &format!("loader contract for {pkg_name}")); + println_action_green("Creating", &format!("loader contract for {pkg_name}")); let loader_contract = build_loader_contract( program_abi, &deployed_contract_ids, diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 5bda253c174..6c32594a662 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -275,7 +275,7 @@ fn generate_run_external(num_targets: usize) -> String { /// /// Constructs: /// - The project folder with a correct structure if it is not already there. -/// - Loader contract soruce code, that is created specifcally for number of +/// - Loader contract source code, that is created specifically for number of /// chunks in hand. pub(crate) fn create_chunk_loader_contract( abi: &ProgramABI, @@ -344,15 +344,15 @@ impl ContractChunk { pub async fn deploy( self, provider: &Provider, - salt: &Salt, signing_key: &SecretKey, ) -> anyhow::Result { let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); let contract_chunk_storage_slot = StorageSlot::default(); + let salt: Salt = rand::random(); let contract_chunk = fuels::programs::contract::Contract::new( self.bytecode, - *salt, + salt, vec![contract_chunk_storage_slot], ); diff --git a/forc-plugins/forc-client/test/data/big_contract/.gitignore b/forc-plugins/forc-client/test/data/big_contract/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/forc-plugins/forc-client/test/data/big_contract/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/forc-plugins/forc-client/test/data/big_contract/Forc.toml b/forc-plugins/forc-client/test/data/big_contract/Forc.toml new file mode 100644 index 00000000000..539c466315e --- /dev/null +++ b/forc-plugins/forc-client/test/data/big_contract/Forc.toml @@ -0,0 +1,8 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "big_contract" + +[dependencies] +std = { path = "../../../../../sway-lib-std/" } diff --git a/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json b/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json new file mode 100644 index 00000000000..f8ba2707608 --- /dev/null +++ b/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json @@ -0,0 +1,26 @@ +{ + "encoding": "1", + "types": [ + { + "typeId": 0, + "type": "bool", + "components": null, + "typeParameters": null + } + ], + "functions": [ + { + "inputs": [], + "name": "test_function", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": null + } + ], + "loggedTypes": [], + "messagesTypes": [], + "configurables": [] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/test/data/big_contract/src/main.sw b/forc-plugins/forc-client/test/data/big_contract/src/main.sw new file mode 100644 index 00000000000..e77fde18346 --- /dev/null +++ b/forc-plugins/forc-client/test/data/big_contract/src/main.sw @@ -0,0 +1,14 @@ +contract; + +abi MyContract { + fn test_function() -> bool; +} + +impl MyContract for Contract { + fn test_function() -> bool { + asm() { + blob i91000; + } + true + } +} diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 4fcb709ffe7..9bfab9c4c29 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -8,7 +8,10 @@ use forc_client::{ use forc_pkg::manifest::Proxy; use fuel_crypto::SecretKey; use fuel_tx::{ContractId, Salt}; -use fuels::{macros::abigen, types::transaction::TxPolicies}; +use fuels::{ + macros::abigen, + types::{bech32::Bech32ContractId, transaction::TxPolicies}, +}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; use rand::thread_rng; @@ -407,3 +410,94 @@ fn test_deploy_interactive_wrong_password() -> Result<(), rexpect::error::Error> node.kill().unwrap(); Ok(()) } + +#[tokio::test] +async fn chunked_deploy() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("big_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let deployed_contract = deploy(cmd).await.unwrap().remove(0); + node.kill().unwrap(); + + let num_chunks = deployed_contract.chunks.len(); + assert_eq!(num_chunks, 4); +} + +#[tokio::test] +async fn chunked_deploy_re_routes_call() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("big_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let deployed_contract = deploy(cmd).await.unwrap().remove(0); + + let provider = Provider::connect(&node_url).await.unwrap(); + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + + abigen!(Contract( + name = "BigContract", + abi = "forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json" + )); + + let instance = BigContract::new(deployed_contract.id, wallet_unlocked); + let chunks: Vec = deployed_contract + .chunks + .iter() + .cloned() + .map(|chunk_id| chunk_id.into()) + .collect(); + + // result should be true. + let result = instance + .methods() + .test_function() + .with_contract_ids(&chunks) + .call() + .await + .unwrap() + .value; + node.kill().unwrap(); + assert!(result) +} From 426d2daa1e906163e6451190b172ba965c63eb27 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 9 Aug 2024 01:12:31 +0300 Subject: [PATCH 06/17] test: chunked deploy with proxy --- forc-plugins/forc-client/tests/deploy.rs | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 9bfab9c4c29..9d399a9813a 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -501,3 +501,66 @@ async fn chunked_deploy_re_routes_call() { node.kill().unwrap(); assert!(result) } + +#[tokio::test] +async fn chunked_deploy_with_proxy_re_routes_call() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("big_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + let proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let deployed_contract = deploy(cmd).await.unwrap().remove(0); + + let provider = Provider::connect(&node_url).await.unwrap(); + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + + abigen!(Contract( + name = "BigContract", + abi = "forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json" + )); + + let instance = BigContract::new(deployed_contract.id, wallet_unlocked); + let chunks: Vec = deployed_contract + .chunks + .iter() + .cloned() + .map(|chunk_id| chunk_id.into()) + .collect(); + + // result should be true. + let result = instance + .methods() + .test_function() + .with_contract_ids(&chunks) + .call() + .await + .unwrap() + .value; + node.kill().unwrap(); + assert!(result) +} From 44168e468bd1fffd872b197246f8d6261e8dcbfe Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 9 Aug 2024 01:46:29 +0300 Subject: [PATCH 07/17] run tests sequantially --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b054468d04a..8225f599c58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: From e0cfba5e61b225c8f84cce2f553e8c8aeed34c85 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:05:36 -0700 Subject: [PATCH 08/17] feat: use sdk for chunked deployment --- forc-plugins/forc-client/src/op/deploy.rs | 136 ++++--- forc-plugins/forc-client/src/util/pkg.rs | 382 +----------------- .../data/big_contract/big_contract-abi.json | 17 +- forc-plugins/forc-client/tests/deploy.rs | 22 +- 4 files changed, 81 insertions(+), 476 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 00915aba640..7f53fbb39d7 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,10 +3,7 @@ use crate::{ constants::TX_SUBMIT_TIMEOUT_MS, util::{ node_url::get_node_url, - pkg::{ - build_loader_contract, built_pkgs, create_proxy_contract, split_into_chunks, - update_proxy_address_in_manifest, - }, + pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, target::Target, tx::{ bech32_from_secret, prompt_forc_wallet_password, select_secret_key, @@ -25,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::transaction_builders::Blob, +}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; use futures::FutureExt; @@ -51,7 +51,7 @@ const MAX_CONTRACT_SIZE: usize = 100_000; pub struct DeployedContract { pub id: fuel_tx::ContractId, pub proxy: Option, - pub chunks: Vec, + pub chunked: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -132,6 +132,22 @@ fn validate_and_parse_salts<'a>( Ok(contract_salt_map) } +fn resolve_storage_slots( + command: &cmd::Deploy, + compiled: &BuiltPackage, +) -> Result> { + 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 = serde_json::from_str(&storage_slots_file)?; + storage_slots + } else { + compiled.storage_slots.clone() + }; + storage_slots.sort(); + Ok(storage_slots) +} + async fn deploy_chunked( command: &cmd::Deploy, compiled: &BuiltPackage, @@ -139,65 +155,42 @@ async fn deploy_chunked( signing_key: &SecretKey, provider: &Provider, pkg_name: &str, -) -> anyhow::Result<(ContractId, Vec)> { - println_action_green("Splitting", &format!("{pkg_name} into chunks")); - let contract_chunks = split_into_chunks(compiled.bytecode.bytes.clone(), MAX_CONTRACT_SIZE)?; - let mut deployed_contracts = vec![]; - println_action_green("Deploying", &format!("{pkg_name} chunks")); +) -> anyhow::Result { + 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(), }; - for contract_chunk in contract_chunks { - let chunk_id = contract_chunk.id(); - let deployed_chunk = contract_chunk.deploy(provider, signing_key).await?; - let chunk_contract_id = deployed_chunk.contract_id(); - println_action_green( - "Finished", - &format!("deploying chunk {chunk_id} for {pkg_name}: {chunk_contract_id}"), - ); - deployed_contracts.push(deployed_chunk); - } - println_action_green("Deployed", &format!("{pkg_name} chunks")); - let deployed_contract_ids: Vec = deployed_contracts - .iter() - .map(|deployed_contract| format!("0x{}", deployed_contract.contract_id())) - .collect(); - let deployed_contracts: Vec<_> = deployed_contracts - .iter() - .map(|deployed_contract| *deployed_contract.contract_id()) + 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 program_abi = match &compiled.program_abi { - sway_core::asm_generation::ProgramABI::Fuel(abi) => abi, - _ => bail!("contract chunking is only supported with fuelVM"), - }; - - println_action_green("Creating", &format!("loader contract for {pkg_name}")); - let loader_contract = build_loader_contract( - program_abi, - &deployed_contract_ids, - deployed_contracts.len(), - pkg_name, - &build_opts_from_cmd(command), - )?; - - println_action_green("Deploying", &format!("loader contract for {pkg_name}")); - let deployed_id = deploy_pkg(command, &loader_contract, salt, provider, signing_key).await?; + 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?; println_action_green( "Finished", - &format!("deploying loader contract for {pkg_name} {contract_url}{deployed_id}"), + &format!("deploying loader contract for {pkg_name} {contract_url}{contract_id}"), ); - Ok((deployed_id, deployed_contracts)) + Ok(contract_id.into()) } /// 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, @@ -223,11 +216,12 @@ async fn deploy_new_proxy( .with_storage_configuration(storage_configuration) .with_configurables(configurables); + let tx_policies = tx_policies_from_cmd(command); let proxy_contract_id = fuels::programs::contract::Contract::load_from( proxy_dir_output.join("proxy.bin"), configuration, )? - .deploy(&wallet, TxPolicies::default()) + .deploy(&wallet, tx_policies) .await?; let chain_info = provider.chain_info().await?; @@ -352,11 +346,11 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { } }; let bytecode_size = pkg.bytecode.bytes.len(); - let (deployed_contract_id, chunk_ids) = if bytecode_size > MAX_CONTRACT_SIZE { + let (deployed_contract_id, chunked) = 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( + let id = deploy_chunked( &command, pkg, salt, @@ -364,11 +358,12 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { &provider, &pkg.descriptor.name, ) - .await? + .await?; + (id, true) } else { let deployed_contract_id = deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?; - (deployed_contract_id, vec![]) + (deployed_contract_id, false) }; let proxy_id = match &pkg.descriptor.manifest_file.proxy { @@ -398,9 +393,14 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { }) => { 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( @@ -416,7 +416,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { let deployed_contract = DeployedContract { id: deployed_contract_id, proxy: proxy_id, - chunks: chunk_ids, + chunked, }; deployed_contracts.push(deployed_contract); } @@ -501,21 +501,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 = 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(), @@ -616,6 +607,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 { diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 6c32594a662..3aa8f6b2236 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,20 +1,12 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; use forc_util::user_forc_directory; -use fuel_abi_types::abi::program::ProgramABI; -use fuel_crypto::SecretKey; -use fuel_tx::{ContractId, Salt, StorageSlot}; -use fuels::types::transaction::TxPolicies; -use fuels_accounts::provider::Provider; -use fuels_accounts::wallet::WalletUnlocked; use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; -use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; use std::{collections::HashMap, path::Path, sync::Arc}; -use sway_utils::{MAIN_ENTRY, MANIFEST_FILE_NAME, SRC_DIR}; /// The name of the folder that forc generated proxy contract project will reside at. pub const GENERATED_CONTRACT_FOLDER_NAME: &str = ".generated_contracts"; @@ -23,15 +15,6 @@ pub const PROXY_CONTRACT_STORAGE_SLOTS: &str = include_str!("../../proxy_abi/proxy_contract-storage_slots.json"); pub const PROXY_BIN_FILE_NAME: &str = "proxy.bin"; pub const PROXY_STORAGE_SLOTS_FILE_NAME: &str = "proxy-storage_slots.json"; -pub const LOADER_CONTRACT_FORC_TOML: &str = r#" -[project] -authors = ["Fuel Labs "] -entry = "main.sw" -license = "Apache-2.0" -name = "loader_contract" - -[dependencies] -"#; /// Updates the given package manifest file such that the address field under the proxy table updated to the given value. /// Updated manifest file is written back to the same location, without thouching anything else such as comments etc. @@ -102,366 +85,3 @@ pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result String { - let mut contract = String::new(); - let types = create_type_map(abi); - - // Contract header - contract.push_str("contract;\n\n"); - - // Configurables - contract.push_str("configurable {\n"); - for i in 1..=num_chunks { - contract.push_str(&format!( - " TARGET_{}: ContractId = ContractId::from({}),\n", - i, - chunk_contract_ids[i - 1] - )); - } - contract.push_str("}\n\n"); - - // ABI - contract.push_str("abi LoaderContract {\n"); - for function in &abi.functions { - let inputs = function - .inputs - .iter() - .map(|input| { - format!( - "{}: {}", - input.name, - type_id_to_string(input.type_id, &types) - ) - }) - .collect::>() - .join(", "); - let output = type_id_to_string(function.output.type_id, &types); - contract.push_str(&format!( - " fn {}({}) -> {};\n", - function.name, inputs, output - )); - } - contract.push_str("}\n\n"); - - // Implementation - contract.push_str("impl LoaderContract for Contract {\n"); - for function in &abi.functions { - let inputs = function - .inputs - .iter() - .map(|input| { - format!( - "{}: {}", - input.name, - type_id_to_string(input.type_id, &types) - ) - }) - .collect::>() - .join(", "); - let output = type_id_to_string(function.output.type_id, &types); - contract.push_str(&format!( - " fn {}({}) -> {} {{\n", - function.name, inputs, output - )); - contract.push_str(&format!(" run_external{}(", num_chunks)); - for i in 1..=num_chunks { - if i > 1 { - contract.push_str(", "); - } - contract.push_str(&format!("TARGET_{}", i)); - } - contract.push_str(")\n }\n"); - } - contract.push_str("}\n\n"); - - // run_external function - contract.push_str(&generate_run_external(num_chunks)); - - contract -} - -/// Creates a map from type id to the name of the type, so that we can do ,ook -fn create_type_map(abi: &ProgramABI) -> HashMap { - abi.types - .iter() - .map(|t| (t.type_id, t.type_field.clone())) - .collect() -} - -fn type_id_to_string(type_id: usize, types: &HashMap) -> String { - types - .get(&type_id) - .cloned() - .unwrap_or_else(|| format!("Type{}", type_id)) -} - -/// Generates an run external function given the `num_targets`. This function -/// is later used in the generated loader contract. -fn generate_run_external(num_targets: usize) -> String { - let mut func = String::new(); - - func.push_str(&format!("fn run_external{}(", num_targets)); - for i in 1..=num_targets { - if i > 1 { - func.push_str(", "); - } - func.push_str(&format!("load_target{}: ContractId", i)); - } - func.push_str(") -> ! {\n"); - - // Generate assembly - func.push_str(" asm(\n"); - for i in 1..=num_targets { - func.push_str(&format!(" load_target{}: load_target{},\n", i, i)); - } - for i in 2..=num_targets { - func.push_str(&format!(" load_target{}_heap,\n", i)); - } - func.push_str(" heap_alloc_size,\n"); - for i in 1..=num_targets { - func.push_str(&format!(" length{},\n", i)); - } - func.push_str(" ssp_saved,\n"); - func.push_str(" cur_stack_size,\n"); - func.push_str(" ) {\n"); - - // Get lengths of all chunks - for i in 1..=num_targets { - func.push_str(&format!(" csiz length{} load_target{};\n", i, i)); - } - - // Store load_target2 and onwards on the heap - for i in 2..=num_targets { - func.push_str(" addi heap_alloc_size zero i32;\n"); - func.push_str(" aloc heap_alloc_size;\n"); - func.push_str(&format!( - " mcp hp load_target{} heap_alloc_size;\n", - i - )); - func.push_str(&format!(" move load_target{}_heap hp;\n", i)); - } - - // Save the old $ssp value and shrink the stack - func.push_str(" move ssp_saved ssp;\n"); - func.push_str(" sub cur_stack_size sp ssp;\n"); - func.push_str(" cfs cur_stack_size;\n"); - - // Do the loads - func.push_str(" ldc load_target1 zero length1;\n"); - for i in 2..=num_targets { - func.push_str(&format!( - " ldc load_target{}_heap zero length{};\n", - i, i - )); - } - - // Set up jump - func.push_str(" addi heap_alloc_size zero i64;\n"); - func.push_str(" aloc heap_alloc_size;\n"); - func.push_str(" sw hp ssp_saved i0;\n"); - func.push_str(" }\n"); - func.push_str(" __jmp_mem()\n"); - func.push_str("}\n"); - - func -} - -/// Creates a loader contract project, ready to be built at the given path. -/// -/// Constructs: -/// - The project folder with a correct structure if it is not already there. -/// - Loader contract source code, that is created specifically for number of -/// chunks in hand. -pub(crate) fn create_chunk_loader_contract( - abi: &ProgramABI, - chunk_contract_ids: &[String], - num_chunks: usize, - pkg_name: &str, -) -> Result { - // Create the proxy contract folder. - let proxy_contract_dir = user_forc_directory() - .join(GENERATED_CONTRACT_FOLDER_NAME) - .join(pkg_name); - std::fs::create_dir_all(&proxy_contract_dir)?; - - // Create the Forc.toml - let mut f = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(proxy_contract_dir.join(MANIFEST_FILE_NAME))?; - write!(f, "{}", LOADER_CONTRACT_FORC_TOML)?; - - // Create the src folder - std::fs::create_dir_all(proxy_contract_dir.join(SRC_DIR))?; - - // Create main.sw - let mut f = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(proxy_contract_dir.join(SRC_DIR).join(MAIN_ENTRY))?; - - let contract_str = generate_chunk_loader_contract_src(abi, num_chunks, chunk_contract_ids); - write!(f, "{}", contract_str)?; - Ok(proxy_contract_dir) -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct ContractChunk { - id: usize, - size: usize, - bytecode: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct DeployedContractChunk { - contract_id: ContractId, -} - -impl DeployedContractChunk { - pub fn contract_id(&self) -> &ContractId { - &self.contract_id - } -} - -impl ContractChunk { - pub fn new(id: usize, size: usize, bytecode: Vec) -> Self { - Self { id, size, bytecode } - } - - pub fn id(&self) -> usize { - self.id - } - - pub async fn deploy( - self, - provider: &Provider, - signing_key: &SecretKey, - ) -> anyhow::Result { - let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); - - let contract_chunk_storage_slot = StorageSlot::default(); - let salt: Salt = rand::random(); - let contract_chunk = fuels::programs::contract::Contract::new( - self.bytecode, - salt, - vec![contract_chunk_storage_slot], - ); - - let policies = TxPolicies::default(); - let bech32 = contract_chunk.deploy(&wallet, policies).await?; - let contract_id = ContractId::from(bech32); - Ok(DeployedContractChunk { contract_id }) - } -} - -/// Split bytecode into chunks of a specified maximum size. Meaning that each -/// chunk up until the last one, is guaranteed to be `chunk_size`, and -/// `chunk_size` is guaranteed to be divisble by 8, and will result an error -/// otherwise. This requirement comes from VM, as LDC'ed bytecode is appended -/// to word boundary. -pub fn split_into_chunks(bytecode: Vec, chunk_size: usize) -> Result> { - // This is done so that LDC'ed bytecode aligns perfectly, as the VM appends - // them to word boundary. This should normally be the case if the bytecode - // is not modified manually. - assert!(chunk_size % 8 == 0); - if chunk_size % 8 != 0 { - bail!( - "Chunks size is not divisible by 8, chunk size: {}", - chunk_size - ); - } - let mut chunks = Vec::new(); - - for (id, chunk) in bytecode.chunks(chunk_size).enumerate() { - let chunk = chunk.to_vec(); - let size = chunk.len(); - let contract_chunk = ContractChunk::new(id, size, chunk); - chunks.push(contract_chunk); - } - - Ok(chunks) -} - -pub fn build_loader_contract( - abi: &ProgramABI, - chunk_contract_ids: &[String], - num_chunks: usize, - pkg_name: &str, - build_opts: &BuildOpts, -) -> Result> { - let loader_contract = - create_chunk_loader_contract(abi, chunk_contract_ids, num_chunks, pkg_name)?; - let mut build_opts = build_opts.clone(); - let proxy_contract_dir_str = format!("{}", loader_contract.clone().display()); - build_opts.pkg.path = Some(proxy_contract_dir_str); - let built_pkgs = built_pkgs(&loader_contract, &build_opts)?; - let built_pkg = built_pkgs - .first() - .cloned() - .ok_or_else(|| anyhow::anyhow!("could not get proxy contract"))?; - Ok(built_pkg) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_split_into_chunks_exact_division() { - let bytecode = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; - let chunk_size = 8; - let chunks = split_into_chunks(bytecode.clone(), chunk_size).unwrap(); - - assert_eq!(chunks.len(), 2); - assert_eq!( - chunks[0], - ContractChunk::new(0, 8, vec![1, 2, 3, 4, 5, 6, 7, 8]) - ); - assert_eq!( - chunks[1], - ContractChunk::new(1, 8, vec![9, 10, 11, 12, 13, 14, 15, 16]) - ); - } - - #[test] - fn test_split_into_chunks_with_remainder() { - let bytecode = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; - let chunk_size = 8; - let chunks = split_into_chunks(bytecode.clone(), chunk_size).unwrap(); - - assert_eq!(chunks.len(), 2); - assert_eq!( - chunks[0], - ContractChunk::new(0, 8, vec![1, 2, 3, 4, 5, 6, 7, 8]) - ); - assert_eq!(chunks[1], ContractChunk::new(1, 1, vec![9])); - } - - #[test] - fn test_split_into_chunks_empty_bytecode() { - let bytecode = vec![]; - let chunk_size = 8; - let chunks = split_into_chunks(bytecode.clone(), chunk_size).unwrap(); - - assert_eq!(chunks.len(), 0); - } - - #[test] - fn test_split_into_chunks_smaller_than_chunk_size() { - let bytecode = vec![1, 2, 3]; - let chunk_size = 8; - let chunks = split_into_chunks(bytecode.clone(), chunk_size).unwrap(); - - assert_eq!(chunks.len(), 1); - assert_eq!(chunks[0], ContractChunk::new(0, 3, vec![1, 2, 3])); - } -} diff --git a/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json b/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json index f8ba2707608..a2d797817fc 100644 --- a/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json +++ b/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json @@ -1,22 +1,19 @@ { - "encoding": "1", - "types": [ + "programType": "contract", + "specVersion": "1", + "encodingVersion": "1", + "concreteTypes": [ { - "typeId": 0, "type": "bool", - "components": null, - "typeParameters": null + "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" } ], + "metadataTypes": [], "functions": [ { "inputs": [], "name": "test_function", - "output": { - "name": "", - "type": 0, - "typeArguments": null - }, + "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null } ], diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index bf529e588d4..5fda639b1b4 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -148,7 +148,7 @@ async fn test_simple_deploy() { ) .unwrap(), proxy: None, - chunks: vec![], + chunked: false, }]; assert_eq!(contract_ids, expected) @@ -190,6 +190,7 @@ async fn test_deploy_submit_only() { ) .unwrap(), proxy: None, + chunked: false, }]; assert_eq!(contract_ids, expected) @@ -239,7 +240,7 @@ async fn test_deploy_fresh_proxy() { ) .unwrap(), ), - chunks: vec![], + chunked: false, }; let expected = vec![impl_contract]; @@ -481,8 +482,7 @@ async fn chunked_deploy() { let deployed_contract = deploy(cmd).await.unwrap().remove(0); node.kill().unwrap(); - let num_chunks = deployed_contract.chunks.len(); - assert_eq!(num_chunks, 4); + assert!(deployed_contract.chunked); } #[tokio::test] @@ -523,18 +523,11 @@ async fn chunked_deploy_re_routes_call() { )); let instance = BigContract::new(deployed_contract.id, wallet_unlocked); - let chunks: Vec = deployed_contract - .chunks - .iter() - .cloned() - .map(|chunk_id| chunk_id.into()) - .collect(); // result should be true. let result = instance .methods() .test_function() - .with_contract_ids(&chunks) .call() .await .unwrap() @@ -586,18 +579,11 @@ async fn chunked_deploy_with_proxy_re_routes_call() { )); let instance = BigContract::new(deployed_contract.id, wallet_unlocked); - let chunks: Vec = deployed_contract - .chunks - .iter() - .cloned() - .map(|chunk_id| chunk_id.into()) - .collect(); // result should be true. let result = instance .methods() .test_function() - .with_contract_ids(&chunks) .call() .await .unwrap() From 9c0b8605e91a3c38edd355d50ec822020deaca2f Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:11:42 -0700 Subject: [PATCH 09/17] fix: invalid contract id reporting fixed for proxy and chunked --- forc-plugins/forc-client/src/op/deploy.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 7f53fbb39d7..7d9ecbaa9bc 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -178,14 +178,15 @@ async fn deploy_chunked( let contract_id = fuels::programs::contract::Contract::loader_from_blobs(blobs, salt, storage_slots)? .deploy(&wallet, tx_policies) - .await?; + .await? + .into(); println_action_green( "Finished", &format!("deploying loader contract for {pkg_name} {contract_url}{contract_id}"), ); - Ok(contract_id.into()) + Ok(contract_id) } /// Deploys a new proxy contract for the given package. @@ -195,7 +196,7 @@ async fn deploy_new_proxy( impl_contract: &fuel_tx::ContractId, provider: &Provider, signing_key: &SecretKey, -) -> Result { +) -> Result { fuels::macros::abigen!(Contract( name = "ProxyContract", abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" @@ -217,12 +218,13 @@ async fn deploy_new_proxy( .with_configurables(configurables); let tx_policies = tx_policies_from_cmd(command); - let proxy_contract_id = fuels::programs::contract::Contract::load_from( + let proxy_contract_id: ContractId = fuels::programs::contract::Contract::load_from( proxy_dir_output.join("proxy.bin"), configuration, )? .deploy(&wallet, tx_policies) - .await?; + .await? + .into(); let chain_info = provider.chain_info().await?; let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); @@ -236,10 +238,10 @@ 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 instance = ProxyContract::new(&proxy_contract_id.into(), 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 From 23a7bcc75d6cf349740c4fa1a97f41e9093cc09f Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:24:35 -0700 Subject: [PATCH 10/17] add blobs keyword to details --- forc-plugins/forc-client/src/op/deploy.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 7d9ecbaa9bc..79d2a8b3f82 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -452,8 +452,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() ) }) From 2da7ee19f453b0584a6f125e535f3af852f3b0b1 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:31:43 -0700 Subject: [PATCH 11/17] chore fix nits --- forc-plugins/forc-client/src/op/deploy.rs | 11 +++++++++-- forc-plugins/forc-client/tests/deploy.rs | 5 +---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 79d2a8b3f82..623a66dca70 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -24,7 +24,7 @@ use fuel_tx::{Salt, Transaction}; use fuel_vm::prelude::*; use fuels::{ programs::contract::{LoadConfiguration, StorageConfiguration}, - types::transaction_builders::Blob, + types::{bech32::Bech32ContractId, transaction_builders::Blob}, }; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; @@ -132,6 +132,8 @@ 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, @@ -148,6 +150,10 @@ fn resolve_storage_slots( 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) make call into. async fn deploy_chunked( command: &cmd::Deploy, compiled: &BuiltPackage, @@ -238,7 +244,8 @@ async fn deploy_new_proxy( &format!("deploying proxy contract for {pkg_name} {contract_url}{proxy_contract_id}"), ); - let instance = ProxyContract::new(&proxy_contract_id.into(), 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) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 5fda639b1b4..c5274b7d572 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -8,10 +8,7 @@ use forc_client::{ use forc_pkg::manifest::Proxy; use fuel_crypto::SecretKey; use fuel_tx::{ContractId, Salt}; -use fuels::{ - macros::abigen, - types::{bech32::Bech32ContractId, transaction::TxPolicies}, -}; +use fuels::{macros::abigen, types::transaction::TxPolicies}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; use rand::thread_rng; From 14bb5d5b377d4876ffa89732f435cddd07a04ba4 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:45:29 -0700 Subject: [PATCH 12/17] fix comment --- forc-plugins/forc-client/src/op/deploy.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 623a66dca70..9bce58e1128 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -41,7 +41,7 @@ use std::{ use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; -/// Maximum contract size allowed to be in a single contract. If the target +/// 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. @@ -355,7 +355,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { } }; let bytecode_size = pkg.bytecode.bytes.len(); - let (deployed_contract_id, chunked) = if bytecode_size > MAX_CONTRACT_SIZE { + 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?; @@ -368,11 +368,11 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { &pkg.descriptor.name, ) .await?; - (id, true) + id } else { let deployed_contract_id = deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?; - (deployed_contract_id, false) + deployed_contract_id }; let proxy_id = match &pkg.descriptor.manifest_file.proxy { @@ -425,7 +425,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { let deployed_contract = DeployedContract { id: deployed_contract_id, proxy: proxy_id, - chunked, + chunked: bytecode_size > MAX_CONTRACT_SIZE, }; deployed_contracts.push(deployed_contract); } From 021330b0d76158fb55c7dc94cb89159051606d90 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:48:50 -0700 Subject: [PATCH 13/17] chore: clippy nits --- forc-plugins/forc-client/src/op/deploy.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 9bce58e1128..94c47f395a6 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -359,7 +359,8 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // Deploy chunked let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?; let provider = Provider::connect(node_url).await?; - let id = deploy_chunked( + + deploy_chunked( &command, pkg, salt, @@ -367,12 +368,10 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { &provider, &pkg.descriptor.name, ) - .await?; - id + .await? } else { - let deployed_contract_id = - deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?; - deployed_contract_id + + deploy_pkg(&command, pkg, salt, &provider, &signing_key).await? }; let proxy_id = match &pkg.descriptor.manifest_file.proxy { From 495e80afc19d21bf241078aa6a9ae3b458ca5534 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 15 Aug 2024 18:55:04 -0700 Subject: [PATCH 14/17] fmt --- forc-plugins/forc-client/src/op/deploy.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 94c47f395a6..55006a54930 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -359,7 +359,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // 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, @@ -370,7 +370,6 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { ) .await? } else { - deploy_pkg(&command, pkg, salt, &provider, &signing_key).await? }; From 368874139b09ecce2e46e4b357d35f2114a7d392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Thu, 15 Aug 2024 19:19:10 -0700 Subject: [PATCH 15/17] Update deploy.rs Co-authored-by: Joshua Batty --- forc-plugins/forc-client/src/op/deploy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 55006a54930..8d5c78d13a5 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -153,7 +153,7 @@ fn resolve_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) make call into. +/// a single contract (loader contract that loads the blobs) to call into. async fn deploy_chunked( command: &cmd::Deploy, compiled: &BuiltPackage, From 2fde7ea701a50833b71bca2336b77b845ab8e20b Mon Sep 17 00:00:00 2001 From: Sophie Dankel <47993817+sdankel@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:08:43 -0700 Subject: [PATCH 16/17] Additional tests for contract chunking in forc (#6428) ## Description Test should all pass once https://github.com/FuelLabs/fuels-rs/pull/1483 is released ## Checklist - [ ] I have linked to any relevant issues. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [ ] I have requested a review from the relevant team or maintainers. --- .../data/big_contract/big_contract-abi.json | 310 +++++++++++++++++- .../test/data/big_contract/src/main.sw | 119 ++++++- forc-plugins/forc-client/tests/deploy.rs | 97 ++++-- 3 files changed, 489 insertions(+), 37 deletions(-) diff --git a/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json b/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json index a2d797817fc..c3c892e05b5 100644 --- a/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json +++ b/forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json @@ -3,21 +3,325 @@ "specVersion": "1", "encodingVersion": "1", "concreteTypes": [ + { + "type": "()", + "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + }, + { + "type": "(bool, u64)", + "concreteTypeId": "c998ca9a5f221fe7b5c66ae70c8a9562b86d964408b00d17f883c906bc1fe4be", + "metadataTypeId": 0 + }, + { + "type": "[bool; 3]", + "concreteTypeId": "4926d35d1a5157936b0a29bc126b8aace6d911209a5c130e9b716b0c73643ea6", + "metadataTypeId": 2 + }, + { + "type": "[u64; 3]", + "concreteTypeId": "776fb5a3824169d6736138565fdc20aad684d9111266a5ff6d5c675280b7e199", + "metadataTypeId": 3 + }, + { + "type": "b256", + "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + }, { "type": "bool", "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + }, + { + "type": "enum Location", + "concreteTypeId": "6966f221767e2951c5d68ba5e81e897449375a9f51f5e4c9e3b65f1dd7defabb", + "metadataTypeId": 4 + }, + { + "type": "str[4]", + "concreteTypeId": "94f0fa95c830be5e4f711963e83259fe7e8bc723278ab6ec34449e791a99b53a" + }, + { + "type": "struct Person", + "concreteTypeId": "a6b272bcf0f572840e2cc1241bdcf8d42db51eeb1aead750c8e608160f83f926", + "metadataTypeId": 6 + }, + { + "type": "struct SimpleStruct", + "concreteTypeId": "75f7f7a06026cab5d7a70984d1fde56001e83505e3a091ff9722b92d7f56d8be", + "metadataTypeId": 7 + }, + { + "type": "u16", + "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef" + }, + { + "type": "u256", + "concreteTypeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e" + }, + { + "type": "u32", + "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" + }, + { + "type": "u64", + "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "type": "u8", + "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" + } + ], + "metadataTypes": [ + { + "type": "(_, _)", + "metadataTypeId": 0, + "components": [ + { + "name": "__tuple_element", + "typeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + }, + { + "name": "__tuple_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "[_; 2]", + "metadataTypeId": 1, + "components": [ + { + "name": "__array_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "[_; 3]", + "metadataTypeId": 2, + "components": [ + { + "name": "__array_element", + "typeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + } + ] + }, + { + "type": "[_; 3]", + "metadataTypeId": 3, + "components": [ + { + "name": "__array_element", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] + }, + { + "type": "enum Location", + "metadataTypeId": 4, + "components": [ + { + "name": "Earth", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "Mars", + "typeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + } + ] + }, + { + "type": "str", + "metadataTypeId": 5 + }, + { + "type": "struct Person", + "metadataTypeId": 6, + "components": [ + { + "name": "name", + "typeId": 5 + }, + { + "name": "age", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + }, + { + "name": "alive", + "typeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + }, + { + "name": "location", + "typeId": 4 + }, + { + "name": "some_tuple", + "typeId": 0 + }, + { + "name": "some_array", + "typeId": 1 + }, + { + "name": "some_b256", + "typeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" + } + ] + }, + { + "type": "struct SimpleStruct", + "metadataTypeId": 7, + "components": [ + { + "name": "a", + "typeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" + }, + { + "name": "b", + "typeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ] } ], - "metadataTypes": [], "functions": [ { "inputs": [], - "name": "test_function", + "name": "assert_configurables", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null + }, + { + "inputs": [ + { + "name": "loc", + "concreteTypeId": "6966f221767e2951c5d68ba5e81e897449375a9f51f5e4c9e3b65f1dd7defabb" + } + ], + "name": "enum_input_output", + "output": "6966f221767e2951c5d68ba5e81e897449375a9f51f5e4c9e3b65f1dd7defabb", + "attributes": null + }, + { + "inputs": [ + { + "name": "index", + "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" + } + ], + "name": "get_storage", + "output": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef", + "attributes": [ + { + "name": "storage", + "arguments": [ + "read" + ] + } + ] + }, + { + "inputs": [], + "name": "large_blob", + "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", + "attributes": null + }, + { + "inputs": [ + { + "name": "value", + "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef" + } + ], + "name": "push_storage", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": [ + { + "name": "storage", + "arguments": [ + "read", + "write" + ] + } + ] + }, + { + "inputs": [ + { + "name": "person", + "concreteTypeId": "a6b272bcf0f572840e2cc1241bdcf8d42db51eeb1aead750c8e608160f83f926" + } + ], + "name": "struct_input_output", + "output": "a6b272bcf0f572840e2cc1241bdcf8d42db51eeb1aead750c8e608160f83f926", + "attributes": null } ], "loggedTypes": [], "messagesTypes": [], - "configurables": [] + "configurables": [ + { + "name": "BOOL", + "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", + "offset": 380112 + }, + { + "name": "U8", + "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b", + "offset": 380232 + }, + { + "name": "U16", + "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef", + "offset": 380176 + }, + { + "name": "U32", + "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc", + "offset": 380216 + }, + { + "name": "U64", + "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc", + "offset": 380224 + }, + { + "name": "U256", + "concreteTypeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e", + "offset": 380184 + }, + { + "name": "B256", + "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b", + "offset": 380080 + }, + { + "name": "CONFIGURABLE_STRUCT", + "concreteTypeId": "75f7f7a06026cab5d7a70984d1fde56001e83505e3a091ff9722b92d7f56d8be", + "offset": 380136 + }, + { + "name": "CONFIGURABLE_ENUM", + "concreteTypeId": "6966f221767e2951c5d68ba5e81e897449375a9f51f5e4c9e3b65f1dd7defabb", + "offset": 380120 + }, + { + "name": "ARRAY_BOOL", + "concreteTypeId": "4926d35d1a5157936b0a29bc126b8aace6d911209a5c130e9b716b0c73643ea6", + "offset": 380048 + }, + { + "name": "ARRAY_U64", + "concreteTypeId": "776fb5a3824169d6736138565fdc20aad684d9111266a5ff6d5c675280b7e199", + "offset": 380056 + }, + { + "name": "TUPLE_BOOL_U64", + "concreteTypeId": "c998ca9a5f221fe7b5c66ae70c8a9562b86d964408b00d17f883c906bc1fe4be", + "offset": 380160 + }, + { + "name": "STR_4", + "concreteTypeId": "94f0fa95c830be5e4f711963e83259fe7e8bc723278ab6ec34449e791a99b53a", + "offset": 380152 + } + ] } \ No newline at end of file diff --git a/forc-plugins/forc-client/test/data/big_contract/src/main.sw b/forc-plugins/forc-client/test/data/big_contract/src/main.sw index e77fde18346..b62ee995940 100644 --- a/forc-plugins/forc-client/test/data/big_contract/src/main.sw +++ b/forc-plugins/forc-client/test/data/big_contract/src/main.sw @@ -1,14 +1,129 @@ contract; +use std::storage::storage_vec::*; +use std::hash::*; + abi MyContract { - fn test_function() -> bool; + fn large_blob() -> bool; + + fn enum_input_output(loc: Location) -> Location; + + fn struct_input_output(person: Person) -> Person; + + #[storage(read, write)] + fn push_storage(value: u16); + + #[storage(read)] + fn get_storage(index: u64) -> u16; + + fn assert_configurables() -> bool; +} + +enum Location { + Earth: u64, + Mars: (), +} + +struct Person { + name: str, + age: u64, + alive: bool, + location: Location, + some_tuple: (bool, u64), + some_array: [u64; 2], + some_b256: b256, +} + +struct SimpleStruct { + a: bool, + b: u64, +} + +storage { + my_vec: StorageVec = StorageVec {}, +} + +configurable { + BOOL: bool = true, + U8: u8 = 1, + U16: u16 = 2, + U32: u32 = 3, + U64: u32 = 4, + U256: u256 = 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAu256, + B256: b256 = 0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB, + CONFIGURABLE_STRUCT: SimpleStruct = SimpleStruct { a: true, b: 5 }, + CONFIGURABLE_ENUM: Location = Location::Earth(1), + ARRAY_BOOL: [bool; 3] = [true, false, true], + ARRAY_U64: [u64; 3] = [9, 8, 7], + TUPLE_BOOL_U64: (bool, u64) = (true, 11), + STR_4: str[4] = __to_str_array("abcd"), +} + +impl core::ops::Eq for Location { + fn eq(self, other: Location) -> bool { + match (self, other) { + (Location::Earth(inner1), Location::Earth(inner2)) => inner1 == inner2, + (Location::Mars, Location::Mars) => true, + _ => false, + } + } } impl MyContract for Contract { - fn test_function() -> bool { + fn large_blob() -> bool { asm() { blob i91000; } true } + + fn enum_input_output(loc: Location) -> Location { + loc + } + + fn struct_input_output(person: Person) -> Person { + person + } + + #[storage(read, write)] + fn push_storage(value: u16) { + storage.my_vec.push(value); + } + + #[storage(read)] + fn get_storage(index: u64) -> u16 { + storage.my_vec.get(index).unwrap().read() + } + + fn assert_configurables() -> bool { + assert(BOOL == true); + assert(U8 == 1); + assert(U16 == 2); + assert(U32 == 3); + assert(U64 == 4); + assert(U256 == 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAu256); + assert(B256 == 0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB); + assert(CONFIGURABLE_STRUCT.a == true); + assert(CONFIGURABLE_STRUCT.b == 5); + assert(CONFIGURABLE_ENUM == Location::Earth(1)); + assert(ARRAY_BOOL[0] == true); + assert(ARRAY_BOOL[1] == false); + assert(ARRAY_BOOL[2] == true); + assert(ARRAY_U64[0] == 9); + assert(ARRAY_U64[1] == 8); + assert(ARRAY_U64[2] == 7); + assert(TUPLE_BOOL_U64.0 == true); + assert(TUPLE_BOOL_U64.1 == 11); + assert(sha256_str_array(STR_4) == sha256("abcd")); + + // Assert address do not change + let addr_1 = asm(addr: __addr_of(&BOOL)) { + addr: u64 + }; + let addr_2 = asm(addr: __addr_of(&BOOL)) { + addr: u64 + }; + assert(addr_1 == addr_2); + true + } } diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index c5274b7d572..1f40025596d 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -8,7 +8,10 @@ use forc_client::{ use forc_pkg::manifest::Proxy; use fuel_crypto::SecretKey; use fuel_tx::{ContractId, Salt}; -use fuels::{macros::abigen, types::transaction::TxPolicies}; +use fuels::{ + macros::abigen, + types::{transaction::TxPolicies, AsciiString, Bits256}, +}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; use rand::thread_rng; @@ -111,6 +114,64 @@ fn update_main_sw(tmp_dir: &Path) -> anyhow::Result<()> { Ok(()) } +async fn assert_big_contract_calls(wallet: WalletUnlocked, contract_id: ContractId) { + abigen!(Contract( + name = "BigContract", + abi = "forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json" + )); + + let instance = BigContract::new(contract_id, wallet); + + let result = instance.methods().large_blob().call().await.unwrap().value; + assert!(result); + + let result = instance + .methods() + .enum_input_output(Location::Mars) + .call() + .await + .unwrap() + .value; + assert_eq!(result, Location::Mars); + + let input = Person { + name: AsciiString::new("Alice".into()).unwrap(), + age: 42, + alive: true, + location: Location::Earth(1), + some_tuple: (false, 42), + some_array: [4, 2], + some_b_256: Bits256::zeroed(), + }; + let result = instance + .methods() + .struct_input_output(input.clone()) + .call() + .await + .unwrap() + .value; + assert_eq!(result, input); + + let _ = instance.methods().push_storage(42).call().await.unwrap(); + let result = instance + .methods() + .get_storage(0) + .call() + .await + .unwrap() + .value; + assert_eq!(result, 42); + + let result = instance + .methods() + .assert_configurables() + .call() + .await + .unwrap() + .value; + assert!(result); +} + #[tokio::test] async fn test_simple_deploy() { let (mut node, port) = run_node(); @@ -483,7 +544,7 @@ async fn chunked_deploy() { } #[tokio::test] -async fn chunked_deploy_re_routes_call() { +async fn chunked_deploy_re_routes_calls() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); let project_dir = test_data_path().join("big_contract"); @@ -514,23 +575,9 @@ async fn chunked_deploy_re_routes_call() { let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); - abigen!(Contract( - name = "BigContract", - abi = "forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json" - )); - - let instance = BigContract::new(deployed_contract.id, wallet_unlocked); + assert_big_contract_calls(wallet_unlocked, deployed_contract.id).await; - // result should be true. - let result = instance - .methods() - .test_function() - .call() - .await - .unwrap() - .value; node.kill().unwrap(); - assert!(result) } #[tokio::test] @@ -570,21 +617,7 @@ async fn chunked_deploy_with_proxy_re_routes_call() { let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); - abigen!(Contract( - name = "BigContract", - abi = "forc-plugins/forc-client/test/data/big_contract/big_contract-abi.json" - )); - - let instance = BigContract::new(deployed_contract.id, wallet_unlocked); + assert_big_contract_calls(wallet_unlocked, deployed_contract.id).await; - // result should be true. - let result = instance - .methods() - .test_function() - .call() - .await - .unwrap() - .value; node.kill().unwrap(); - assert!(result) } From 2f861aee5912f6ed32ef250edbe9d902d30692af Mon Sep 17 00:00:00 2001 From: Sophie Date: Fri, 16 Aug 2024 17:08:34 -0700 Subject: [PATCH 17/17] Update forc-client docs --- docs/book/src/forc/plugins/forc_client/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index 986be423dac..4127c9956de 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -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.