From 5f2d4beac44c2fdc88c077485e2df34d79538668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 11:17:54 +0200 Subject: [PATCH 01/12] Add example for cross-contract-call tracing --- Cargo.lock | 4 +- Cargo.toml | 3 +- .../cross-contract-call-tracing/Cargo.toml | 28 +++++++++ examples/cross-contract-call-tracing/lib.rs | 61 +++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 examples/cross-contract-call-tracing/Cargo.toml create mode 100644 examples/cross-contract-call-tracing/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4fba308..e87296d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2372,9 +2372,9 @@ dependencies = [ [[package]] name = "pallet-contracts-for-drink" -version = "21.0.1" +version = "21.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e9f6abd1ee4a2d6e9702a45a646b87260cc9d36886b750496888454471cda69" +checksum = "5f1071ed81959cbc593389d0f000fe6dbc7b1726af1c1580df162a6e94d0d0cb" dependencies = [ "bitflags", "environmental", diff --git a/Cargo.toml b/Cargo.toml index 7755a97..8630e62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ exclude = [ "examples/counter", "examples/flipper", + "examples/cross-contract-call-tracing", ] [workspace.package] @@ -37,7 +38,7 @@ wat = { version = "1.0.71" } frame-support = { version = "22.0.0" } frame-system = { version = "22.0.0" } pallet-balances = { version = "22.0.0" } -pallet-contracts = { package = "pallet-contracts-for-drink", version = "21.0.1" } +pallet-contracts = { package = "pallet-contracts-for-drink", version = "21.0.2" } pallet-contracts-primitives = { version = "25.0.0" } pallet-timestamp = { version = "21.0.0" } sp-core = { version = "22.0.0" } diff --git a/examples/cross-contract-call-tracing/Cargo.toml b/examples/cross-contract-call-tracing/Cargo.toml new file mode 100644 index 0000000..51a4d38 --- /dev/null +++ b/examples/cross-contract-call-tracing/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "cross-contract-call-tracing" +authors = ["Cardinal"] +edition = "2021" +homepage = "https://alephzero.org" +repository = "https://github.com/Cardinal-Cryptography/drink" +version = "0.1.0" + +[dependencies] +ink = { version = "=4.2.1", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +drink = { path = "../../drink", features = ["session"] } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/examples/cross-contract-call-tracing/lib.rs b/examples/cross-contract-call-tracing/lib.rs new file mode 100644 index 0000000..f2be0d7 --- /dev/null +++ b/examples/cross-contract-call-tracing/lib.rs @@ -0,0 +1,61 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod contract { + use ink::env::{ + call::{build_call, ExecutionInput, Selector}, + DefaultEnvironment, + }; + + #[ink(storage)] + pub struct CrossCallingContract; + + impl CrossCallingContract { + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + #[ink(message)] + pub fn outer_call( + &self, + next_callee: AccountId, + next_next_callee: AccountId, + arg: u32, + ) -> u32 { + build_call::() + .call(next_callee) + .gas_limit(0) + .transferred_value(0) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("middle_call"))) + .push_arg(next_next_callee) + .push_arg(arg), + ) + .returns::() + .invoke() + } + + #[ink(message)] + pub fn middle_call(&self, next_callee: AccountId, arg: u32) -> u32 { + build_call::() + .call(next_callee) + .gas_limit(0) + .transferred_value(0) + .exec_input( + ExecutionInput::new(Selector::new(ink::selector_bytes!("inner_call"))) + .push_arg(arg), + ) + .returns::() + .invoke() + } + + #[ink(message)] + pub fn inner_call(&self, arg: u32) -> u32 { + match arg % 2 { + 0 => arg / 2, + _ => 3 * arg + 1, + } + } + } +} From e393ab2018ea23db77cd24793671257d4a8e3068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 11:38:45 +0200 Subject: [PATCH 02/12] Add trait implementation and callspan --- drink/src/runtime/minimal.rs | 19 +++++----- drink/src/runtime/mod.rs | 1 + .../src/runtime/pallet_contracts_debugging.rs | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 drink/src/runtime/pallet_contracts_debugging.rs diff --git a/drink/src/runtime/minimal.rs b/drink/src/runtime/minimal.rs index 7c405b3..2b69ebb 100644 --- a/drink/src/runtime/minimal.rs +++ b/drink/src/runtime/minimal.rs @@ -1,23 +1,26 @@ #![allow(missing_docs)] // `construct_macro` doesn't allow doc comments for the runtime type. +use std::time::SystemTime; + use frame_support::{ parameter_types, sp_runtime::{ testing::H256, traits::{BlakeTwo256, Convert, IdentityLookup}, - AccountId32, BuildStorage, + AccountId32, BuildStorage, Storage, }, - traits::{ConstBool, ConstU128, ConstU32, ConstU64, Currency, Randomness}, + traits::{ConstBool, ConstU128, ConstU32, ConstU64, Currency, Hooks, Randomness}, weights::Weight, }; -use pallet_contracts::{DefaultAddressGenerator, Frame, Schedule}; - // Re-export all pallets. pub use frame_system; pub use pallet_balances; pub use pallet_contracts; +use pallet_contracts::{DefaultAddressGenerator, Frame, Schedule}; pub use pallet_timestamp; +use crate::{runtime::pallet_contracts_debugging::DrinkDebug, Runtime, DEFAULT_ACTOR}; + type Block = frame_system::mocking::MockBlock; frame_support::construct_runtime!( @@ -121,15 +124,9 @@ impl pallet_contracts::Config for MinimalRuntime { type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; type Migrations = (); type DefaultDepositLimit = DefaultDepositLimit; - type Debug = (); + type Debug = DrinkDebug; } -use std::time::SystemTime; - -use frame_support::{sp_runtime::Storage, traits::Hooks}; - -use crate::{Runtime, DEFAULT_ACTOR}; - /// Default initial balance for the default account. pub const INITIAL_BALANCE: u128 = 1_000_000_000_000_000; diff --git a/drink/src/runtime/mod.rs b/drink/src/runtime/mod.rs index f801297..cf4f54c 100644 --- a/drink/src/runtime/mod.rs +++ b/drink/src/runtime/mod.rs @@ -2,6 +2,7 @@ //! `drink` with any runtime that implements the `Runtime` trait. pub mod minimal; +pub mod pallet_contracts_debugging; use frame_support::sp_runtime::{AccountId32, Storage}; pub use minimal::MinimalRuntime; diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs new file mode 100644 index 0000000..a25a51e --- /dev/null +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -0,0 +1,36 @@ +use pallet_contracts::debug::{CallSpan, ExportedFunction, Tracing}; +use pallet_contracts_primitives::ExecReturnValue; + +use crate::runtime::Runtime; + +type CodeHash = ::Hash; + +pub enum DrinkDebug {} + +impl Tracing for DrinkDebug { + type CallSpan = DrinkCallSpan>; + + fn new_call_span( + code_hash: &CodeHash, + entry_point: ExportedFunction, + input_data: &[u8], + ) -> Self::CallSpan { + DrinkCallSpan { + code_hash: *code_hash, + entry_point, + input_data: input_data.to_vec(), + } + } +} + +pub struct DrinkCallSpan { + pub code_hash: CodeHash, + pub entry_point: ExportedFunction, + pub input_data: Vec, +} + +impl CallSpan for DrinkCallSpan { + fn after_call(self, output: &ExecReturnValue) { + todo!() + } +} From c2092cfdddb22c46c48838bf246875362e592c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 12:10:23 +0200 Subject: [PATCH 03/12] Add heavy machinery (runtime interface and externalities) --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ drink/Cargo.toml | 5 +++ .../src/runtime/pallet_contracts_debugging.rs | 35 +++++++++++++++++-- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e87296d..453b3de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -949,6 +949,8 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "scale-info", + "sp-externalities", + "sp-runtime-interface", "thiserror", "wat", ] diff --git a/Cargo.toml b/Cargo.toml index 8630e62..a5be590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ pallet-contracts = { package = "pallet-contracts-for-drink", version = "21.0.2" pallet-contracts-primitives = { version = "25.0.0" } pallet-timestamp = { version = "21.0.0" } sp-core = { version = "22.0.0" } +sp-externalities = { version = "0.20.0" } +sp-runtime-interface = { version = "18.0.0" } # Local dependencies diff --git a/drink/Cargo.toml b/drink/Cargo.toml index f6c60f8..4288de1 100644 --- a/drink/Cargo.toml +++ b/drink/Cargo.toml @@ -18,6 +18,9 @@ pallet-contracts = { workspace = true } pallet-contracts-primitives = { workspace = true } pallet-timestamp = { workspace = true } parity-scale-codec = { workspace = true } +sp-externalities = { workspace = true } +sp-runtime-interface = { workspace = true } + scale-info = { workspace = true } thiserror = { workspace = true } @@ -25,4 +28,6 @@ thiserror = { workspace = true } wat = { workspace = true } [features] +default = ["std"] session = ["contract-transcode"] +std = [] diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index a25a51e..194fb6d 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -1,10 +1,35 @@ use pallet_contracts::debug::{CallSpan, ExportedFunction, Tracing}; use pallet_contracts_primitives::ExecReturnValue; +use sp_externalities::{decl_extension, ExternalitiesExt}; +use sp_runtime_interface::runtime_interface; use crate::runtime::Runtime; type CodeHash = ::Hash; +pub trait DebugExtT { + fn after_call(&self, code_hash: Vec, is_call: bool, input_data: Vec, result: Vec); +} + +decl_extension! { + pub struct DebugExt(Box); +} + +#[runtime_interface] +pub trait ContractCallDebugger { + fn after_call( + &mut self, + code_hash: Vec, + is_call: bool, + input_data: Vec, + result: Vec, + ) { + self.extension::() + .expect("Failed to find `DebugExt` extension") + .after_call(code_hash, is_call, input_data, result); + } +} + pub enum DrinkDebug {} impl Tracing for DrinkDebug { @@ -29,8 +54,14 @@ pub struct DrinkCallSpan { pub input_data: Vec, } -impl CallSpan for DrinkCallSpan { +impl> CallSpan for DrinkCallSpan { fn after_call(self, output: &ExecReturnValue) { - todo!() + let raw_code_hash: &[u8] = self.code_hash.as_ref(); + contract_call_debugger::after_call( + raw_code_hash.to_vec(), + matches!(self.entry_point, ExportedFunction::Call), + self.input_data.to_vec(), + output.data.clone(), + ); } } From d910580f8d1f52e8a2e37b279353e8aba37f2481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 12:13:09 +0200 Subject: [PATCH 04/12] Allow registering extension --- drink/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 8da4224..245d715 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -16,6 +16,7 @@ use frame_support::{sp_io::TestExternalities, sp_runtime::BuildStorage}; pub use frame_support::{sp_runtime::AccountId32, weights::Weight}; use frame_system::{EventRecord, GenesisConfig}; +use crate::pallet_contracts_debugging::DebugExt; use crate::runtime::*; /// Main result type for the drink crate. @@ -64,4 +65,8 @@ impl Sandbox { Ok(sandbox) } + + pub fn register_debug_handle(&mut self, d: DebugExt) { + self.externalities.register_extension(d); + } } From e5f104bdb460275da4486e8f3a6376433b7a75ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 12:19:32 +0200 Subject: [PATCH 05/12] Have a default noop debugger --- drink/src/lib.rs | 5 ++++- drink/src/runtime/pallet_contracts_debugging.rs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 245d715..1b4b41c 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -17,6 +17,7 @@ pub use frame_support::{sp_runtime::AccountId32, weights::Weight}; use frame_system::{EventRecord, GenesisConfig}; use crate::pallet_contracts_debugging::DebugExt; +use crate::runtime::pallet_contracts_debugging::NoopDebugExt; use crate::runtime::*; /// Main result type for the drink crate. @@ -63,10 +64,12 @@ impl Sandbox { .execute_with(|| R::initialize_block(1, Default::default())) .map_err(Error::BlockInitialize)?; + sandbox.override_debug_handle(DebugExt(Box::new(NoopDebugExt {}))); + Ok(sandbox) } - pub fn register_debug_handle(&mut self, d: DebugExt) { + pub fn override_debug_handle(&mut self, d: DebugExt) { self.externalities.register_extension(d); } } diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 194fb6d..e434623 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -8,13 +8,16 @@ use crate::runtime::Runtime; type CodeHash = ::Hash; pub trait DebugExtT { - fn after_call(&self, code_hash: Vec, is_call: bool, input_data: Vec, result: Vec); + fn after_call(&self, code_hash: Vec, is_call: bool, input_data: Vec, result: Vec) {} } decl_extension! { pub struct DebugExt(Box); } +pub struct NoopDebugExt {} +impl DebugExtT for NoopDebugExt {} + #[runtime_interface] pub trait ContractCallDebugger { fn after_call( From 210da08bff7efa84f8fd559bf928ae1736fd1149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 12:36:06 +0200 Subject: [PATCH 06/12] Use contract-address instead of code-hash --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- .../src/runtime/pallet_contracts_debugging.rs | 22 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 453b3de..a1adf25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2374,9 +2374,9 @@ dependencies = [ [[package]] name = "pallet-contracts-for-drink" -version = "21.0.2" +version = "21.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f1071ed81959cbc593389d0f000fe6dbc7b1726af1c1580df162a6e94d0d0cb" +checksum = "e73bc2df30641c195d3be0a105548a3f0cc4f250c9de7d24d4c51fc43d8c3ef4" dependencies = [ "bitflags", "environmental", diff --git a/Cargo.toml b/Cargo.toml index a5be590..89a8b56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ wat = { version = "1.0.71" } frame-support = { version = "22.0.0" } frame-system = { version = "22.0.0" } pallet-balances = { version = "22.0.0" } -pallet-contracts = { package = "pallet-contracts-for-drink", version = "21.0.2" } +pallet-contracts = { package = "pallet-contracts-for-drink", version = "21.0.3" } pallet-contracts-primitives = { version = "25.0.0" } pallet-timestamp = { version = "21.0.0" } sp-core = { version = "22.0.0" } diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index e434623..48df5ad 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -5,7 +5,7 @@ use sp_runtime_interface::runtime_interface; use crate::runtime::Runtime; -type CodeHash = ::Hash; +type AccountIdOf = ::AccountId; pub trait DebugExtT { fn after_call(&self, code_hash: Vec, is_call: bool, input_data: Vec, result: Vec) {} @@ -22,46 +22,46 @@ impl DebugExtT for NoopDebugExt {} pub trait ContractCallDebugger { fn after_call( &mut self, - code_hash: Vec, + contract_address: Vec, is_call: bool, input_data: Vec, result: Vec, ) { self.extension::() .expect("Failed to find `DebugExt` extension") - .after_call(code_hash, is_call, input_data, result); + .after_call(contract_address, is_call, input_data, result); } } pub enum DrinkDebug {} impl Tracing for DrinkDebug { - type CallSpan = DrinkCallSpan>; + type CallSpan = DrinkCallSpan>; fn new_call_span( - code_hash: &CodeHash, + contract_address: &AccountIdOf, entry_point: ExportedFunction, input_data: &[u8], ) -> Self::CallSpan { DrinkCallSpan { - code_hash: *code_hash, + contract_address: contract_address.clone(), entry_point, input_data: input_data.to_vec(), } } } -pub struct DrinkCallSpan { - pub code_hash: CodeHash, +pub struct DrinkCallSpan { + pub contract_address: AccountId, pub entry_point: ExportedFunction, pub input_data: Vec, } -impl> CallSpan for DrinkCallSpan { +impl> CallSpan for DrinkCallSpan { fn after_call(self, output: &ExecReturnValue) { - let raw_code_hash: &[u8] = self.code_hash.as_ref(); + let raw_contract_address: &[u8] = self.contract_address.as_ref(); contract_call_debugger::after_call( - raw_code_hash.to_vec(), + raw_contract_address.to_vec(), matches!(self.entry_point, ExportedFunction::Call), self.input_data.to_vec(), output.data.clone(), From bccda6a317c296fcd3be8133ff725163cc28bcf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 12:58:54 +0200 Subject: [PATCH 07/12] Test working --- .../src/runtime/pallet_contracts_debugging.rs | 9 +- drink/src/session.rs | 8 +- examples/cross-contract-call-tracing/lib.rs | 91 +++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 48df5ad..c79857c 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -8,7 +8,14 @@ use crate::runtime::Runtime; type AccountIdOf = ::AccountId; pub trait DebugExtT { - fn after_call(&self, code_hash: Vec, is_call: bool, input_data: Vec, result: Vec) {} + fn after_call( + &self, + contract_address: Vec, + is_call: bool, + input_data: Vec, + result: Vec, + ) { + } } decl_extension! { diff --git a/drink/src/session.rs b/drink/src/session.rs index 203a4c6..aa36293 100644 --- a/drink/src/session.rs +++ b/drink/src/session.rs @@ -9,8 +9,8 @@ use pallet_contracts_primitives::{ContractExecResult, ContractInstantiateResult} use thiserror::Error; use crate::{ - chain_api::ChainApi, contract_api::ContractApi, runtime::Runtime, AccountId32, EventRecordOf, - Sandbox, DEFAULT_ACTOR, DEFAULT_GAS_LIMIT, + chain_api::ChainApi, contract_api::ContractApi, pallet_contracts_debugging::DebugExt, + runtime::Runtime, AccountId32, EventRecordOf, Sandbox, DEFAULT_ACTOR, DEFAULT_GAS_LIMIT, }; const ZERO_TRANSFER: u128 = 0; @@ -332,4 +332,8 @@ impl Session { pub fn last_call_return(&self) -> Option { self.call_returns.last().cloned() } + + pub fn override_debug_handle(&mut self, d: DebugExt) { + self.sandbox.override_debug_handle(d); + } } diff --git a/examples/cross-contract-call-tracing/lib.rs b/examples/cross-contract-call-tracing/lib.rs index f2be0d7..918c17c 100644 --- a/examples/cross-contract-call-tracing/lib.rs +++ b/examples/cross-contract-call-tracing/lib.rs @@ -59,3 +59,94 @@ mod contract { } } } + +#[cfg(test)] +mod tests { + use ink::storage::traits::Storable; + use std::{error::Error, fs, path::PathBuf, rc::Rc}; + + use drink::{ + runtime::{ + pallet_contracts_debugging::{DebugExt, DebugExtT}, + MinimalRuntime, + }, + session::{ + contract_transcode::{ContractMessageTranscoder, Tuple, Value}, + Session, + }, + AccountId32, + }; + + fn transcoder() -> Rc { + let path = PathBuf::from("target/ink/cross_contract_call_tracing.json"); + Rc::new(ContractMessageTranscoder::load(path).expect("Failed to create transcoder")) + } + + fn bytes() -> Vec { + let path = "target/ink/cross_contract_call_tracing.wasm"; + fs::read(path).expect("Failed to find or read contract file") + } + + fn ok(v: Value) -> Value { + Value::Tuple(Tuple::new(Some("Ok"), vec![v])) + } + + struct TestDebugger; + impl DebugExtT for TestDebugger { + fn after_call( + &self, + contract_address: Vec, + is_call: bool, + input_data: Vec, + result: Vec, + ) { + let contract_address = AccountId32::decode(&mut contract_address.as_slice()) + .expect("Failed to decode contract address"); + let transcoder = transcoder(); + + let data_decoded = if is_call { + transcoder.decode_contract_message(&mut input_data.as_slice()) + } else { + transcoder.decode_contract_constructor(&mut input_data.as_slice()) + } + .unwrap(); + + let return_decoded = if is_call { + transcoder + .decode_return("outer_call", &mut result.as_slice()) + .unwrap() + } else { + Value::Unit + }; + + println!( + "Contract at address `{contract_address}` has been called with data: \ + \n {data_decoded}\nand returned:\n {return_decoded}\n" + ) + } + } + + #[test] + fn test() -> Result<(), Box> { + let mut session = Session::::new(Some(transcoder()))?; + session.override_debug_handle(DebugExt(Box::new(TestDebugger {}))); + + let outer_address = session.deploy(bytes(), "new", &[], vec![1])?; + let middle_address = session.deploy(bytes(), "new", &[], vec![2])?; + let inner_address = session.deploy(bytes(), "new", &[], vec![3])?; + + let value = session.call_with_address( + outer_address, + "outer_call", + &[ + middle_address.to_string(), + inner_address.to_string(), + "7".to_string(), + ], + )?; + + assert_eq!(value, ok(Value::UInt(22))); + + Ok(()) + } +} From 3c9d879a6444b418fdb4c48b8df0efbec4ff9c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 13:06:13 +0200 Subject: [PATCH 08/12] Contract awareness --- examples/cross-contract-call-tracing/lib.rs | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/examples/cross-contract-call-tracing/lib.rs b/examples/cross-contract-call-tracing/lib.rs index 918c17c..ee6b100 100644 --- a/examples/cross-contract-call-tracing/lib.rs +++ b/examples/cross-contract-call-tracing/lib.rs @@ -63,7 +63,7 @@ mod contract { #[cfg(test)] mod tests { use ink::storage::traits::Storable; - use std::{error::Error, fs, path::PathBuf, rc::Rc}; + use std::{cell::RefCell, error::Error, fs, path::PathBuf, rc::Rc}; use drink::{ runtime::{ @@ -91,6 +91,12 @@ mod tests { Value::Tuple(Tuple::new(Some("Ok"), vec![v])) } + thread_local! { + static OUTER_ADDRESS: RefCell> = RefCell::new(None); + static MIDDLE_ADDRESS: RefCell> = RefCell::new(None); + static INNER_ADDRESS: RefCell> = RefCell::new(None); + } + struct TestDebugger; impl DebugExtT for TestDebugger { fn after_call( @@ -112,8 +118,20 @@ mod tests { .unwrap(); let return_decoded = if is_call { + let call_name = if contract_address + == OUTER_ADDRESS.with(|a| a.borrow().clone().unwrap()) + { + "outer_call" + } else if contract_address == MIDDLE_ADDRESS.with(|a| a.borrow().clone().unwrap()) { + "middle_call" + } else if contract_address == INNER_ADDRESS.with(|a| a.borrow().clone().unwrap()) { + "inner_call" + } else { + panic!("Unexpected contract address") + }; + transcoder - .decode_return("outer_call", &mut result.as_slice()) + .decode_return(call_name, &mut result.as_slice()) .unwrap() } else { Value::Unit @@ -132,8 +150,11 @@ mod tests { session.override_debug_handle(DebugExt(Box::new(TestDebugger {}))); let outer_address = session.deploy(bytes(), "new", &[], vec![1])?; + OUTER_ADDRESS.with(|a| *a.borrow_mut() = Some(outer_address.clone())); let middle_address = session.deploy(bytes(), "new", &[], vec![2])?; + MIDDLE_ADDRESS.with(|a| *a.borrow_mut() = Some(middle_address.clone())); let inner_address = session.deploy(bytes(), "new", &[], vec![3])?; + INNER_ADDRESS.with(|a| *a.borrow_mut() = Some(inner_address.clone())); let value = session.call_with_address( outer_address, From 46425767f7f60389e2e67d993786161fbe72a4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 13:33:49 +0200 Subject: [PATCH 09/12] Add docs --- drink/src/lib.rs | 5 ++ .../src/runtime/pallet_contracts_debugging.rs | 47 +++++++++++++++++-- drink/src/session.rs | 4 ++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/drink/src/lib.rs b/drink/src/lib.rs index 1b4b41c..e7bc280 100644 --- a/drink/src/lib.rs +++ b/drink/src/lib.rs @@ -64,11 +64,16 @@ impl Sandbox { .execute_with(|| R::initialize_block(1, Default::default())) .map_err(Error::BlockInitialize)?; + // We register a noop debug extension by default. sandbox.override_debug_handle(DebugExt(Box::new(NoopDebugExt {}))); Ok(sandbox) } + /// Overrides the debug extension. + /// + /// By default, a new `Sandbox` instance is created with a noop debug extension. This method + /// allows to override it with a custom debug extension. pub fn override_debug_handle(&mut self, d: DebugExt) { self.externalities.register_extension(d); } diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index c79857c..7ee58c8 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -1,3 +1,26 @@ +//! This module provides all the necessary elements for supporting contract debugging directly in +//! the contracts pallet. +//! +//! # Smart-contract developer <-> pallet-contracts interaction flow +//! +//! The interaction between end-user and runtime is as follows: +//! 1. At some points during execution, the pallet invokes some callback through its configuration +//! parameter `Debug`. +//! 2. In order to forward the callback outside the runtime, `Debug` will call a runtime interface, +//! that will then forward the call further to the proper runtime extension. +//! 3. The runtime extension can be fully controlled by the end-user. It just has to be registered +//! in the runtime. +//! +//! So, in brief: pallet-contracts -> runtime interface -> runtime extension +//! |<-----------runtime side-------------->|<---user side--->| +//! +//! # Passing objects between runtime and runtime extension +//! +//! Unfortunately, runtime interface that lies between runtime and the end-user accepts only +//! very simlpe argument types and those that implement some specific traits. This means that +//! usually, complext objects will be passed in their encoded form (`Vec` obtained with scale +//! encoding). + use pallet_contracts::debug::{CallSpan, ExportedFunction, Tracing}; use pallet_contracts_primitives::ExecReturnValue; use sp_externalities::{decl_extension, ExternalitiesExt}; @@ -7,26 +30,31 @@ use crate::runtime::Runtime; type AccountIdOf = ::AccountId; +/// The trait that allows injecting custom logic to handle contract debugging directly in the +/// contracts pallet. pub trait DebugExtT { + /// Called after a contract call is made. fn after_call( &self, - contract_address: Vec, - is_call: bool, - input_data: Vec, - result: Vec, + _contract_address: Vec, + _is_call: bool, + _input_data: Vec, + _result: Vec, ) { } } decl_extension! { + /// A wrapper type for the `DebugExtT` debug extension. pub struct DebugExt(Box); } +/// The simplest debug extension - does nothing. pub struct NoopDebugExt {} impl DebugExtT for NoopDebugExt {} #[runtime_interface] -pub trait ContractCallDebugger { +trait ContractCallDebugger { fn after_call( &mut self, contract_address: Vec, @@ -40,6 +68,8 @@ pub trait ContractCallDebugger { } } +/// Configuration parameter for the contracts pallet. Provides all the necessary trait +/// implementations. pub enum DrinkDebug {} impl Tracing for DrinkDebug { @@ -58,9 +88,16 @@ impl Tracing for DrinkDebug { } } +/// A contract's call span. +/// +/// It is created just before the call is made and `Self::after_call` is called after the call is +/// done. pub struct DrinkCallSpan { + /// The address of the contract that has been called. pub contract_address: AccountId, + /// The entry point that has been called (either constructor or call). pub entry_point: ExportedFunction, + /// The input data of the call. pub input_data: Vec, } diff --git a/drink/src/session.rs b/drink/src/session.rs index aa36293..2f636d8 100644 --- a/drink/src/session.rs +++ b/drink/src/session.rs @@ -333,6 +333,10 @@ impl Session { self.call_returns.last().cloned() } + /// Overrides the debug extension. + /// + /// By default, a new `Session` instance will use a noop debug extension. This method allows to + /// override it with a custom debug extension. pub fn override_debug_handle(&mut self, d: DebugExt) { self.sandbox.override_debug_handle(d); } From c52ed2dc72b0cacc4c9e601e3985c86ffa7ba26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 13:34:42 +0200 Subject: [PATCH 10/12] Add feature explanation --- drink/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/drink/Cargo.toml b/drink/Cargo.toml index 4288de1..830cb43 100644 --- a/drink/Cargo.toml +++ b/drink/Cargo.toml @@ -28,6 +28,7 @@ thiserror = { workspace = true } wat = { workspace = true } [features] +# This is required for the runtime-interface to work properly in the std env. default = ["std"] session = ["contract-transcode"] std = [] From 1472fc39304fb27bccc66e741ad4c4ab12c7a6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Mon, 4 Sep 2023 13:45:03 +0200 Subject: [PATCH 11/12] Example readme --- .../cross-contract-call-tracing/README.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 examples/cross-contract-call-tracing/README.md diff --git a/examples/cross-contract-call-tracing/README.md b/examples/cross-contract-call-tracing/README.md new file mode 100644 index 0000000..403fad1 --- /dev/null +++ b/examples/cross-contract-call-tracing/README.md @@ -0,0 +1,60 @@ +# Cross contract call tracing + +This example shows how you can trace and debug cross contract calls. + +## Scenario + +Here we have a single contract with 3 methods: + - `call_inner(arg: u32)`: returns the result of some simple computation on `arg` + - `call_middle(next_callee: AccountId, arg: u32)`: calls `call_inner(arg)` at `next_callee` and forwards the result + - `call_outer(next_callee: AccountId, next_next_callee: AccountId, arg: u32)`: calls `call_middle(next_next_callee, arg)` at `next_callee` and forwards the result + +We deploy three instances of this contract, `inner`, `middle` and `outer`, and call `call_outer` on `outer` with `inner` and `middle` and some integer as arguments. + +If we were using just `cargo-contract` or some other tooling, we would be able to see only the final result of the call. +However, it wouldn't be possible to trace the intermediate steps. +With `drink`, we can provide handlers for (synchronous) observing every level of the call stack. + +## Running + +```bash +cargo contract build --release +cargo test --release -- --show-output +``` + +You should be able to see similar output: +``` +Contract at address `5CmHh6aBH6YZLjHGHjVtDDU4PfvDvk9s8n5xAcZQajxikksr` has been called with data: + new +and returned: + () + +Contract at address `5FNvS4rLX8Y5NotoRzyBpmeNq2cfcSRpBWbHvgNrEiY3ero7` has been called with data: + new +and returned: + () + +Contract at address `5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf` has been called with data: + new +and returned: + () + +Contract at address `5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf` has been called with data: + inner_call { arg: 7 } +and returned: + Ok(22) + +Contract at address `5FNvS4rLX8Y5NotoRzyBpmeNq2cfcSRpBWbHvgNrEiY3ero7` has been called with data: + middle_call { next_callee: 5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf, arg: 7 } +and returned: + Ok(22) + +Contract at address `5CmHh6aBH6YZLjHGHjVtDDU4PfvDvk9s8n5xAcZQajxikksr` has been called with data: + outer_call { next_callee: 5FNvS4rLX8Y5NotoRzyBpmeNq2cfcSRpBWbHvgNrEiY3ero7, next_next_callee: 5DhNNsxhPMhg8R7StY3LbHraQWTDRFEbK2C1CaAD2AGvDCAf, arg: 7 } +and returned: + Ok(22) + + +successes: + tests::test +``` \ No newline at end of file From ce580b9443aae55ef2a8e6017ffb0ad62f71d831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Miko=C5=82ajczyk?= Date: Wed, 6 Sep 2023 08:29:06 +0200 Subject: [PATCH 12/12] Typos --- drink/src/runtime/pallet_contracts_debugging.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drink/src/runtime/pallet_contracts_debugging.rs b/drink/src/runtime/pallet_contracts_debugging.rs index 7ee58c8..f2ca81d 100644 --- a/drink/src/runtime/pallet_contracts_debugging.rs +++ b/drink/src/runtime/pallet_contracts_debugging.rs @@ -17,8 +17,8 @@ //! # Passing objects between runtime and runtime extension //! //! Unfortunately, runtime interface that lies between runtime and the end-user accepts only -//! very simlpe argument types and those that implement some specific traits. This means that -//! usually, complext objects will be passed in their encoded form (`Vec` obtained with scale +//! very simple argument types and those that implement some specific traits. This means that +//! usually, complex objects will be passed in their encoded form (`Vec` obtained with scale //! encoding). use pallet_contracts::debug::{CallSpan, ExportedFunction, Tracing};