contracts: Add new seal_call that offers new features (#8909)

* Add new `seal_call` that offers new features

* Fix doc typo

Co-authored-by: Michael Müller <michi@parity.io>

* Fix doc typos

Co-authored-by: Michael Müller <michi@parity.io>

* Fix comment on assert

* Update CHANGELOG.md

Co-authored-by: Michael Müller <michi@parity.io>
This commit is contained in:
Alexander Theißen
2021-06-07 19:40:23 +02:00
committed by GitHub
parent 5c14dd3f32
commit 60256d752e
7 changed files with 503 additions and 71 deletions
+1
View File
@@ -4838,6 +4838,7 @@ name = "pallet-contracts"
version = "3.0.0" version = "3.0.0"
dependencies = [ dependencies = [
"assert_matches", "assert_matches",
"bitflags",
"frame-benchmarking", "frame-benchmarking",
"frame-support", "frame-support",
"frame-system", "frame-system",
+3
View File
@@ -20,6 +20,9 @@ In other words: Upgrading this pallet will not break pre-existing contracts.
### Added ### Added
- New **unstable** version of `seal_call` that offers more features.
[#8909](https://github.com/paritytech/substrate/pull/8909)
- New **unstable** `seal_rent_params` and `seal_rent_status` contract callable function. - New **unstable** `seal_rent_params` and `seal_rent_status` contract callable function.
[#8231](https://github.com/paritytech/substrate/pull/8231) [#8231](https://github.com/paritytech/substrate/pull/8231)
[#8780](https://github.com/paritytech/substrate/pull/8780) [#8780](https://github.com/paritytech/substrate/pull/8780)
+1
View File
@@ -13,6 +13,7 @@ readme = "README.md"
targets = ["x86_64-unknown-linux-gnu"] targets = ["x86_64-unknown-linux-gnu"]
[dependencies] [dependencies]
bitflags = "1.0"
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
log = { version = "0.4", default-features = false } log = { version = "0.4", default-features = false }
pwasm-utils = { version = "0.18", default-features = false } pwasm-utils = { version = "0.18", default-features = false }
+118 -8
View File
@@ -167,6 +167,7 @@ pub trait Ext: sealing::Sealed {
to: AccountIdOf<Self::T>, to: AccountIdOf<Self::T>,
value: BalanceOf<Self::T>, value: BalanceOf<Self::T>,
input_data: Vec<u8>, input_data: Vec<u8>,
allows_reentry: bool,
) -> Result<(ExecReturnValue, u32), (ExecError, u32)>; ) -> Result<(ExecReturnValue, u32), (ExecError, u32)>;
/// Instantiate a contract from the given code. /// Instantiate a contract from the given code.
@@ -457,6 +458,8 @@ pub struct Frame<T: Config> {
entry_point: ExportedFunction, entry_point: ExportedFunction,
/// The gas meter capped to the supplied gas limit. /// The gas meter capped to the supplied gas limit.
nested_meter: GasMeter<T>, nested_meter: GasMeter<T>,
/// If `false` the contract enabled its defense against reentrance attacks.
allows_reentry: bool,
} }
/// Parameter passed in when creating a new `Frame`. /// Parameter passed in when creating a new `Frame`.
@@ -731,6 +734,7 @@ where
entry_point, entry_point,
nested_meter: gas_meter.nested(gas_limit) nested_meter: gas_meter.nested(gas_limit)
.map_err(|e| (e.into(), executable.code_len()))?, .map_err(|e| (e.into(), executable.code_len()))?,
allows_reentry: true,
}; };
Ok((frame, executable)) Ok((frame, executable))
@@ -1014,6 +1018,11 @@ where
self.frames().skip(1).any(|f| &f.account_id == account_id) self.frames().skip(1).any(|f| &f.account_id == account_id)
} }
/// Returns whether the specified contract allows to be reentered right now.
fn allows_reentry(&self, id: &AccountIdOf<T>) -> bool {
!self.frames().any(|f| &f.account_id == id && !f.allows_reentry)
}
/// Increments the cached account id and returns the value to be used for the trie_id. /// Increments the cached account id and returns the value to be used for the trie_id.
fn next_trie_seed(&mut self) -> u64 { fn next_trie_seed(&mut self) -> u64 {
let next = if let Some(current) = self.account_counter { let next = if let Some(current) = self.account_counter {
@@ -1045,7 +1054,17 @@ where
to: T::AccountId, to: T::AccountId,
value: BalanceOf<T>, value: BalanceOf<T>,
input_data: Vec<u8>, input_data: Vec<u8>,
allows_reentry: bool,
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> { ) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
// Before pushing the new frame: Protect the caller contract against reentrancy attacks.
// It is important to do this before calling `allows_reentry` so that a direct recursion
// is caught by it.
self.top_frame_mut().allows_reentry = allows_reentry;
let try_call = || {
if !self.allows_reentry(&to) {
return Err((<Error<T>>::ReentranceDenied.into(), 0));
}
// We ignore instantiate frames in our search for a cached contract. // We ignore instantiate frames in our search for a cached contract.
// Otherwise it would be possible to recursively call a contract from its own // Otherwise it would be possible to recursively call a contract from its own
// constructor: We disallow calling not fully constructed contracts. // constructor: We disallow calling not fully constructed contracts.
@@ -1064,6 +1083,15 @@ where
gas_limit gas_limit
)?; )?;
self.run(executable, input_data) self.run(executable, input_data)
};
// We need to make sure to reset `allows_reentry` even on failure.
let result = try_call();
// Protection is on a per call basis.
self.top_frame_mut().allows_reentry = true;
result
} }
fn instantiate( fn instantiate(
@@ -1097,7 +1125,7 @@ where
beneficiary: &AccountIdOf<Self::T>, beneficiary: &AccountIdOf<Self::T>,
) -> Result<u32, (DispatchError, u32)> { ) -> Result<u32, (DispatchError, u32)> {
if self.is_recursive() { if self.is_recursive() {
return Err((Error::<T>::ReentranceDenied.into(), 0)); return Err((Error::<T>::TerminatedWhileReentrant.into(), 0));
} }
let frame = self.top_frame_mut(); let frame = self.top_frame_mut();
let info = frame.terminate(); let info = frame.terminate();
@@ -1125,7 +1153,7 @@ where
delta: Vec<StorageKey>, delta: Vec<StorageKey>,
) -> Result<(u32, u32), (DispatchError, u32, u32)> { ) -> Result<(u32, u32), (DispatchError, u32, u32)> {
if self.is_recursive() { if self.is_recursive() {
return Err((Error::<T>::ReentranceDenied.into(), 0, 0)); return Err((Error::<T>::TerminatedWhileReentrant.into(), 0, 0));
} }
let origin_contract = self.top_frame_mut().contract_info().clone(); let origin_contract = self.top_frame_mut().contract_info().clone();
let result = Rent::<T, E>::restore_to( let result = Rent::<T, E>::restore_to(
@@ -1308,12 +1336,14 @@ mod tests {
exec::ExportedFunction::*, exec::ExportedFunction::*,
Error, Weight, Error, Weight,
}; };
use codec::{Encode, Decode};
use sp_core::Bytes; use sp_core::Bytes;
use sp_runtime::DispatchError; use sp_runtime::DispatchError;
use assert_matches::assert_matches; use assert_matches::assert_matches;
use std::{cell::RefCell, collections::HashMap, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
use pallet_contracts_primitives::ReturnFlags; use pallet_contracts_primitives::ReturnFlags;
use frame_support::{assert_ok, assert_err};
type MockStack<'a> = Stack<'a, Test, MockExecutable>; type MockStack<'a> = Stack<'a, Test, MockExecutable>;
@@ -1731,7 +1761,7 @@ mod tests {
let value = Default::default(); let value = Default::default();
let recurse_ch = MockLoader::insert(Call, |ctx, _| { let recurse_ch = MockLoader::insert(Call, |ctx, _| {
// Try to call into yourself. // Try to call into yourself.
let r = ctx.ext.call(0, BOB, 0, vec![]); let r = ctx.ext.call(0, BOB, 0, vec![], true);
REACHED_BOTTOM.with(|reached_bottom| { REACHED_BOTTOM.with(|reached_bottom| {
let mut reached_bottom = reached_bottom.borrow_mut(); let mut reached_bottom = reached_bottom.borrow_mut();
@@ -1789,7 +1819,7 @@ mod tests {
// Call into CHARLIE contract. // Call into CHARLIE contract.
assert_matches!( assert_matches!(
ctx.ext.call(0, CHARLIE, 0, vec![]), ctx.ext.call(0, CHARLIE, 0, vec![], true),
Ok(_) Ok(_)
); );
exec_success() exec_success()
@@ -1832,7 +1862,7 @@ mod tests {
// Call into charlie contract. // Call into charlie contract.
assert_matches!( assert_matches!(
ctx.ext.call(0, CHARLIE, 0, vec![]), ctx.ext.call(0, CHARLIE, 0, vec![], true),
Ok(_) Ok(_)
); );
exec_success() exec_success()
@@ -2263,7 +2293,7 @@ mod tests {
assert_ne!(original_allowance, changed_allowance); assert_ne!(original_allowance, changed_allowance);
ctx.ext.set_rent_allowance(changed_allowance); ctx.ext.set_rent_allowance(changed_allowance);
assert_eq!( assert_eq!(
ctx.ext.call(0, CHARLIE, 0, vec![]).map(|v| v.0).map_err(|e| e.0), ctx.ext.call(0, CHARLIE, 0, vec![], true).map(|v| v.0).map_err(|e| e.0),
exec_trapped() exec_trapped()
); );
assert_eq!(ctx.ext.rent_allowance(), changed_allowance); assert_eq!(ctx.ext.rent_allowance(), changed_allowance);
@@ -2272,7 +2302,7 @@ mod tests {
exec_success() exec_success()
}); });
let code_charlie = MockLoader::insert(Call, |ctx, _| { let code_charlie = MockLoader::insert(Call, |ctx, _| {
assert!(ctx.ext.call(0, BOB, 0, vec![99]).is_ok()); assert!(ctx.ext.call(0, BOB, 0, vec![99], true).is_ok());
exec_trapped() exec_trapped()
}); });
@@ -2299,7 +2329,7 @@ mod tests {
fn recursive_call_during_constructor_fails() { fn recursive_call_during_constructor_fails() {
let code = MockLoader::insert(Constructor, |ctx, _| { let code = MockLoader::insert(Constructor, |ctx, _| {
assert_matches!( assert_matches!(
ctx.ext.call(0, ctx.ext.address().clone(), 0, vec![]), ctx.ext.call(0, ctx.ext.address().clone(), 0, vec![], true),
Err((ExecError{error, ..}, _)) if error == <Error<Test>>::ContractNotFound.into() Err((ExecError{error, ..}, _)) if error == <Error<Test>>::ContractNotFound.into()
); );
exec_success() exec_success()
@@ -2390,4 +2420,84 @@ mod tests {
assert_eq!(&String::from_utf8(debug_buffer).unwrap(), "This is a testMore text"); assert_eq!(&String::from_utf8(debug_buffer).unwrap(), "This is a testMore text");
} }
#[test]
fn call_reentry_direct_recursion() {
// call the contract passed as input with disabled reentry
let code_bob = MockLoader::insert(Call, |ctx, _| {
let dest = Decode::decode(&mut ctx.input_data.as_ref()).unwrap();
ctx.ext.call(0, dest, 0, vec![], false).map(|v| v.0).map_err(|e| e.0)
});
let code_charlie = MockLoader::insert(Call, |_, _| {
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let schedule = <Test as Config>::Schedule::get();
place_contract(&BOB, code_bob);
place_contract(&CHARLIE, code_charlie);
// Calling another contract should succeed
assert_ok!(MockStack::run_call(
ALICE,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&schedule,
0,
CHARLIE.encode(),
None,
));
// Calling into oneself fails
assert_err!(
MockStack::run_call(
ALICE,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&schedule,
0,
BOB.encode(),
None,
).map_err(|e| e.0.error),
<Error<Test>>::ReentranceDenied,
);
});
}
#[test]
fn call_deny_reentry() {
let code_bob = MockLoader::insert(Call, |ctx, _| {
if ctx.input_data[0] == 0 {
ctx.ext.call(0, CHARLIE, 0, vec![], false).map(|v| v.0).map_err(|e| e.0)
} else {
exec_success()
}
});
// call BOB with input set to '1'
let code_charlie = MockLoader::insert(Call, |ctx, _| {
ctx.ext.call(0, BOB, 0, vec![1], true).map(|v| v.0).map_err(|e| e.0)
});
ExtBuilder::default().build().execute_with(|| {
let schedule = <Test as Config>::Schedule::get();
place_contract(&BOB, code_bob);
place_contract(&CHARLIE, code_charlie);
// BOB -> CHARLIE -> BOB fails as BOB denies reentry.
assert_err!(
MockStack::run_call(
ALICE,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&schedule,
0,
vec![0],
None,
).map_err(|e| e.0.error),
<Error<Test>>::ReentranceDenied,
);
});
}
} }
+7 -6
View File
@@ -562,12 +562,11 @@ pub mod pallet {
ContractTrapped, ContractTrapped,
/// The size defined in `T::MaxValueSize` was exceeded. /// The size defined in `T::MaxValueSize` was exceeded.
ValueTooLarge, ValueTooLarge,
/// The action performed is not allowed while the contract performing it is already /// Termination of a contract is not allowed while the contract is already
/// on the call stack. Those actions are contract self destruction and restoration /// on the call stack. Can be triggered by `seal_terminate` or `seal_restore_to.
/// of a tombstone. TerminatedWhileReentrant,
ReentranceDenied, /// `seal_call` forwarded this contracts input. It therefore is no longer available.
/// `seal_input` was called twice from the same contract execution context. InputForwarded,
InputAlreadyRead,
/// The subject passed to `seal_random` exceeds the limit. /// The subject passed to `seal_random` exceeds the limit.
RandomSubjectTooLong, RandomSubjectTooLong,
/// The amount of topics passed to `seal_deposit_events` exceeds the limit. /// The amount of topics passed to `seal_deposit_events` exceeds the limit.
@@ -602,6 +601,8 @@ pub mod pallet {
TerminatedInConstructor, TerminatedInConstructor,
/// The debug message specified to `seal_debug_message` does contain invalid UTF-8. /// The debug message specified to `seal_debug_message` does contain invalid UTF-8.
DebugMessageInvalidUTF8, DebugMessageInvalidUTF8,
/// A call tried to invoke a contract that is flagged as non-reentrant.
ReentranceDenied,
} }
/// A mapping from an original code hash to the original code, untouched by instrumentation. /// A mapping from an original code hash to the original code, untouched by instrumentation.
+205 -9
View File
@@ -289,7 +289,14 @@ mod tests {
struct TransferEntry { struct TransferEntry {
to: AccountIdOf<Test>, to: AccountIdOf<Test>,
value: u64, value: u64,
}
#[derive(Debug, PartialEq, Eq)]
struct CallEntry {
to: AccountIdOf<Test>,
value: u64,
data: Vec<u8>, data: Vec<u8>,
allows_reentry: bool,
} }
pub struct MockExt { pub struct MockExt {
@@ -297,6 +304,7 @@ mod tests {
rent_allowance: u64, rent_allowance: u64,
instantiates: Vec<InstantiateEntry>, instantiates: Vec<InstantiateEntry>,
terminations: Vec<TerminationEntry>, terminations: Vec<TerminationEntry>,
calls: Vec<CallEntry>,
transfers: Vec<TransferEntry>, transfers: Vec<TransferEntry>,
restores: Vec<RestoreEntry>, restores: Vec<RestoreEntry>,
// (topics, data) // (topics, data)
@@ -307,6 +315,11 @@ mod tests {
debug_buffer: Vec<u8>, debug_buffer: Vec<u8>,
} }
/// The call is mocked and just returns this hardcoded value.
fn call_return_data() -> Bytes {
Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF])
}
impl Default for MockExt { impl Default for MockExt {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -314,6 +327,7 @@ mod tests {
rent_allowance: Default::default(), rent_allowance: Default::default(),
instantiates: Default::default(), instantiates: Default::default(),
terminations: Default::default(), terminations: Default::default(),
calls: Default::default(),
transfers: Default::default(), transfers: Default::default(),
restores: Default::default(), restores: Default::default(),
events: Default::default(), events: Default::default(),
@@ -334,13 +348,15 @@ mod tests {
to: AccountIdOf<Self::T>, to: AccountIdOf<Self::T>,
value: u64, value: u64,
data: Vec<u8>, data: Vec<u8>,
allows_reentry: bool,
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> { ) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
self.transfers.push(TransferEntry { self.calls.push(CallEntry {
to, to,
value, value,
data: data, data,
allows_reentry,
}); });
Ok((ExecReturnValue { flags: ReturnFlags::empty(), data: Bytes(Vec::new()) }, 0)) Ok((ExecReturnValue { flags: ReturnFlags::empty(), data: call_return_data() }, 0))
} }
fn instantiate( fn instantiate(
&mut self, &mut self,
@@ -374,7 +390,6 @@ mod tests {
self.transfers.push(TransferEntry { self.transfers.push(TransferEntry {
to: to.clone(), to: to.clone(),
value, value,
data: Vec::new(),
}); });
Ok(()) Ok(())
} }
@@ -526,7 +541,6 @@ mod tests {
&[TransferEntry { &[TransferEntry {
to: ALICE, to: ALICE,
value: 153, value: 153,
data: Vec::new(),
}] }]
); );
} }
@@ -587,11 +601,192 @@ mod tests {
)); ));
assert_eq!( assert_eq!(
&mock_ext.transfers, &mock_ext.calls,
&[TransferEntry { &[CallEntry {
to: ALICE, to: ALICE,
value: 6, value: 6,
data: vec![1, 2, 3, 4], data: vec![1, 2, 3, 4],
allows_reentry: true,
}]
);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn contract_call_forward_input() {
const CODE: &str = r#"
(module
(import "__unstable__" "seal_call" (func $seal_call (param i32 i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "env" "memory" (memory 1 1))
(func (export "call")
(drop
(call $seal_call
(i32.const 1) ;; Set FORWARD_INPUT bit
(i32.const 4) ;; Pointer to "callee" address.
(i32.const 32) ;; Length of "callee" address.
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 36) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 44) ;; 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
)
)
;; triggers a trap because we already forwarded the input
(call $seal_input (i32.const 1) (i32.const 44))
)
(func (export "deploy"))
;; Destination AccountId (ALICE)
(data (i32.const 4)
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
)
;; Amount of value to transfer.
;; Represented by u64 (8 bytes long) in little endian.
(data (i32.const 36) "\2A\00\00\00\00\00\00\00")
;; The input is ignored because we forward our own input
(data (i32.const 44) "\01\02\03\04")
)
"#;
let mut mock_ext = MockExt::default();
let input = vec![0xff, 0x2a, 0x99, 0x88];
frame_support::assert_err!(
execute(CODE, input.clone(), &mut mock_ext),
<Error<Test>>::InputForwarded,
);
assert_eq!(
&mock_ext.calls,
&[CallEntry {
to: ALICE,
value: 0x2a,
data: input,
allows_reentry: false,
}]
);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn contract_call_clone_input() {
const CODE: &str = r#"
(module
(import "__unstable__" "seal_call" (func $seal_call (param i32 i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
(func (export "call")
(drop
(call $seal_call
(i32.const 11) ;; Set FORWARD_INPUT | CLONE_INPUT | ALLOW_REENTRY bits
(i32.const 4) ;; Pointer to "callee" address.
(i32.const 32) ;; Length of "callee" address.
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 36) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 44) ;; 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
)
)
;; works because the input was cloned
(call $seal_input (i32.const 0) (i32.const 44))
;; return the input to caller for inspection
(call $seal_return (i32.const 0) (i32.const 0) (i32.load (i32.const 44)))
)
(func (export "deploy"))
;; Destination AccountId (ALICE)
(data (i32.const 4)
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
)
;; Amount of value to transfer.
;; Represented by u64 (8 bytes long) in little endian.
(data (i32.const 36) "\2A\00\00\00\00\00\00\00")
;; The input is ignored because we forward our own input
(data (i32.const 44) "\01\02\03\04")
)
"#;
let mut mock_ext = MockExt::default();
let input = vec![0xff, 0x2a, 0x99, 0x88];
let result = execute(CODE, input.clone(), &mut mock_ext).unwrap();
assert_eq!(result.data.0, input);
assert_eq!(
&mock_ext.calls,
&[CallEntry {
to: ALICE,
value: 0x2a,
data: input,
allows_reentry: true,
}]
);
}
#[test]
#[cfg(feature = "unstable-interface")]
fn contract_call_tail_call() {
const CODE: &str = r#"
(module
(import "__unstable__" "seal_call" (func $seal_call (param i32 i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1))
(func (export "call")
(drop
(call $seal_call
(i32.const 5) ;; Set FORWARD_INPUT | TAIL_CALL bit
(i32.const 4) ;; Pointer to "callee" address.
(i32.const 32) ;; Length of "callee" address.
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 36) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 0) ;; Pointer to input data buffer address
(i32.const 0) ;; 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
)
)
;; a tail call never returns
(unreachable)
)
(func (export "deploy"))
;; Destination AccountId (ALICE)
(data (i32.const 4)
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
)
;; Amount of value to transfer.
;; Represented by u64 (8 bytes long) in little endian.
(data (i32.const 36) "\2A\00\00\00\00\00\00\00")
)
"#;
let mut mock_ext = MockExt::default();
let input = vec![0xff, 0x2a, 0x99, 0x88];
let result = execute(CODE, input.clone(), &mut mock_ext).unwrap();
assert_eq!(result.data, call_return_data());
assert_eq!(
&mock_ext.calls,
&[CallEntry {
to: ALICE,
value: 0x2a,
data: input,
allows_reentry: false,
}] }]
); );
} }
@@ -772,11 +967,12 @@ mod tests {
)); ));
assert_eq!( assert_eq!(
&mock_ext.transfers, &mock_ext.calls,
&[TransferEntry { &[CallEntry {
to: ALICE, to: ALICE,
value: 6, value: 6,
data: vec![1, 2, 3, 4], data: vec![1, 2, 3, 4],
allows_reentry: true,
}] }]
); );
} }
+150 -30
View File
@@ -24,6 +24,7 @@ use crate::{
wasm::env_def::ConvertibleToWasm, wasm::env_def::ConvertibleToWasm,
schedule::HostFnWeights, schedule::HostFnWeights,
}; };
use bitflags::bitflags;
use pwasm_utils::parity_wasm::elements::ValueType; use pwasm_utils::parity_wasm::elements::ValueType;
use frame_support::{dispatch::DispatchError, ensure, traits::Get, weights::Weight}; use frame_support::{dispatch::DispatchError, ensure, traits::Get, weights::Weight};
use sp_std::prelude::*; use sp_std::prelude::*;
@@ -318,6 +319,47 @@ where
} }
} }
bitflags! {
/// Flags used to change the behaviour of `seal_call`.
struct CallFlags: u32 {
/// Forward the input of current function to the callee.
///
/// Supplied input pointers are ignored when set.
///
/// # Note
///
/// A forwarding call will consume the current contracts input. Any attempt to
/// access the input after this call returns will lead to [`Error::InputForwarded`].
/// It does not matter if this is due to calling `seal_input` or trying another
/// forwarding call. Consider using [`Self::CLONE_INPUT`] in order to preserve
/// the input.
const FORWARD_INPUT = 0b0000_0001;
/// Identical to [`Self::FORWARD_INPUT`] but without consuming the input.
///
/// This adds some additional weight costs to the call.
///
/// # Note
///
/// This implies [`Self::FORWARD_INPUT`] and takes precedence when both are set.
const CLONE_INPUT = 0b0000_0010;
/// 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 by the callee ones. Any code after `seal_call`
/// can be safely considered unreachable.
const TAIL_CALL = 0b0000_0100;
/// 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.
const ALLOW_REENTRY = 0b0000_1000;
}
}
/// This is only appropriate when writing out data of constant size that does not depend on user /// 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 /// 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. /// the beginning of the API entry point.
@@ -402,8 +444,7 @@ where
// //
// Because panics are really undesirable in the runtime code, we treat this as // Because panics are really undesirable in the runtime code, we treat this as
// a trap for now. Eventually, we might want to revisit this. // a trap for now. Eventually, we might want to revisit this.
Err(sp_sandbox::Error::Module) => Err(sp_sandbox::Error::Module) => Err("validation error")?,
Err("validation error")?,
// Any other kind of a trap should result in a failure. // Any other kind of a trap should result in a failure.
Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) => Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) =>
Err(Error::<E::T>::ContractTrapped)? Err(Error::<E::T>::ContractTrapped)?
@@ -629,6 +670,65 @@ where
(err, _) => Self::err_into_return_code(err) (err, _) => Self::err_into_return_code(err)
} }
} }
fn call(
&mut self,
flags: CallFlags,
callee_ptr: u32,
callee_len: u32,
gas: u64,
value_ptr: u32,
value_len: u32,
input_data_ptr: u32,
input_data_len: u32,
output_ptr: u32,
output_len_ptr: u32
) -> Result<ReturnCode, TrapReason> {
self.charge_gas(RuntimeCosts::CallBase(input_data_len))?;
let callee: <<E as Ext>::T as frame_system::Config>::AccountId =
self.read_sandbox_memory_as(callee_ptr, callee_len)?;
let value: BalanceOf<<E as Ext>::T> = self.read_sandbox_memory_as(value_ptr, value_len)?;
let input_data = if flags.contains(CallFlags::CLONE_INPUT) {
self.input_data.as_ref().ok_or_else(|| Error::<E::T>::InputForwarded)?.clone()
} else if flags.contains(CallFlags::FORWARD_INPUT) {
self.input_data.take().ok_or_else(|| Error::<E::T>::InputForwarded)?
} else {
self.read_sandbox_memory(input_data_ptr, input_data_len)?
};
if value > 0u32.into() {
self.charge_gas(RuntimeCosts::CallSurchargeTransfer)?;
}
let charged = self.charge_gas(
RuntimeCosts::CallSurchargeCodeSize(<E::T as Config>::Schedule::get().limits.code_len)
)?;
let ext = &mut self.ext;
let call_outcome = ext.call(
gas, callee, value, input_data, flags.contains(CallFlags::ALLOW_REENTRY),
);
let code_len = match &call_outcome {
Ok((_, len)) => len,
Err((_, len)) => len,
};
self.adjust_gas(charged, RuntimeCosts::CallSurchargeCodeSize(*code_len));
// `TAIL_CALL` only matters on an `OK` result. Otherwise the call stack comes to
// a halt anyways without anymore code being executed.
if flags.contains(CallFlags::TAIL_CALL) {
if let Ok((return_value, _)) = call_outcome {
return Err(TrapReason::Return(ReturnData {
flags: return_value.flags.bits(),
data: return_value.data.0,
}));
}
}
if let Ok((output, _)) = &call_outcome {
self.write_sandbox_output(output_ptr, output_len_ptr, &output.data, true, |len| {
Some(RuntimeCosts::CallCopyOut(len))
})?;
}
Ok(Runtime::<E>::exec_into_return_code(call_outcome.map(|r| r.0).map_err(|r| r.0))?)
}
} }
// *********************************************************** // ***********************************************************
@@ -758,6 +858,36 @@ define_env!(Env, <E: Ext>,
} }
}, },
// Make a call to another contract.
//
// This is equivalent to calling the newer version of this function with
// `flags` set to `ALLOW_REENTRY`. See the newer version for documentation.
[seal0] seal_call(
ctx,
callee_ptr: u32,
callee_len: u32,
gas: u64,
value_ptr: u32,
value_len: u32,
input_data_ptr: u32,
input_data_len: u32,
output_ptr: u32,
output_len_ptr: u32
) -> ReturnCode => {
ctx.call(
CallFlags::ALLOW_REENTRY,
callee_ptr,
callee_len,
gas,
value_ptr,
value_len,
input_data_ptr,
input_data_len,
output_ptr,
output_len_ptr,
)
},
// Make a call to another contract. // Make a call to another contract.
// //
// The callees output buffer is copied to `output_ptr` and its length to `output_len_ptr`. // The callees output buffer is copied to `output_ptr` and its length to `output_len_ptr`.
@@ -766,6 +896,7 @@ define_env!(Env, <E: Ext>,
// //
// # Parameters // # Parameters
// //
// - flags: See [`CallFlags`] for a documenation of the supported flags.
// - callee_ptr: a pointer to the address of the callee contract. // - callee_ptr: a pointer to the address of the callee contract.
// Should be decodable as an `T::AccountId`. Traps otherwise. // Should be decodable as an `T::AccountId`. Traps otherwise.
// - callee_len: length of the address buffer. // - callee_len: length of the address buffer.
@@ -789,8 +920,9 @@ define_env!(Env, <E: Ext>,
// `ReturnCode::BelowSubsistenceThreshold` // `ReturnCode::BelowSubsistenceThreshold`
// `ReturnCode::TransferFailed` // `ReturnCode::TransferFailed`
// `ReturnCode::NotCallable` // `ReturnCode::NotCallable`
[seal0] seal_call( [__unstable__] seal_call(
ctx, ctx,
flags: u32,
callee_ptr: u32, callee_ptr: u32,
callee_len: u32, callee_len: u32,
gas: u64, gas: u64,
@@ -801,30 +933,18 @@ define_env!(Env, <E: Ext>,
output_ptr: u32, output_ptr: u32,
output_len_ptr: u32 output_len_ptr: u32
) -> ReturnCode => { ) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::CallBase(input_data_len))?; ctx.call(
let callee: <<E as Ext>::T as frame_system::Config>::AccountId = CallFlags::from_bits(flags).ok_or_else(|| "used rerved bit in CallFlags")?,
ctx.read_sandbox_memory_as(callee_ptr, callee_len)?; callee_ptr,
let value: BalanceOf<<E as Ext>::T> = ctx.read_sandbox_memory_as(value_ptr, value_len)?; callee_len,
let input_data = ctx.read_sandbox_memory(input_data_ptr, input_data_len)?; gas,
if value > 0u32.into() { value_ptr,
ctx.charge_gas(RuntimeCosts::CallSurchargeTransfer)?; value_len,
} input_data_ptr,
let charged = ctx.charge_gas( input_data_len,
RuntimeCosts::CallSurchargeCodeSize(<E::T as Config>::Schedule::get().limits.code_len) output_ptr,
)?; output_len_ptr,
let ext = &mut ctx.ext; )
let call_outcome = ext.call(gas, callee, value, input_data);
let code_len = match &call_outcome {
Ok((_, len)) => len,
Err((_, len)) => len,
};
ctx.adjust_gas(charged, RuntimeCosts::CallSurchargeCodeSize(*code_len));
if let Ok((output, _)) = &call_outcome {
ctx.write_sandbox_output(output_ptr, output_len_ptr, &output.data, true, |len| {
Some(RuntimeCosts::CallCopyOut(len))
})?;
}
Ok(Runtime::<E>::exec_into_return_code(call_outcome.map(|r| r.0).map_err(|r| r.0))?)
}, },
// Instantiate a contract with the specified code hash. // Instantiate a contract with the specified code hash.
@@ -945,7 +1065,6 @@ define_env!(Env, <E: Ext>,
ctx.charge_gas(RuntimeCosts::Terminate)?; ctx.charge_gas(RuntimeCosts::Terminate)?;
let beneficiary: <<E as Ext>::T as frame_system::Config>::AccountId = let beneficiary: <<E as Ext>::T as frame_system::Config>::AccountId =
ctx.read_sandbox_memory_as(beneficiary_ptr, beneficiary_len)?; ctx.read_sandbox_memory_as(beneficiary_ptr, beneficiary_len)?;
let charged = ctx.charge_gas( let charged = ctx.charge_gas(
RuntimeCosts::TerminateSurchargeCodeSize( RuntimeCosts::TerminateSurchargeCodeSize(
<E::T as Config>::Schedule::get().limits.code_len <E::T as Config>::Schedule::get().limits.code_len
@@ -969,16 +1088,17 @@ define_env!(Env, <E: Ext>,
// //
// # Note // # Note
// //
// This function can only be called once. Calling it multiple times will trigger a trap. // This function traps if the input was previously forwarded by a `seal_call`.
[seal0] seal_input(ctx, out_ptr: u32, out_len_ptr: u32) => { [seal0] seal_input(ctx, out_ptr: u32, out_len_ptr: u32) => {
ctx.charge_gas(RuntimeCosts::InputBase)?; ctx.charge_gas(RuntimeCosts::InputBase)?;
if let Some(input) = ctx.input_data.take() { if let Some(input) = ctx.input_data.take() {
ctx.write_sandbox_output(out_ptr, out_len_ptr, &input, false, |len| { ctx.write_sandbox_output(out_ptr, out_len_ptr, &input, false, |len| {
Some(RuntimeCosts::InputCopyOut(len)) Some(RuntimeCosts::InputCopyOut(len))
})?; })?;
ctx.input_data = Some(input);
Ok(()) Ok(())
} else { } else {
Err(Error::<E::T>::InputAlreadyRead.into()) Err(Error::<E::T>::InputForwarded.into())
} }
}, },