mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-26 02:57:57 +00:00
Enable mocking contracts (#1331)
# Description This PR introduces two changes: - the previous `Tracing` trait has been modified to accept contract address instead of code hash (seems to be way more convenient) - a new trait `CallInterceptor` that allows intercepting contract calls; in particular the default implementation for `()` will just proceed in a standard way (after compilation optimizations, there will be no footprint of that); however, implementing type might decide to mock invocation and return `ExecResult` instead Note: one might try merging `before_call` and `intercept_call`. However, IMHO this would be bad, since it would mix two completely different abstractions - tracing without any effects and actual intervention into execution process. This will unblock working on mocking contracts utility in drink and similar tools (https://github.com/Cardinal-Cryptography/drink/issues/33) # Checklist - [x] My PR includes a detailed description as outlined in the "Description" section above - [ ] My PR follows the [labeling requirements](https://github.com/paritytech/polkadot-sdk/blob/master/docs/CONTRIBUTING.md#process) of this project (at minimum one label for `T` required) - [x] I have made corresponding changes to the documentation (if applicable) - [x] I have added tests that prove my fix is effective or that my feature works (if applicable)
This commit is contained in:
committed by
GitHub
parent
ef3adf9a01
commit
d8d90a82a7
@@ -15,14 +15,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub use crate::exec::ExportedFunction;
|
||||
use crate::{CodeHash, Config, LOG_TARGET};
|
||||
use pallet_contracts_primitives::ExecReturnValue;
|
||||
pub use crate::exec::{ExecResult, ExportedFunction};
|
||||
use crate::{Config, LOG_TARGET};
|
||||
pub use pallet_contracts_primitives::ExecReturnValue;
|
||||
|
||||
/// Umbrella trait for all interfaces that serves for debugging.
|
||||
pub trait Debugger<T: Config>: Tracing<T> {}
|
||||
pub trait Debugger<T: Config>: Tracing<T> + CallInterceptor<T> {}
|
||||
|
||||
impl<T: Config, V> Debugger<T> for V where V: Tracing<T> {}
|
||||
impl<T: Config, V> Debugger<T> for V where V: Tracing<T> + CallInterceptor<T> {}
|
||||
|
||||
/// Defines methods to capture contract calls, enabling external observers to
|
||||
/// measure, trace, and react to contract interactions.
|
||||
@@ -37,11 +37,11 @@ pub trait Tracing<T: Config> {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `code_hash` - The code hash of the contract being called.
|
||||
/// * `contract_address` - The address of the contract that is about to be executed.
|
||||
/// * `entry_point` - Describes whether the call is the constructor or a regular call.
|
||||
/// * `input_data` - The raw input data of the call.
|
||||
fn new_call_span(
|
||||
code_hash: &CodeHash<T>,
|
||||
contract_address: &T::AccountId,
|
||||
entry_point: ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) -> Self::CallSpan;
|
||||
@@ -60,8 +60,12 @@ pub trait CallSpan {
|
||||
impl<T: Config> Tracing<T> for () {
|
||||
type CallSpan = ();
|
||||
|
||||
fn new_call_span(code_hash: &CodeHash<T>, entry_point: ExportedFunction, input_data: &[u8]) {
|
||||
log::trace!(target: LOG_TARGET, "call {entry_point:?} hash: {code_hash:?}, input_data: {input_data:?}")
|
||||
fn new_call_span(
|
||||
contract_address: &T::AccountId,
|
||||
entry_point: ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) {
|
||||
log::trace!(target: LOG_TARGET, "call {entry_point:?} account: {contract_address:?}, input_data: {input_data:?}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,3 +74,37 @@ impl CallSpan for () {
|
||||
log::trace!(target: LOG_TARGET, "call result {output:?}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an interface for intercepting contract calls.
|
||||
pub trait CallInterceptor<T: Config> {
|
||||
/// Allows to intercept contract calls and decide whether they should be executed or not.
|
||||
/// If the call is intercepted, the mocked result of the call is returned.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `contract_address` - The address of the contract that is about to be executed.
|
||||
/// * `entry_point` - Describes whether the call is the constructor or a regular call.
|
||||
/// * `input_data` - The raw input data of the call.
|
||||
///
|
||||
/// # Expected behavior
|
||||
///
|
||||
/// This method should return:
|
||||
/// * `Some(ExecResult)` - if the call should be intercepted and the mocked result of the call
|
||||
/// is returned.
|
||||
/// * `None` - otherwise, i.e. the call should be executed normally.
|
||||
fn intercept_call(
|
||||
contract_address: &T::AccountId,
|
||||
entry_point: &ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) -> Option<ExecResult>;
|
||||
}
|
||||
|
||||
impl<T: Config> CallInterceptor<T> for () {
|
||||
fn intercept_call(
|
||||
_contract_address: &T::AccountId,
|
||||
_entry_point: &ExportedFunction,
|
||||
_input_data: &[u8],
|
||||
) -> Option<ExecResult> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{
|
||||
debug::{CallSpan, Tracing},
|
||||
debug::{CallInterceptor, CallSpan, Tracing},
|
||||
gas::GasMeter,
|
||||
storage::{self, meter::Diff, WriteOutcome},
|
||||
BalanceOf, CodeHash, CodeInfo, CodeInfoOf, Config, ContractInfo, ContractInfoOf,
|
||||
@@ -908,13 +908,16 @@ where
|
||||
// Every non delegate call or instantiate also optionally transfers the balance.
|
||||
self.initial_transfer()?;
|
||||
|
||||
let call_span =
|
||||
T::Debug::new_call_span(executable.code_hash(), entry_point, &input_data);
|
||||
let contract_address = &top_frame!(self).account_id;
|
||||
|
||||
// Call into the Wasm blob.
|
||||
let output = executable
|
||||
.execute(self, &entry_point, input_data)
|
||||
.map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })?;
|
||||
let call_span = T::Debug::new_call_span(contract_address, entry_point, &input_data);
|
||||
|
||||
let output = T::Debug::intercept_call(contract_address, &entry_point, &input_data)
|
||||
.unwrap_or_else(|| {
|
||||
executable
|
||||
.execute(self, &entry_point, input_data)
|
||||
.map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })
|
||||
})?;
|
||||
|
||||
call_span.after_call(&output);
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use crate::debug::{CallSpan, ExportedFunction, Tracing};
|
||||
use crate::{
|
||||
debug::{CallInterceptor, CallSpan, ExecResult, ExportedFunction, Tracing},
|
||||
AccountIdOf,
|
||||
};
|
||||
use frame_support::traits::Currency;
|
||||
use pallet_contracts_primitives::ExecReturnValue;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -24,7 +27,7 @@ use std::cell::RefCell;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
struct DebugFrame {
|
||||
code_hash: CodeHash<Test>,
|
||||
contract_account: AccountId32,
|
||||
call: ExportedFunction,
|
||||
input: Vec<u8>,
|
||||
result: Option<Vec<u8>>,
|
||||
@@ -32,11 +35,12 @@ struct DebugFrame {
|
||||
|
||||
thread_local! {
|
||||
static DEBUG_EXECUTION_TRACE: RefCell<Vec<DebugFrame>> = RefCell::new(Vec::new());
|
||||
static INTERCEPTED_ADDRESS: RefCell<Option<AccountId32>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
pub struct TestDebug;
|
||||
pub struct TestCallSpan {
|
||||
code_hash: CodeHash<Test>,
|
||||
contract_account: AccountId32,
|
||||
call: ExportedFunction,
|
||||
input: Vec<u8>,
|
||||
}
|
||||
@@ -45,19 +49,39 @@ impl Tracing<Test> for TestDebug {
|
||||
type CallSpan = TestCallSpan;
|
||||
|
||||
fn new_call_span(
|
||||
code_hash: &CodeHash<Test>,
|
||||
contract_account: &AccountIdOf<Test>,
|
||||
entry_point: ExportedFunction,
|
||||
input_data: &[u8],
|
||||
) -> TestCallSpan {
|
||||
DEBUG_EXECUTION_TRACE.with(|d| {
|
||||
d.borrow_mut().push(DebugFrame {
|
||||
code_hash: *code_hash,
|
||||
contract_account: contract_account.clone(),
|
||||
call: entry_point,
|
||||
input: input_data.to_vec(),
|
||||
result: None,
|
||||
})
|
||||
});
|
||||
TestCallSpan { code_hash: *code_hash, call: entry_point, input: input_data.to_vec() }
|
||||
TestCallSpan {
|
||||
contract_account: contract_account.clone(),
|
||||
call: entry_point,
|
||||
input: input_data.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallInterceptor<Test> for TestDebug {
|
||||
fn intercept_call(
|
||||
contract_address: &<Test as frame_system::Config>::AccountId,
|
||||
_entry_point: &ExportedFunction,
|
||||
_input_data: &[u8],
|
||||
) -> Option<ExecResult> {
|
||||
INTERCEPTED_ADDRESS.with(|i| {
|
||||
if i.borrow().as_ref() == Some(contract_address) {
|
||||
Some(Ok(ExecReturnValue { flags: ReturnFlags::REVERT, data: vec![] }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +89,7 @@ impl CallSpan for TestCallSpan {
|
||||
fn after_call(self, output: &ExecReturnValue) {
|
||||
DEBUG_EXECUTION_TRACE.with(|d| {
|
||||
d.borrow_mut().push(DebugFrame {
|
||||
code_hash: self.code_hash,
|
||||
contract_account: self.contract_account,
|
||||
call: self.call,
|
||||
input: self.input,
|
||||
result: Some(output.data.clone()),
|
||||
@@ -75,9 +99,9 @@ impl CallSpan for TestCallSpan {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsafe_debugging_works() {
|
||||
let (wasm_caller, code_hash_caller) = compile_module::<Test>("call").unwrap();
|
||||
let (wasm_callee, code_hash_callee) = compile_module::<Test>("store_call").unwrap();
|
||||
fn debugging_works() {
|
||||
let (wasm_caller, _) = compile_module::<Test>("call").unwrap();
|
||||
let (wasm_callee, _) = compile_module::<Test>("store_call").unwrap();
|
||||
|
||||
fn current_stack() -> Vec<DebugFrame> {
|
||||
DEBUG_EXECUTION_TRACE.with(|stack| stack.borrow().clone())
|
||||
@@ -100,18 +124,18 @@ fn unsafe_debugging_works() {
|
||||
.account_id
|
||||
}
|
||||
|
||||
fn constructor_frame(hash: CodeHash<Test>, after: bool) -> DebugFrame {
|
||||
fn constructor_frame(contract_account: &AccountId32, after: bool) -> DebugFrame {
|
||||
DebugFrame {
|
||||
code_hash: hash,
|
||||
contract_account: contract_account.clone(),
|
||||
call: ExportedFunction::Constructor,
|
||||
input: vec![],
|
||||
result: if after { Some(vec![]) } else { None },
|
||||
}
|
||||
}
|
||||
|
||||
fn call_frame(hash: CodeHash<Test>, args: Vec<u8>, after: bool) -> DebugFrame {
|
||||
fn call_frame(contract_account: &AccountId32, args: Vec<u8>, after: bool) -> DebugFrame {
|
||||
DebugFrame {
|
||||
code_hash: hash,
|
||||
contract_account: contract_account.clone(),
|
||||
call: ExportedFunction::Call,
|
||||
input: args,
|
||||
result: if after { Some(vec![]) } else { None },
|
||||
@@ -129,19 +153,19 @@ fn unsafe_debugging_works() {
|
||||
assert_eq!(
|
||||
current_stack(),
|
||||
vec![
|
||||
constructor_frame(code_hash_caller, false),
|
||||
constructor_frame(code_hash_caller, true),
|
||||
constructor_frame(code_hash_callee, false),
|
||||
constructor_frame(code_hash_callee, true),
|
||||
constructor_frame(&addr_caller, false),
|
||||
constructor_frame(&addr_caller, true),
|
||||
constructor_frame(&addr_callee, false),
|
||||
constructor_frame(&addr_callee, true),
|
||||
]
|
||||
);
|
||||
|
||||
let main_args = (100u32, &addr_callee).encode();
|
||||
let main_args = (100u32, &addr_callee.clone()).encode();
|
||||
let inner_args = (100u32).encode();
|
||||
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr_caller,
|
||||
addr_caller.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
@@ -152,11 +176,54 @@ fn unsafe_debugging_works() {
|
||||
assert_eq!(
|
||||
stack_top,
|
||||
vec![
|
||||
call_frame(code_hash_caller, main_args.clone(), false),
|
||||
call_frame(code_hash_callee, inner_args.clone(), false),
|
||||
call_frame(code_hash_callee, inner_args, true),
|
||||
call_frame(code_hash_caller, main_args, true),
|
||||
call_frame(&addr_caller, main_args.clone(), false),
|
||||
call_frame(&addr_callee, inner_args.clone(), false),
|
||||
call_frame(&addr_callee, inner_args, true),
|
||||
call_frame(&addr_caller, main_args, true),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_interception_works() {
|
||||
let (wasm, _) = compile_module::<Test>("dummy").unwrap();
|
||||
|
||||
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
|
||||
let account_id = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm),
|
||||
vec![],
|
||||
// some salt to ensure that the address of this contract is unique among all tests
|
||||
vec![0x41, 0x41, 0x41, 0x41],
|
||||
DebugInfo::Skip,
|
||||
CollectEvents::Skip,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
|
||||
// no interception yet
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
account_id.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
vec![],
|
||||
));
|
||||
|
||||
// intercept calls to this contract
|
||||
INTERCEPTED_ADDRESS.with(|i| *i.borrow_mut() = Some(account_id.clone()));
|
||||
|
||||
assert_err_ignore_postinfo!(
|
||||
Contracts::call(RuntimeOrigin::signed(ALICE), account_id, 0, GAS_LIMIT, None, vec![],),
|
||||
<Error<Test>>::ContractReverted,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user