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

Commit 28e906d

Browse files
Cross-contract calling: simple debugger (#14678)
* Provide basic breakpoints * Rename to Observer * Rename feature. Single trait. Borrow-checker * : frame_system::Config * Confused type name * Minor bugs * pub trait * No unnecessary cloning * Make node compile with all features * Move everything debug-related to a single module * Add docs and implementation for () * fmt * Make it feature-gated or for tests * Prepare testing kit * Testcase * Fmt * Use feature in dev-deps * ? * feature propagation * AAAA * lol, that doesn't make much sense to me * Turn on * clippy * Remove self dep * fmt, feature-gating test * Noop to trigger CI * idk * add feature to pipeline * Corrupt test to see if it is actually being run * Revert change * Doc for conf type * Review * Imports * ... * Remove debug for kitchen-sink * Move test * Fix imports * I must have already tried this one...
1 parent 6800101 commit 28e906d

File tree

9 files changed

+216
-3
lines changed

9 files changed

+216
-3
lines changed

bin/node/runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,4 @@ try-runtime = [
373373
"frame-election-provider-support/try-runtime",
374374
"sp-runtime/try-runtime"
375375
]
376+
unsafe-debug = ["pallet-contracts/unsafe-debug"]

bin/node/runtime/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,6 +1258,8 @@ impl pallet_contracts::Config for Runtime {
12581258
type Migrations = pallet_contracts::migration::codegen::BenchMigrations;
12591259
type MaxDelegateDependencies = ConstU32<32>;
12601260
type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent;
1261+
#[cfg(feature = "unsafe-debug")]
1262+
type Debug = ();
12611263
}
12621264

12631265
impl pallet_sudo::Config for Runtime {

frame/contracts/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,6 @@ try-runtime = [
112112
"pallet-proxy/try-runtime",
113113
"pallet-timestamp/try-runtime",
114114
"pallet-utility/try-runtime",
115-
"sp-runtime/try-runtime"
115+
"sp-runtime/try-runtime",
116116
]
117+
unsafe-debug = []

frame/contracts/src/exec.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
// See the License for the specific language governing permissions and
1616
// limitations under the License.
1717

18+
#[cfg(feature = "unsafe-debug")]
19+
use crate::unsafe_debug::ExecutionObserver;
1820
use crate::{
1921
gas::GasMeter,
2022
storage::{self, DepositAccount, WriteOutcome},
@@ -344,7 +346,7 @@ pub trait Ext: sealing::Sealed {
344346
}
345347

346348
/// Describes the different functions that can be exported by an [`Executable`].
347-
#[derive(Clone, Copy, PartialEq)]
349+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
348350
pub enum ExportedFunction {
349351
/// The constructor function which is executed on deployment of a contract.
350352
Constructor,
@@ -904,11 +906,21 @@ where
904906
// Every non delegate call or instantiate also optionally transfers the balance.
905907
self.initial_transfer()?;
906908

909+
#[cfg(feature = "unsafe-debug")]
910+
let (code_hash, input_clone) = {
911+
let code_hash = *executable.code_hash();
912+
T::Debug::before_call(&code_hash, entry_point, &input_data);
913+
(code_hash, input_data.clone())
914+
};
915+
907916
// Call into the Wasm blob.
908917
let output = executable
909918
.execute(self, &entry_point, input_data)
910919
.map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })?;
911920

921+
#[cfg(feature = "unsafe-debug")]
922+
T::Debug::after_call(&code_hash, entry_point, input_clone, &output);
923+
912924
// Avoid useless work that would be reverted anyways.
913925
if output.did_revert() {
914926
return Ok(output)

frame/contracts/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ mod wasm;
9797

9898
pub mod chain_extension;
9999
pub mod migration;
100+
pub mod unsafe_debug;
100101
pub mod weights;
101102

102103
#[cfg(test)]
@@ -351,6 +352,14 @@ pub mod pallet {
351352
/// type Migrations = (v10::Migration<Runtime, Currency>,);
352353
/// ```
353354
type Migrations: MigrateSequence;
355+
356+
/// Type that provides debug handling for the contract execution process.
357+
///
358+
/// # Warning
359+
///
360+
/// Do **not** use it in a production environment or for benchmarking purposes.
361+
#[cfg(feature = "unsafe-debug")]
362+
type Debug: unsafe_debug::UnsafeDebug<Self>;
354363
}
355364

356365
#[pallet::hooks]

frame/contracts/src/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// limitations under the License.
1717

1818
mod pallet_dummy;
19+
mod unsafe_debug;
1920

2021
use self::test_utils::{ensure_stored, expected_deposit, hash};
2122
use crate::{
@@ -464,6 +465,8 @@ impl Config for Test {
464465
type Migrations = crate::migration::codegen::BenchMigrations;
465466
type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent;
466467
type MaxDelegateDependencies = MaxDelegateDependencies;
468+
#[cfg(feature = "unsafe-debug")]
469+
type Debug = unsafe_debug::TestDebugger;
467470
}
468471

469472
pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]);
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#![cfg(feature = "unsafe-debug")]
2+
3+
use super::*;
4+
use crate::unsafe_debug::{ExecutionObserver, ExportedFunction};
5+
use frame_support::traits::Currency;
6+
use pallet_contracts_primitives::ExecReturnValue;
7+
use pretty_assertions::assert_eq;
8+
use std::cell::RefCell;
9+
10+
#[derive(Clone, PartialEq, Eq, Debug)]
11+
struct DebugFrame {
12+
code_hash: CodeHash<Test>,
13+
call: ExportedFunction,
14+
input: Vec<u8>,
15+
result: Option<Vec<u8>>,
16+
}
17+
18+
thread_local! {
19+
static DEBUG_EXECUTION_TRACE: RefCell<Vec<DebugFrame>> = RefCell::new(Vec::new());
20+
}
21+
22+
pub struct TestDebugger;
23+
24+
impl ExecutionObserver<CodeHash<Test>> for TestDebugger {
25+
fn before_call(code_hash: &CodeHash<Test>, entry_point: ExportedFunction, input_data: &[u8]) {
26+
DEBUG_EXECUTION_TRACE.with(|d| {
27+
d.borrow_mut().push(DebugFrame {
28+
code_hash: code_hash.clone(),
29+
call: entry_point,
30+
input: input_data.to_vec(),
31+
result: None,
32+
})
33+
});
34+
}
35+
36+
fn after_call(
37+
code_hash: &CodeHash<Test>,
38+
entry_point: ExportedFunction,
39+
input_data: Vec<u8>,
40+
output: &ExecReturnValue,
41+
) {
42+
DEBUG_EXECUTION_TRACE.with(|d| {
43+
d.borrow_mut().push(DebugFrame {
44+
code_hash: code_hash.clone(),
45+
call: entry_point,
46+
input: input_data,
47+
result: Some(output.data.clone()),
48+
})
49+
});
50+
}
51+
}
52+
53+
#[test]
54+
fn unsafe_debugging_works() {
55+
let (wasm_caller, code_hash_caller) = compile_module::<Test>("call").unwrap();
56+
let (wasm_callee, code_hash_callee) = compile_module::<Test>("store_call").unwrap();
57+
58+
fn current_stack() -> Vec<DebugFrame> {
59+
DEBUG_EXECUTION_TRACE.with(|stack| stack.borrow().clone())
60+
}
61+
62+
fn deploy(wasm: Vec<u8>) -> AccountId32 {
63+
Contracts::bare_instantiate(
64+
ALICE,
65+
0,
66+
GAS_LIMIT,
67+
None,
68+
Code::Upload(wasm),
69+
vec![],
70+
vec![],
71+
DebugInfo::Skip,
72+
CollectEvents::Skip,
73+
)
74+
.result
75+
.unwrap()
76+
.account_id
77+
}
78+
79+
fn constructor_frame(hash: CodeHash<Test>, after: bool) -> DebugFrame {
80+
DebugFrame {
81+
code_hash: hash,
82+
call: ExportedFunction::Constructor,
83+
input: vec![],
84+
result: if after { Some(vec![]) } else { None },
85+
}
86+
}
87+
88+
fn call_frame(hash: CodeHash<Test>, args: Vec<u8>, after: bool) -> DebugFrame {
89+
DebugFrame {
90+
code_hash: hash,
91+
call: ExportedFunction::Call,
92+
input: args,
93+
result: if after { Some(vec![]) } else { None },
94+
}
95+
}
96+
97+
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
98+
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
99+
100+
assert_eq!(current_stack(), vec![]);
101+
102+
let addr_caller = deploy(wasm_caller);
103+
let addr_callee = deploy(wasm_callee);
104+
105+
assert_eq!(
106+
current_stack(),
107+
vec![
108+
constructor_frame(code_hash_caller, false),
109+
constructor_frame(code_hash_caller, true),
110+
constructor_frame(code_hash_callee, false),
111+
constructor_frame(code_hash_callee, true),
112+
]
113+
);
114+
115+
let main_args = (100u32, &addr_callee).encode();
116+
let inner_args = (100u32).encode();
117+
118+
assert_ok!(Contracts::call(
119+
RuntimeOrigin::signed(ALICE),
120+
addr_caller,
121+
0,
122+
GAS_LIMIT,
123+
None,
124+
main_args.clone()
125+
));
126+
127+
let stack_top = current_stack()[4..].to_vec();
128+
assert_eq!(
129+
stack_top,
130+
vec![
131+
call_frame(code_hash_caller, main_args.clone(), false),
132+
call_frame(code_hash_callee, inner_args.clone(), false),
133+
call_frame(code_hash_callee, inner_args, true),
134+
call_frame(code_hash_caller, main_args, true),
135+
]
136+
);
137+
});
138+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#![cfg(feature = "unsafe-debug")]
2+
3+
pub use crate::exec::ExportedFunction;
4+
use crate::{CodeHash, Vec};
5+
use pallet_contracts_primitives::ExecReturnValue;
6+
7+
/// Umbrella trait for all interfaces that serves for debugging, but are not suitable for any
8+
/// production or benchmarking use.
9+
pub trait UnsafeDebug<T: frame_system::Config>: ExecutionObserver<CodeHash<T>> {}
10+
11+
impl<T: frame_system::Config, D> UnsafeDebug<T> for D where D: ExecutionObserver<CodeHash<T>> {}
12+
13+
/// Defines the interface between pallet contracts and the outside observer.
14+
///
15+
/// The intended use is the environment, where the observer holds directly the whole runtime
16+
/// (externalities) and thus can react to the execution breakpoints synchronously.
17+
///
18+
/// This definitely *should not* be used in any production or benchmarking setting, since handling
19+
/// callbacks might be arbitrarily expensive and thus significantly influence performance.
20+
pub trait ExecutionObserver<CodeHash> {
21+
/// Called just before the execution of a contract.
22+
///
23+
/// # Arguments
24+
///
25+
/// * `code_hash` - The code hash of the contract being called.
26+
/// * `entry_point` - Describes whether the call is the constructor or a regular call.
27+
/// * `input_data` - The raw input data of the call.
28+
fn before_call(_code_hash: &CodeHash, _entry_point: ExportedFunction, _input_data: &[u8]) {}
29+
30+
/// Called just after the execution of a contract.
31+
///
32+
/// # Arguments
33+
///
34+
/// * `code_hash` - The code hash of the contract being called.
35+
/// * `entry_point` - Describes whether the call was the constructor or a regular call.
36+
/// * `input_data` - The raw input data of the call.
37+
/// * `output` - The raw output of the call.
38+
fn after_call(
39+
_code_hash: &CodeHash,
40+
_entry_point: ExportedFunction,
41+
_input_data: Vec<u8>,
42+
_output: &ExecReturnValue,
43+
) {
44+
}
45+
}
46+
47+
impl<CodeHash> ExecutionObserver<CodeHash> for () {}

scripts/ci/gitlab/pipeline/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ test-linux-stable:
221221
--locked
222222
--release
223223
--verbose
224-
--features runtime-benchmarks,try-runtime,experimental
224+
--features runtime-benchmarks,try-runtime,experimental,unsafe-debug
225225
--manifest-path ./bin/node/cli/Cargo.toml
226226
--partition count:${CI_NODE_INDEX}/${CI_NODE_TOTAL}
227227
# we need to update cache only from one job

0 commit comments

Comments
 (0)