Skip to content

Commit

Permalink
feat: tracing eip3074 (#1041)
Browse files Browse the repository at this point in the history
* add eip 3074 to tracing

* add testing for eip 3074 tracing

* cleaning

* fix test

* fix comments
  • Loading branch information
greged93 authored May 2, 2024
1 parent b26b148 commit ef85ab9
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .trunk/trunk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ lint:
- clippy@1.65.0
- git-diff-check
- hadolint@2.12.0
- markdownlint@0.39.0
- markdownlint@0.40.0
- osv-scanner@1.7.2
- oxipng@9.1.1
- prettier@3.2.5
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 19 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,17 @@ cainome = { git = "https://github.com/cartridge-gg/cainome.git", tag = "v0.2.6",
"abigen-rs",
] }
cairo-lang-starknet = { version = "2.5.4", default-features = false }
ef-testing = { git = "https://github.com/kkrt-labs/ef-tests.git", rev = "e215eb2", default-features = false, features = ["v0"], optional = true }
ef-testing = { git = "https://github.com/kkrt-labs/ef-tests.git", rev = "e215eb2", default-features = false, features = [
"v0",
], optional = true }
starknet = { version = "0.9.0", default-features = false }
starknet-crypto = { version = "0.6.1", default-features = false }
starknet_api = { version = "0.7.0-dev.0", default-features = false }

# Ethereum dependencies
alloy-primitives = "0.7.0"
alloy-rlp = { version = "0.3.4", default-features = false }
alphanet-instructions = { git = "https://github.com/paradigmxyz/alphanet.git", rev = "87be409", default-features = false }
ethers = { version = "2.0.9", default-features = false }
ethers-solc = { version = "2.0.9", default-features = false }
jsonrpsee = { version = "0.21.0", features = ["macros", "server"] }
Expand Down Expand Up @@ -126,15 +129,19 @@ hex = { version = "0.4.3", default-features = false }
itertools = { version = "0.12.1", default-features = false }
lazy_static = { version = "1.4.0", default-features = false }
log = { version = "0.4.21", default-features = false }
mongodb = { version = "2.8.2", default-features = false, features = ["tokio-runtime"] }
mongodb = { version = "2.8.2", default-features = false, features = [
"tokio-runtime",
] }
rayon = { version = "1.10.0", default-features = false, optional = true }
reqwest = { version = "0.12.3", default-features = false }
rstest = { version = "0.19.0", default-features = false }

testcontainers = { version = "0.15.0", default-features = false, optional = true }
thiserror = { version = "1.0.58", default-features = false }
tokio = { version = "1.37.0", features = ["macros"] }
tokio-util = { version = "0.7.10", features = ["codec"], default-features = false, optional = true }
tokio-util = { version = "0.7.10", features = [
"codec",
], default-features = false, optional = true }
tokio-stream = { version = "0.1.15", default-features = false, optional = true }
tower = { version = "0.4.13", default-features = false }
tower-http = { version = "0.4.4", default-features = false }
Expand All @@ -148,7 +155,9 @@ rand = { version = "0.8.5", default-features = false, optional = true }
governor = { version = "0.6.0", default-features = false, features = ["std"] }
prometheus = { version = "0.13.0", default-features = false }
hyper = { version = "1.3.1", default-features = false }
hyper-util = { version = "0.1.3", default-features = false, features = ["server"] }
hyper-util = { version = "0.1.3", default-features = false, features = [
"server",
] }
http-body-util = { version = "0.1.1", default-features = false }
pin-project-lite = { version = "0.2", default-features = false }

Expand All @@ -158,8 +167,12 @@ pin-project-lite = { version = "0.2", default-features = false }
# see https://github.com/dojoengine/dojo/issues/563
# When making changes to the rev, please also update to make file to the same rev in the `install-katana` rule.
dojo-test-utils = { git = 'https://github.com/dojoengine/dojo', tag = "v0.6.0-alpha.6", default-features = false }
katana-core = { git = 'https://github.com/dojoengine/dojo', tag = "v0.6.0-alpha.6", features = ["messaging"] }
katana-primitives = { git = 'https://github.com/dojoengine/dojo', tag = "v0.6.0-alpha.6", default-features = false, features = ["serde"] }
katana-core = { git = 'https://github.com/dojoengine/dojo', tag = "v0.6.0-alpha.6", features = [
"messaging",
] }
katana-primitives = { git = 'https://github.com/dojoengine/dojo', tag = "v0.6.0-alpha.6", default-features = false, features = [
"serde",
] }
arbitrary = { version = "1", features = ["derive"] }

