Unverified Commit 870095c8 authored by Hero Bird's avatar Hero Bird Committed by GitHub

Avoid heap memory allocations for cross-contract calls and instantiations (#439)

* [core] add ExecutionInput abstraction

* [core, lang] adjust env and lang abstractions for call builder changes

* [lang/macro] adjust cross-call codegen to use new call infrastructure

* [core] add #[inline] to many call infrastructure functions

* [core, lang] apply rustfmt

* [core, lang, lang/macro] avoid heap mem alloc for instantiations as well

* [core] add inline annotations to some instantation infrastructure

* [core] fix clippy warning

* [core] apply rustfmt

* [examples] fix multisig_plain example contract

* [examples] apply rustfmt
parent 1e312d1e
Pipeline #96628 canceled with stages
in 5 minutes and 53 seconds
......@@ -274,12 +274,13 @@ where
/// - If arguments passed to the called contract message are invalid.
/// - If the called contract execution has trapped.
/// - If the called contract ran out of gas upon execution.
pub fn invoke_contract<T>(params: &CallParams<T, ()>) -> Result<()>
pub fn invoke_contract<T, Args>(params: &CallParams<T, Args, ()>) -> Result<()>
where
T: EnvTypes,
Args: scale::Encode,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
TypedEnv::invoke_contract::<T>(instance, params)
TypedEnv::invoke_contract::<T, Args>(instance, params)
})
}
......@@ -298,13 +299,14 @@ where
/// - If the called contract execution has trapped.
/// - If the called contract ran out of gas upon execution.
/// - If the returned value failed to decode properly.
pub fn eval_contract<T, R>(params: &CallParams<T, ReturnType<R>>) -> Result<R>
pub fn eval_contract<T, Args, R>(params: &CallParams<T, Args, ReturnType<R>>) -> Result<R>
where
T: EnvTypes,
Args: scale::Encode,
R: scale::Decode,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
TypedEnv::eval_contract::<T, R>(instance, params)
TypedEnv::eval_contract::<T, Args, R>(instance, params)
})
}
......@@ -323,14 +325,15 @@ where
/// - If the instantiation process runs out of gas.
/// - If given insufficient endowment.
/// - If the returned account ID failed to decode properly.
pub fn instantiate_contract<T, C>(
params: &InstantiateParams<T, C>,
pub fn instantiate_contract<T, Args, C>(
params: &InstantiateParams<T, Args, C>,
) -> Result<T::AccountId>
where
T: EnvTypes,
Args: scale::Encode,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
TypedEnv::instantiate_contract::<T, C>(instance, params)
TypedEnv::instantiate_contract::<T, Args, C>(instance, params)
})
}
......
......@@ -207,21 +207,26 @@ pub trait TypedEnv: Env {
/// # Note
///
/// For more details visit: [`ink_core::env::invoke_contract`]
fn invoke_contract<T>(&mut self, call_data: &CallParams<T, ()>) -> Result<()>
fn invoke_contract<T, Args>(
&mut self,
call_data: &CallParams<T, Args, ()>,
) -> Result<()>
where
T: EnvTypes;
T: EnvTypes,
Args: scale::Encode;
/// Evaluates a contract message and returns its result.
///
/// # Note
///
/// For more details visit: [`ink_core::env::eval_contract`]
fn eval_contract<T, R>(
fn eval_contract<T, Args, R>(
&mut self,
call_data: &CallParams<T, ReturnType<R>>,
call_data: &CallParams<T, Args, ReturnType<R>>,
) -> Result<R>
where
T: EnvTypes,
Args: scale::Encode,
R: scale::Decode;
/// Instantiates another contract.
......@@ -229,12 +234,13 @@ pub trait TypedEnv: Env {
/// # Note
///
/// For more details visit: [`ink_core::env::instantiate_contract`]
fn instantiate_contract<T, C>(
fn instantiate_contract<T, Args, C>(
&mut self,
params: &InstantiateParams<T, C>,
params: &InstantiateParams<T, Args, C>,
) -> Result<T::AccountId>
where
T: EnvTypes;
T: EnvTypes,
Args: scale::Encode;
/// Restores a smart contract tombstone.
///
......
......@@ -17,7 +17,11 @@ use core::marker::PhantomData;
use crate::env::{
call::{
state,
CallData,
ArgsList,
Argument,
ArgumentList,
EmptyArgumentList,
ExecutionInput,
Selector,
},
EnvTypes,
......@@ -31,7 +35,7 @@ use crate::env::{
pub struct ReturnType<T>(PhantomData<fn() -> T>);
/// The final parameters to the cross-contract call.
pub struct CallParams<E, R>
pub struct CallParams<E, Args, R>
where
E: EnvTypes,
{
......@@ -44,65 +48,71 @@ where
/// The expected return type.
return_type: PhantomData<ReturnType<R>>,
/// The already encoded call data respecting the ABI.
call_data: CallData,
call_data: ExecutionInput<Args>,
}
/// Builds up a call.
pub struct CallBuilder<E, R, Seal>
pub struct CallBuilder<E, Args, R, Seal>
where
E: EnvTypes,
{
/// The current parameters that have been built up so far.
params: CallParams<E, R>,
params: CallParams<E, Args, R>,
/// Seal state.
seal: PhantomData<Seal>,
}
impl<E, R> CallParams<E, R>
impl<E, Args, R> CallParams<E, Args, R>
where
E: EnvTypes,
{
/// The code hash of the contract.
#[inline]
pub fn callee(&self) -> &E::AccountId {
&self.callee
}
/// The gas limit for the contract instantiation.
#[inline]
pub fn gas_limit(&self) -> u64 {
self.gas_limit
}
/// The transferred value for the called contract.
#[inline]
pub fn transferred_value(&self) -> &E::Balance {
&self.transferred_value
}
/// The raw encoded input data.
pub fn input_data(&self) -> &CallData {
#[inline]
pub fn input_data(&self) -> &ExecutionInput<Args> {
&self.call_data
}
}
impl<E, R> CallParams<E, R>
impl<E, R> CallParams<E, EmptyArgumentList, R>
where
E: EnvTypes,
E::Balance: Default,
{
/// Creates the default set of parameters for the cross-contract call.
#[inline]
fn new(callee: E::AccountId, selector: Selector) -> Self {
Self {
callee,
gas_limit: 0,
transferred_value: E::Balance::default(),
return_type: PhantomData,
call_data: CallData::new(selector),
call_data: ExecutionInput::new(selector),
}
}
/// Returns a builder for a cross-contract call that might return data.
#[inline]
pub fn eval(
callee: E::AccountId,
selector: Selector,
) -> CallBuilder<E, ReturnType<R>, state::Unsealed> {
) -> CallBuilder<E, EmptyArgumentList, ReturnType<R>, state::Unsealed> {
CallBuilder {
params: CallParams::new(callee, selector),
seal: Default::default(),
......@@ -112,10 +122,11 @@ where
/// Returns a builder for a cross-contract call that cannot return data.
///
/// Prefer this over [`CallParams::eval`] if possible since it is the more efficient operation.
#[inline]
pub fn invoke(
callee: E::AccountId,
selector: Selector,
) -> CallBuilder<E, (), state::Unsealed> {
) -> CallBuilder<E, EmptyArgumentList, (), state::Unsealed> {
CallBuilder {
params: CallParams::new(callee, selector),
seal: Default::default(),
......@@ -123,38 +134,85 @@ where
}
}
impl<E, R, Seal> CallBuilder<E, R, Seal>
impl<E, Args, R, Seal> CallBuilder<E, Args, R, Seal>
where
E: EnvTypes,
{
/// Sets the maximumly allowed gas costs for the call.
#[inline]
pub fn gas_limit(mut self, gas_limit: u64) -> Self {
self.params.gas_limit = gas_limit;
self
}
/// Sets the value transferred upon the execution of the call.
#[inline]
pub fn transferred_value(mut self, value: E::Balance) -> Self {
self.params.transferred_value = value;
self
}
}
impl<E, R> CallBuilder<E, R, state::Unsealed>
impl<E, R> CallBuilder<E, EmptyArgumentList, R, state::Unsealed>
where
E: EnvTypes,
{
/// Pushes an argument to the inputs of the call.
pub fn push_arg<A>(mut self, arg: &A) -> Self
#[inline]
pub fn push_arg<A>(
self,
arg: A,
) -> CallBuilder<E, ArgumentList<Argument<A>, EmptyArgumentList>, R, state::Unsealed>
where
A: scale::Encode,
{
self.params.call_data.push_arg(arg);
self
CallBuilder {
params: CallParams {
call_data: self.params.call_data.push_arg(arg),
callee: self.params.callee,
gas_limit: self.params.gas_limit,
transferred_value: self.params.transferred_value,
return_type: self.params.return_type,
},
seal: Default::default(),
}
}
}
impl<'a, E, ArgsHead, ArgsRest, R>
CallBuilder<E, ArgsList<ArgsHead, ArgsRest>, R, state::Unsealed>
where
E: EnvTypes,
{
/// Pushes an argument to the inputs of the call.
#[inline]
pub fn push_arg<A>(
self,
arg: A,
) -> CallBuilder<E, ArgsList<A, ArgsList<ArgsHead, ArgsRest>>, R, state::Unsealed>
where
A: scale::Encode,
{
CallBuilder {
params: CallParams {
call_data: self.params.call_data.push_arg(arg),
callee: self.params.callee,
gas_limit: self.params.gas_limit,
transferred_value: self.params.transferred_value,
return_type: self.params.return_type,
},
seal: Default::default(),
}
}
}
impl<E, Args, R> CallBuilder<E, Args, R, state::Unsealed>
where
E: EnvTypes,
{
/// Seals the call builder to prevent further arguments.
pub fn seal(self) -> CallBuilder<E, R, state::Sealed> {
#[inline]
pub fn seal(self) -> CallBuilder<E, Args, R, state::Sealed> {
CallBuilder {
params: self.params,
seal: Default::default(),
......@@ -162,13 +220,15 @@ where
}
}
impl<E, R, Seal> CallBuilder<E, ReturnType<R>, Seal>
impl<E, Args, R, Seal> CallBuilder<E, Args, ReturnType<R>, Seal>
where
E: EnvTypes,
Args: scale::Encode,
R: scale::Decode,
{
/// Fires the call to the remote smart contract.
/// Returns the returned data back to the caller.
#[inline]
pub fn fire(self) -> Result<R>
where
R: scale::Decode,
......@@ -177,11 +237,13 @@ where
}
}
impl<E, Seal> CallBuilder<E, (), Seal>
impl<E, Args, Seal> CallBuilder<E, Args, (), Seal>
where
E: EnvTypes,
Args: scale::Encode,
{
/// Fires the cross-call to the smart contract.
#[inline]
pub fn fire(self) -> Result<()> {
crate::env::invoke_contract(&self.params)
}
......
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::env::call::Selector;
/// The input data for a smart contract execution.
pub struct ExecutionInput<Args> {
/// The selector for the smart contract execution.
selector: Selector,
/// The arguments of the smart contract execution.
args: Args,
}
impl ExecutionInput<EmptyArgumentList> {
/// Creates a new execution input with the given selector.
#[inline]
pub fn new(selector: Selector) -> Self {
Self {
selector,
args: ArgumentList::empty(),
}
}
/// Pushes an argument to the execution input.
#[inline]
pub fn push_arg<T>(
self,
arg: T,
) -> ExecutionInput<ArgumentList<Argument<T>, EmptyArgumentList>>
where
T: scale::Encode,
{
ExecutionInput {
selector: self.selector,
args: self.args.push_arg(arg),
}
}
}
impl<'a, Head, Rest> ExecutionInput<ArgumentList<Argument<Head>, Rest>> {
/// Pushes an argument to the execution input.
#[inline]
pub fn push_arg<T>(self, arg: T) -> ExecutionInput<ArgsList<T, ArgsList<Head, Rest>>>
where
T: scale::Encode,
{
ExecutionInput {
selector: self.selector,
args: self.args.push_arg(arg),
}
}
}
/// An argument list.
///
/// This type is constructed mainly at compile type via type constructions
/// to avoid having to allocate heap memory while constructing the encoded
/// 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.
pub struct ArgumentList<Head, Rest> {
/// The first argument of the argument list.
head: Head,
/// All the rest arguments.
rest: Rest,
}
/// Minor simplification of an argument list with a head and rest.
pub type ArgsList<Head, Rest> = ArgumentList<Argument<Head>, Rest>;
/// A single argument and its reference to a known value.
pub struct Argument<T> {
/// The reference to the known value.
///
/// Used for the encoding at the end of the construction.
arg: T,
}
impl<T> Argument<T> {
/// Creates a new argument.
#[inline]
fn new(arg: T) -> Self {
Self { arg }
}
}
/// The end of an argument list.
pub struct ArgumentListEnd;
/// An empty argument list.
pub type EmptyArgumentList = ArgumentList<ArgumentListEnd, ArgumentListEnd>;
impl EmptyArgumentList {
/// Creates a new empty argument list.
#[inline]
pub fn empty() -> EmptyArgumentList {
ArgumentList {
head: ArgumentListEnd,
rest: ArgumentListEnd,
}
}
/// Pushes the first argument to the empty argument list.
#[inline]
pub fn push_arg<T>(self, arg: T) -> ArgumentList<Argument<T>, Self>
where
T: scale::Encode,
{
ArgumentList {
head: Argument::new(arg),
rest: self,
}
}
}
impl<Head, Rest> ArgumentList<Argument<Head>, Rest> {
/// Pushes another argument to the argument list.
#[inline]
pub fn push_arg<T>(self, arg: T) -> ArgumentList<Argument<T>, Self>
where
T: scale::Encode,
{
ArgumentList {
head: Argument::new(arg),
rest: self,
}
}
}
impl<T> scale::Encode for Argument<T>
where
T: scale::Encode,
{
#[inline]
fn size_hint(&self) -> usize {
<T as scale::Encode>::size_hint(&self.arg)
}
#[inline]
fn encode_to<O: scale::Output>(&self, output: &mut O) {
<T as scale::Encode>::encode_to(&self.arg, output)
}
}
impl scale::Encode for EmptyArgumentList {
#[inline]
fn size_hint(&self) -> usize {
0
}
#[inline]
fn encode_to<O: scale::Output>(&self, _output: &mut O) {}
}
impl<'a, Head, Rest> scale::Encode for ArgumentList<Argument<Head>, Rest>
where
Head: scale::Encode,
Rest: scale::Encode,
{
#[inline]
fn size_hint(&self) -> usize {
scale::Encode::size_hint(&self.head) + scale::Encode::size_hint(&self.rest)
}
#[inline]
fn encode_to<O: scale::Output>(&self, output: &mut O) {
// We reverse the order of encoding because we build up the list of
// arguments in reverse order, too. This way we encode the arguments
// in the same order in which they have been pushed to the argument list
// while the argument list itself organizes them in reverse order.
scale::Encode::encode_to(&self.rest, output);
scale::Encode::encode_to(&self.head, output);
}
}
impl<Args> scale::Encode for ExecutionInput<Args>
where
Args: scale::Encode,
{
#[inline]
fn size_hint(&self) -> usize {
scale::Encode::size_hint(&self.selector) + scale::Encode::size_hint(&self.args)
}
#[inline]
fn encode_to<O: scale::Output>(&self, output: &mut O) {
scale::Encode::encode_to(&self.selector, output);
scale::Encode::encode_to(&self.args, output);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_exec_input_works() {
let selector = Selector::new([0x01, 0x02, 0x03, 0x04]);
let exec_input = ExecutionInput::new(selector);
let encoded = scale::Encode::encode(&exec_input);
assert!(!encoded.is_empty());
let decoded = <Selector as scale::Decode>::decode(&mut &encoded[..]).unwrap();
assert_eq!(decoded, selector);
}
#[test]
fn empty_args_works() {
let empty_list = ArgumentList::empty();
let encoded = scale::Encode::encode(&empty_list);
assert_eq!(encoded, Vec::new());
}
#[test]
fn single_argument_works() {
let empty_list = ArgumentList::empty().push_arg(&1i32);
let encoded = scale::Encode::encode(&empty_list);
assert!(!encoded.is_empty());
let decoded = <i32 as scale::Decode>::decode(&mut &encoded[..]).unwrap();
assert_eq!(decoded, 1i32);
}
#[test]
fn multiple_arguments_works() {
let empty_list = ArgumentList::empty()
.push_arg(&42i32)
.push_arg(&true)
.push_arg(&[0x66u8; 4]);
let encoded = scale::Encode::encode(&empty_list);
assert!(!encoded.is_empty());
let decoded =
<(i32, bool, [u8; 4]) as scale::Decode>::decode(&mut &encoded[..]).unwrap();
assert_eq!(decoded, (42i32, true, [0x66; 4]));
}
}
......@@ -16,7 +16,11 @@ use core::marker::PhantomData;
use crate::env::{
call::{
CallData,
ArgsList,
Argument,
ArgumentList,
EmptyArgumentList,
ExecutionInput,
Selector,
},
EnvTypes,
......@@ -52,7 +56,7 @@ where
}
/// Builds up contract instantiations.
pub struct InstantiateParams<T, C>
pub struct InstantiateParams<T, Args, C>
where
T: EnvTypes,
{
......@@ -63,47 +67,51 @@ where
/// The endowment for the instantiated contract.
endowment: T::Balance,
/// The input data for the instantation.
call_data: CallData,
call_data: ExecutionInput<Args>,
/// The type of the instantiated contract.
contract_marker: PhantomData<fn() -> C>,
}
/// Builds up contract instantiations.
pub struct InstantiateBuilder<T, C, Seal, CodeHash>
pub struct InstantiateBuilder<T, Args, C, Seal, CodeHash>
where
T: EnvTypes,
{
/// The parameters of the cross-contract instantiation.
params: InstantiateParams<T, C>,
params: InstantiateParams<T, Args, C>,
/// Seal state.
state: PhantomData<fn() -> (Seal, CodeHash)>,
}
impl<T, C> InstantiateParams<T, C>
impl<T, Args, C> InstantiateParams<T, Args, C>
where
T: EnvTypes,
{
/// The code hash of the contract.
#[inline]
pub fn code_hash(&self) -> &T::Hash {
&self.code_hash
}
/// The gas limit for the contract instantiation.
#[inline]
pub fn gas_limit(&self) -> u64 {
self.gas_limit
}
/// The endowment for the instantiated contract.
#[inline]
pub fn endowment(&self) -> &T::Balance {
&self.endowment
}
/// The raw encoded input data.
pub fn input_data(&self) -> &CallData {
#[inline]
pub fn input_data(&self) -> &ExecutionInput<Args> {
&self.call_data
}
}
impl<T, C> InstantiateParams<T, C>
impl<T, C> InstantiateParams<T, EmptyArgumentList, C>
where
T: EnvTypes,
T::Hash: Default,
......@@ -115,15 +123,22 @@ where
code_hash: Default::default(),
gas_limit: 0,
endowment: Default::default(),
call_data: CallData::new(selector),
call_data: ExecutionInput::new(selector),
contract_marker: Default::default(),
}
}
/// Creates a new create builder without setting any presets.
#[inline]