contracts: add seal_code_hash and seal_own_code_hash to API (#10933)

* `seal_origin` + tests added

* `seal_origin` benchmark added

* `seal_code_hash` + tests added

* `seal_code_hash` benchmark added

* `seal_own_code_hash` + tests added

* `seal_own_code_hash` benchmark added

* fmt lil fix

* akward accident bug fix

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* benchmark fix

* `WasmModule::getter()` to take `module_name` arg

* test enhanced

* fixes based on review feedback

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* Hash left as const to return a ref to it from mock

* HASH test val to local const in mock

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* fixes to benchmarks according to review feedback

* cargo run --quiet --profile=production  --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* removed `seal_origin` from API

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: Parity Bot <admin@parity.io>
This commit is contained in:
Alexander Gryaznov
2022-03-29 17:14:49 +02:00
committed by GitHub
parent 83d4be6151
commit e4caf7388a
7 changed files with 995 additions and 658 deletions
@@ -339,12 +339,12 @@ where
/// Creates a wasm module that calls the imported function named `getter_name` `repeat`
/// times. The imported function is expected to have the "getter signature" of
/// (out_ptr: u32, len_ptr: u32) -> ().
pub fn getter(getter_name: &'static str, repeat: u32) -> Self {
pub fn getter(module_name: &'static str, getter_name: &'static str, repeat: u32) -> Self {
let pages = max_pages::<T>();
ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "seal0",
module: module_name,
name: getter_name,
params: vec![ValueType::I32, ValueType::I32],
return_type: None,
@@ -394,7 +394,7 @@ benchmarks! {
seal_caller {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_caller", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_caller", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -436,6 +436,59 @@ benchmarks! {
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
seal_code_hash {
let r in 0 .. API_BENCHMARK_BATCHES;
let accounts = (0 .. r * API_BENCHMARK_BATCH_SIZE)
.map(|n| account::<T::AccountId>("account", n, 0))
.collect::<Vec<_>>();
let account_len = accounts.get(0).map(|i| i.encode().len()).unwrap_or(0);
let accounts_bytes = accounts.iter().map(|a| a.encode()).flatten().collect::<Vec<_>>();
let accounts_len = accounts_bytes.len();
let pages = code::max_pages::<T>();
let code = WasmModule::<T>::from(ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "__unstable__",
name: "seal_code_hash",
params: vec![ValueType::I32, ValueType::I32, ValueType::I32],
return_type: Some(ValueType::I32),
}],
data_segments: vec![
DataSegment {
offset: 0,
value: 32u32.to_le_bytes().to_vec(), // output length
},
DataSegment {
offset: 36,
value: accounts_bytes,
},
],
call_body: Some(body::repeated_dyn(r * API_BENCHMARK_BATCH_SIZE, vec![
Counter(36, account_len as u32), // address_ptr
Regular(Instruction::I32Const(4)), // ptr to output data
Regular(Instruction::I32Const(0)), // ptr to output length
Regular(Instruction::Call(0)),
Regular(Instruction::Drop),
])),
.. Default::default()
});
let instance = Contract::<T>::new(code, vec![])?;
let info = instance.info()?;
// every account would be a contract (worst case)
for acc in accounts.iter() {
<ContractInfoOf<T>>::insert(acc, info.clone());
}
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
seal_own_code_hash {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"__unstable__", "seal_own_code_hash", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
seal_caller_is_origin {
let r in 0 .. API_BENCHMARK_BATCHES;
let code = WasmModule::<T>::from(ModuleDefinition {
@@ -459,7 +512,7 @@ benchmarks! {
seal_address {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_address", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_address", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -467,7 +520,7 @@ benchmarks! {
seal_gas_left {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_gas_left", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_gas_left", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -475,7 +528,7 @@ benchmarks! {
seal_balance {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_balance", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_balance", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -483,7 +536,7 @@ benchmarks! {
seal_value_transferred {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_value_transferred", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_value_transferred", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -491,7 +544,7 @@ benchmarks! {
seal_minimum_balance {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_minimum_balance", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_minimum_balance", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -499,7 +552,7 @@ benchmarks! {
seal_block_number {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_block_number", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_block_number", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -507,7 +560,7 @@ benchmarks! {
seal_now {
let r in 0 .. API_BENCHMARK_BATCHES;
let instance = Contract::<T>::new(WasmModule::getter(
"seal_now", r * API_BENCHMARK_BATCH_SIZE
"seal0", "seal_now", r * API_BENCHMARK_BATCH_SIZE
), vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
@@ -2341,7 +2394,7 @@ benchmarks! {
}
// w_memory_grow = w_bench - 2 * w_param
// We can only allow allocate as much memory as it is allowed in a a contract.
// We can only allow allocate as much memory as it is allowed in a contract.
// Therefore the repeat count is limited by the maximum memory any contract can have.
// Using a contract with more memory will skew the benchmark because the runtime of grow
// depends on how much memory is already allocated.
+72
View File
@@ -162,6 +162,14 @@ pub trait Ext: sealing::Sealed {
/// Check if a contract lives at the specified `address`.
fn is_contract(&self, address: &AccountIdOf<Self::T>) -> bool;
/// Returns the code hash of the contract for the given `address`.
///
/// Returns `None` if the `address` does not belong to a contract.
fn code_hash(&self, address: &AccountIdOf<Self::T>) -> Option<CodeHash<Self::T>>;
/// Returns the code hash of the contract being executed.
fn own_code_hash(&mut self) -> &CodeHash<Self::T>;
/// Check if the caller of the current contract is the origin of the whole call stack.
///
/// This can be checked with `is_contract(self.caller())` as well.
@@ -1103,6 +1111,14 @@ where
ContractInfoOf::<T>::contains_key(&address)
}
fn code_hash(&self, address: &T::AccountId) -> Option<CodeHash<Self::T>> {
<ContractInfoOf<T>>::get(&address).map(|contract| contract.code_hash)
}
fn own_code_hash(&mut self) -> &CodeHash<Self::T> {
&self.top_frame_mut().contract_info().code_hash
}
fn caller_is_origin(&self) -> bool {
self.caller() == &self.origin
}
@@ -1753,6 +1769,62 @@ mod tests {
});
}
#[test]
fn code_hash_returns_proper_values() {
let code_bob = MockLoader::insert(Call, |ctx, _| {
// ALICE is not a contract and hence she does not have a code_hash
assert!(ctx.ext.code_hash(&ALICE).is_none());
// BOB is a contract and hence he has a code_hash
assert!(ctx.ext.code_hash(&BOB).is_some());
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let schedule = <Test as Config>::Schedule::get();
place_contract(&BOB, code_bob);
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(0), 0).unwrap();
// ALICE (not contract) -> BOB (contract)
let result = MockStack::run_call(
ALICE,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&mut storage_meter,
&schedule,
0,
vec![0],
None,
);
assert_matches!(result, Ok(_));
});
}
#[test]
fn own_code_hash_returns_proper_values() {
let bob_ch = MockLoader::insert(Call, |ctx, _| {
let code_hash = ctx.ext.code_hash(&BOB).unwrap();
assert_eq!(*ctx.ext.own_code_hash(), code_hash);
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let schedule = <Test as Config>::Schedule::get();
place_contract(&BOB, bob_ch);
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(0), 0).unwrap();
// ALICE (not contract) -> BOB (contract)
let result = MockStack::run_call(
ALICE,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&mut storage_meter,
&schedule,
0,
vec![0],
None,
);
assert_matches!(result, Ok(_));
});
}
#[test]
fn caller_is_origin_returns_proper_values() {
let code_charlie = MockLoader::insert(Call, |ctx, _| {
@@ -265,6 +265,12 @@ pub struct HostFnWeights<T: Config> {
/// Weight of calling `seal_is_contract`.
pub is_contract: Weight,
/// Weight of calling `seal_code_hash`.
pub code_hash: Weight,
/// Weight of calling `seal_own_code_hash`.
pub own_code_hash: Weight,
/// Weight of calling `seal_caller_is_origin`.
pub caller_is_origin: Weight,
@@ -584,6 +590,8 @@ impl<T: Config> Default for HostFnWeights<T> {
Self {
caller: cost_batched!(seal_caller),
is_contract: cost_batched!(seal_is_contract),
code_hash: cost_batched!(seal_code_hash),
own_code_hash: cost_batched!(seal_own_code_hash),
caller_is_origin: cost_batched!(seal_caller_is_origin),
address: cost_batched!(seal_address),
gas_left: cost_batched!(seal_gas_left),
+116 -4
View File
@@ -438,6 +438,13 @@ mod tests {
fn is_contract(&self, _address: &AccountIdOf<Self::T>) -> bool {
true
}
fn code_hash(&self, _address: &AccountIdOf<Self::T>) -> Option<CodeHash<Self::T>> {
Some(H256::from_slice(&[0x11; 32]))
}
fn own_code_hash(&mut self) -> &CodeHash<Self::T> {
const HASH: H256 = H256::repeat_byte(0x10);
&HASH
}
fn caller_is_origin(&self) -> bool {
false
}
@@ -1155,7 +1162,7 @@ mod tests {
);
}
/// calls `seal_caller` and compares the result with the constant 42.
/// calls `seal_caller` and compares the result with the constant (ALICE's address part).
const CODE_CALLER: &str = r#"
(module
(import "seal0" "seal_caller" (func $seal_caller (param i32 i32)))
@@ -1185,7 +1192,7 @@ mod tests {
)
)
;; assert that the first 64 byte are the beginning of "ALICE"
;; assert that the first 8 bytes are the beginning of "ALICE"
(call $assert
(i64.eq
(i64.load (i32.const 0))
@@ -1203,7 +1210,7 @@ mod tests {
assert_ok!(execute(CODE_CALLER, vec![], MockExt::default()));
}
/// calls `seal_address` and compares the result with the constant 69.
/// calls `seal_address` and compares the result with the constant (BOB's address part).
const CODE_ADDRESS: &str = r#"
(module
(import "seal0" "seal_address" (func $seal_address (param i32 i32)))
@@ -1233,7 +1240,7 @@ mod tests {
)
)
;; assert that the first 64 byte are the beginning of "BOB"
;; assert that the first 8 bytes are the beginning of "BOB"
(call $assert
(i64.eq
(i64.load (i32.const 0))
@@ -2361,6 +2368,111 @@ mod tests {
}
#[test]
#[cfg(feature = "unstable-interface")]
fn code_hash_works() {
/// calls `seal_code_hash` and compares the result with the constant.
const CODE_CODE_HASH: &str = r#"
(module
(import "__unstable__" "seal_code_hash" (func $seal_code_hash (param i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1))
;; size of our buffer is 32 bytes
(data (i32.const 32) "\20")
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
;; fill the buffer with the code hash.
(call $seal_code_hash
(i32.const 0) ;; input: address_ptr (before call)
(i32.const 0) ;; output: code_hash_ptr (after call)
(i32.const 32) ;; same 32 bytes length for input and output
)
;; assert size == 32
(call $assert
(i32.eq
(i32.load (i32.const 32))
(i32.const 32)
)
)
;; assert that the first 8 bytes are "1111111111111111"
(call $assert
(i64.eq
(i64.load (i32.const 0))
(i64.const 0x1111111111111111)
)
)
drop
)
(func (export "deploy"))
)
"#;
assert_ok!(execute(CODE_CODE_HASH, vec![], MockExt::default()));
}
#[test]
#[cfg(feature = "unstable-interface")]
fn own_code_hash_works() {
/// calls `seal_own_code_hash` and compares the result with the constant.
const CODE_OWN_CODE_HASH: &str = r#"
(module
(import "__unstable__" "seal_own_code_hash" (func $seal_own_code_hash (param i32 i32)))
(import "env" "memory" (memory 1 1))
;; size of our buffer is 32 bytes
(data (i32.const 32) "\20")
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "call")
;; fill the buffer with the code hash
(call $seal_own_code_hash
(i32.const 0) ;; output: code_hash_ptr
(i32.const 32) ;; 32 bytes length of code_hash output
)
;; assert size == 32
(call $assert
(i32.eq
(i32.load (i32.const 32))
(i32.const 32)
)
)
;; assert that the first 8 bytes are "1010101010101010"
(call $assert
(i64.eq
(i64.load (i32.const 0))
(i64.const 0x1010101010101010)
)
)
)
(func (export "deploy"))
)
"#;
assert_ok!(execute(CODE_OWN_CODE_HASH, vec![], MockExt::default()));
}
#[test]
#[cfg(feature = "unstable-interface")]
fn caller_is_origin_works() {
const CODE_CALLER_IS_ORIGIN: &str = r#"
;; This runs `caller_is_origin` check on zero account address
@@ -144,6 +144,12 @@ pub enum RuntimeCosts {
Caller,
/// Weight of calling `seal_is_contract`.
IsContract,
/// Weight of calling `seal_code_hash`.
#[cfg(feature = "unstable-interface")]
CodeHash,
/// Weight of calling `seal_own_code_hash`.
#[cfg(feature = "unstable-interface")]
OwnCodeHash,
/// Weight of calling `seal_caller_is_origin`.
CallerIsOrigin,
/// Weight of calling `seal_address`.
@@ -234,6 +240,10 @@ impl RuntimeCosts {
CopyToContract(len) => s.input_per_byte.saturating_mul(len.into()),
Caller => s.caller,
IsContract => s.is_contract,
#[cfg(feature = "unstable-interface")]
CodeHash => s.code_hash,
#[cfg(feature = "unstable-interface")]
OwnCodeHash => s.own_code_hash,
CallerIsOrigin => s.caller_is_origin,
Address => s.address,
GasLeft => s.gas_left,
@@ -1371,6 +1381,44 @@ define_env!(Env, <E: Ext>,
Ok(ctx.ext.is_contract(&address) as u32)
},
// Retrieve the code hash for a specified contract address.
//
// # Parameters
//
// - `account_ptr`: a pointer to the address in question.
// Should be decodable as an `T::AccountId`. Traps otherwise.
// - `out_ptr`: pointer to the linear memory where the returning value is written to.
// - `out_len_ptr`: in-out pointer into linear memory where the buffer length
// is read from and the value length is written to.
//
// # Errors
//
// `ReturnCode::KeyNotFound`
[__unstable__] seal_code_hash(ctx, account_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::CodeHash)?;
let address: <<E as Ext>::T as frame_system::Config>::AccountId =
ctx.read_sandbox_memory_as(account_ptr)?;
if let Some(value) = ctx.ext.code_hash(&address) {
ctx.write_sandbox_output(out_ptr, out_len_ptr, &value.encode(), false, already_charged)?;
Ok(ReturnCode::Success)
} else {
Ok(ReturnCode::KeyNotFound)
}
},
// Retrieve the code hash of the currently executing contract.
//
// # Parameters
//
// - `out_ptr`: pointer to the linear memory where the returning value is written to.
// - `out_len_ptr`: in-out pointer into linear memory where the buffer length
// is read from and the value length is written to.
[__unstable__] seal_own_code_hash(ctx, out_ptr: u32, out_len_ptr: u32) => {
ctx.charge_gas(RuntimeCosts::OwnCodeHash)?;
let code_hash_encoded = &ctx.ext.own_code_hash().encode();
Ok(ctx.write_sandbox_output(out_ptr, out_len_ptr, code_hash_encoded, false, already_charged)?)
},
// Checks whether the caller of the current contract is the origin of the whole call stack.
//
// Prefer this over `seal_is_contract` when checking whether your contract is being called by a contract
File diff suppressed because it is too large Load Diff