From 6f3d7c00f34ae4c0951bb41b6b4aa388a9afbc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20M=C3=BCller?= Date: Tue, 9 Nov 2021 08:25:13 +0100 Subject: [PATCH] Update `seal_call` to `__unstable__` version (#960) * Update `seal_call` to `__unstable__` version * Add `Debug` derive * Make `cargo-spellcheck` happy * Make `into_u32` take `self` * Update release notes * Fix crate docs link * Use type-system encoded version of empty `ExecutionInput` * Add `const` for getters and setters * Shorten if-branches logic * Update release notes * Remove dead code * Improve release notes --- .config/cargo_spellcheck.dic | 1 + RELEASES.md | 29 ++--- crates/env/src/backend.rs | 106 ++++++++++++++++++ crates/env/src/call/call_builder.rs | 89 +++++++++++++++ crates/env/src/call/execution_input.rs | 6 +- crates/env/src/call/selector.rs | 4 +- .../engine/experimental_off_chain/impls.rs | 1 + crates/env/src/engine/off_chain/impls.rs | 1 + crates/env/src/engine/on_chain/ext.rs | 27 +++-- crates/env/src/engine/on_chain/impls.rs | 9 +- crates/env/src/lib.rs | 5 +- crates/env/src/tests.rs | 39 +++++++ 12 files changed, 284 insertions(+), 33 deletions(-) diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index 5eef28f2adc..ba8bea6f4f5 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -53,6 +53,7 @@ multi postfix prefilled recurse +reentrancy refcount scalability scalable diff --git a/RELEASES.md b/RELEASES.md index 28eabe67826..f0367b135ac 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,32 +3,36 @@ This is the 7th release candidate for ink! 3.0. ## Added - - The ink! codegen now heavily relies on static type information based on traits defined in `ink_lang`. - Some of those traits and their carried information can be used for static reflection of ink! smart contracts. Those types and traits reside in the new `ink_lang::reflect` module and is publicly usable by ink! smart contract authors. ## Changed - -- ink! Contract via `#[ink::contract]`: - - ink! smart contracts now always generated two contract types. Given `MyContract`: - +- Upgraded to the unstable `seal_call` API ‒ [#960](https://github.com/paritytech/ink/pull/960). + - This API now enables control over the behavior of cross-contract calls, e.g. to forward/clone input, + enable tail calls and control reentrancy. + The crate documentation contains more details on the [`CallFlags`](https://paritytech.github.io/ink/ink_env/struct.CallFlags.html). + - **Note:** The default behavior of cross-contract calls now disallows reentering the calling contract. + - **Note:** In order to support this you currently have to enable the `unstable-interface` of + the `contracts` pallet, [like here](https://github.com/paritytech/substrate-contracts-node/blob/main/runtime/Cargo.toml#L104-L108). +- ink! contract definitions via `#[ink::contract]`: + - ink! smart contracts now generate two contract types. Given `MyContract`: - `MyContract` will still be the storage struct. However, it can now additionally be used as static dependency in other smart contracts. Static dependencies can be envisioned as being directly embedded into a smart contract. - `MyContractRef` is pretty much the same of what we had gotten with the old `ink-as-dependency`. It is a typed thin-wrapper around an `AccountId` that is mirroring the ink! smart contract's API and implemented traits. -- ink! Trait Definitions via `#[ink::trait_definition]`: +- ink! trait definitions via `#[ink::trait_definition]`: - ink! trait definitions no longer can define trait constructors. - ink! trait implementations now inherit `selector` and `payable` properties for trait messages. - Now explicitly setting `selector` or `payable` property for an implemented ink! trait method will only act as a guard that the set property is in fact the same as defined by the ink! trait definition. -- Improve quite a few ink! specific compile errors: - - For example when using ink! messages and constructors that have inputs or outputs that cannot - be encoded or decoded using the SCALE codec. +- Improved some ink! specific compile errors: + - For example, when using ink! messages and constructors that have inputs or + outputs that cannot be encoded or decoded using the SCALE codec. - Simplified selector computation for ink! trait methods. - Now selectors are encoded as `blake2b({namespace}::{trait_identifier}::{message_identifier})[0..4]`. If no `namespace` is set for the ink! trait definition then the formula is @@ -37,7 +41,6 @@ This is the 7th release candidate for ink! 3.0. definition and ink! trait message respectively. ## Fixed - - Contracts that are compiled as root (the default) now properly revert the transaction if a message returned `Result::Err`. - This does not apply to ink! smart contracts that are used as dependencies. Therefore it is still possible to match against a result return type for a called dependency. @@ -72,11 +75,11 @@ scale = { package = "parity-scale-codec", version = "2", default-features = fals ### New metadata format -There are breaking changes to the metadata format in this release. +There are breaking changes to the metadata format in this release. - Removes top level `metadataVersion` field from the contract metadata (https://github.com/paritytech/cargo-contract/pull/342/files). -- Introduces new top level versioned metadata [enum](https://github.com/paritytech/ink/blob/master/crates/metadata/src/lib.rs#L68). -- Upgrades to `scale-info` version `1.0` (https://github.com/paritytech/ink/pull/845). +- Introduces new top level versioned metadata [enum](https://github.com/paritytech/ink/blob/master/crates/metadata/src/lib.rs#L68). +- Upgrades to `scale-info` version `1.0` (https://github.com/paritytech/ink/pull/845). - The previous supported version was `0.6`, so check release notes for all changes since then: https://github.com/paritytech/ink/pull/845 - One of the main changes to be aware of is the change to 0 based type lookup ids: https://github.com/paritytech/scale-info/pull/90 diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 6c69c0bc0f6..743ecdfc356 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -55,6 +55,112 @@ impl ReturnFlags { } } +/// The flags used to change the behavior of a contract call. +#[derive(Copy, Clone, Debug, Default)] +pub struct CallFlags { + forward_input: bool, + clone_input: bool, + tail_call: bool, + allow_reentry: bool, +} + +impl CallFlags { + /// Forwards the input for the current function to the callee. + /// + /// # Note + /// + /// A forwarding call will consume the current contracts input. Any attempt to + /// access the input after this call returns (e.g. by trying another forwarding call) + /// will lead to a contract revert. + /// Consider using [`Self::set_clone_input`] in order to preserve the input. + pub const fn set_forward_input(mut self, forward_input: bool) -> Self { + self.forward_input = forward_input; + self + } + + /// Identical to [`Self::set_forward_input`] but without consuming the input. + /// + /// This adds some additional weight costs to the call. + /// + /// # Note + /// + /// This implies [`Self::set_forward_input`] and takes precedence when both are set. + pub const fn set_clone_input(mut self, clone_input: bool) -> Self { + self.clone_input = clone_input; + self + } + + /// Do not return from the call but rather return the result of the callee to the + /// callers caller. + /// + /// # Note + /// + /// This makes the current contract completely transparent to its caller by replacing + /// this contracts potential output with the callee ones. Any code after the contract + /// calls has been invoked can be safely considered unreachable. + pub const fn set_tail_call(mut self, tail_call: bool) -> Self { + self.tail_call = tail_call; + self + } + + /// Allow the callee to reenter into the current contract. + /// + /// 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. + pub const fn set_allow_reentry(mut self, allow_reentry: bool) -> Self { + self.allow_reentry = allow_reentry; + self + } + + /// Returns the underlying `u32` representation of the call flags. + /// + /// This value is used to forward the call flag information to the + /// `contracts` pallet. + pub(crate) const fn into_u32(self) -> u32 { + self.forward_input as u32 + | ((self.clone_input as u32) << 1) + | ((self.tail_call as u32) << 2) + | ((self.allow_reentry as u32) << 3) + } + + /// Returns `true` if input forwarding is set. + /// + /// # Note + /// + /// See [`Self::set_forward_input`] for more information. + pub const fn forward_input(&self) -> bool { + self.forward_input + } + + /// Returns `true` if input cloning is set. + /// + /// # Note + /// + /// See [`Self::set_clone_input`] for more information. + pub const fn clone_input(&self) -> bool { + self.clone_input + } + + /// Returns `true` if the tail call property is set. + /// + /// # Note + /// + /// See [`Self::set_tail_call`] for more information. + pub const fn tail_call(&self) -> bool { + self.tail_call + } + + /// Returns `true` if call reentry is allowed. + /// + /// # Note + /// + /// See [`Self::set_allow_reentry`] for more information. + pub const fn allow_reentry(&self) -> bool { + self.allow_reentry + } +} + /// Environmental contract functionality that does not require `Environment`. pub trait EnvBackend { /// Writes the value to the contract storage under the given key. diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs index c4e472f6ac9..b449365bfc1 100644 --- a/crates/env/src/call/call_builder.rs +++ b/crates/env/src/call/call_builder.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::{ + backend::CallFlags, call::{ utils::{ EmptyArgumentList, @@ -36,6 +37,8 @@ where { /// The account ID of the to-be-called smart contract. callee: E::AccountId, + /// The flags used to change the behavior of a contract call. + call_flags: CallFlags, /// The maximum gas costs allowed for the call. gas_limit: u64, /// The transferred value for the call. @@ -56,6 +59,12 @@ where &self.callee } + /// Returns the call flags. + #[inline] + pub(crate) fn call_flags(&self) -> &CallFlags { + &self.call_flags + } + /// Returns the chosen gas limit for the called contract execution. #[inline] pub(crate) fn gas_limit(&self) -> u64 { @@ -202,6 +211,7 @@ where CallBuilder { env: Default::default(), callee: Default::default(), + call_flags: Default::default(), gas_limit: Default::default(), transferred_value: Default::default(), exec_input: Default::default(), @@ -217,6 +227,7 @@ where env: PhantomData E>, /// The current parameters that have been built up so far. callee: Callee, + call_flags: CallFlags, gas_limit: GasLimit, transferred_value: TransferredValue, exec_input: Args, @@ -238,6 +249,30 @@ where CallBuilder { env: Default::default(), callee: Set(callee), + call_flags: self.call_flags, + gas_limit: self.gas_limit, + transferred_value: self.transferred_value, + exec_input: self.exec_input, + return_type: self.return_type, + } + } +} + +impl + CallBuilder +where + E: Environment, +{ + /// The flags used to change the behavior of the contract call. + #[inline] + pub fn call_flags( + self, + call_flags: CallFlags, + ) -> CallBuilder { + CallBuilder { + env: Default::default(), + callee: self.callee, + call_flags, gas_limit: self.gas_limit, transferred_value: self.transferred_value, exec_input: self.exec_input, @@ -260,6 +295,7 @@ where CallBuilder { env: Default::default(), callee: self.callee, + call_flags: self.call_flags, gas_limit: Set(gas_limit), transferred_value: self.transferred_value, exec_input: self.exec_input, @@ -282,6 +318,7 @@ where CallBuilder { env: Default::default(), callee: self.callee, + call_flags: self.call_flags, gas_limit: self.gas_limit, transferred_value: Set(transferred_value), exec_input: self.exec_input, @@ -324,6 +361,7 @@ where CallBuilder { env: Default::default(), callee: self.callee, + call_flags: self.call_flags, gas_limit: self.gas_limit, transferred_value: self.transferred_value, exec_input: self.exec_input, @@ -359,6 +397,7 @@ where CallBuilder { env: Default::default(), callee: self.callee, + call_flags: self.call_flags, gas_limit: self.gas_limit, transferred_value: self.transferred_value, exec_input: Set(exec_input), @@ -385,6 +424,7 @@ where pub fn params(self) -> CallParams { CallParams { callee: self.callee.value(), + call_flags: self.call_flags, gas_limit: self.gas_limit.unwrap_or_else(|| 0), transferred_value: self .transferred_value @@ -395,6 +435,35 @@ where } } +impl + CallBuilder< + E, + Set, + GasLimit, + TransferredValue, + Unset>, + Unset, + > +where + E: Environment, + GasLimit: Unwrap, + TransferredValue: Unwrap, +{ + /// Finalizes the call builder to call a function. + pub fn params(self) -> CallParams { + CallParams { + callee: self.callee.value(), + call_flags: self.call_flags, + gas_limit: self.gas_limit.unwrap_or_else(|| 0), + transferred_value: self + .transferred_value + .unwrap_or_else(|| E::Balance::from(0u32)), + _return_type: Default::default(), + exec_input: Default::default(), + } + } +} + impl CallBuilder< E, @@ -416,6 +485,26 @@ where } } +impl + CallBuilder< + E, + Set, + GasLimit, + TransferredValue, + Unset>, + Unset>, + > +where + E: Environment, + GasLimit: Unwrap, + TransferredValue: Unwrap, +{ + /// Invokes the cross-chain function call. + pub fn fire(self) -> Result<(), Error> { + self.params().invoke() + } +} + impl CallBuilder< E, diff --git a/crates/env/src/call/execution_input.rs b/crates/env/src/call/execution_input.rs index 148dda60b95..149e691150c 100644 --- a/crates/env/src/call/execution_input.rs +++ b/crates/env/src/call/execution_input.rs @@ -15,7 +15,7 @@ use crate::call::Selector; /// The input data for a smart contract execution. -#[derive(Debug)] +#[derive(Default, Debug)] pub struct ExecutionInput { /// The selector for the smart contract execution. selector: Selector, @@ -70,7 +70,7 @@ impl<'a, Head, Rest> ExecutionInput, Rest>> { /// arguments. The potentially heap allocating encoding is done right at the end /// where we can leverage the static environmental buffer instead of allocating /// heap memory. -#[derive(Debug)] +#[derive(Default, Debug)] pub struct ArgumentList { /// The first argument of the argument list. head: Head, @@ -99,7 +99,7 @@ impl Argument { } /// The end of an argument list. -#[derive(Debug)] +#[derive(Default, Debug)] pub struct ArgumentListEnd; /// An empty argument list. diff --git a/crates/env/src/call/selector.rs b/crates/env/src/call/selector.rs index fa6e6db56d0..bfc771adf49 100644 --- a/crates/env/src/call/selector.rs +++ b/crates/env/src/call/selector.rs @@ -15,7 +15,9 @@ use derive_more::From; /// The function selector. -#[derive(Debug, Copy, Clone, PartialEq, Eq, From, scale::Decode, scale::Encode)] +#[derive( + Default, Debug, Copy, Clone, PartialEq, Eq, From, scale::Decode, scale::Encode, +)] pub struct Selector { /// The 4 underlying bytes. bytes: [u8; 4], diff --git a/crates/env/src/engine/experimental_off_chain/impls.rs b/crates/env/src/engine/experimental_off_chain/impls.rs index f2f7ceff243..98f754a3523 100644 --- a/crates/env/src/engine/experimental_off_chain/impls.rs +++ b/crates/env/src/engine/experimental_off_chain/impls.rs @@ -425,6 +425,7 @@ impl TypedEnvBackend for EnvInstance { { let _gas_limit = params.gas_limit(); let _callee = params.callee(); + let _call_flags = params.call_flags().into_u32(); let _transferred_value = params.transferred_value(); let _input = params.exec_input(); unimplemented!("off-chain environment does not support contract invocation") diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 45d18fcf044..2b25b45ddf5 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -491,6 +491,7 @@ impl TypedEnvBackend for EnvInstance { { let _gas_limit = params.gas_limit(); let _callee = params.callee(); + let _call_flags = params.call_flags().into_u32(); let _transferred_value = params.transferred_value(); let _input = params.exec_input(); unimplemented!("off-chain environment does not support contract invocation") diff --git a/crates/env/src/engine/on_chain/ext.rs b/crates/env/src/engine/on_chain/ext.rs index 92b2be4e0b3..badbb39be82 100644 --- a/crates/env/src/engine/on_chain/ext.rs +++ b/crates/env/src/engine/on_chain/ext.rs @@ -219,18 +219,6 @@ mod sys { salt_len: u32, ) -> ReturnCode; - pub fn seal_call( - callee_ptr: Ptr32<[u8]>, - callee_len: u32, - gas: u64, - transferred_value_ptr: Ptr32<[u8]>, - transferred_value_len: u32, - input_ptr: Ptr32<[u8]>, - input_len: u32, - output_ptr: Ptr32Mut<[u8]>, - output_len_ptr: Ptr32Mut, - ) -> ReturnCode; - pub fn seal_transfer( account_id_ptr: Ptr32<[u8]>, account_id_len: u32, @@ -350,6 +338,17 @@ mod sys { #[link(wasm_import_module = "__unstable__")] extern "C" { + pub fn seal_call( + flags: u32, + callee_ptr: Ptr32<[u8]>, + gas: u64, + transferred_value_ptr: Ptr32<[u8]>, + input_data_ptr: Ptr32<[u8]>, + input_data_len: u32, + output_ptr: Ptr32Mut<[u8]>, + output_len_ptr: Ptr32Mut, + ) -> ReturnCode; + pub fn seal_rent_params( output_ptr: Ptr32Mut<[u8]>, output_len_ptr: Ptr32Mut, @@ -413,6 +412,7 @@ pub fn instantiate( } pub fn call( + flags: u32, callee: &[u8], gas_limit: u64, value: &[u8], @@ -423,11 +423,10 @@ pub fn call( let ret_code = { unsafe { sys::seal_call( + flags, Ptr32::from_slice(callee), - callee.len() as u32, gas_limit, Ptr32::from_slice(value), - value.len() as u32, Ptr32::from_slice(input), input.len() as u32, Ptr32Mut::from_slice(output), diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index fa8f4804d39..31a9cd5230e 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -229,9 +229,16 @@ impl EnvInstance { let gas_limit = params.gas_limit(); let enc_callee = scope.take_encoded(params.callee()); let enc_transferred_value = scope.take_encoded(params.transferred_value()); - let enc_input = scope.take_encoded(params.exec_input()); + let call_flags = params.call_flags(); + let enc_input = if !call_flags.forward_input() && !call_flags.clone_input() { + scope.take_encoded(params.exec_input()) + } else { + &mut [] + }; let output = &mut scope.take_rest(); + let flags = params.call_flags().into_u32(); let call_result = ext::call( + flags, enc_callee, gas_limit, enc_transferred_value, diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index 60cbd60335c..609cd35c622 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -86,7 +86,10 @@ use self::backend::{ }; pub use self::{ api::*, - backend::ReturnFlags, + backend::{ + CallFlags, + ReturnFlags, + }, error::{ Error, Result, diff --git a/crates/env/src/tests.rs b/crates/env/src/tests.rs index 8f954af2db3..fd2aa51afa1 100644 --- a/crates/env/src/tests.rs +++ b/crates/env/src/tests.rs @@ -62,3 +62,42 @@ fn test_hash_blake2_128() { [180, 158, 48, 21, 171, 163, 217, 175, 145, 160, 25, 159, 213, 142, 103, 242] ); } + +#[test] +fn test_call_flags() { + let flags = crate::CallFlags::default(); + + // enable each flag one after the other + let flags = flags.set_forward_input(true); + assert!(flags.forward_input()); + assert_eq!(flags.into_u32(), 0b0000_0001); + + let flags = flags.set_clone_input(true); + assert!(flags.clone_input()); + assert_eq!(flags.into_u32(), 0b0000_0011); + + let flags = flags.set_tail_call(true); + assert!(flags.tail_call()); + assert_eq!(flags.into_u32(), 0b0000_0111); + + let flags = flags.set_allow_reentry(true); + assert!(flags.allow_reentry()); + assert_eq!(flags.into_u32(), 0b0000_1111); + + // disable each flag one after the other + let flags = flags.set_allow_reentry(false); + assert!(!flags.allow_reentry()); + assert_eq!(flags.into_u32(), 0b0000_0111); + + let flags = flags.set_tail_call(false); + assert!(!flags.tail_call()); + assert_eq!(flags.into_u32(), 0b0000_0011); + + let flags = flags.set_clone_input(false); + assert!(!flags.clone_input()); + assert_eq!(flags.into_u32(), 0b0000_0001); + + let flags = flags.set_forward_input(false); + assert!(!flags.forward_input()); + assert_eq!(flags.into_u32(), 0b0000_0000); +}