diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 3636a98..54a7ca7 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -8,7 +8,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "once_cell", "version_check", "zerocopy", @@ -341,6 +341,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.4" @@ -498,7 +504,7 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures", "curve25519-dalek-derive", "digest", @@ -776,6 +782,7 @@ name = "fees" version = "0.0.0" dependencies = [ "soroban-sdk", + "wee_alloc", ] [[package]] @@ -829,7 +836,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "js-sys", "libc", "wasi", @@ -842,7 +849,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "libc", "r-efi 5.3.0", "wasip2", @@ -854,7 +861,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "libc", "r-efi 6.0.0", "wasip2", @@ -1053,7 +1060,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "ecdsa", "elliptic-curve", "sha2", @@ -1121,6 +1128,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "meter-aggregator" version = "0.0.0" @@ -1191,6 +1204,7 @@ name = "oracle-aggregator" version = "0.0.0" dependencies = [ "soroban-sdk", + "wee_alloc", ] [[package]] @@ -1251,6 +1265,7 @@ name = "price_oracle" version = "0.0.0" dependencies = [ "soroban-sdk", + "wee_alloc", ] [[package]] @@ -1608,6 +1623,7 @@ version = "0.1.0" dependencies = [ "soroban-sdk", "utility-contracts-common", + "wee_alloc", ] [[package]] @@ -1616,7 +1632,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -2054,6 +2070,7 @@ name = "utility-contracts-common" version = "0.1.0" dependencies = [ "soroban-sdk", + "wee_alloc", ] [[package]] @@ -2064,6 +2081,7 @@ dependencies = [ "lazy_static", "proptest", "soroban-sdk", + "wee_alloc", ] [[package]] @@ -2122,7 +2140,7 @@ version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ - "cfg-if", + "cfg-if 1.0.4", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -2232,6 +2250,40 @@ dependencies = [ "indexmap-nostd", ] +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" diff --git a/contracts/utility_contracts/src/batch_executor.rs b/contracts/utility_contracts/src/batch_executor.rs new file mode 100644 index 0000000..f15d47e --- /dev/null +++ b/contracts/utility_contracts/src/batch_executor.rs @@ -0,0 +1,36 @@ +use soroban_sdk::{contracttype, Address, Env, Symbol, Val, Vec}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BatchOperation { + pub contract: Address, + pub function: Symbol, + pub args: Vec, +} + +pub fn execute_batch(env: &Env, ops: Vec) -> Vec { + if ops.len() > 20 { + panic!("Batch exceeds maximum of 20 operations"); + } + + let mut results: Vec = Vec::new(env); + + for op in ops.iter() { + // Any failure here will bubble up and panic, reverting the entire transaction. + // This satisfies "Atomic execution (all or nothing)" and "rollback on failure". + let res: Val = env.invoke_contract(&op.contract, &op.function, op.args); + results.push_back(res); + } + + results +} + +pub fn estimate_batch_gas(_env: &Env, ops: Vec) -> u64 { + // Basic heuristic for estimating gas usage for batch operations off-chain. + // Base cost + (per operation overhead * number of operations). + let base_cost = 10_000; + let per_op_cost = 5_000; + + let total_cost = base_cost + (ops.len() as u64 * per_op_cost); + total_cost +} diff --git a/contracts/utility_contracts/src/batch_executor_tests.rs b/contracts/utility_contracts/src/batch_executor_tests.rs new file mode 100644 index 0000000..19e1430 --- /dev/null +++ b/contracts/utility_contracts/src/batch_executor_tests.rs @@ -0,0 +1,122 @@ +#![cfg(test)] + +use crate::batch_executor::{BatchOperation, estimate_batch_gas}; +use crate::{UtilityContract, UtilityContractClient}; +use soroban_sdk::{testutils::Address as _, Address, Env, IntoVal, Symbol, Vec, symbol_short}; + +// A dummy contract to test the batch executor. +#[soroban_sdk::contract] +pub struct DummyContract; + +#[soroban_sdk::contractimpl] +impl DummyContract { + pub fn add(env: Env, a: u32, b: u32) -> u32 { + a + b + } + + pub fn fail(_env: Env) { + panic!("Intended failure"); + } +} + +#[test] +fn test_batch_execution_success() { + let env = Env::default(); + + let utility_id = env.register_contract(None, UtilityContract); + let utility_client = UtilityContractClient::new(&env, &utility_id); + + let dummy_id = env.register_contract(None, DummyContract); + + let mut ops = Vec::new(&env); + + ops.push_back(BatchOperation { + contract: dummy_id.clone(), + function: symbol_short!("add"), + args: (2u32, 3u32).into_val(&env), + }); + + ops.push_back(BatchOperation { + contract: dummy_id.clone(), + function: symbol_short!("add"), + args: (10u32, 20u32).into_val(&env), + }); + + let results = utility_client.execute_batch(&ops); + + assert_eq!(results.len(), 2); + let res1: u32 = results.get(0).unwrap().into_val(&env); + let res2: u32 = results.get(1).unwrap().into_val(&env); + + assert_eq!(res1, 5); + assert_eq!(res2, 30); +} + +#[test] +#[should_panic(expected = "Intended failure")] +fn test_batch_execution_partial_failure() { + let env = Env::default(); + let utility_id = env.register_contract(None, UtilityContract); + let utility_client = UtilityContractClient::new(&env, &utility_id); + let dummy_id = env.register_contract(None, DummyContract); + + let mut ops = Vec::new(&env); + + ops.push_back(BatchOperation { + contract: dummy_id.clone(), + function: symbol_short!("add"), + args: (2u32, 3u32).into_val(&env), + }); + + ops.push_back(BatchOperation { + contract: dummy_id.clone(), + function: symbol_short!("fail"), + args: ().into_val(&env), + }); + + utility_client.execute_batch(&ops); +} + +#[test] +#[should_panic(expected = "Batch exceeds maximum of 20 operations")] +fn test_batch_execution_limit() { + let env = Env::default(); + let utility_id = env.register_contract(None, UtilityContract); + let utility_client = UtilityContractClient::new(&env, &utility_id); + let dummy_id = env.register_contract(None, DummyContract); + + let mut ops = Vec::new(&env); + + for _ in 0..21 { + ops.push_back(BatchOperation { + contract: dummy_id.clone(), + function: symbol_short!("add"), + args: (1u32, 1u32).into_val(&env), + }); + } + + utility_client.execute_batch(&ops); +} + +#[test] +fn test_gas_estimation() { + let env = Env::default(); + let dummy_id = Address::generate(&env); + + let mut ops = Vec::new(&env); + for _ in 0..5 { + ops.push_back(BatchOperation { + contract: dummy_id.clone(), + function: symbol_short!("add"), + args: (1u32, 1u32).into_val(&env), + }); + } + + let utility_id = env.register_contract(None, UtilityContract); + let utility_client = UtilityContractClient::new(&env, &utility_id); + + let estimated_gas = utility_client.estimate_batch_gas(&ops); + + // Base 10000 + 5 * 5000 = 35000 + assert_eq!(estimated_gas, 35000); +} diff --git a/contracts/utility_contracts/src/lib.rs b/contracts/utility_contracts/src/lib.rs index 877971e..692ec46 100644 --- a/contracts/utility_contracts/src/lib.rs +++ b/contracts/utility_contracts/src/lib.rs @@ -10,6 +10,10 @@ use soroban_sdk::{ symbol_short, token, Address, Bytes, BytesN, Env, String, Symbol, Vec, }; +pub mod batch_executor; +#[cfg(test)] +pub mod batch_executor_tests; + #[contractclient(name = "PriceOracleClient")] pub trait PriceOracle { fn xlm_to_usd_cents(env: Env, xlm_amount: i128) -> i128; @@ -3429,6 +3433,18 @@ pub struct UtilityContract; #[contractimpl] impl UtilityContract { + /// Executes a batch of operations atomically to save gas. + /// Fails the entire batch if any single operation fails. + /// Limited to 20 operations per batch. + pub fn execute_batch(env: Env, ops: Vec) -> Vec { + crate::batch_executor::execute_batch(&env, ops) + } + + /// Estimates the gas required for a batch of operations. + pub fn estimate_batch_gas(env: Env, ops: Vec) -> u64 { + crate::batch_executor::estimate_batch_gas(&env, ops) + } + /// Assigns a reseller to a specific meter with a defined fee percentage. /// /// # Arguments