[patch."https://github.com/starkware-libs/blockifier"]
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test: katana-genesis load-env

# Example: `make test-target TARGET=test_raw_transaction`
test-target: load-env
cargo test --tests --features "testing,hive" $(TARGET) -- --nocapture
cargo test --tests --features testing $(TARGET) -- --nocapture

benchmark:
cd benchmarks && bun i && bun run benchmark
Expand Down
9 changes: 7 additions & 2 deletions src/test_utils/eoa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ pub trait Eoa<P: Provider + Send + Sync> {
Ok(eth_provider.transaction_count(evm_address, None).await?)
}

fn sign_transaction(&self, tx: Transaction) -> Result<TransactionSigned, eyre::Error> {
fn sign_payload(&self, payload: B256) -> Result<reth_primitives::Signature, eyre::Error> {
let pk = self.private_key();
let signature = sign_message(pk, tx.signature_hash())?;
let signature = sign_message(pk, payload)?;
Ok(signature)
}

fn sign_transaction(&self, tx: Transaction) -> Result<TransactionSigned, eyre::Error> {
let signature = self.sign_payload(tx.signature_hash())?;
Ok(TransactionSigned::from_transaction_and_signature(tx, signature))
}

Expand Down
2 changes: 1 addition & 1 deletion src/test_utils/evm_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub trait EvmContract {
fn prepare_call_transaction<T: Tokenize>(
&self,
selector: &str,
constructor_args: T,
args: T,
tx_info: &TransactionInfo,
) -> Result<Transaction, eyre::Error>;
}
Expand Down
17 changes: 16 additions & 1 deletion src/test_utils/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,25 @@ pub async fn plain_opcodes(#[future] counter: (Katana, KakarotEvmContract)) -> (
),
)
.await
.expect("Failed to deploy ERC20 contract");
.expect("Failed to deploy PlainOpcodes contract");
(katana, contract)
}

/// This fixture deploys an eip 3074 invoker contract on Katana.
#[cfg(any(test, feature = "arbitrary", feature = "testing"))]
#[fixture]
#[awt]
pub async fn eip_3074_invoker(
#[future] counter: (Katana, KakarotEvmContract),
) -> (Katana, KakarotEvmContract, KakarotEvmContract) {
let eoa = counter.0.eoa();
let contract = eoa
.deploy_evm_contract(Some("GasSponsorInvoker"), ())
.await
.expect("Failed to deploy GasSponsorInvoker contract");
(counter.0, counter.1, contract)
}

/// This fixture creates a new test environment on Katana.
#[cfg(any(test, feature = "arbitrary", feature = "testing"))]
#[fixture]
Expand Down
22 changes: 22 additions & 0 deletions src/tracing/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::sync::Arc;

use alphanet_instructions::{context::InstructionsContext, eip3074};
use reth_revm::{inspector_handle_register, primitives::EnvWithHandlerCfg, Database, Evm, EvmBuilder};

#[derive(Debug, Clone)]
Expand All @@ -12,9 +15,28 @@ impl KakarotEvmConfig {
env: EnvWithHandlerCfg,
inspector: I,
) -> Evm<'a, I, DB> {
let instructions_context = InstructionsContext::default();
let to_capture_instructions = instructions_context.clone();

let mut evm = EvmBuilder::default()
.with_db(db)
.with_external_context(inspector)
.append_handler_register_box(Box::new(move |handler| {
if let Some(ref mut table) = handler.instruction_table {
for boxed_instruction_with_opcode in eip3074::boxed_instructions(to_capture_instructions.clone()) {
table.insert_boxed(
boxed_instruction_with_opcode.opcode,
boxed_instruction_with_opcode.boxed_instruction,
);
}
}
let post_execution_context = instructions_context.clone();
handler.post_execution.end = Arc::new(move |_, outcome: _| {
// at the end of the transaction execution we clear the instructions
post_execution_context.clear();
outcome
});
}))
.append_handler_register(inspector_handle_register)
.build();
evm.modify_spec_id(env.spec_id());
Expand Down
116 changes: 100 additions & 16 deletions tests/tests/trace_api.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
#![cfg(feature = "testing")]
use ethers::abi::{Token, Tokenize};
use kakarot_rpc::eth_provider::provider::EthereumProvider;
use kakarot_rpc::test_utils::eoa::Eoa;
use kakarot_rpc::test_utils::evm_contract::{
EvmContract, KakarotEvmContract, TransactionInfo, TxCommonInfo, TxFeeMarketInfo,
EvmContract, KakarotEvmContract, TransactionInfo, TxCommonInfo, TxFeeMarketInfo, TxLegacyInfo,
};
use kakarot_rpc::test_utils::fixtures::{plain_opcodes, setup};
use kakarot_rpc::test_utils::fixtures::{eip_3074_invoker, plain_opcodes, setup};
use kakarot_rpc::test_utils::katana::Katana;
use kakarot_rpc::test_utils::rpc::start_kakarot_rpc_server;
use kakarot_rpc::test_utils::rpc::RawRpcParamsBuilder;
use reth_primitives::{B256, U256};
use reth_primitives::{Address, B256, U256};
use reth_rpc_types::trace::geth::TraceResult;
use reth_rpc_types::trace::parity::LocalizedTransactionTrace;
use rstest::*;
use serde_json::{json, Value};
use starknet::core::types::MaybePendingBlockWithTxHashes;
use starknet::providers::Provider;

