From aa7a7563bf8cdd3e60a5b1b379f287ae1457b864 Mon Sep 17 00:00:00 2001 From: Yarik Bratashchuk Date: Tue, 8 Feb 2022 13:43:32 +0200 Subject: [PATCH] `seal_delegate_call` api function (support for library contracts) (#10617) * seal_call_code implementation - tests - benchmark * Addressing @xgreenx's comments * Fix test-linux-stable-int * Rename seal_call_code to seal_delegate_call * Pass value unchanged into lib contract * Address @athei's comments - whitespace .wat issues - wrong/missing .wat comments - redundant .wat calls/declarations - change order of functions (seal_delegate_call right after seal_call) in decls, tests, benchmark - fix comments, move doc comments to enum variants - remove unnecessary empty lines - rename runtime cost DelegateCall to DelegateCallBase - do not set CallFlags::ALLOW_REENTRY for delegate_call * Do not pass CallFlags::ALLOWS_REENTRY for delegate_call * Update comment for seal_delegate_call and CallFlags * Addressing @athei's comments (minor) * Allow reentry for a new frame after delegate_call (revert) * Same seal_caller and seal_value_transferred for lib contract - test - refactor frame args due to review - logic for seal_caller (please review) * Put caller on frame for delegate_call, minor fixes * Update comment for delegate_call * Addressing @athei's comments * Update weights generated by benchmark * Improve comments * Address @HCastano's comments * Update weights, thanks @joao-paulo-parity * Improve InvalidCallFlags error comment --- frame/contracts/fixtures/delegate_call.wat | 111 +++++++++++++++ .../contracts/fixtures/delegate_call_lib.wat | 79 +++++++++++ frame/contracts/src/benchmarking/mod.rs | 52 +++++++ frame/contracts/src/exec.rs | 127 +++++++++++++---- frame/contracts/src/lib.rs | 2 + frame/contracts/src/schedule.rs | 4 + frame/contracts/src/tests.rs | 38 ++++++ frame/contracts/src/wasm/mod.rs | 63 +++++++++ frame/contracts/src/wasm/runtime.rs | 128 +++++++++++++++--- frame/contracts/src/weights.rs | 23 ++++ 10 files changed, 576 insertions(+), 51 deletions(-) create mode 100644 frame/contracts/fixtures/delegate_call.wat create mode 100644 frame/contracts/fixtures/delegate_call_lib.wat diff --git a/frame/contracts/fixtures/delegate_call.wat b/frame/contracts/fixtures/delegate_call.wat new file mode 100644 index 0000000000000..e1c5fa813e590 --- /dev/null +++ b/frame/contracts/fixtures/delegate_call.wat @@ -0,0 +1,111 @@ +(module + (import "seal0" "seal_input" (func $seal_input (param i32 i32))) + (import "seal0" "seal_get_storage" (func $seal_get_storage (param i32 i32 i32) (result i32))) + (import "seal0" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32))) + (import "__unstable__" "seal_delegate_call" (func $seal_delegate_call (param i32 i32 i32 i32 i32 i32) (result i32))) + (import "env" "memory" (memory 3 3)) + + ;; [0, 32) storage key + (data (i32.const 0) "\01") + + ;; [32, 64) storage key + (data (i32.const 32) "\02") + + ;; [64, 96) buffer where input is copied + + ;; [96, 100) size of the input buffer + (data (i32.const 96) "\20") + + ;; [100, 104) size of buffer for seal_get_storage + (data (i32.const 100) "\20") + + ;; [104, 136) seal_get_storage buffer + + (func $assert (param i32) + (block $ok + (br_if $ok + (get_local 0) + ) + (unreachable) + ) + ) + + (func (export "call") + (local $exit_code i32) + + ;; Reading "callee" code_hash + (call $seal_input (i32.const 64) (i32.const 96)) + + ;; assert input size == 32 + (call $assert + (i32.eq + (i32.load (i32.const 96)) + (i32.const 32) + ) + ) + + ;; place a value in storage, the size of which is specified by the call input. + (call $seal_set_storage + (i32.const 0) ;; Pointer to storage key + (i32.const 32) ;; Pointer to initial value + (i32.load (i32.const 100)) ;; Size of value + ) + + (call $assert + (i32.eq + (call $seal_get_storage + (i32.const 0) ;; Pointer to storage key + (i32.const 104) ;; buffer where to copy result + (i32.const 100) ;; pointer to size of buffer + ) + (i32.const 0) ;; ReturnCode::Success + ) + ) + + (call $assert + (i32.eq + (i32.load (i32.const 104)) ;; value received from storage + (i32.load (i32.const 32)) ;; initial value + ) + ) + + ;; Call deployed library contract code. + (set_local $exit_code + (call $seal_delegate_call + (i32.const 0) ;; Set no call flags + (i32.const 64) ;; Pointer to "callee" code_hash. + (i32.const 0) ;; Input is ignored + (i32.const 0) ;; Length of the input + (i32.const 4294967295) ;; u32 max sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + ) + ) + + ;; Check for success exit status. + (call $assert + (i32.eq (get_local $exit_code) (i32.const 0)) ;; ReturnCode::Success + ) + + (call $assert + (i32.eq + (call $seal_get_storage + (i32.const 0) ;; Pointer to storage key + (i32.const 104) ;; buffer where to copy result + (i32.const 100) ;; pointer to size of buffer + ) + (i32.const 0) ;; ReturnCode::Success + ) + ) + + ;; Make sure that 'callee' code changed the value + (call $assert + (i32.eq + (i32.load (i32.const 104)) + (i32.const 1) + ) + ) + ) + + (func (export "deploy")) + +) diff --git a/frame/contracts/fixtures/delegate_call_lib.wat b/frame/contracts/fixtures/delegate_call_lib.wat new file mode 100644 index 0000000000000..340b9699f8755 --- /dev/null +++ b/frame/contracts/fixtures/delegate_call_lib.wat @@ -0,0 +1,79 @@ +(module + (import "seal0" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32))) + (import "seal0" "seal_caller" (func $seal_caller (param i32 i32))) + (import "seal0" "seal_value_transferred" (func $seal_value_transferred (param i32 i32))) + (import "env" "memory" (memory 1 1)) + + ;; [0, 32) storage key + (data (i32.const 0) "\01") + + ;; [32, 64) buffer for transferred value + + ;; [64, 96) size of the buffer for transferred value + (data (i32.const 64) "\20") + + ;; [96, 128) buffer for the caller + + ;; [128, 160) size of the buffer for caller + (data (i32.const 128) "\20") + + (func $assert (param i32) + (block $ok + (br_if $ok + (get_local 0) + ) + (unreachable) + ) + ) + + (func (export "call") + ;; place a value in storage + (call $seal_set_storage + (i32.const 0) ;; Pointer to storage key + (i32.const 0) ;; Pointer to value + (i32.const 32) ;; Size of value + ) + + ;; This stores the value transferred in the buffer + (call $seal_value_transferred (i32.const 32) (i32.const 64)) + + ;; assert len == 8 + (call $assert + (i32.eq + (i32.load (i32.const 64)) + (i32.const 8) + ) + ) + + ;; assert that contents of the buffer is equal to the value + ;; passed to the `caller` contract: 1337 + (call $assert + (i64.eq + (i64.load (i32.const 32)) + (i64.const 1337) + ) + ) + + ;; fill the buffer with the caller. + (call $seal_caller (i32.const 96) (i32.const 128)) + + ;; assert len == 32 + (call $assert + (i32.eq + (i32.load (i32.const 128)) + (i32.const 32) + ) + ) + + ;; assert that the first 64 byte are the beginning of "ALICE", + ;; who is the caller of the `caller` contract + (call $assert + (i64.eq + (i64.load (i32.const 96)) + (i64.const 0x0101010101010101) + ) + ) + ) + + (func (export "deploy")) +) diff --git a/frame/contracts/src/benchmarking/mod.rs b/frame/contracts/src/benchmarking/mod.rs index 71045bde21525..88405eba44205 100644 --- a/frame/contracts/src/benchmarking/mod.rs +++ b/frame/contracts/src/benchmarking/mod.rs @@ -1474,6 +1474,58 @@ benchmarks! { let origin = RawOrigin::Signed(instance.caller.clone()); }: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![]) + seal_delegate_call { + let r in 0 .. API_BENCHMARK_BATCHES; + let hashes = (0..r * API_BENCHMARK_BATCH_SIZE) + .map(|i| { + let code = WasmModule::::dummy_with_bytes(i); + Contracts::::store_code_raw(code.code, whitelisted_caller())?; + Ok(code.hash) + }) + .collect::, &'static str>>()?; + let hash_len = hashes.get(0).map(|x| x.encode().len()).unwrap_or(0); + let hashes_bytes = hashes.iter().flat_map(|x| x.encode()).collect::>(); + let hashes_len = hashes_bytes.len(); + let hashes_offset = 0; + + let code = WasmModule::::from(ModuleDefinition { + memory: Some(ImportedMemory::max::()), + imported_functions: vec![ImportedFunction { + module: "__unstable__", + name: "seal_delegate_call", + params: vec![ + ValueType::I32, + ValueType::I32, + ValueType::I32, + ValueType::I32, + ValueType::I32, + ValueType::I32, + ], + return_type: Some(ValueType::I32), + }], + data_segments: vec![ + DataSegment { + offset: hashes_offset as u32, + value: hashes_bytes, + }, + ], + call_body: Some(body::repeated_dyn(r * API_BENCHMARK_BATCH_SIZE, vec![ + Regular(Instruction::I32Const(0)), // flags + Counter(hashes_offset as u32, hash_len as u32), // code_hash_ptr + Regular(Instruction::I32Const(0)), // input_data_ptr + Regular(Instruction::I32Const(0)), // input_data_len + Regular(Instruction::I32Const(u32::max_value() as i32)), // output_ptr + Regular(Instruction::I32Const(0)), // output_len_ptr + Regular(Instruction::Call(0)), + Regular(Instruction::Drop), + ])), + .. Default::default() + }); + let instance = Contract::::new(code, vec![])?; + let callee = instance.addr.clone(); + let origin = RawOrigin::Signed(instance.caller.clone()); + }: call(origin, callee, 0u32.into(), Weight::MAX, None, vec![]) + seal_call_per_transfer_input_output_kb { let t in 0 .. 1; let i in 0 .. code::max_pages::() * 64; diff --git a/frame/contracts/src/exec.rs b/frame/contracts/src/exec.rs index befdf592f51c0..c6e647e0c8d96 100644 --- a/frame/contracts/src/exec.rs +++ b/frame/contracts/src/exec.rs @@ -104,6 +104,19 @@ pub trait Ext: sealing::Sealed { allows_reentry: bool, ) -> Result; + /// Execute code in the current frame. + /// + /// Returns the original code size of the called contract. + /// + /// # Return Value + /// + /// Result + fn delegate_call( + &mut self, + code: CodeHash, + input_data: Vec, + ) -> Result; + /// Instantiate a contract from the given code. /// /// Returns the original code size of the called contract. @@ -347,6 +360,16 @@ pub struct Frame { nested_storage: storage::meter::NestedMeter, /// If `false` the contract enabled its defense against reentrance attacks. allows_reentry: bool, + /// The caller of the currently executing frame which was spawned by `delegate_call`. + delegate_caller: Option, +} + +/// Used in a delegate call frame arguments in order to override the executable and caller. +struct DelegatedCall { + /// The executable which is run instead of the contracts own `executable`. + executable: E, + /// The account id of the caller contract. + caller: T::AccountId, } /// Parameter passed in when creating a new `Frame`. @@ -358,6 +381,10 @@ enum FrameArgs<'a, T: Config, E> { dest: T::AccountId, /// If `None` the contract info needs to be reloaded from storage. cached_info: Option>, + /// This frame was created by `seal_delegate_call` and hence uses different code than + /// what is stored at [`Self::dest`]. Its caller ([`Frame::delegated_caller`]) is the + /// account which called the caller contract + delegated_call: Option>, }, Instantiate { /// The contract or signed origin which instantiates the new contract. @@ -513,7 +540,7 @@ where debug_message: Option<&'a mut Vec>, ) -> Result { let (mut stack, executable) = Self::new( - FrameArgs::Call { dest, cached_info: None }, + FrameArgs::Call { dest, cached_info: None, delegated_call: None }, origin, gas_meter, storage_meter, @@ -604,33 +631,46 @@ where gas_limit: Weight, schedule: &Schedule, ) -> Result<(Frame, E, Option), ExecError> { - let (account_id, contract_info, executable, entry_point, account_counter) = match frame_args - { - FrameArgs::Call { dest, cached_info } => { - let contract = if let Some(contract) = cached_info { - contract - } else { - >::get(&dest).ok_or(>::ContractNotFound)? - }; - - let executable = E::from_storage(contract.code_hash, schedule, gas_meter)?; - - (dest, contract, executable, ExportedFunction::Call, None) - }, - FrameArgs::Instantiate { sender, trie_seed, executable, salt } => { - let account_id = - >::contract_address(&sender, executable.code_hash(), &salt); - let trie_id = Storage::::generate_trie_id(&account_id, trie_seed); - let contract = Storage::::new_contract( - &account_id, - trie_id, - executable.code_hash().clone(), - )?; - (account_id, contract, executable, ExportedFunction::Constructor, Some(trie_seed)) - }, - }; + let (account_id, contract_info, executable, delegate_caller, entry_point, account_counter) = + match frame_args { + FrameArgs::Call { dest, cached_info, delegated_call } => { + let contract = if let Some(contract) = cached_info { + contract + } else { + >::get(&dest).ok_or(>::ContractNotFound)? + }; + + let (executable, delegate_caller) = + if let Some(DelegatedCall { executable, caller }) = delegated_call { + (executable, Some(caller)) + } else { + (E::from_storage(contract.code_hash, schedule, gas_meter)?, None) + }; + + (dest, contract, executable, delegate_caller, ExportedFunction::Call, None) + }, + FrameArgs::Instantiate { sender, trie_seed, executable, salt } => { + let account_id = + >::contract_address(&sender, executable.code_hash(), &salt); + let trie_id = Storage::::generate_trie_id(&account_id, trie_seed); + let contract = Storage::::new_contract( + &account_id, + trie_id, + executable.code_hash().clone(), + )?; + ( + account_id, + contract, + executable, + None, + ExportedFunction::Constructor, + Some(trie_seed), + ) + }, + }; let frame = Frame { + delegate_caller, value_transferred, contract_info: CachedContract::Cached(contract_info), account_id, @@ -936,8 +976,11 @@ where CachedContract::Cached(contract) => Some(contract.clone()), _ => None, }); - let executable = - self.push_frame(FrameArgs::Call { dest: to, cached_info }, value, gas_limit)?; + let executable = self.push_frame( + FrameArgs::Call { dest: to, cached_info, delegated_call: None }, + value, + gas_limit, + )?; self.run(executable, input_data) }; @@ -950,6 +993,28 @@ where result } + fn delegate_call( + &mut self, + code_hash: CodeHash, + input_data: Vec, + ) -> Result { + let executable = E::from_storage(code_hash, &self.schedule, self.gas_meter())?; + let top_frame = self.top_frame_mut(); + let contract_info = top_frame.contract_info().clone(); + let account_id = top_frame.account_id.clone(); + let value = top_frame.value_transferred.clone(); + let executable = self.push_frame( + FrameArgs::Call { + dest: account_id, + cached_info: Some(contract_info), + delegated_call: Some(DelegatedCall { executable, caller: self.caller().clone() }), + }, + value, + 0, + )?; + self.run(executable, input_data) + } + fn instantiate( &mut self, gas_limit: Weight, @@ -1030,7 +1095,11 @@ where } fn caller(&self) -> &T::AccountId { - self.frames().nth(1).map(|f| &f.account_id).unwrap_or(&self.origin) + if let Some(caller) = &self.top_frame().delegate_caller { + &caller + } else { + self.frames().nth(1).map(|f| &f.account_id).unwrap_or(&self.origin) + } } fn is_contract(&self, address: &T::AccountId) -> bool { diff --git a/frame/contracts/src/lib.rs b/frame/contracts/src/lib.rs index 9f9cc09f6430a..7e4515868b745 100644 --- a/frame/contracts/src/lib.rs +++ b/frame/contracts/src/lib.rs @@ -570,6 +570,8 @@ pub mod pallet { pub enum Error { /// A new schedule must have a greater version than the current one. InvalidScheduleVersion, + /// Invalid combination of flags supplied to `seal_call` or `seal_delegate_call`. + InvalidCallFlags, /// The executed contract exhausted its gas limit. OutOfGas, /// The output buffer supplied to a contract API call was too small. diff --git a/frame/contracts/src/schedule.rs b/frame/contracts/src/schedule.rs index 266c0a6e2748d..e599bdc3125bd 100644 --- a/frame/contracts/src/schedule.rs +++ b/frame/contracts/src/schedule.rs @@ -358,6 +358,9 @@ pub struct HostFnWeights { /// Weight of calling `seal_call`. pub call: Weight, + /// Weight of calling `seal_delegate_call`. + pub delegate_call: Weight, + /// Weight surcharge that is claimed if `seal_call` does a balance transfer. pub call_transfer_surcharge: Weight, @@ -615,6 +618,7 @@ impl Default for HostFnWeights { take_storage_per_byte: cost_byte_batched!(seal_take_storage_per_kb), transfer: cost_batched!(seal_transfer), call: cost_batched!(seal_call), + delegate_call: cost_batched!(seal_delegate_call), call_transfer_surcharge: cost_batched_args!( seal_call_per_transfer_input_output_kb, 1, diff --git a/frame/contracts/src/tests.rs b/frame/contracts/src/tests.rs index d95d434e6a743..6919705206d4a 100644 --- a/frame/contracts/src/tests.rs +++ b/frame/contracts/src/tests.rs @@ -692,6 +692,44 @@ fn deploy_and_call_other_contract() { }); } +#[test] +#[cfg(feature = "unstable-interface")] +fn delegate_call() { + let (caller_wasm, caller_code_hash) = compile_module::("delegate_call").unwrap(); + let (callee_wasm, callee_code_hash) = compile_module::("delegate_call_lib").unwrap(); + let caller_addr = Contracts::contract_address(&ALICE, &caller_code_hash, &[]); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = Balances::deposit_creating(&ALICE, 1_000_000); + + // Instantiate the 'caller' + assert_ok!(Contracts::instantiate_with_code( + Origin::signed(ALICE), + 300_000, + GAS_LIMIT, + None, + caller_wasm, + vec![], + vec![], + )); + // Only upload 'callee' code + assert_ok!(Contracts::upload_code( + Origin::signed(ALICE), + callee_wasm, + Some(codec::Compact(100_000)), + )); + + assert_ok!(Contracts::call( + Origin::signed(ALICE), + caller_addr.clone(), + 1337, + GAS_LIMIT, + None, + callee_code_hash.as_ref().to_vec(), + )); + }); +} + #[test] fn cannot_self_destruct_through_draning() { let (wasm, code_hash) = compile_module::("drain").unwrap(); diff --git a/frame/contracts/src/wasm/mod.rs b/frame/contracts/src/wasm/mod.rs index 7c98ee7cf5b1a..3be1062ce969a 100644 --- a/frame/contracts/src/wasm/mod.rs +++ b/frame/contracts/src/wasm/mod.rs @@ -302,11 +302,18 @@ mod tests { allows_reentry: bool, } + #[derive(Debug, PartialEq, Eq)] + struct CallCodeEntry { + code_hash: H256, + data: Vec, + } + pub struct MockExt { storage: HashMap>, instantiates: Vec, terminations: Vec, calls: Vec, + code_calls: Vec, transfers: Vec, // (topics, data) events: Vec<(Vec, Vec)>, @@ -329,6 +336,7 @@ mod tests { instantiates: Default::default(), terminations: Default::default(), calls: Default::default(), + code_calls: Default::default(), transfers: Default::default(), events: Default::default(), runtime_calls: Default::default(), @@ -354,6 +362,14 @@ mod tests { self.calls.push(CallEntry { to, value, data, allows_reentry }); Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: call_return_data() }) } + fn delegate_call( + &mut self, + code_hash: CodeHash, + data: Vec, + ) -> Result { + self.code_calls.push(CallCodeEntry { code_hash, data }); + Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: call_return_data() }) + } fn instantiate( &mut self, gas_limit: Weight, @@ -579,6 +595,53 @@ mod tests { ); } + #[test] + #[cfg(feature = "unstable-interface")] + fn contract_delegate_call() { + const CODE: &str = r#" +(module + ;; seal_delegate_call( + ;; flags: u32, + ;; code_hash_ptr: u32, + ;; input_data_ptr: u32, + ;; input_data_len: u32, + ;; output_ptr: u32, + ;; output_len_ptr: u32 + ;;) -> u32 + (import "__unstable__" "seal_delegate_call" (func $seal_delegate_call (param i32 i32 i32 i32 i32 i32) (result i32))) + (import "env" "memory" (memory 1 1)) + (func (export "call") + (drop + (call $seal_delegate_call + (i32.const 0) ;; No flags are set + (i32.const 4) ;; Pointer to "callee" code_hash. + (i32.const 36) ;; Pointer to input data buffer address + (i32.const 4) ;; Length of input data buffer + (i32.const 4294967295) ;; u32 max value is the sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + ) + ) + ) + (func (export "deploy")) + + ;; Callee code_hash + (data (i32.const 4) + "\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11" + "\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11\11" + ) + + (data (i32.const 36) "\01\02\03\04") +) +"#; + let mut mock_ext = MockExt::default(); + assert_ok!(execute(CODE, vec![], &mut mock_ext)); + + assert_eq!( + &mock_ext.code_calls, + &[CallCodeEntry { code_hash: [0x11; 32].into(), data: vec![1, 2, 3, 4] }] + ); + } + #[test] fn contract_call_forward_input() { const CODE: &str = r#" diff --git a/frame/contracts/src/wasm/runtime.rs b/frame/contracts/src/wasm/runtime.rs index 194d5ffbaaf4f..55ab0dfa0adef 100644 --- a/frame/contracts/src/wasm/runtime.rs +++ b/frame/contracts/src/wasm/runtime.rs @@ -190,6 +190,9 @@ pub enum RuntimeCosts { Transfer, /// Weight of calling `seal_call` for the given input size. CallBase(u32), + /// Weight of calling `seal_delegate_call` for the given input size. + #[cfg(feature = "unstable-interface")] + DelegateCallBase(u32), /// Weight of the transfer performed during a call. CallSurchargeTransfer, /// Weight of output received through `seal_call` for the given size. @@ -275,6 +278,9 @@ impl RuntimeCosts { s.call.saturating_add(s.call_per_input_byte.saturating_mul(len.into())), CallSurchargeTransfer => s.call_transfer_surcharge, CallCopyOut(len) => s.call_per_output_byte.saturating_mul(len.into()), + #[cfg(feature = "unstable-interface")] + DelegateCallBase(len) => + s.delegate_call.saturating_add(s.call_per_input_byte.saturating_mul(len.into())), InstantiateBase { input_data_len, salt_len } => s .instantiate .saturating_add(s.instantiate_per_input_byte.saturating_mul(input_data_len.into())) @@ -327,7 +333,7 @@ where } bitflags! { - /// Flags used to change the behaviour of `seal_call`. + /// Flags used to change the behaviour of `seal_call` and `seal_delegate_call`. struct CallFlags: u32 { /// Forward the input of current function to the callee. /// @@ -363,10 +369,34 @@ bitflags! { /// Without this flag any reentrancy into the current contract that originates from /// the callee (or any of its callees) is denied. This includes the first callee: /// You cannot call into yourself with this flag set. + /// + /// # Note + /// + /// For `seal_delegate_call` should be always unset, otherwise + /// [`Error::InvalidCallFlags`] is returned. const ALLOW_REENTRY = 0b0000_1000; } } +/// The kind of call that should be performed. +enum CallType { + /// Execute another instantiated contract + Call { callee_ptr: u32, value_ptr: u32, gas: u64 }, + #[cfg(feature = "unstable-interface")] + /// Execute deployed code in the context (storage, account ID, value) of the caller contract + DelegateCall { code_hash_ptr: u32 }, +} + +impl CallType { + fn cost(&self, input_data_len: u32) -> RuntimeCosts { + match self { + CallType::Call { .. } => RuntimeCosts::CallBase(input_data_len), + #[cfg(feature = "unstable-interface")] + CallType::DelegateCall { .. } => RuntimeCosts::DelegateCallBase(input_data_len), + } + } +} + /// This is only appropriate when writing out data of constant size that does not depend on user /// input. In this case the costs for this copy was already charged as part of the token at /// the beginning of the API entry point. @@ -411,7 +441,7 @@ where // The trap was the result of the execution `return` host function. TrapReason::Return(ReturnData { flags, data }) => { let flags = ReturnFlags::from_bits(flags) - .ok_or_else(|| "used reserved bit in return flags")?; + .ok_or_else(|| Error::::InvalidCallFlags)?; Ok(ExecReturnValue { flags, data: Bytes(data) }) }, TrapReason::Termination => @@ -693,18 +723,13 @@ where fn call( &mut self, flags: CallFlags, - callee_ptr: u32, - gas: u64, - value_ptr: u32, + call_type: CallType, input_data_ptr: u32, input_data_len: u32, output_ptr: u32, output_len_ptr: u32, ) -> Result { - self.charge_gas(RuntimeCosts::CallBase(input_data_len))?; - let callee: <::T as frame_system::Config>::AccountId = - self.read_sandbox_memory_as(callee_ptr)?; - let value: BalanceOf<::T> = self.read_sandbox_memory_as(value_ptr)?; + self.charge_gas(call_type.cost(input_data_len))?; let input_data = if flags.contains(CallFlags::CLONE_INPUT) { self.input_data.as_ref().ok_or_else(|| Error::::InputForwarded)?.clone() } else if flags.contains(CallFlags::FORWARD_INPUT) { @@ -712,12 +737,32 @@ where } else { self.read_sandbox_memory(input_data_ptr, input_data_len)? }; - if value > 0u32.into() { - self.charge_gas(RuntimeCosts::CallSurchargeTransfer)?; - } - let ext = &mut self.ext; - let call_outcome = - ext.call(gas, callee, value, input_data, flags.contains(CallFlags::ALLOW_REENTRY)); + + let call_outcome = match call_type { + CallType::Call { callee_ptr, value_ptr, gas } => { + let callee: <::T as frame_system::Config>::AccountId = + self.read_sandbox_memory_as(callee_ptr)?; + let value: BalanceOf<::T> = self.read_sandbox_memory_as(value_ptr)?; + if value > 0u32.into() { + self.charge_gas(RuntimeCosts::CallSurchargeTransfer)?; + } + self.ext.call( + gas, + callee, + value, + input_data, + flags.contains(CallFlags::ALLOW_REENTRY), + ) + }, + #[cfg(feature = "unstable-interface")] + CallType::DelegateCall { code_hash_ptr } => { + if flags.contains(CallFlags::ALLOW_REENTRY) { + return Err(Error::::InvalidCallFlags.into()) + } + let code_hash = self.read_sandbox_memory_as(code_hash_ptr)?; + self.ext.delegate_call(code_hash, input_data) + }, + }; // `TAIL_CALL` only matters on an `OK` result. Otherwise the call stack comes to // a halt anyways without anymore code being executed. @@ -990,9 +1035,7 @@ define_env!(Env, , ) -> ReturnCode => { ctx.call( CallFlags::ALLOW_REENTRY, - callee_ptr, - gas, - value_ptr, + CallType::Call{callee_ptr, value_ptr, gas}, input_data_ptr, input_data_len, output_ptr, @@ -1041,10 +1084,51 @@ define_env!(Env, , output_len_ptr: u32 ) -> ReturnCode => { ctx.call( - CallFlags::from_bits(flags).ok_or_else(|| "used reserved bit in CallFlags")?, - callee_ptr, - gas, - value_ptr, + CallFlags::from_bits(flags).ok_or_else(|| Error::::InvalidCallFlags)?, + CallType::Call{callee_ptr, value_ptr, gas}, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + }, + + // Execute code in the context (storage, caller, value) of the current contract. + // + // Reentrancy protection is always disabled since the callee is allowed + // to modify the callers storage. This makes going through a reentrancy attack + // unnecessary for the callee when it wants to exploit the caller. + // + // # Parameters + // + // - flags: See [`CallFlags`] for a documentation of the supported flags. + // - code_hash: a pointer to the hash of the code to be called. + // - input_data_ptr: a pointer to a buffer to be used as input data to the callee. + // - input_data_len: length of the input data buffer. + // - output_ptr: a pointer where the output buffer is copied to. + // - output_len_ptr: in-out pointer to where the length of the buffer is read from + // and the actual length is written to. + // + // # Errors + // + // An error means that the call wasn't successful and no output buffer is returned unless + // stated otherwise. + // + // `ReturnCode::CalleeReverted`: Output buffer is returned. + // `ReturnCode::CalleeTrapped` + // `ReturnCode::CodeNotFound` + [__unstable__] seal_delegate_call( + ctx, + flags: u32, + code_hash_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32 + ) -> ReturnCode => { + ctx.call( + CallFlags::from_bits(flags).ok_or_else(|| Error::::InvalidCallFlags)?, + CallType::DelegateCall{code_hash_ptr}, input_data_ptr, input_data_len, output_ptr, diff --git a/frame/contracts/src/weights.rs b/frame/contracts/src/weights.rs index b58660b694ca9..ec6dcd2eee892 100644 --- a/frame/contracts/src/weights.rs +++ b/frame/contracts/src/weights.rs @@ -88,6 +88,7 @@ pub trait WeightInfo { fn seal_take_storage_per_kb(n: u32, ) -> Weight; fn seal_transfer(r: u32, ) -> Weight; fn seal_call(r: u32, ) -> Weight; + fn seal_delegate_call(r: u32, ) -> Weight; fn seal_call_per_transfer_input_output_kb(t: u32, i: u32, o: u32, ) -> Weight; fn seal_instantiate(r: u32, ) -> Weight; fn seal_instantiate_per_input_output_salt_kb(i: u32, o: u32, s: u32, ) -> Weight; @@ -618,6 +619,17 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().writes((100 as Weight).saturating_mul(r as Weight))) } // Storage: System Account (r:1 w:0) + // Storage: Contracts ContractInfoOf (r:1 w:1) + // Storage: Contracts CodeStorage (r:1 w:0) + // Storage: Timestamp Now (r:1 w:0) + fn seal_delegate_call(r: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 11_788_000 + .saturating_add((19_855_594_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(T::DbWeight::get().reads((99 as Weight).saturating_mul(r as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: System Account (r:1 w:0) // Storage: Contracts ContractInfoOf (r:101 w:101) // Storage: Contracts CodeStorage (r:2 w:0) // Storage: Timestamp Now (r:1 w:0) @@ -1486,6 +1498,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes((100 as Weight).saturating_mul(r as Weight))) } // Storage: System Account (r:1 w:0) + // Storage: Contracts ContractInfoOf (r:1 w:1) + // Storage: Contracts CodeStorage (r:1 w:0) + // Storage: Timestamp Now (r:1 w:0) + fn seal_delegate_call(r: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 11_788_000 + .saturating_add((19_855_594_000 as Weight).saturating_mul(r as Weight)) + .saturating_add(RocksDbWeight::get().reads((99 as Weight).saturating_mul(r as Weight))) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: System Account (r:1 w:0) // Storage: Contracts ContractInfoOf (r:101 w:101) // Storage: Contracts CodeStorage (r:2 w:0) // Storage: Timestamp Now (r:1 w:0)