Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
452145d
test: add reorg testing framework
MegaRedHand Sep 22, 2025
547fff8
test: add additional checks
MegaRedHand Sep 23, 2025
89f5548
refactor: split into two files
MegaRedHand Sep 23, 2025
496bfaf
feat: connect peers via P2P
MegaRedHand Sep 23, 2025
e685093
test: add second test
MegaRedHand Sep 23, 2025
05bf17d
fix: handle syncing status
MegaRedHand Sep 23, 2025
db158b4
chore: remove unused import
MegaRedHand Sep 23, 2025
8ed7a6c
feat: improve error log
MegaRedHand Sep 23, 2025
f3fe845
docs: add readme with instructions to run it
MegaRedHand Sep 23, 2025
ffd27bd
fix: improve multi-test runs
MegaRedHand Sep 23, 2025
0741172
ci: run reorg tests in CI
MegaRedHand Sep 23, 2025
df64404
Merge branch 'main' into add-reorg-framework
MegaRedHand Sep 23, 2025
de50cc8
feat: print ethrex version when running tests
MegaRedHand Sep 23, 2025
1726494
chore: comment failing test
MegaRedHand Sep 23, 2025
9c6774d
chore: fix clippy lint
MegaRedHand Sep 24, 2025
032970d
Merge branch 'main' into add-reorg-framework
MegaRedHand Sep 24, 2025
ec108d8
Merge branch 'main' into add-reorg-framework
MegaRedHand Sep 24, 2025
0ea4e9b
Merge branch 'main' into add-reorg-framework
MegaRedHand Sep 25, 2025
3fec4a2
Merge branch 'main' into add-reorg-framework
MegaRedHand Sep 25, 2025
9279bf0
fix: differentiate blocks according to builder
MegaRedHand Sep 25, 2025
e8d65ca
chore: comment failing test again
MegaRedHand Sep 25, 2025
a8e2e22
test(l1): add storage reorg test
MegaRedHand Sep 24, 2025
2a75471
refactor: use one datadir and logs file per test
MegaRedHand Sep 25, 2025
c0da247
Merge branch 'main' into add-storage-slot-reorg-test
MegaRedHand Sep 26, 2025
51e4b97
test: add additional check to test
MegaRedHand Sep 26, 2025
36d57ee
docs: add comment
MegaRedHand Sep 26, 2025
fbaa687
feat: use unique ports across each run
MegaRedHand Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion tooling/reorgs/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
};

