Skip to content

Commit d06a6a7

Browse files
agryaznovatheiHCastanoParity Bot
authored andcommitted
contracts: is_contract(address) and caller_is_origin() are added to API (paritytech#10789)
* is_contract() and caller_is_origin() added to Ext API * is_contract() exposed in wasm runtime.rs * + test for is_contract() * + seal_is_contract benchmark * caller_is_origin() exposed to wasm/runtime.rs and covered by a test * + seal_caller_is_origin benchmark * Update frame/contracts/src/exec.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Update frame/contracts/src/exec.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Update frame/contracts/src/exec.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Update frame/contracts/src/wasm/runtime.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Update frame/contracts/src/wasm/runtime.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Update frame/contracts/src/wasm/runtime.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * Update frame/contracts/src/exec.rs Co-authored-by: Alexander Theißen <alex.theissen@me.com> * identation fix for benchmark macroses; test cosmetic improvement * benchmark fix * + is_contract() wasm test * + caller_is_origin() wasm test * Apply suggestions from code review Co-authored-by: Alexander Theißen <alex.theissen@me.com> * is_contract() to borrow param instead of taking ownership * phrasing improved Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com> * fixed wasm tests according to @athei feedback * dead code warnings suppressed by unstable-interface attributes * 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 Co-authored-by: Alexander Theißen <alex.theissen@me.com> Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com> Co-authored-by: Parity Bot <admin@parity.io>
1 parent 9d2bbbf commit d06a6a7

File tree

6 files changed

+917
-604
lines changed

6 files changed

+917
-604
lines changed

frame/contracts/src/benchmarking/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,63 @@ benchmarks! {
398398
let origin = RawOrigin::Signed(instance.caller.clone());
399399
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
400400

401+
seal_is_contract {
402+
let r in 0 .. API_BENCHMARK_BATCHES;
403+
let accounts = (0 .. r * API_BENCHMARK_BATCH_SIZE)
404+
.map(|n| account::<T::AccountId>("account", n, 0))
405+
.collect::<Vec<_>>();
406+
let account_len = accounts.get(0).map(|i| i.encode().len()).unwrap_or(0);
407+
let accounts_bytes = accounts.iter().map(|a| a.encode()).flatten().collect::<Vec<_>>();
408+
let code = WasmModule::<T>::from(ModuleDefinition {
409+
memory: Some(ImportedMemory::max::<T>()),
410+
imported_functions: vec![ImportedFunction {
411+
module: "__unstable__",
412+
name: "seal_is_contract",
413+
params: vec![ValueType::I32],
414+
return_type: Some(ValueType::I32),
415+
}],
416+
data_segments: vec![
417+
DataSegment {
418+
offset: 0,
419+
value: accounts_bytes
420+
},
421+
],
422+
call_body: Some(body::repeated_dyn(r * API_BENCHMARK_BATCH_SIZE, vec![
423+
Counter(0, account_len as u32), // address_ptr
424+
Regular(Instruction::Call(0)),
425+
Regular(Instruction::Drop),
426+
])),
427+
.. Default::default()
428+
});
429+
let instance = Contract::<T>::new(code, vec![])?;
430+
let info = instance.info()?;
431+
// every account would be a contract (worst case)
432+
for acc in accounts.iter() {
433+
<ContractInfoOf<T>>::insert(acc, info.clone());
434+
}
435+
let origin = RawOrigin::Signed(instance.caller.clone());
436+
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
437+
438+
seal_caller_is_origin {
439+
let r in 0 .. API_BENCHMARK_BATCHES;
440+
let code = WasmModule::<T>::from(ModuleDefinition {
441+
memory: Some(ImportedMemory::max::<T>()),
442+
imported_functions: vec![ImportedFunction {
443+
module: "__unstable__",
444+
name: "seal_caller_is_origin",
445+
params: vec![],
446+
return_type: Some(ValueType::I32),
447+
}],
448+
call_body: Some(body::repeated(r * API_BENCHMARK_BATCH_SIZE, &[
449+
Instruction::Call(0),
450+
Instruction::Drop,
451+
])),
452+
.. Default::default()
453+
});
454+
let instance = Contract::<T>::new(code, vec![])?;
455+
let origin = RawOrigin::Signed(instance.caller.clone());
456+
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
457+
401458
seal_address {
402459
let r in 0 .. API_BENCHMARK_BATCHES;
403460
let instance = Contract::<T>::new(WasmModule::getter(

frame/contracts/src/exec.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ pub trait Ext: sealing::Sealed {
158158
/// Returns a reference to the account id of the caller.
159159
fn caller(&self) -> &AccountIdOf<Self::T>;
160160

161+
/// Check if a contract lives at the specified `address`.
162+
fn is_contract(&self, address: &AccountIdOf<Self::T>) -> bool;
163+
164+
/// Check if the caller of the current contract is the origin of the whole call stack.
165+
///
166+
/// This can be checked with `is_contract(self.caller())` as well.
167+
/// However, this function does not require any storage lookup and therefore uses less weight.
168+
fn caller_is_origin(&self) -> bool;
169+
161170
/// Returns a reference to the account id of the current contract.
162171
fn address(&self) -> &AccountIdOf<Self::T>;
163172

@@ -483,7 +492,7 @@ where
483492
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
484493
E: Executable<T>,
485494
{
486-
/// Create an run a new call stack by calling into `dest`.
495+
/// Create and run a new call stack by calling into `dest`.
487496
///
488497
/// # Note
489498
///
@@ -1024,6 +1033,14 @@ where
10241033
self.frames().nth(1).map(|f| &f.account_id).unwrap_or(&self.origin)
10251034
}
10261035

1036+
fn is_contract(&self, address: &T::AccountId) -> bool {
1037+
ContractInfoOf::<T>::contains_key(&address)
1038+
}
1039+
1040+
fn caller_is_origin(&self) -> bool {
1041+
self.caller() == &self.origin
1042+
}
1043+
10271044
fn balance(&self) -> BalanceOf<T> {
10281045
T::Currency::free_balance(&self.top_frame().account_id)
10291046
}
@@ -1620,6 +1637,70 @@ mod tests {
16201637
WITNESSED_CALLER_CHARLIE.with(|caller| assert_eq!(*caller.borrow(), Some(dest)));
16211638
}
16221639

1640+
#[test]
1641+
fn is_contract_returns_proper_values() {
1642+
let bob_ch = MockLoader::insert(Call, |ctx, _| {
1643+
// Verify that BOB is a contract
1644+
assert!(ctx.ext.is_contract(&BOB));
1645+
// Verify that ALICE is not a contract
1646+
assert!(!ctx.ext.is_contract(&ALICE));
1647+
exec_success()
1648+
});
1649+
1650+
ExtBuilder::default().build().execute_with(|| {
1651+
let schedule = <Test as Config>::Schedule::get();
1652+
place_contract(&BOB, bob_ch);
1653+
1654+
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(0), 0).unwrap();
1655+
let result = MockStack::run_call(
1656+
ALICE,
1657+
BOB,
1658+
&mut GasMeter::<Test>::new(GAS_LIMIT),
1659+
&mut storage_meter,
1660+
&schedule,
1661+
0,
1662+
vec![],
1663+
None,
1664+
);
1665+
assert_matches!(result, Ok(_));
1666+
});
1667+
}
1668+
1669+
#[test]
1670+
fn caller_is_origin_returns_proper_values() {
1671+
let code_charlie = MockLoader::insert(Call, |ctx, _| {
1672+
// BOB is not the origin of the stack call
1673+
assert!(!ctx.ext.caller_is_origin());
1674+
exec_success()
1675+
});
1676+
1677+
let code_bob = MockLoader::insert(Call, |ctx, _| {
1678+
// ALICE is the origin of the call stack
1679+
assert!(ctx.ext.caller_is_origin());
1680+
// BOB calls CHARLIE
1681+
ctx.ext.call(0, CHARLIE, 0, vec![], true)
1682+
});
1683+
1684+
ExtBuilder::default().build().execute_with(|| {
1685+
let schedule = <Test as Config>::Schedule::get();
1686+
place_contract(&BOB, code_bob);
1687+
place_contract(&CHARLIE, code_charlie);
1688+
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(0), 0).unwrap();
1689+
// ALICE -> BOB (caller is origin) -> CHARLIE (caller is not origin)
1690+
let result = MockStack::run_call(
1691+
ALICE,
1692+
BOB,
1693+
&mut GasMeter::<Test>::new(GAS_LIMIT),
1694+
&mut storage_meter,
1695+
&schedule,
1696+
0,
1697+
vec![0],
1698+
None,
1699+
);
1700+
assert_matches!(result, Ok(_));
1701+
});
1702+
}
1703+
16231704
#[test]
16241705
fn address_returns_proper_values() {
16251706
let bob_ch = MockLoader::insert(Call, |ctx, _| {

frame/contracts/src/schedule.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ pub struct HostFnWeights<T: Config> {
256256
/// Weight of calling `seal_caller`.
257257
pub caller: Weight,
258258

259+
/// Weight of calling `seal_is_contract`.
260+
pub is_contract: Weight,
261+
262+
/// Weight of calling `seal_caller_is_origin`.
263+
pub caller_is_origin: Weight,
264+
259265
/// Weight of calling `seal_address`.
260266
pub address: Weight,
261267

@@ -571,6 +577,8 @@ impl<T: Config> Default for HostFnWeights<T> {
571577
fn default() -> Self {
572578
Self {
573579
caller: cost_batched!(seal_caller),
580+
is_contract: cost_batched!(seal_is_contract),
581+
caller_is_origin: cost_batched!(seal_caller_is_origin),
574582
address: cost_batched!(seal_address),
575583
gas_left: cost_batched!(seal_gas_left),
576584
balance: cost_batched!(seal_balance),

frame/contracts/src/wasm/mod.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ mod tests {
409409
fn caller(&self) -> &AccountIdOf<Self::T> {
410410
&ALICE
411411
}
412+
fn is_contract(&self, _address: &AccountIdOf<Self::T>) -> bool {
413+
true
414+
}
415+
fn caller_is_origin(&self) -> bool {
416+
false
417+
}
412418
fn address(&self) -> &AccountIdOf<Self::T> {
413419
&BOB
414420
}
@@ -2240,4 +2246,80 @@ mod tests {
22402246
let result = execute(CODE, [2u8; 32].encode(), &mut ext).unwrap();
22412247
assert_eq!(u32::from_le_bytes(result.data.0.try_into().unwrap()), 0,);
22422248
}
2249+
2250+
#[test]
2251+
#[cfg(feature = "unstable-interface")]
2252+
fn is_contract_works() {
2253+
const CODE_IS_CONTRACT: &str = r#"
2254+
;; This runs `is_contract` check on zero account address
2255+
(module
2256+
(import "__unstable__" "seal_is_contract" (func $seal_is_contract (param i32) (result i32)))
2257+
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
2258+
(import "env" "memory" (memory 1 1))
2259+
2260+
;; [0, 32) zero-adress
2261+
(data (i32.const 0)
2262+
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
2263+
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
2264+
)
2265+
2266+
;; [32, 36) here we store the return code of the `seal_is_contract`
2267+
2268+
(func (export "deploy"))
2269+
2270+
(func (export "call")
2271+
(i32.store
2272+
(i32.const 32)
2273+
(call $seal_is_contract
2274+
(i32.const 0) ;; ptr to destination address
2275+
)
2276+
)
2277+
;; exit with success and take `seal_is_contract` return code to the output buffer
2278+
(call $seal_return (i32.const 0) (i32.const 32) (i32.const 4))
2279+
)
2280+
)
2281+
"#;
2282+
let output = execute(CODE_IS_CONTRACT, vec![], MockExt::default()).unwrap();
2283+
2284+
// The mock ext just always returns 1u32 (`true`).
2285+
assert_eq!(
2286+
output,
2287+
ExecReturnValue { flags: ReturnFlags::empty(), data: Bytes(1u32.encode()) },
2288+
);
2289+
}
2290+
2291+
#[test]
2292+
#[cfg(feature = "unstable-interface")]
2293+
fn caller_is_origin_works() {
2294+
const CODE_CALLER_IS_ORIGIN: &str = r#"
2295+
;; This runs `caller_is_origin` check on zero account address
2296+
(module
2297+
(import "__unstable__" "seal_caller_is_origin" (func $seal_caller_is_origin (result i32)))
2298+
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
2299+
(import "env" "memory" (memory 1 1))
2300+
2301+
;; [0, 4) here the return code of the `seal_caller_is_origin` will be stored
2302+
;; we initialize it with non-zero value to be sure that it's being overwritten below
2303+
(data (i32.const 0) "\10\10\10\10")
2304+
2305+
(func (export "deploy"))
2306+
2307+
(func (export "call")
2308+
(i32.store
2309+
(i32.const 0)
2310+
(call $seal_caller_is_origin)
2311+
)
2312+
;; exit with success and take `seal_caller_is_origin` return code to the output buffer
2313+
(call $seal_return (i32.const 0) (i32.const 0) (i32.const 4))
2314+
)
2315+
)
2316+
"#;
2317+
let output = execute(CODE_CALLER_IS_ORIGIN, vec![], MockExt::default()).unwrap();
2318+
2319+
// The mock ext just always returns 0u32 (`false`)
2320+
assert_eq!(
2321+
output,
2322+
ExecReturnValue { flags: ReturnFlags::empty(), data: Bytes(0u32.encode()) },
2323+
);
2324+
}
22432325
}

frame/contracts/src/wasm/runtime.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ pub enum RuntimeCosts {
138138
MeteringBlock(u32),
139139
/// Weight of calling `seal_caller`.
140140
Caller,
141+
/// Weight of calling `seal_is_contract`.
142+
#[cfg(feature = "unstable-interface")]
143+
IsContract,
144+
/// Weight of calling `seal_caller_is_origin`.
145+
#[cfg(feature = "unstable-interface")]
146+
CallerIsOrigin,
141147
/// Weight of calling `seal_address`.
142148
Address,
143149
/// Weight of calling `seal_gas_left`.
@@ -225,6 +231,10 @@ impl RuntimeCosts {
225231
let weight = match *self {
226232
MeteringBlock(amount) => s.gas.saturating_add(amount.into()),
227233
Caller => s.caller,
234+
#[cfg(feature = "unstable-interface")]
235+
IsContract => s.is_contract,
236+
#[cfg(feature = "unstable-interface")]
237+
CallerIsOrigin => s.caller_is_origin,
228238
Address => s.address,
229239
GasLeft => s.gas_left,
230240
Balance => s.balance,
@@ -1254,6 +1264,37 @@ define_env!(Env, <E: Ext>,
12541264
)?)
12551265
},
12561266

1267+
// Checks whether a specified address belongs to a contract.
1268+
//
1269+
// # Parameters
1270+
//
1271+
// - account_ptr: a pointer to the address of the beneficiary account
1272+
// Should be decodable as an `T::AccountId`. Traps otherwise.
1273+
//
1274+
// Returned value is a u32-encoded boolean: (0 = false, 1 = true).
1275+
[__unstable__] seal_is_contract(ctx, account_ptr: u32) -> u32 => {
1276+
ctx.charge_gas(RuntimeCosts::IsContract)?;
1277+
let address: <<E as Ext>::T as frame_system::Config>::AccountId =
1278+
ctx.read_sandbox_memory_as(account_ptr)?;
1279+
1280+
Ok(ctx.ext.is_contract(&address) as u32)
1281+
},
1282+
1283+
// Checks whether the caller of the current contract is the origin of the whole call stack.
1284+
//
1285+
// Prefer this over `seal_is_contract` when checking whether your contract is being called by a contract
1286+
// or a plain account. The reason is that it performs better since it does not need to
1287+
// do any storage lookups.
1288+
//
1289+
// A return value of`true` indicates that this contract is being called by a plain account
1290+
// and `false` indicates that the caller is another contract.
1291+
//
1292+
// Returned value is a u32-encoded boolean: (0 = false, 1 = true).
1293+
[__unstable__] seal_caller_is_origin(ctx) -> u32 => {
1294+
ctx.charge_gas(RuntimeCosts::CallerIsOrigin)?;
1295+
Ok(ctx.ext.caller_is_origin() as u32)
1296+
},
1297+
12571298
// Stores the address of the current contract into the supplied buffer.
12581299
//
12591300
// The value is stored to linear memory at the address pointed to by `out_ptr`.

0 commit comments

Comments
 (0)