Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #5 from Supercolony-net/seal_reentrant_count
Browse files Browse the repository at this point in the history
`seal_reentrant_count`
  • Loading branch information
yarikbratashchuk authored May 29, 2022
2 parents 6386eea + 0a70239 commit 523a683
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 0 deletions.
83 changes: 83 additions & 0 deletions frame/contracts/fixtures/reentrant_count_call.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
(module
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "seal0" "seal_delegate_call" (func $seal_delegate_call (param i32 i32 i32 i32 i32 i32) (result i32)))
(import "__unstable__" "seal_reentrant_count" (func $seal_reentrant_count (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")
(local $exit_code i32)
(local $reentrant_count i32)

(set_local $reentrant_count
(call $seal_reentrant_count)
)

(get_local $reentrant_count)
(if
(then
;; assert reentrant_count == 1
(call $assert
(i32.eq (get_local $reentrant_count) (i32.const 1))
)

;; Delegated call to itself
(set_local $exit_code
(call $seal_delegate_call
(i32.const 0) ;; Set no call flags (reentrance is forbidden)
(i32.const 0) ;; Pointer to "callee" code_hash.
(i32.const 0) ;; Input is ignored
(i32.const 0) ;; Length of the input
(i32.const 4294967295) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
)
)

;; Second reentrance is forbidden
(call $assert
(i32.eq (get_local $exit_code) (i32.const 1))
)
)
(else
;; Reading "callee" contract address (which is the address of the caller)
(call $seal_input (i32.const 0) (i32.const 32))

;; Call to itself
(set_local $exit_code
(call $seal_call
(i32.const 0) ;; 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 0) ;; Pointer to the buffer with value to transfer
(i32.const 0) ;; Length of the buffer with value to transfer.
(i32.const 0) ;; Pointer to input data buffer address
(i32.const 32) ;; Length of input data buffer
(i32.const 0xffffffff) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Ptr to output buffer len
)
)

;; Check for status code 1, due to reentrance in delegated call.
(call $assert
(i32.eq (get_local $exit_code) (i32.const 1)) ;; ReturnCode::ContractTrapped
)
)
)
)

(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 @@ -2055,6 +2055,59 @@ benchmarks! {
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])

seal_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: "seal_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![])

seal_account_entrance_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: "seal_account_entrance_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 as u32, 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
18 changes: 18 additions & 0 deletions frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,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 then number of times 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 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_entrance_count(&self, account_id: &AccountIdOf<Self::T>) -> u32;
}

/// Describes the different functions that can be exported by an [`Executable`].
Expand Down Expand Up @@ -1227,6 +1236,15 @@ where
});
Ok(())
}

fn reentrant_count(&self) -> u32 {
let id: &AccountIdOf<Self::T> = &self.top_frame().account_id;
self.account_entrance_count(id) - 1u32
}

fn account_entrance_count(&self, account_id: &AccountIdOf<Self::T>) -> u32 {
self.frames().filter_map(|f| Some(f.delegate_caller.is_none() && &f.account_id == account_id)).count() as u32
}
}

fn deposit_event<T: Config>(topics: Vec<T::Hash>, event: Event<T>) {
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 @@ -416,6 +416,12 @@ pub struct HostFnWeights<T: Config> {
/// Weight of calling `seal_ecdsa_to_eth_address`.
pub ecdsa_to_eth_address: Weight,

/// Weight of calling `seal_reentrant_count`.
pub reentrant_count: Weight,

/// Weight of calling `seal_account_entrance_count`.
pub account_entrance_count: Weight,

/// The type parameter is used in the default implementation.
#[codec(skip)]
pub _phantom: PhantomData<T>,
Expand Down Expand Up @@ -651,6 +657,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_entrance_count: cost_batched!(seal_account_entrance_count),
_phantom: PhantomData,
}
}
Expand Down
33 changes: 33 additions & 0 deletions frame/contracts/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3228,3 +3228,36 @@ fn set_code_hash() {
);
});
}

#[test]
#[cfg(feature = "unstable-interface")]
fn reentrant_count() {
let (wasm1, code_hash1) = compile_module::<Test>("reentrant_count_call").unwrap();
let contract_addr1 = Contracts::contract_address(&ALICE, &code_hash1, &[]);

ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);

assert_ok!(Contracts::instantiate_with_code(
Origin::signed(ALICE),
300_000,
GAS_LIMIT,
None,
wasm1,
vec![],
vec![],
));

Contracts::bare_call(
ALICE,
contract_addr1.clone(),
0,
GAS_LIMIT,
None,
AsRef::<[u8]>::as_ref(&contract_addr1).to_vec(),
true,
)
.result
.unwrap();
});
}
37 changes: 37 additions & 0 deletions frame/contracts/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ 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_entrance_count(&self, _account_id: &AccountIdOf<Self::T>) -> u32 { unimplemented!() }
}

