mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 09:21:04 +00:00
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:
committed by
GitHub
parent
5c14dd3f32
commit
60256d752e
Generated
+1
@@ -4838,6 +4838,7 @@ name = "pallet-contracts"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"bitflags",
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
|
||||
@@ -20,6 +20,9 @@ In other words: Upgrading this pallet will not break pre-existing contracts.
|
||||
|
||||
### 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.
|
||||
[#8231](https://github.com/paritytech/substrate/pull/8231)
|
||||
[#8780](https://github.com/paritytech/substrate/pull/8780)
|
||||
|
||||
@@ -13,6 +13,7 @@ readme = "README.md"
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
|
||||
log = { version = "0.4", default-features = false }
|
||||
pwasm-utils = { version = "0.18", default-features = false }
|
||||
|
||||
@@ -167,6 +167,7 @@ pub trait Ext: sealing::Sealed {
|
||||
to: AccountIdOf<Self::T>,
|
||||
value: BalanceOf<Self::T>,
|
||||
input_data: Vec<u8>,
|
||||
allows_reentry: bool,
|
||||
) -> Result<(ExecReturnValue, u32), (ExecError, u32)>;
|
||||
|
||||
/// Instantiate a contract from the given code.
|
||||
@@ -457,6 +458,8 @@ pub struct Frame<T: Config> {
|
||||
entry_point: ExportedFunction,
|
||||
/// The gas meter capped to the supplied gas limit.
|
||||
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`.
|
||||
@@ -731,6 +734,7 @@ where
|
||||
entry_point,
|
||||
nested_meter: gas_meter.nested(gas_limit)
|
||||
.map_err(|e| (e.into(), executable.code_len()))?,
|
||||
allows_reentry: true,
|
||||
};
|
||||
|
||||
Ok((frame, executable))
|
||||
@@ -1014,6 +1018,11 @@ where
|
||||
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.
|
||||
fn next_trie_seed(&mut self) -> u64 {
|
||||
let next = if let Some(current) = self.account_counter {
|
||||
@@ -1045,25 +1054,44 @@ where
|
||||
to: T::AccountId,
|
||||
value: BalanceOf<T>,
|
||||
input_data: Vec<u8>,
|
||||
allows_reentry: bool,
|
||||
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
|
||||
// We ignore instantiate frames in our search for a cached contract.
|
||||
// Otherwise it would be possible to recursively call a contract from its own
|
||||
// constructor: We disallow calling not fully constructed contracts.
|
||||
let cached_info = self
|
||||
.frames()
|
||||
.find(|f| f.entry_point == ExportedFunction::Call && f.account_id == to)
|
||||
.and_then(|f| {
|
||||
match &f.contract_info {
|
||||
CachedContract::Cached(contract) => Some(contract.clone()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let executable = self.push_frame(
|
||||
FrameArgs::Call{dest: to, cached_info},
|
||||
value,
|
||||
gas_limit
|
||||
)?;
|
||||
self.run(executable, input_data)
|
||||
// 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.
|
||||
// Otherwise it would be possible to recursively call a contract from its own
|
||||
// constructor: We disallow calling not fully constructed contracts.
|
||||
let cached_info = self
|
||||
.frames()
|
||||
.find(|f| f.entry_point == ExportedFunction::Call && f.account_id == to)
|
||||
.and_then(|f| {
|
||||
match &f.contract_info {
|
||||
CachedContract::Cached(contract) => Some(contract.clone()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let executable = self.push_frame(
|
||||
FrameArgs::Call{dest: to, cached_info},
|
||||
value,
|
||||
gas_limit
|
||||
)?;
|
||||
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(
|
||||
@@ -1097,7 +1125,7 @@ where
|
||||
beneficiary: &AccountIdOf<Self::T>,
|
||||
) -> Result<u32, (DispatchError, u32)> {
|
||||
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 info = frame.terminate();
|
||||
@@ -1125,7 +1153,7 @@ where
|
||||
delta: Vec<StorageKey>,
|
||||
) -> Result<(u32, u32), (DispatchError, u32, u32)> {
|
||||
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 result = Rent::<T, E>::restore_to(
|
||||
@@ -1308,12 +1336,14 @@ mod tests {
|
||||
exec::ExportedFunction::*,
|
||||
Error, Weight,
|
||||
};
|
||||
use codec::{Encode, Decode};
|
||||
use sp_core::Bytes;
|
||||
use sp_runtime::DispatchError;
|
||||
use assert_matches::assert_matches;
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use pallet_contracts_primitives::ReturnFlags;
|
||||
use frame_support::{assert_ok, assert_err};
|
||||
|
||||
type MockStack<'a> = Stack<'a, Test, MockExecutable>;
|
||||
|
||||
@@ -1731,7 +1761,7 @@ mod tests {
|
||||
let value = Default::default();
|
||||
let recurse_ch = MockLoader::insert(Call, |ctx, _| {
|
||||
// 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| {
|
||||
let mut reached_bottom = reached_bottom.borrow_mut();
|
||||
@@ -1789,7 +1819,7 @@ mod tests {
|
||||
|
||||
// Call into CHARLIE contract.
|
||||
assert_matches!(
|
||||
ctx.ext.call(0, CHARLIE, 0, vec![]),
|
||||
ctx.ext.call(0, CHARLIE, 0, vec![], true),
|
||||
Ok(_)
|
||||
);
|
||||
exec_success()
|
||||
@@ -1832,7 +1862,7 @@ mod tests {
|
||||
|
||||
// Call into charlie contract.
|
||||
assert_matches!(
|
||||
ctx.ext.call(0, CHARLIE, 0, vec![]),
|
||||
ctx.ext.call(0, CHARLIE, 0, vec![], true),
|
||||
Ok(_)
|
||||
);
|
||||
exec_success()
|
||||
@@ -2263,7 +2293,7 @@ mod tests {
|
||||
assert_ne!(original_allowance, changed_allowance);
|
||||
ctx.ext.set_rent_allowance(changed_allowance);
|
||||
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()
|
||||
);
|
||||
assert_eq!(ctx.ext.rent_allowance(), changed_allowance);
|
||||
@@ -2272,7 +2302,7 @@ mod tests {
|
||||
exec_success()
|
||||
});
|
||||
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()
|
||||
});
|
||||
|
||||
@@ -2299,7 +2329,7 @@ mod tests {
|
||||
fn recursive_call_during_constructor_fails() {
|
||||
let code = MockLoader::insert(Constructor, |ctx, _| {
|
||||
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()
|
||||
);
|
||||
exec_success()
|
||||
@@ -2390,4 +2420,84 @@ mod tests {
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,12 +562,11 @@ pub mod pallet {
|
||||
ContractTrapped,
|
||||
/// The size defined in `T::MaxValueSize` was exceeded.
|
||||
ValueTooLarge,
|
||||
/// The action performed is not allowed while the contract performing it is already
|
||||
/// on the call stack. Those actions are contract self destruction and restoration
|
||||
/// of a tombstone.
|
||||
ReentranceDenied,
|
||||
/// `seal_input` was called twice from the same contract execution context.
|
||||
InputAlreadyRead,
|
||||
/// Termination of a contract is not allowed while the contract is already
|
||||
/// on the call stack. Can be triggered by `seal_terminate` or `seal_restore_to.
|
||||
TerminatedWhileReentrant,
|
||||
/// `seal_call` forwarded this contracts input. It therefore is no longer available.
|
||||
InputForwarded,
|
||||
/// The subject passed to `seal_random` exceeds the limit.
|
||||
RandomSubjectTooLong,
|
||||
/// The amount of topics passed to `seal_deposit_events` exceeds the limit.
|
||||
@@ -602,6 +601,8 @@ pub mod pallet {
|
||||
TerminatedInConstructor,
|
||||
/// The debug message specified to `seal_debug_message` does contain invalid UTF-8.
|
||||
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.
|
||||
|
||||
@@ -289,7 +289,14 @@ mod tests {
|
||||
struct TransferEntry {
|
||||
to: AccountIdOf<Test>,
|
||||
value: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct CallEntry {
|
||||
to: AccountIdOf<Test>,
|
||||
value: u64,
|
||||
data: Vec<u8>,
|
||||
allows_reentry: bool,
|
||||
}
|
||||
|
||||
pub struct MockExt {
|
||||
@@ -297,6 +304,7 @@ mod tests {
|
||||
rent_allowance: u64,
|
||||
instantiates: Vec<InstantiateEntry>,
|
||||
terminations: Vec<TerminationEntry>,
|
||||
calls: Vec<CallEntry>,
|
||||
transfers: Vec<TransferEntry>,
|
||||
restores: Vec<RestoreEntry>,
|
||||
// (topics, data)
|
||||
@@ -307,6 +315,11 @@ mod tests {
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -314,6 +327,7 @@ mod tests {
|
||||
rent_allowance: Default::default(),
|
||||
instantiates: Default::default(),
|
||||
terminations: Default::default(),
|
||||
calls: Default::default(),
|
||||
transfers: Default::default(),
|
||||
restores: Default::default(),
|
||||
events: Default::default(),
|
||||
@@ -334,13 +348,15 @@ mod tests {
|
||||
to: AccountIdOf<Self::T>,
|
||||
value: u64,
|
||||
data: Vec<u8>,
|
||||
allows_reentry: bool,
|
||||
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
|
||||
self.transfers.push(TransferEntry {
|
||||
self.calls.push(CallEntry {
|
||||
to,
|
||||
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(
|
||||
&mut self,
|
||||
@@ -374,7 +390,6 @@ mod tests {
|
||||
self.transfers.push(TransferEntry {
|
||||
to: to.clone(),
|
||||
value,
|
||||
data: Vec::new(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -526,7 +541,6 @@ mod tests {
|
||||
&[TransferEntry {
|
||||
to: ALICE,
|
||||
value: 153,
|
||||
data: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
@@ -587,11 +601,192 @@ mod tests {
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
&mock_ext.transfers,
|
||||
&[TransferEntry {
|
||||
&mock_ext.calls,
|
||||
&[CallEntry {
|
||||
to: ALICE,
|
||||
value: 6,
|
||||
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!(
|
||||
&mock_ext.transfers,
|
||||
&[TransferEntry {
|
||||
&mock_ext.calls,
|
||||
&[CallEntry {
|
||||
to: ALICE,
|
||||
value: 6,
|
||||
data: vec![1, 2, 3, 4],
|
||||
allows_reentry: true,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::{
|
||||
wasm::env_def::ConvertibleToWasm,
|
||||
schedule::HostFnWeights,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use pwasm_utils::parity_wasm::elements::ValueType;
|
||||
use frame_support::{dispatch::DispatchError, ensure, traits::Get, weights::Weight};
|
||||
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
|
||||
/// 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.
|
||||
@@ -402,8 +444,7 @@ where
|
||||
//
|
||||
// Because panics are really undesirable in the runtime code, we treat this as
|
||||
// a trap for now. Eventually, we might want to revisit this.
|
||||
Err(sp_sandbox::Error::Module) =>
|
||||
Err("validation error")?,
|
||||
Err(sp_sandbox::Error::Module) => Err("validation error")?,
|
||||
// Any other kind of a trap should result in a failure.
|
||||
Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) =>
|
||||
Err(Error::<E::T>::ContractTrapped)?
|
||||
@@ -629,6 +670,65 @@ where
|
||||
(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.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// - flags: See [`CallFlags`] for a documenation of the supported flags.
|
||||
// - callee_ptr: a pointer to the address of the callee contract.
|
||||
// Should be decodable as an `T::AccountId`. Traps otherwise.
|
||||
// - callee_len: length of the address buffer.
|
||||
@@ -789,8 +920,9 @@ define_env!(Env, <E: Ext>,
|
||||
// `ReturnCode::BelowSubsistenceThreshold`
|
||||
// `ReturnCode::TransferFailed`
|
||||
// `ReturnCode::NotCallable`
|
||||
[seal0] seal_call(
|
||||
[__unstable__] seal_call(
|
||||
ctx,
|
||||
flags: u32,
|
||||
callee_ptr: u32,
|
||||
callee_len: u32,
|
||||
gas: u64,
|
||||
@@ -801,30 +933,18 @@ define_env!(Env, <E: Ext>,
|
||||
output_ptr: u32,
|
||||
output_len_ptr: u32
|
||||
) -> ReturnCode => {
|
||||
ctx.charge_gas(RuntimeCosts::CallBase(input_data_len))?;
|
||||
let callee: <<E as Ext>::T as frame_system::Config>::AccountId =
|
||||
ctx.read_sandbox_memory_as(callee_ptr, callee_len)?;
|
||||
let value: BalanceOf<<E as Ext>::T> = ctx.read_sandbox_memory_as(value_ptr, value_len)?;
|
||||
let input_data = ctx.read_sandbox_memory(input_data_ptr, input_data_len)?;
|
||||
if value > 0u32.into() {
|
||||
ctx.charge_gas(RuntimeCosts::CallSurchargeTransfer)?;
|
||||
}
|
||||
let charged = ctx.charge_gas(
|
||||
RuntimeCosts::CallSurchargeCodeSize(<E::T as Config>::Schedule::get().limits.code_len)
|
||||
)?;
|
||||
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))?)
|
||||
ctx.call(
|
||||
CallFlags::from_bits(flags).ok_or_else(|| "used rerved bit in CallFlags")?,
|
||||
callee_ptr,
|
||||
callee_len,
|
||||
gas,
|
||||
value_ptr,
|
||||
value_len,
|
||||
input_data_ptr,
|
||||
input_data_len,
|
||||
output_ptr,
|
||||
output_len_ptr,
|
||||
)
|
||||
},
|
||||
|
||||
// Instantiate a contract with the specified code hash.
|
||||
@@ -945,7 +1065,6 @@ define_env!(Env, <E: Ext>,
|
||||
ctx.charge_gas(RuntimeCosts::Terminate)?;
|
||||
let beneficiary: <<E as Ext>::T as frame_system::Config>::AccountId =
|
||||
ctx.read_sandbox_memory_as(beneficiary_ptr, beneficiary_len)?;
|
||||
|
||||
let charged = ctx.charge_gas(
|
||||
RuntimeCosts::TerminateSurchargeCodeSize(
|
||||
<E::T as Config>::Schedule::get().limits.code_len
|
||||
@@ -969,16 +1088,17 @@ define_env!(Env, <E: Ext>,
|
||||
//
|
||||
// # 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) => {
|
||||
ctx.charge_gas(RuntimeCosts::InputBase)?;
|
||||
if let Some(input) = ctx.input_data.take() {
|
||||
ctx.write_sandbox_output(out_ptr, out_len_ptr, &input, false, |len| {
|
||||
Some(RuntimeCosts::InputCopyOut(len))
|
||||
})?;
|
||||
ctx.input_data = Some(input);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::<E::T>::InputAlreadyRead.into())
|
||||
Err(Error::<E::T>::InputForwarded.into())
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user