use ethrex::{cli::Options, initializers::init_tracing};
use ethrex_common::U256;
use ethrex_l2_rpc::signer::{LocalSigner, Signer};
use tokio::sync::Mutex;
use tracing::{error, info, warn};
Expand Down Expand Up @@ -32,6 +33,7 @@ async fn main() {
info!("");

run_test(&cmd_path, test_one_block_reorg_and_back).await;
run_test(&cmd_path, test_storage_slots_reorg).await;

// TODO: this test is failing
// run_test(&cmd_path, test_many_blocks_reorg).await;
Expand All @@ -54,7 +56,10 @@ where
let start = std::time::Instant::now();

info!(test=%test_name, "Running test");
let simulator = Arc::new(Mutex::new(Simulator::new(cmd_path.to_path_buf())));
let simulator = Arc::new(Mutex::new(Simulator::new(
cmd_path.to_path_buf(),
test_name.to_string(),
)));

// Run in another task to clean up properly on panic
let result = tokio::spawn(test_fn(simulator.clone())).await;
Expand Down Expand Up @@ -217,3 +222,107 @@ async fn test_many_blocks_reorg(simulator: Arc<Mutex<Simulator>>) {
let new_balance = node0.get_balance(recipient).await;
assert_eq!(new_balance, initial_balance + transfer_amount);
}

async fn test_storage_slots_reorg(simulator: Arc<Mutex<Simulator>>) {
let mut simulator = simulator.lock().await;
// Initcode for deploying a contract that receives two `bytes32` parameters and sets `storage[param0] = param1`
let contract_deploy_bytecode = hex::decode("656020355f35555f526006601af3").unwrap().into();
let signer: Signer = LocalSigner::new(
"941e103320615d394a55708be13e45994c7d93b932b064dbcb2b511fe3254e2e"
.parse()
.unwrap(),
)
.into();

let slot_key0 = U256::from(42);
let slot_value0 = U256::from(1163);
let slot_key1 = U256::from(25);
let slot_value1 = U256::from(7474);

let node0 = simulator.start_node().await;
let node1 = simulator.start_node().await;

// Create a chain with a few empty blocks
let mut base_chain = simulator.get_base_chain();

// Send a deploy tx for a contract which receives: `(bytes32 key, bytes32 value)` as parameters
let contract_address = node0
.send_contract_deploy(&signer, contract_deploy_bytecode)
.await;

for _ in 0..10 {
let extended_base_chain = node0.build_payload(base_chain).await;
node0.notify_new_payload(&extended_base_chain).await;
node0.update_forkchoice(&extended_base_chain).await;

node1.notify_new_payload(&extended_base_chain).await;
node1.update_forkchoice(&extended_base_chain).await;
base_chain = extended_base_chain;
}

// Sanity check: storage slots are initially empty
let initial_value = node0.get_storage_at(contract_address, slot_key0).await;
assert_eq!(initial_value, U256::zero());
let initial_value = node0.get_storage_at(contract_address, slot_key1).await;
assert_eq!(initial_value, U256::zero());

// Fork the chain
let mut side_chain = base_chain.fork();

// Create a side chain with multiple blocks only known to node0
for _ in 0..10 {
side_chain = node0.build_payload(side_chain).await;
node0.notify_new_payload(&side_chain).await;
node0.update_forkchoice(&side_chain).await;
}

// Advance the base chain with multiple blocks only known to node1
for _ in 0..10 {
base_chain = node1.build_payload(base_chain).await;
node1.notify_new_payload(&base_chain).await;
node1.update_forkchoice(&base_chain).await;
}

// Set a storage slot in the contract in node0
let calldata0 = [slot_key0.to_big_endian(), slot_value0.to_big_endian()]
.concat()
.into();
node0.send_call(&signer, contract_address, calldata0).await;

// Set another storage slot in the contract in node1
let calldata1 = [slot_key1.to_big_endian(), slot_value1.to_big_endian()]
.concat()
.into();
node1.send_call(&signer, contract_address, calldata1).await;

// Build a block in the side chain
side_chain = node0.build_payload(side_chain).await;
node0.notify_new_payload(&side_chain).await;
node0.update_forkchoice(&side_chain).await;

// Build a block in the base chain
base_chain = node1.build_payload(base_chain).await;
node1.notify_new_payload(&base_chain).await;
node1.update_forkchoice(&base_chain).await;

// Assert the storage slots are as expected in both forks
let value_slot0 = node0.get_storage_at(contract_address, slot_key0).await;
assert_eq!(value_slot0, slot_value0);
let value_slot1 = node0.get_storage_at(contract_address, slot_key1).await;
assert_eq!(value_slot1, U256::zero());

let value_slot0 = node1.get_storage_at(contract_address, slot_key0).await;
assert_eq!(value_slot0, U256::zero());
let value_slot1 = node1.get_storage_at(contract_address, slot_key1).await;
assert_eq!(value_slot1, slot_value1);

// Reorg the node0 to the base chain
node0.notify_new_payload(&base_chain).await;
node0.update_forkchoice(&base_chain).await;

// Check the storage slots are as expected after the reorg
let value_slot0 = node0.get_storage_at(contract_address, slot_key0).await;
assert_eq!(value_slot0, U256::zero());
let value_slot1 = node0.get_storage_at(contract_address, slot_key1).await;
assert_eq!(value_slot1, slot_value1);
}
116 changes: 107 additions & 9 deletions tooling/reorgs/src/simulator.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::{fs::File, io::Read, path::PathBuf, process::Stdio, time::Duration};
use std::{
fs::File, io::Read, path::PathBuf, process::Stdio, sync::atomic::AtomicU16, time::Duration,
};

use ethrex::{cli::Options, initializers::get_network};
use ethrex_common::{
Bytes, H160, H256, U256,
Address, Bytes, H160, H256, U256,
evm::calculate_create_address,
types::{
Block, EIP1559Transaction, Genesis, Transaction, TxKind, requests::compute_requests_hash,
},
Expand All @@ -26,6 +29,8 @@ use tracing::{error, info};

pub struct Simulator {
cmd_path: PathBuf,
test_name: String,

base_opts: Options,
jwt_secret: Bytes,
genesis_path: PathBuf,
Expand All @@ -35,7 +40,7 @@ pub struct Simulator {
}

impl Simulator {
pub fn new(cmd_path: PathBuf) -> Self {
pub fn new(cmd_path: PathBuf, test_name: String) -> Self {
let mut opts = Options::default_l1();
let jwt_secret = generate_jwt_secret();
std::fs::write("jwt.hex", hex::encode(&jwt_secret)).unwrap();
Expand All @@ -52,6 +57,7 @@ impl Simulator {
opts.network = Some(Network::GenesisPath(genesis_path.clone()));
Self {
cmd_path,
test_name,
base_opts: opts,
genesis_path,
jwt_secret,
Expand All @@ -69,18 +75,23 @@ impl Simulator {

pub async fn start_node(&mut self) -> Node {
let n = self.configs.len();
let test_name = &self.test_name;
info!(node = n, "Starting node");
let mut opts = self.base_opts.clone();
opts.http_port = (8545 + n * 2).to_string();
opts.authrpc_port = (8545 + n * 2 + 1).to_string();
opts.p2p_port = (30303 + n).to_string();
opts.discovery_port = (30303 + n).to_string();
opts.datadir = format!("data/node{n}").into();
opts.datadir = format!("data/{test_name}/node{n}").into();

opts.http_port = get_next_port().to_string();
opts.authrpc_port = get_next_port().to_string();

// These are one TCP and one UDP
let p2p_port = get_next_port();
opts.p2p_port = p2p_port.to_string();
opts.discovery_port = p2p_port.to_string();

let _ = std::fs::remove_dir_all(&opts.datadir);
std::fs::create_dir_all(&opts.datadir).expect("Failed to create data directory");

let logs_file_path = format!("data/node{n}.log");
let logs_file_path = format!("data/{test_name}/node{n}.log");
let logs_file = File::create(&logs_file_path).expect("Failed to create logs file");

let cancel = CancellationToken::new();
Expand Down Expand Up @@ -333,14 +344,96 @@ impl Node {
.unwrap();
}

pub async fn send_call(&self, signer: &Signer, contract: H160, data: Bytes) {
info!(node = self.index, sender=%signer.address(), %contract, "Sending contract call");
let chain_id = self
.rpc_client
.get_chain_id()
.await
.unwrap()
.try_into()
.unwrap();
let sender_address = signer.address();
let nonce = self
.rpc_client
.get_nonce(sender_address, BlockIdentifier::Tag(BlockTag::Latest))
.await
.unwrap();
let tx = EIP1559Transaction {
chain_id,
nonce,
max_priority_fee_per_gas: 0,
max_fee_per_gas: 1_000_000_000,
gas_limit: 50_000,
to: TxKind::Call(contract),
data,
..Default::default()
};
let mut tx = Transaction::EIP1559Transaction(tx);
tx.sign_inplace(signer).await.unwrap();
let encoded_tx = tx.encode_canonical_to_vec();
self.rpc_client
.send_raw_transaction(&encoded_tx)
.await
.unwrap();
}

pub async fn send_contract_deploy(
&self,
signer: &Signer,
contract_deploy_bytecode: Bytes,
) -> Address {
info!(node = self.index, sender=%signer.address(), "Deploying contract");
let chain_id = self
.rpc_client
.get_chain_id()
.await
.unwrap()
.try_into()
.unwrap();
let sender_address = signer.address();
let nonce = self
.rpc_client
.get_nonce(sender_address, BlockIdentifier::Tag(BlockTag::Latest))
.await
.unwrap();
let tx = EIP1559Transaction {
chain_id,
nonce,
max_priority_fee_per_gas: 0,
max_fee_per_gas: 1_000_000_000,
gas_limit: 100_000,
to: TxKind::Create,
data: contract_deploy_bytecode,
..Default::default()
};
let mut tx = Transaction::EIP1559Transaction(tx);
tx.sign_inplace(signer).await.unwrap();
let encoded_tx = tx.encode_canonical_to_vec();
self.rpc_client
.send_raw_transaction(&encoded_tx)
.await
.unwrap();

calculate_create_address(sender_address, nonce)
}

pub async fn get_balance(&self, address: H160) -> U256 {
self.rpc_client
.get_balance(address, Default::default())
.await
.unwrap()
}

pub async fn get_storage_at(&self, address: H160, key: U256) -> U256 {
self.rpc_client
.get_storage_at(address, key, Default::default())
.await
.unwrap()
}
}

#[derive(Debug)]
pub struct Chain {
block_hashes: Vec<H256>,
blocks: Vec<Block>,
Expand Down Expand Up @@ -431,3 +524,8 @@ async fn wait_until_synced(engine_client: &EngineClient, fork_choice_state: Fork
tokio::time::sleep(Duration::from_millis(100)).await;
}
}

fn get_next_port() -> u16 {
static NEXT_PORT: AtomicU16 = AtomicU16::new(8560);
NEXT_PORT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
Loading