seal_reentrant_count returns contract reentrant count (#12695)

* Add logic, test, broken benchmark

* account_entrance_count

* Addressing comments

* Address @agryaznov's comments

* Add test for account_entrance_count, fix ci

* Cargo fmt

* Fix tests

* Fix tests

* Remove delegated call from test, address comments

* Minor fixes and indentation in wat files

* Update test for account_entrance_count

* Update reentrant_count_call test

* Delegate call test

* Cargo +nightly fmt

* Address comments

* Update reentrant_count_works test

* Apply weights diff

* Add fixture descriptions

* Update comments as suggested

* Update reentrant_count_call test to use seal_address

* add missing code

* cargo fmt

* account_entrance_count -> account_reentrance_count

* fix tests

* fmt

* normalize signatures

Co-authored-by: yarikbratashchuk <yarik.bratashchuk@gmail.com>
This commit is contained in:
Artemka374
2022-11-15 15:12:08 +02:00
committed by GitHub
parent 679d2dcd25
commit 103ea38f95
10 changed files with 569 additions and 1 deletions
@@ -0,0 +1,37 @@
;; This fixture tests if account_reentrance_count works as expected
;; testing it with 2 different addresses
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_caller" (func $seal_caller (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "__unstable__" "account_reentrance_count" (func $account_reentrance_count (param i32) (result i32)))
(import "env" "memory" (memory 1 1))
;; [0, 32) buffer where input is copied
;; [32, 36) size of the input buffer
(data (i32.const 32) "\20")
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
;; Reading "callee" input address
(call $seal_input (i32.const 0) (i32.const 32))
(i32.store
(i32.const 36)
(call $account_reentrance_count (i32.const 0))
)
(call $seal_return (i32.const 0) (i32.const 36) (i32.const 4))
)
(func (export "deploy"))
)
@@ -0,0 +1,76 @@
;; This fixture recursively tests if reentrant_count returns correct reentrant count value when
;; using seal_call to make caller contract call to itself
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_address" (func $seal_address (param i32 i32)))
(import "seal1" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32) (result i32)))
(import "__unstable__" "reentrant_count" (func $reentrant_count (result i32)))
(import "env" "memory" (memory 1 1))
;; [0, 32) reserved for $seal_address output
;; [32, 36) buffer for the call stack height
;; [36, 40) size of the input buffer
(data (i32.const 36) "\04")
;; [40, 44) length of the buffer for $seal_address
(data (i32.const 40) "\20")
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
(local $expected_reentrant_count i32)
(local $seal_call_exit_code i32)
;; reading current contract address
(call $seal_address (i32.const 0) (i32.const 40))
;; reading passed input
(call $seal_input (i32.const 32) (i32.const 36))
;; reading manually passed reentrant count
(set_local $expected_reentrant_count (i32.load (i32.const 32)))
;; reentrance count is calculated correctly
(call $assert
(i32.eq (call $reentrant_count) (get_local $expected_reentrant_count))
)
;; re-enter 5 times in a row and assert that the reentrant counter works as expected
(i32.eq (call $reentrant_count) (i32.const 5))
(if
(then) ;; recursion exit case
(else
;; incrementing $expected_reentrant_count passed to the contract
(i32.store (i32.const 32) (i32.add (i32.load (i32.const 32)) (i32.const 1)))
;; Call to itself
(set_local $seal_call_exit_code
(call $seal_call
(i32.const 8) ;; Allow reentrancy flag set
(i32.const 0) ;; Pointer to "callee" address
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 32) ;; Pointer to input data buffer address
(i32.const 4) ;; Length of input data buffer
(i32.const 0xffffffff) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Ptr to output buffer len
)
)
(call $assert
(i32.eq (get_local $seal_call_exit_code) (i32.const 0))
)
)
)
)
(func (export "deploy"))
)
@@ -0,0 +1,71 @@
;; This fixture recursively tests if reentrant_count returns correct reentrant count value when
;; using seal_delegate_call to make caller contract delegate call to itself
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_set_storage" (func $seal_set_storage (param i32 i32 i32)))
(import "seal0" "seal_delegate_call" (func $seal_delegate_call (param i32 i32 i32 i32 i32 i32) (result i32)))
(import "__unstable__" "reentrant_count" (func $reentrant_count (result i32)))
(import "env" "memory" (memory 1 1))
;; [0, 32) buffer where code hash is copied
;; [32, 36) buffer for the call stack height
;; [36, 40) size of the input buffer
(data (i32.const 36) "\24")
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
(local $callstack_height i32)
(local $delegate_call_exit_code i32)
;; Reading input
(call $seal_input (i32.const 0) (i32.const 36))
;; reading passed callstack height
(set_local $callstack_height (i32.load (i32.const 32)))
;; incrementing callstack height
(i32.store (i32.const 32) (i32.add (i32.load (i32.const 32)) (i32.const 1)))
;; reentrance count stays 0
(call $assert
(i32.eq (call $reentrant_count) (i32.const 0))
)
(i32.eq (get_local $callstack_height) (i32.const 5))
(if
(then) ;; exit recursion case
(else
;; Call to itself
(set_local $delegate_call_exit_code
(call $seal_delegate_call
(i32.const 0) ;; Set no call flags
(i32.const 0) ;; Pointer to "callee" code_hash.
(i32.const 0) ;; Pointer to the input data
(i32.const 36) ;; Length of the input
(i32.const 4294967295) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
)
)
(call $assert
(i32.eq (get_local $delegate_call_exit_code) (i32.const 0))
)
)
)
(call $assert
(i32.le_s (get_local $callstack_height) (i32.const 5))
)
)
(func (export "deploy"))
)
@@ -2086,6 +2086,59 @@ benchmarks! {
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
reentrant_count {
let r in 0 .. API_BENCHMARK_BATCHES;
let code = WasmModule::<T>::from(ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "__unstable__",
name: "reentrant_count",
params: vec![],
return_type: Some(ValueType::I32),
}],
call_body: Some(body::repeated(r * API_BENCHMARK_BATCH_SIZE, &[
Instruction::Call(0),
Instruction::Drop,
])),
.. Default::default()
});
let instance = Contract::<T>::new(code, vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
account_reentrance_count {
let r in 0 .. API_BENCHMARK_BATCHES;
let dummy_code = WasmModule::<T>::dummy_with_bytes(0);
let accounts = (0..r * API_BENCHMARK_BATCH_SIZE)
.map(|i| Contract::with_index(i + 1, dummy_code.clone(), vec![]))
.collect::<Result<Vec<_>, _>>()?;
let account_id_len = accounts.get(0).map(|i| i.account_id.encode().len()).unwrap_or(0);
let account_id_bytes = accounts.iter().flat_map(|x| x.account_id.encode()).collect();
let code = WasmModule::<T>::from(ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "__unstable__",
name: "account_reentrance_count",
params: vec![ValueType::I32],
return_type: Some(ValueType::I32),
}],
data_segments: vec![
DataSegment {
offset: 0,
value: account_id_bytes,
},
],
call_body: Some(body::repeated_dyn(r * API_BENCHMARK_BATCH_SIZE, vec![
Counter(0, account_id_len as u32), // account_ptr
Regular(Instruction::Call(0)),
Regular(Instruction::Drop),
])),
.. Default::default()
});
let instance = Contract::<T>::new(code, vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
// We make the assumption that pushing a constant and dropping a value takes roughly
// the same amount of time. We follow that `t.load` and `drop` both have the weight
// of this benchmark / 2. We need to make this assumption because there is no way
+20
View File
@@ -296,6 +296,15 @@ pub trait Ext: sealing::Sealed {
/// Sets new code hash for existing contract.
fn set_code_hash(&mut self, hash: CodeHash<Self::T>) -> Result<(), DispatchError>;
/// Returns the number of times the currently executing contract exists on the call stack in
/// addition to the calling instance. A value of 0 means no reentrancy.
fn reentrant_count(&self) -> u32;
/// Returns the number of times the specified contract exists on the call stack. Delegated calls
/// are not calculated as separate entrance.
/// A value of 0 means it does not exist on the call stack.
fn account_reentrance_count(&self, account_id: &AccountIdOf<Self::T>) -> u32;
}
/// Describes the different functions that can be exported by an [`Executable`].
@@ -1374,6 +1383,17 @@ where
);
Ok(())
}
fn reentrant_count(&self) -> u32 {
let id: &AccountIdOf<Self::T> = &self.top_frame().account_id;
self.account_reentrance_count(id).saturating_sub(1)
}
fn account_reentrance_count(&self, account_id: &AccountIdOf<Self::T>) -> u32 {
self.frames()
.filter(|f| f.delegate_caller.is_none() && &f.account_id == account_id)
.count() as u32
}
}
mod sealing {
@@ -423,6 +423,12 @@ pub struct HostFnWeights<T: Config> {
/// Weight of calling `seal_ecdsa_to_eth_address`.
pub ecdsa_to_eth_address: u64,
/// Weight of calling `seal_reentrant_count`.
pub reentrant_count: u64,
/// Weight of calling `seal_account_reentrance_count`.
pub account_reentrance_count: u64,
/// The type parameter is used in the default implementation.
#[codec(skip)]
pub _phantom: PhantomData<T>,
@@ -659,6 +665,8 @@ impl<T: Config> Default for HostFnWeights<T> {
hash_blake2_128_per_byte: cost_byte_batched!(seal_hash_blake2_128_per_kb),
ecdsa_recover: cost_batched!(seal_ecdsa_recover),
ecdsa_to_eth_address: cost_batched!(seal_ecdsa_to_eth_address),
reentrant_count: cost_batched!(seal_reentrant_count),
account_reentrance_count: cost_batched!(seal_account_reentrance_count),
_phantom: PhantomData,
}
}
+139
View File
@@ -4383,3 +4383,142 @@ fn delegate_call_indeterministic_code() {
);
});
}
#[test]
#[cfg(feature = "unstable-interface")]
fn reentrant_count_works_with_call() {
let (wasm, code_hash) = compile_module::<Test>("reentrant_count_call").unwrap();
let contract_addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contracts::instantiate_with_code(
RuntimeOrigin::signed(ALICE),
300_000,
GAS_LIMIT,
None,
wasm,
vec![],
vec![],
));
// passing reentrant count to the input
let input = 0.encode();
Contracts::bare_call(
ALICE,
contract_addr,
0,
GAS_LIMIT,
None,
input,
true,
Determinism::Deterministic,
)
.result
.unwrap();
});
}
#[test]
#[cfg(feature = "unstable-interface")]
fn reentrant_count_works_with_delegated_call() {
let (wasm, code_hash) = compile_module::<Test>("reentrant_count_delegated_call").unwrap();
let contract_addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contracts::instantiate_with_code(
RuntimeOrigin::signed(ALICE),
300_000,
GAS_LIMIT,
None,
wasm,
vec![],
vec![],
));
// adding a callstack height to the input
let input = (code_hash, 1).encode();
Contracts::bare_call(
ALICE,
contract_addr.clone(),
0,
GAS_LIMIT,
None,
input,
true,
Determinism::Deterministic,
)
.result
.unwrap();
});
}
#[test]
#[cfg(feature = "unstable-interface")]
fn account_reentrance_count_works() {
let (wasm, code_hash) = compile_module::<Test>("account_reentrance_count_call").unwrap();
let (wasm_reentrant_count, code_hash_reentrant_count) =
compile_module::<Test>("reentrant_count_call").unwrap();
ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contracts::instantiate_with_code(
RuntimeOrigin::signed(ALICE),
300_000,
GAS_LIMIT,
None,
wasm,
vec![],
vec![],
));
assert_ok!(Contracts::instantiate_with_code(
RuntimeOrigin::signed(ALICE),
300_000,
GAS_LIMIT,
None,
wasm_reentrant_count,
vec![],
vec![]
));
let contract_addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
let another_contract_addr =
Contracts::contract_address(&ALICE, &code_hash_reentrant_count, &[]);
let result1 = Contracts::bare_call(
ALICE,
contract_addr.clone(),
0,
GAS_LIMIT,
None,
contract_addr.encode(),
true,
Determinism::Deterministic,
)
.result
.unwrap();
let result2 = Contracts::bare_call(
ALICE,
contract_addr.clone(),
0,
GAS_LIMIT,
None,
another_contract_addr.encode(),
true,
Determinism::Deterministic,
)
.result
.unwrap();
assert_eq!(result1.data, 1.encode());
assert_eq!(result2.data, 0.encode());
});
}
+72
View File
@@ -578,6 +578,12 @@ mod tests {
fn ecdsa_to_eth_address(&self, _pk: &[u8; 33]) -> Result<[u8; 20], ()> {
Ok([2u8; 20])
}
fn reentrant_count(&self) -> u32 {
12
}
fn account_reentrance_count(&self, _account_id: &AccountIdOf<Self::T>) -> u32 {
12
}
}
fn execute<E: BorrowMut<MockExt>>(wat: &str, input_data: Vec<u8>, mut ext: E) -> ExecResult {
@@ -2850,4 +2856,70 @@ mod tests {
assert_eq!(mock_ext.code_hashes.pop().unwrap(), H256::from_slice(&[17u8; 32]));
}
#[test]
#[cfg(feature = "unstable-interface")]
fn reentrant_count_works() {
const CODE: &str = r#"
(module
(import "__unstable__" "reentrant_count" (func $reentrant_count (result i32)))
(import "env" "memory" (memory 1 1))
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
(local $return_val i32)
(set_local $return_val
(call $reentrant_count)
)
(call $assert
(i32.eq (get_local $return_val) (i32.const 12))
)
)
(func (export "deploy"))
)
"#;
let mut mock_ext = MockExt::default();
execute(CODE, vec![], &mut mock_ext).unwrap();
}
#[test]
#[cfg(feature = "unstable-interface")]
fn account_reentrance_count_works() {
const CODE: &str = r#"
(module
(import "__unstable__" "account_reentrance_count" (func $account_reentrance_count (param i32) (result i32)))
(import "env" "memory" (memory 1 1))
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
(local $return_val i32)
(set_local $return_val
(call $account_reentrance_count (i32.const 0))
)
(call $assert
(i32.eq (get_local $return_val) (i32.const 12))
)
)
(func (export "deploy"))
)
"#;
let mut mock_ext = MockExt::default();
execute(CODE, vec![], &mut mock_ext).unwrap();
}
}
@@ -251,6 +251,12 @@ pub enum RuntimeCosts {
SetCodeHash,
/// Weight of calling `ecdsa_to_eth_address`
EcdsaToEthAddress,
/// Weight of calling `seal_reentrant_count`
#[cfg(feature = "unstable-interface")]
ReentrantCount,
/// Weight of calling `seal_account_reentrance_count`
#[cfg(feature = "unstable-interface")]
AccountEntranceCount,
}
impl RuntimeCosts {
@@ -330,6 +336,10 @@ impl RuntimeCosts {
CallRuntime(weight) => weight.ref_time(),
SetCodeHash => s.set_code_hash,
EcdsaToEthAddress => s.ecdsa_to_eth_address,
#[cfg(feature = "unstable-interface")]
ReentrantCount => s.reentrant_count,
#[cfg(feature = "unstable-interface")]
AccountEntranceCount => s.account_reentrance_count,
};
RuntimeToken {
#[cfg(test)]
@@ -1188,6 +1198,7 @@ pub mod env {
Ok(ReturnCode::KeyNotFound)
}
}
/// Transfer some value to another account.
///
/// # Parameters
@@ -1354,6 +1365,7 @@ pub mod env {
output_len_ptr,
)
}
/// Instantiate a contract with the specified code hash.
///
/// # Deprecation
@@ -2444,4 +2456,34 @@ pub mod env {
Err(_) => Ok(ReturnCode::EcdsaRecoverFailed),
}
}
/// Returns the number of times the currently executing contract exists on the call stack in
/// addition to the calling instance.
///
/// # Return Value
///
/// Returns 0 when there is no reentrancy.
#[unstable]
fn reentrant_count(ctx: Runtime<E>) -> Result<u32, TrapReason> {
ctx.charge_gas(RuntimeCosts::ReentrantCount)?;
Ok(ctx.ext.reentrant_count())
}
/// Returns the number of times specified contract exists on the call stack. Delegated calls are
/// not counted as separate calls.
///
/// # Parameters
///
/// - `account_ptr`: a pointer to the contract address.
///
/// # Return Value
///
/// Returns 0 when the contract does not exist on the call stack.
#[unstable]
fn account_reentrance_count(ctx: Runtime<E>, account_ptr: u32) -> Result<u32, TrapReason> {
ctx.charge_gas(RuntimeCosts::AccountEntranceCount)?;
let account_id: <<E as Ext>::T as frame_system::Config>::AccountId =
ctx.read_sandbox_memory_as(account_ptr)?;
Ok(ctx.ext.account_reentrance_count(&account_id))
}
}
+51 -1
View File
@@ -109,6 +109,8 @@ pub trait WeightInfo {
fn seal_ecdsa_recover(r: u32, ) -> Weight;
fn seal_ecdsa_to_eth_address(r: u32, ) -> Weight;
fn seal_set_code_hash(r: u32, ) -> Weight;
fn seal_reentrant_count(r: u32, ) -> Weight;
fn seal_account_reentrance_count(r: u32, ) -> Weight;
fn instr_i64const(r: u32, ) -> Weight;
fn instr_i64load(r: u32, ) -> Weight;
fn instr_i64store(r: u32, ) -> Weight;
@@ -1020,6 +1022,30 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
.saturating_add(T::DbWeight::get().writes(3 as u64))
.saturating_add(T::DbWeight::get().writes((150 as u64).saturating_mul(r as u64)))
}
// Storage: System Account (r:1 w:0)
// Storage: Contracts ContractInfoOf (r:1 w:1)
// Storage: Contracts CodeStorage (r:1 w:0)
// Storage: Timestamp Now (r:1 w:0)
/// The range of component `r` is `[0, 20]`.
fn seal_reentrant_count(r: u32, ) -> Weight {
Weight::from_ref_time(304_709_000 as u64)
// Standard Error: 67_000
.saturating_add(Weight::from_ref_time(15_411_000 as u64).saturating_mul(r as u64))
.saturating_add(T::DbWeight::get().reads(4 as u64))
.saturating_add(T::DbWeight::get().writes(1 as u64))
}
// Storage: System Account (r:1 w:0)
// Storage: Contracts ContractInfoOf (r:1 w:1)
// Storage: Contracts CodeStorage (r:1 w:0)
// Storage: Timestamp Now (r:1 w:0)
/// The range of component `r` is `[0, 20]`.
fn seal_account_reentrance_count(r: u32, ) -> Weight {
Weight::from_ref_time(328_378_000 as u64)
// Standard Error: 137_000
.saturating_add(Weight::from_ref_time(37_448_000 as u64).saturating_mul(r as u64))
.saturating_add(T::DbWeight::get().reads(4 as u64))
.saturating_add(T::DbWeight::get().writes(1 as u64))
}
/// The range of component `r` is `[0, 50]`.
fn instr_i64const(r: u32, ) -> Weight {
// Minimum execution time: 69_022 nanoseconds.
@@ -2236,6 +2262,30 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().writes(3 as u64))
.saturating_add(RocksDbWeight::get().writes((150 as u64).saturating_mul(r as u64)))
}
// Storage: System Account (r:1 w:0)
// Storage: Contracts ContractInfoOf (r:1 w:1)
// Storage: Contracts CodeStorage (r:1 w:0)
// Storage: Timestamp Now (r:1 w:0)
/// The range of component `r` is `[0, 20]`.
fn seal_reentrant_count(r: u32, ) -> Weight {
Weight::from_ref_time(304_709_000 as u64)
// Standard Error: 67_000
.saturating_add(Weight::from_ref_time(15_411_000 as u64).saturating_mul(r as u64))
.saturating_add(RocksDbWeight::get().reads(4 as u64))
.saturating_add(RocksDbWeight::get().writes(1 as u64))
}
// Storage: System Account (r:1 w:0)
// Storage: Contracts ContractInfoOf (r:1 w:1)
// Storage: Contracts CodeStorage (r:1 w:0)
// Storage: Timestamp Now (r:1 w:0)
/// The range of component `r` is `[0, 20]`.
fn seal_account_reentrance_count(r: u32, ) -> Weight {
Weight::from_ref_time(328_378_000 as u64)
// Standard Error: 137_000
.saturating_add(Weight::from_ref_time(37_448_000 as u64).saturating_mul(r as u64))
.saturating_add(RocksDbWeight::get().reads(4 as u64))
.saturating_add(RocksDbWeight::get().writes(1 as u64))
}
/// The range of component `r` is `[0, 50]`.
fn instr_i64const(r: u32, ) -> Weight {
// Minimum execution time: 69_022 nanoseconds.
@@ -2593,4 +2643,4 @@ impl WeightInfo for () {
// Standard Error: 986
.saturating_add(Weight::from_ref_time(1_867_001 as u64).saturating_mul(r as u64))
}
}
}