fn execute<E: BorrowMut<MockExt>>(wat: &str, input_data: Vec<u8>, mut ext: E) -> ExecResult {
Expand Down Expand Up @@ -2594,4 +2596,39 @@ mod tests {

assert_eq!(mock_ext.code_hashes.pop().unwrap(), H256::from_slice(&[17u8; 32]));
}

#[test]
#[cfg(feature = "unstable-interface")]
fn reentrant_count() {
const CODE: &str = r#"
(module
(import "__unstable__" "seal_reentrant_count" (func $seal_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 $exit_code i32)
(set_local $exit_code
(call $seal_reentrant_count)
)
(call $assert
(i32.eq (get_local $exit_code) (i32.const 12))
)
)
(func (export "deploy"))
)
"#;

let mut mock_ext = MockExt::default();
execute(CODE, vec![], &mut mock_ext).unwrap();

assert_eq!(mock_ext.reentrant_count.pop().unwrap(), 12u8);
}
}
26 changes: 26 additions & 0 deletions frame/contracts/src/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,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_entrance_count`
#[cfg(feature = "unstable-interface")]
AccountEntranceCount,
}

impl RuntimeCosts {
Expand Down Expand Up @@ -302,6 +308,10 @@ impl RuntimeCosts {
CallRuntime(weight) => weight,
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_entrance_count,
};
RuntimeToken {
#[cfg(test)]
Expand Down Expand Up @@ -2076,4 +2086,20 @@ define_env!(Env, <E: Ext>,
Err(_) => Ok(ReturnCode::EcdsaRecoverFailed),
}
},

// Returns then number of times currently executing contract exists on the call stack in addition
// to the calling instance. A value of 0 means no reentrancy.
[__unstable__] seal_reentrant_count(ctx) -> u32 => {
ctx.charge_gas(RuntimeCosts::ReentrantCount)?;
Ok(ctx.ext.reentrant_count() as u32)
},

// Returns the number of times specified contract exists on the call stack. Delegated calls are
// not calculated as separate calls.
// A value of 0 means it does not exist on the stack.
[__unstable__] seal_account_entrance_count(ctx, account_ptr: u32) -> u32 => {
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_entrance_count(account_id) as u32)
},
);
48 changes: 48 additions & 0 deletions frame/contracts/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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_entrance_count(r: u32, ) -> Weight;
fn instr_i64const(r: u32, ) -> Weight;
fn instr_i64load(r: u32, ) -> Weight;
fn instr_i64store(r: u32, ) -> Weight;
Expand Down Expand Up @@ -836,6 +838,29 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
.saturating_add(T::DbWeight::get().reads((79 as Weight).saturating_mul(r as Weight)))
.saturating_add(T::DbWeight::get().writes((79 as Weight).saturating_mul(r as Weight)))
}
// 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)
// Storage: unknown [0x3a7472616e73616374696f6e5f6c6576656c3a] (r:1 w:1)
fn seal_reentrant_count(r: u32, ) -> Weight {
(91_522_000 as Weight)
// Standard Error: 50_000
.saturating_add((24_064_000 as Weight).saturating_mul(r as Weight))
.saturating_add(T::DbWeight::get().reads(5 as Weight))
.saturating_add(T::DbWeight::get().writes(2 as Weight))
}
// 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)
fn seal_account_entrance_count(r: u32, ) -> Weight {
(179_313_000 as Weight)
// Standard Error: 610_000
.saturating_add((63_085_000 as Weight).saturating_mul(r as Weight))
.saturating_add(T::DbWeight::get().reads(4 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn instr_i64const(r: u32, ) -> Weight {
(74_627_000 as Weight)
// Standard Error: 1_000
Expand Down Expand Up @@ -1768,6 +1793,29 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().reads((79 as Weight).saturating_mul(r as Weight)))
.saturating_add(RocksDbWeight::get().writes((79 as Weight).saturating_mul(r as Weight)))
}
// 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)
// Storage: unknown [0x3a7472616e73616374696f6e5f6c6576656c3a] (r:1 w:1)
fn seal_reentrant_count(r: u32, ) -> Weight {
(91_522_000 as Weight)
// Standard Error: 50_000
.saturating_add((24_064_000 as Weight).saturating_mul(r as Weight))
.saturating_add(RocksDbWeight::get().reads(5 as Weight))
.saturating_add(RocksDbWeight::get().writes(2 as Weight))
}
// 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)
fn seal_account_entrance_count(r: u32, ) -> Weight {
(179_313_000 as Weight)
// Standard Error: 610_000
.saturating_add((63_085_000 as Weight).saturating_mul(r as Weight))
.saturating_add(RocksDbWeight::get().reads(4 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn instr_i64const(r: u32, ) -> Weight {
(74_627_000 as Weight)
// Standard Error: 1_000
Expand Down

0 comments on commit 523a683

Please sign in to comment.