Skip to content

Commit

Permalink
seal_reentrant_count returns contract reentrant count (paritytech#1…
Browse files Browse the repository at this point in the history
…2695)

* 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>
  • Loading branch information
2 people authored and ark0f committed Feb 27, 2023
1 parent e124365 commit 4440884
Show file tree
Hide file tree
Showing 10 changed files with 569 additions and 1 deletion.
37 changes: 37 additions & 0 deletions frame/contracts/fixtures/account_reentrance_count_call.wat
Original file line number Diff line number Diff line change
@@ -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"))

)
76 changes: 76 additions & 0 deletions frame/contracts/fixtures/reentrant_count_call.wat
Original file line number Diff line number Diff line change
@@ -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"))
)
71 changes: 71 additions & 0 deletions frame/contracts/fixtures/reentrant_count_delegated_call.wat
Original file line number Diff line number Diff line change
@@ -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"))
)
53 changes: 53 additions & 0 deletions frame/contracts/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions frame/contracts/src/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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,
}
}
Expand Down
Loading

0 comments on commit 4440884

Please sign in to comment.