/// The block number on which tracing will be performed.
const TRACING_BLOCK_NUMBER: u64 = 0x3;
const TRANSACTIONS_COUNT: usize = 1;
/// The amount of transactions to be traced.
const TRACING_TRANSACTIONS_COUNT: usize = 5;

/// Helper to create a header.
fn header(block_number: u64, hash: B256, parent_hash: B256, base_fee: u128) -> reth_rpc_types::Header {
Expand Down Expand Up @@ -48,22 +51,26 @@ fn header(block_number: u64, hash: B256, parent_hash: B256, base_fee: u128) -> r
}

/// Helper to set up the debug/tracing environment on Katana.
pub async fn tracing(katana: &Katana, plain_opcodes: &KakarotEvmContract) {
pub async fn tracing<T: Tokenize>(
katana: &Katana,
contract: &KakarotEvmContract,
entry_point: &str,
get_args: Box<dyn Fn(u64) -> T>,
) {
let eoa = katana.eoa();
let eoa_address = eoa.evm_address().expect("Failed to get eoa address");
let nonce: u64 = eoa.nonce().await.expect("Failed to get nonce").to();
let chain_id = eoa.eth_provider().chain_id().await.expect("Failed to get chain id").unwrap_or_default().to();

// Push 10 RPC transactions into the database.
let mut txs = Vec::with_capacity(TRANSACTIONS_COUNT);
let mut txs = Vec::with_capacity(TRACING_TRANSACTIONS_COUNT);
let max_fee_per_gas = 10;
let max_priority_fee_per_gas = 1;
for i in 0..TRANSACTIONS_COUNT {
// We want to trace the "createCounterAndInvoke" which does a CREATE followed by a CALL.
let tx = plain_opcodes
for i in 0..TRACING_TRANSACTIONS_COUNT {
let tx = contract
.prepare_call_transaction(
"createCounterAndInvoke",
(),
entry_point,
get_args(nonce + i as u64),
&TransactionInfo::FeeMarketInfo(TxFeeMarketInfo {
common: TxCommonInfo { nonce: nonce + i as u64, value: 0, chain_id },
max_fee_per_gas,
Expand Down Expand Up @@ -125,7 +132,7 @@ async fn test_trace_block(#[future] plain_opcodes: (Katana, KakarotEvmContract),
// Setup the Kakarot RPC server.
let katana = plain_opcodes.0;
let plain_opcodes = plain_opcodes.1;
tracing(&katana, &plain_opcodes).await;
tracing(&katana, &plain_opcodes, "createCounterAndInvoke", Box::new(|_| ())).await;

let (server_addr, server_handle) =
start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server");
Expand All @@ -146,7 +153,7 @@ async fn test_trace_block(#[future] plain_opcodes: (Katana, KakarotEvmContract),

assert!(traces.is_some());
// We expect 3 traces per transaction: CALL, CREATE, and CALL.
assert!(traces.unwrap().len() == TRANSACTIONS_COUNT * 3);
assert!(traces.unwrap().len() == 3 * TRACING_TRANSACTIONS_COUNT);
drop(server_handle);
}

Expand All @@ -157,7 +164,7 @@ async fn test_debug_trace_block_by_number(#[future] plain_opcodes: (Katana, Kaka
// Setup the Kakarot RPC server.
let katana = plain_opcodes.0;
let plain_opcodes = plain_opcodes.1;
tracing(&katana, &plain_opcodes).await;
tracing(&katana, &plain_opcodes, "createCounterAndInvoke", Box::new(|_| ())).await;

let (server_addr, server_handle) =
start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server");
Expand Down Expand Up @@ -188,7 +195,84 @@ async fn test_debug_trace_block_by_number(#[future] plain_opcodes: (Katana, Kaka
serde_json::from_value(raw["result"].clone()).expect("Failed to deserialize result");

assert!(traces.is_some());
// We expect 3 traces per transaction: CALL, CREATE, and CALL.
assert!(traces.unwrap().len() == TRANSACTIONS_COUNT);
// We expect 1 trace per transaction given the formatting of the debug_traceBlockByNumber response.
assert!(traces.unwrap().len() == TRACING_TRANSACTIONS_COUNT);
drop(server_handle);
}

#[rstest]
#[awt]
#[tokio::test(flavor = "multi_thread")]
async fn test_trace_eip3074(#[future] eip_3074_invoker: (Katana, KakarotEvmContract, KakarotEvmContract), _setup: ()) {
// Setup the Kakarot RPC server.
let katana = eip_3074_invoker.0;
let counter = eip_3074_invoker.1;
let invoker = eip_3074_invoker.2;

let eoa = katana.eoa();
let eoa_address = eoa.evm_address().expect("Failed to get eoa address");

let chain_id = katana.eth_provider().chain_id().await.expect("Failed to get chain id").unwrap_or_default().to();
let invoker_address = Address::from_slice(&invoker.evm_address.to_bytes_be()[12..]);
let commit = B256::default();

let get_args = Box::new(move |nonce| {
// Taken from https://github.com/paradigmxyz/alphanet/blob/87be409101bab9c8977b0e74edfb25334511a74c/crates/instructions/src/eip3074.rs#L129
// Composes the message expected by the AUTH instruction in this format:
// `keccak256(MAGIC || chainId || nonce || invokerAddress || commit)`
fn compose_msg(chain_id: u64, nonce: u64, invoker_address: Address, commit: B256) -> B256 {
let mut msg = [0u8; 129];
// MAGIC constant is used for [EIP-3074](https://eips.ethereum.org/EIPS/eip-3074) signatures to prevent signature collisions with other signing formats.
msg[0] = 0x4;
msg[1..33].copy_from_slice(B256::left_padding_from(&chain_id.to_be_bytes()).as_slice());
msg[33..65].copy_from_slice(B256::left_padding_from(&nonce.to_be_bytes()).as_slice());
msg[65..97].copy_from_slice(B256::left_padding_from(invoker_address.as_slice()).as_slice());
msg[97..].copy_from_slice(commit.as_slice());
reth_primitives::keccak256(msg.as_slice())
}
// We use nonce + 1 because authority == sender.
let msg = compose_msg(chain_id, nonce + 1, invoker_address, commit);
let signature = eoa.sign_payload(msg).expect("Failed to sign message");
let calldata = counter
.prepare_call_transaction("inc", (), &TransactionInfo::LegacyInfo(TxLegacyInfo::default()))
.expect("Failed to prepare call transaction")
.input()
.clone();

(
Token::Address(ethers::abi::Address::from_slice(eoa_address.as_slice())),
Token::FixedBytes(commit.as_slice().to_vec()),
Token::Uint(ethers::abi::Uint::from(signature.odd_y_parity as u8 + 27)),
Token::FixedBytes(signature.r.to_be_bytes::<32>().to_vec()),
Token::FixedBytes(signature.s.to_be_bytes::<32>().to_vec()),
Token::Address(ethers::abi::Address::from_slice(&counter.evm_address.to_bytes_be()[12..])),
Token::Bytes(calldata.to_vec()),
)
});

// Set up the transactions for tracing. We call the sponsorCall entry point which should
// auth the invoker as the sender and then call the inc entry point on the counter contract.
tracing(&katana, &invoker, "sponsorCall", get_args).await;

let (server_addr, server_handle) =
start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server");

// Send the trace_block RPC request.
let reqwest_client = reqwest::Client::new();
let res = reqwest_client
.post(format!("http://localhost:{}", server_addr.port()))
.header("Content-Type", "application/json")
.body(RawRpcParamsBuilder::new("trace_block").add_param(format!("0x{:016x}", TRACING_BLOCK_NUMBER)).build())
.send()
.await
.expect("Failed to call Debug RPC");
let response = res.text().await.expect("Failed to get response body");
let raw: Value = serde_json::from_str(&response).expect("Failed to deserialize response body");
let traces: Option<Vec<LocalizedTransactionTrace>> =
serde_json::from_value(raw["result"].clone()).expect("Failed to deserialize result");

assert!(traces.is_some());
// We expect 2 traces per transaction: CALL and CALL.
assert!(traces.unwrap().len() == 2 * TRACING_TRANSACTIONS_COUNT);
drop(server_handle);
}

0 comments on commit ef85ab9

Please sign in to comment.