Skip to content
Merged
86 changes: 78 additions & 8 deletions client/rpc/debug/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ use ethereum_types::H256;
use fc_rpc::{frontier_backend_client, internal_err};
use fc_storage::StorageOverride;
use fp_rpc::EthereumRuntimeRPCApi;
use moonbeam_client_evm_tracing::formatters::call_tracer::CallTracerInner;
use moonbeam_client_evm_tracing::types::block;
use moonbeam_client_evm_tracing::types::block::BlockTransactionTrace;
use moonbeam_client_evm_tracing::types::single::TransactionTrace;
use moonbeam_client_evm_tracing::{formatters::ResponseFormatter, types::single};
use moonbeam_rpc_core_types::{RequestBlockId, RequestBlockTag};
use moonbeam_rpc_primitives_debug::{DebugRuntimeApi, TracerInput};
Expand All @@ -38,6 +40,7 @@ use sp_block_builder::BlockBuilder;
use sp_blockchain::{
Backend as BlockchainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata,
};
use sp_core::H160;
use sp_runtime::{
generic::BlockId,
traits::{BlakeTwo256, Block as BlockT, Header as HeaderT, UniqueSaturatedInto},
Expand Down Expand Up @@ -422,13 +425,32 @@ where
.current_transaction_statuses(hash)
.unwrap_or_default();

// Partial ethereum transaction data to check if a trace match an ethereum transaction
struct EthTxPartial {
transaction_hash: H256,
from: H160,
to: Option<H160>,
}

// Known ethereum transaction hashes.
let eth_transactions_by_index: BTreeMap<u32, H256> = statuses
let eth_transactions_by_index: BTreeMap<u32, EthTxPartial> = statuses
.iter()
.map(|t| (t.transaction_index, t.transaction_hash))
.map(|status| {
(
status.transaction_index,
EthTxPartial {
transaction_hash: status.transaction_hash,
from: status.from,
to: status.to,
},
)
})
.collect();

let eth_tx_hashes: Vec<_> = eth_transactions_by_index.values().cloned().collect();
let eth_tx_hashes: Vec<_> = eth_transactions_by_index
.values()
.map(|tx| tx.transaction_hash)
.collect();

// If there are no ethereum transactions in the block return empty trace right away.
if eth_tx_hashes.is_empty() {
Expand Down Expand Up @@ -504,6 +526,9 @@ where
Ok(moonbeam_rpc_primitives_debug::Response::Block)
};

// Offset to account for old buggy transactions that are in trace not in the ethereum block
let mut tx_position_offset = 0;

return match trace_type {
single::TraceType::CallList => {
let mut proxy = moonbeam_client_evm_tracing::listeners::CallList::default();
Expand All @@ -517,13 +542,58 @@ where
.ok_or("Trace result is empty.")
.map_err(|e| internal_err(format!("{:?}", e)))?
.into_iter()
.map(|mut trace| {
if let Some(transaction_hash) =
eth_transactions_by_index.get(&trace.tx_position)
.filter_map(|mut trace: BlockTransactionTrace| {
if let Some(EthTxPartial {
transaction_hash,
from,
to,
}) = eth_transactions_by_index
.get(&(trace.tx_position - tx_position_offset))
{
trace.tx_hash = *transaction_hash;
// verify that the trace matches the ethereum transaction
let (trace_from, trace_to) = match trace.result {
TransactionTrace::Raw { .. } => {
(Default::default(), None)
}
TransactionTrace::CallList(_) => {
(Default::default(), None)
}
TransactionTrace::CallListNested(ref call) => {
match call {
single::Call::Blockscout(_) => {
(Default::default(), None)
}
single::Call::CallTracer(call) => (
call.from,
match call.inner {
CallTracerInner::Call {
to, ..
} => Some(to),
CallTracerInner::Create { .. } => None,
CallTracerInner::SelfDestruct {
..
} => None,
},
),
}
}
};
if trace_from == *from && trace_to == *to {
trace.tx_hash = *transaction_hash;
Some(trace)
} else {
// if the trace does not match the ethereum transaction
// it means that the trace is about a buggy transaction that is not in the block
// we need to offset the tx_position
tx_position_offset += 1;
None
}
} else {
// If the transaction is not in the ethereum block
// it should not appear in the block trace
tx_position_offset += 1;
None
}
trace
})
.collect::<Vec<BlockTransactionTrace>>();

Expand Down
180 changes: 180 additions & 0 deletions test/suites/tracing-tests/test-trace-erc20-xcm-2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli";
import { ALITH_ADDRESS, CHARLETH_ADDRESS, alith } from "@moonwall/util";
import { hexToNumber, parseEther } from "viem";
import {
ERC20_TOTAL_SUPPLY,
XcmFragment,
type XcmFragmentConfig,
expectEVMResult,
injectEncodedHrmpMessageAndSeal,
injectHrmpMessage,
injectHrmpMessageAndSeal,
sovereignAccountOfSibling,
} from "../../helpers";

describeSuite({
id: "T21",
title: "Trace ERC20 xcm #2",
foundationMethods: "dev",
testCases: ({ context, it }) => {
let erc20ContractAddress: string;
let eventEmitterAddress: `0x${string}`;
let ethXcmTxHash: string;
let regularEthTxHash: string;
beforeAll(async () => {
const { contractAddress, status } = await context.deployContract!("ERC20WithInitialSupply", {
args: ["ERC20", "20S", ALITH_ADDRESS, ERC20_TOTAL_SUPPLY],
});
erc20ContractAddress = contractAddress;
expect(status).eq("success");

const paraId = 888;
const paraSovereign = sovereignAccountOfSibling(context, paraId);
const amountTransferred = 1_000_000n;

// Get pallet indices
const metadata = await context.polkadotJs().rpc.state.getMetadata();
const balancesPalletIndex = metadata.asLatest.pallets
.find(({ name }) => name.toString() === "Balances")!
.index.toNumber();
const erc20XcmPalletIndex = metadata.asLatest.pallets
.find(({ name }) => name.toString() === "Erc20XcmBridge")!
.index.toNumber();

// Send some native tokens to the sovereign account of paraId (to pay fees)
await context
.polkadotJs()
.tx.balances.transferAllowDeath(paraSovereign, parseEther("1"))
.signAndSend(alith);
await context.createBlock();

// Send some erc20 tokens to the sovereign account of paraId
const rawTx = await context.writeContract!({
contractName: "ERC20WithInitialSupply",
contractAddress: erc20ContractAddress as `0x${string}`,
functionName: "transfer",
args: [paraSovereign, amountTransferred],
rawTxOnly: true,
});
const { result } = await context.createBlock(rawTx);
expectEVMResult(result!.events, "Succeed");
expect(
await context.readContract!({
contractName: "ERC20WithInitialSupply",
contractAddress: erc20ContractAddress as `0x${string}`,
functionName: "balanceOf",
args: [paraSovereign],
})
).equals(amountTransferred);

// Create an XCM message that try to transfer more than available
const failedConfig: XcmFragmentConfig = {
assets: [
{
multilocation: {
parents: 0,
interior: {
X1: { PalletInstance: Number(balancesPalletIndex) },
},
},
fungible: 1_700_000_000_000_000n,
},
{
multilocation: {
parents: 0,
interior: {
X2: [
{
PalletInstance: erc20XcmPalletIndex,
},
{
AccountKey20: {
network: null,
key: erc20ContractAddress,
},
},
],
},
},
fungible: amountTransferred * 2n, // Try to transfer twice the available amount
},
],
beneficiary: CHARLETH_ADDRESS,
};

const failedXcmMessage = new XcmFragment(failedConfig)
.withdraw_asset()
.clear_origin()
.buy_execution()
.deposit_asset(2n)
.as_v3();

// Mock the reception of the failed xcm message N times
// N should be high enough to fill IdleMaxServiceWeigh
// The goal is to have at least one XCM message queud for next block on_initialize
for (let i = 0; i < 20; i++) {
await injectHrmpMessage(context, paraId, {
type: "XcmVersionedXcm",
payload: failedXcmMessage,
});
}
await context.createBlock();

// By calling deployContract() a new block will be created,
// including the ethereum-xcm transaction (on_initialize) + regular ethereum transaction
const { contractAddress: eventEmitterAddress_ } = await context.deployContract!(
"EventEmitter",
{
from: alith.address,
} as any
);
eventEmitterAddress = eventEmitterAddress_;

// The old buggy runtime rollback the eth-xcm tx because XCM executor rollback evm reverts
regularEthTxHash = (await context.viem().getBlock()).transactions[0];

// Get the latest block events
const block = await context.polkadotJs().rpc.chain.getBlock();
const allRecords = await context.polkadotJs().query.system.events.at(block.block.header.hash);

// Compute XCM message ID
const messageHash = context.polkadotJs().createType("XcmVersionedXcm", failedXcmMessage).hash;

// Find messageQueue.Processed event with matching message ID
const processedEvent = allRecords.find(
({ event }) =>
event.section === "messageQueue" &&
event.method === "Processed" &&
event.data[0].toString() === messageHash.toHex()
);

expect(processedEvent).to.not.be.undefined;
});

// IMPORTANT: this test will fail once we will merge https://github.com/moonbeam-foundation/moonbeam/pull/3258
it({
id: "T01",
title: "should doesn't include the failed ERC20 xcm transaction in block trace",
test: async function () {
const number = await context.viem().getBlockNumber();
const trace = await customDevRpcRequest("debug_traceBlockByNumber", [
number.toString(),
{ tracer: "callTracer" },
]);

// Verify that only the regular eth transaction is included in the block trace.
expect(trace.length).to.eq(1);

// 1st traced transaction is regular ethereum transaction.
// - `From` is Alith's adddress.
// - `To` is the ethereum contract address.
const txHash = trace[0].txHash;
expect(txHash).to.eq(regularEthTxHash);
const call = trace[0].result;
expect(call.from).to.eq(alith.address.toLowerCase());
expect(call.to).to.eq(eventEmitterAddress.toLowerCase());
expect(call.type).be.eq("CREATE");
},
});
},
});
Loading
Loading