From e61b0089a3c821c86961219c2838a3144816e88d Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:35:35 -0500 Subject: [PATCH 1/8] wip: create test harness independently --- Cargo.lock | 55 +++++ Cargo.toml | 8 +- crates/flashblocks-rpc/Cargo.toml | 2 + .../src/tests/assets/genesis.json | 6 + .../src/tests/framework_test.rs | 135 +++++++++++ crates/flashblocks-rpc/src/tests/mod.rs | 1 + crates/flashblocks-rpc/src/tests/rpc.rs | 43 +++- crates/test-utils/Cargo.toml | 82 +++++++ crates/test-utils/README.md | 225 ++++++++++++++++++ crates/test-utils/assets/genesis.json | 106 +++++++++ crates/test-utils/src/accounts.rs | 88 +++++++ crates/test-utils/src/engine.rs | 183 ++++++++++++++ crates/test-utils/src/flashblocks.rs | 156 ++++++++++++ crates/test-utils/src/harness.rs | 171 +++++++++++++ crates/test-utils/src/lib.rs | 44 ++++ crates/test-utils/src/node.rs | 160 +++++++++++++ 16 files changed, 1457 insertions(+), 8 deletions(-) create mode 100644 crates/flashblocks-rpc/src/tests/framework_test.rs create mode 100644 crates/test-utils/Cargo.toml create mode 100644 crates/test-utils/README.md create mode 100644 crates/test-utils/assets/genesis.json create mode 100644 crates/test-utils/src/accounts.rs create mode 100644 crates/test-utils/src/engine.rs create mode 100644 crates/test-utils/src/flashblocks.rs create mode 100644 crates/test-utils/src/harness.rs create mode 100644 crates/test-utils/src/lib.rs create mode 100644 crates/test-utils/src/node.rs diff --git a/Cargo.lock b/Cargo.lock index f0dc3a14..598767fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,6 +1510,7 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "arc-swap", + "base-reth-test-utils", "brotli", "eyre", "futures-util", @@ -1517,6 +1518,7 @@ dependencies = [ "jsonrpsee-types 0.26.0", "metrics", "metrics-derive", + "once_cell", "op-alloy-consensus 0.20.0", "op-alloy-network", "op-alloy-rpc-types", @@ -1646,6 +1648,59 @@ dependencies = [ "uuid", ] +[[package]] +name = "base-reth-test-utils" +version = "0.1.15" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "base-reth-flashblocks-rpc", + "chrono", + "eyre", + "futures", + "futures-util", + "jsonrpsee 0.26.0", + "once_cell", + "op-alloy-consensus 0.20.0", + "op-alloy-network", + "op-alloy-rpc-types", + "op-alloy-rpc-types-engine", + "reth", + "reth-db", + "reth-db-common", + "reth-e2e-test-utils", + "reth-exex", + "reth-ipc", + "reth-optimism-chainspec 1.8.2", + "reth-optimism-cli", + "reth-optimism-node", + "reth-optimism-primitives 1.8.2", + "reth-optimism-rpc", + "reth-primitives", + "reth-primitives-traits 1.8.2", + "reth-provider", + "reth-rpc-layer", + "reth-testing-utils", + "reth-tracing", + "rollup-boost", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.2", + "tracing", + "url", +] + [[package]] name = "base-reth-transaction-tracing" version = "0.1.15" diff --git a/Cargo.toml b/Cargo.toml index 338a3fb9..5b0aad0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/flashblocks-rpc", "crates/metering", "crates/node", + "crates/test-utils", "crates/transaction-tracing", ] @@ -41,6 +42,7 @@ codegen-units = 1 base-reth-flashblocks-rpc = { path = "crates/flashblocks-rpc" } base-reth-metering = { path = "crates/metering" } base-reth-node = { path = "crates/node" } +base-reth-test-utils = { path = "crates/test-utils" } base-reth-transaction-tracing = { path = "crates/transaction-tracing" } # base/tips @@ -55,7 +57,10 @@ reth-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-optimism-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } -reth-optimism-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } +reth-optimism-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2", features = [ + "client", +] } +reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-optimism-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } @@ -69,6 +74,7 @@ reth-exex = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } +reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.8.2" } # revm revm = { version = "29.0.0", default-features = false } diff --git a/crates/flashblocks-rpc/Cargo.toml b/crates/flashblocks-rpc/Cargo.toml index 61467431..cb543821 100644 --- a/crates/flashblocks-rpc/Cargo.toml +++ b/crates/flashblocks-rpc/Cargo.toml @@ -74,8 +74,10 @@ brotli.workspace = true arc-swap.workspace = true [dev-dependencies] +base-reth-test-utils.workspace = true rand.workspace = true reth-db.workspace = true reth-testing-utils.workspace = true reth-db-common.workspace = true reth-e2e-test-utils.workspace = true +once_cell.workspace = true diff --git a/crates/flashblocks-rpc/src/tests/assets/genesis.json b/crates/flashblocks-rpc/src/tests/assets/genesis.json index 4d703497..79ab75e9 100644 --- a/crates/flashblocks-rpc/src/tests/assets/genesis.json +++ b/crates/flashblocks-rpc/src/tests/assets/genesis.json @@ -17,6 +17,12 @@ "mergeNetsplitBlock": 0, "bedrockBlock": 0, "regolithTime": 0, + "canyonTime": 0, + "ecotoneTime": 0, + "fjordTime": 0, + "graniteTime": 0, + "isthmusTime": 0, + "pragueTime": 0, "terminalTotalDifficulty": 0, "terminalTotalDifficultyPassed": true, "optimism": { diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs new file mode 100644 index 00000000..5628145f --- /dev/null +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -0,0 +1,135 @@ +//! Integration tests using the new test-utils framework +//! +//! These tests demonstrate using the test-utils framework with: +//! - TestNode for node setup +//! - EngineContext for canonical block production +//! - FlashblocksContext for pending state testing +//! - Pre-funded test accounts + +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::U256; +use alloy_provider::Provider; +use base_reth_test_utils::{EngineContext, TestNode}; +use eyre::Result; + +#[tokio::test] +async fn test_framework_node_setup() -> Result<()> { + reth_tracing::init_test_tracing(); + + // Create test node with Base Sepolia and pre-funded accounts + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Verify chain ID + let chain_id = provider.get_chain_id().await?; + assert_eq!(chain_id, 84532); // Base Sepolia + + // Verify test accounts are funded + let alice_balance = node.get_balance(node.alice().address).await?; + assert!(alice_balance > U256::ZERO, "Alice should have initial balance"); + + let bob_balance = node.get_balance(node.bob().address).await?; + assert!(bob_balance > U256::ZERO, "Bob should have initial balance"); + + Ok(()) +} + +#[tokio::test] +async fn test_framework_engine_api_block_production() -> Result<()> { + reth_tracing::init_test_tracing(); + + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Get genesis block + let genesis = provider + .get_block_by_number(BlockNumberOrTag::Number(0)) + .await? + .expect("Genesis block should exist"); + + let genesis_hash = genesis.header.hash; + + // Create engine context for canonical block production + let mut engine = EngineContext::new( + node.http_url(), + genesis_hash, + genesis.header.timestamp, + ) + .await?; + + // Build and finalize a single canonical block + let block_1_hash = engine.build_and_finalize_block().await?; + assert_ne!(block_1_hash, genesis_hash); + assert_eq!(engine.block_number(), 1); + + // Verify the block exists + let block_1 = provider + .get_block_by_hash(block_1_hash) + .await? + .expect("Block 1 should exist"); + assert_eq!(block_1.header.number, 1); + + // Advance chain by multiple blocks + let block_hashes = engine.advance_chain(3).await?; + assert_eq!(block_hashes.len(), 3); + assert_eq!(engine.block_number(), 4); + + // Verify latest block + let latest = provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("Latest block should exist"); + assert_eq!(latest.header.number, 4); + assert_eq!(latest.header.hash, engine.head_hash()); + + Ok(()) +} + +#[tokio::test] +async fn test_framework_account_balances() -> Result<()> { + reth_tracing::init_test_tracing(); + + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Check all test accounts have their initial balances + let accounts = &node.accounts; + + for account in accounts.all() { + let balance = provider.get_balance(account.address).await?; + assert_eq!( + balance, + account.initial_balance_wei(), + "{} should have initial balance", + account.name + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_framework_parallel_nodes() -> Result<()> { + reth_tracing::init_test_tracing(); + + // Launch multiple nodes in parallel to verify isolation + let (node1_result, node2_result) = tokio::join!(TestNode::new(), TestNode::new()); + + let node1 = node1_result?; + let node2 = node2_result?; + + // Verify they have different ports + assert_ne!(node1.http_api_addr, node2.http_api_addr); + + // Verify both are functional + let provider1 = node1.provider().await?; + let provider2 = node2.provider().await?; + + let chain_id_1 = provider1.get_chain_id().await?; + let chain_id_2 = provider2.get_chain_id().await?; + + assert_eq!(chain_id_1, 84532); + assert_eq!(chain_id_2, 84532); + + Ok(()) +} diff --git a/crates/flashblocks-rpc/src/tests/mod.rs b/crates/flashblocks-rpc/src/tests/mod.rs index 820364fb..d3e9f6b1 100644 --- a/crates/flashblocks-rpc/src/tests/mod.rs +++ b/crates/flashblocks-rpc/src/tests/mod.rs @@ -1,5 +1,6 @@ use alloy_primitives::{b256, bytes, Bytes, B256}; +mod framework_test; mod rpc; mod state; mod utils; diff --git a/crates/flashblocks-rpc/src/tests/rpc.rs b/crates/flashblocks-rpc/src/tests/rpc.rs index 9c36d858..0652deb6 100644 --- a/crates/flashblocks-rpc/src/tests/rpc.rs +++ b/crates/flashblocks-rpc/src/tests/rpc.rs @@ -16,6 +16,7 @@ mod tests { use alloy_rpc_types_engine::PayloadId; use alloy_rpc_types_eth::error::EthRpcErrorCode; use alloy_rpc_types_eth::TransactionInput; + use once_cell::sync::OnceCell; use op_alloy_consensus::OpDepositReceipt; use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; use op_alloy_rpc_types::OpTransactionRequest; @@ -24,6 +25,7 @@ mod tests { use reth::chainspec::Chain; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; + use reth_exex::ExExEvent; use reth_optimism_chainspec::OpChainSpecBuilder; use reth_optimism_node::args::RollupArgs; use reth_optimism_node::OpNode; @@ -37,6 +39,7 @@ mod tests { use std::str::FromStr; use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; + use tokio_stream::StreamExt; pub struct NodeContext { sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, @@ -117,9 +120,11 @@ mod tests { let node = OpNode::new(RollupArgs::default()); - // Start websocket server to simulate the builder and send payloads back to the node + // Start dummy websocket server to simulate the builder and send payloads back to the node let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let NodeHandle { node, node_exit_future, @@ -128,23 +133,47 @@ mod tests { .with_types_and_provider::>() .with_components(node.components_builder()) .with_add_ons(node.add_ons()) + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + move |mut ctx| async move { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for b in committed.blocks_iter() { + fb.on_canonical_block_received(b); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) .extend_rpc_modules(move |ctx| { - // We are not going to use the websocket connection to send payloads so we use - // a dummy url. - let flashblocks_state = Arc::new(FlashblocksState::new(ctx.provider().clone())); - flashblocks_state.start(); + // We are not going to use the websocket connection to send payloads so we don't + // initialize a flashblocks subscriber + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + + fb.start(); let api_ext = EthApiExt::new( ctx.registry.eth_api().clone(), ctx.registry.eth_handlers().filter.clone(), - flashblocks_state.clone(), + fb.clone(), ); ctx.modules.replace_configured(api_ext.into_rpc())?; tokio::spawn(async move { while let Some((payload, tx)) = receiver.recv().await { - flashblocks_state.on_flashblock_received(payload); + fb.on_flashblock_received(payload); tx.send(()).unwrap(); } }); diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml new file mode 100644 index 00000000..bd01f9ab --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,82 @@ +[package] +name = "base-reth-test-utils" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +description = "Common integration test utilities for node-reth crates" + +[lints] +workspace = true + +[dependencies] +# internal +base-reth-flashblocks-rpc.workspace = true + +# reth +reth.workspace = true +reth-optimism-node.workspace = true +reth-optimism-chainspec.workspace = true +reth-optimism-cli.workspace = true +reth-optimism-primitives.workspace = true +reth-optimism-rpc.workspace = true +reth-provider.workspace = true +reth-primitives.workspace = true +reth-primitives-traits.workspace = true +reth-db.workspace = true +reth-db-common.workspace = true +reth-testing-utils.workspace = true +reth-e2e-test-utils.workspace = true +reth-exex.workspace = true +reth-tracing.workspace = true +reth-rpc-layer.workspace = true +reth-ipc.workspace = true + +# alloy +alloy-primitives.workspace = true +alloy-genesis.workspace = true +alloy-eips.workspace = true +alloy-rpc-types.workspace = true +alloy-rpc-types-engine.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-consensus.workspace = true +alloy-provider.workspace = true +alloy-rpc-client.workspace = true +alloy-serde.workspace = true + +# op-alloy +op-alloy-rpc-types.workspace = true +op-alloy-rpc-types-engine.workspace = true +op-alloy-network.workspace = true +op-alloy-consensus.workspace = true + +# rollup-boost +rollup-boost.workspace = true + +# tokio +tokio.workspace = true +tokio-stream.workspace = true +tokio-util = { version = "0.7", features = ["compat"] } + +# async +futures.workspace = true +futures-util.workspace = true + +# rpc +jsonrpsee.workspace = true + +# misc +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true +eyre.workspace = true +once_cell.workspace = true +url.workspace = true +chrono.workspace = true + +# tower for middleware +tower = "0.5" + +[dev-dependencies] diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md new file mode 100644 index 00000000..08426b3b --- /dev/null +++ b/crates/test-utils/README.md @@ -0,0 +1,225 @@ +# Test Utils + +A comprehensive integration test framework for node-reth crates. + +## Overview + +This crate provides reusable testing utilities for integration tests across the node-reth workspace. It includes: + +- **Node Setup**: Easy creation of test nodes with Base Sepolia chainspec +- **Engine API Integration**: Control canonical block production and chain advancement +- **Flashblocks Support**: Dummy flashblocks delivery mechanism for testing pending state +- **Test Accounts**: Pre-funded hardcoded accounts (Alice, Bob, Charlie, Deployer) + +## Features + +### 1. Test Node (`TestNode`) + +Create isolated test nodes with Base Sepolia configuration: + +```rust +use base_reth_test_utils::TestNode; + +#[tokio::test] +async fn test_example() -> eyre::Result<()> { + // Create a test node with Base Sepolia chainspec and pre-funded accounts + let node = TestNode::new().await?; + + // Get an alloy provider + let provider = node.provider().await?; + + // Access test accounts + let alice = node.alice(); + let balance = node.get_balance(alice.address).await?; + + Ok(()) +} +``` + +**Key Features:** +- Automatic port allocation (enables parallel test execution) +- Disabled P2P discovery (isolated testing) +- Pre-funded test accounts +- HTTP RPC server enabled + +### 2. Test Accounts (`TestAccounts`) + +Hardcoded test accounts with deterministic addresses and private keys: + +```rust +use base_reth_test_utils::TestAccounts; + +let accounts = TestAccounts::new(); + +// Access individual accounts +let alice = &accounts.alice; +let bob = &accounts.bob; +let charlie = &accounts.charlie; +let deployer = &accounts.deployer; + +// Each account has: +// - name: Account identifier +// - address: Ethereum address +// - private_key: Private key (hex string) +// - initial_balance_eth: Starting balance in ETH +``` + +**Account Details:** +- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH +- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH +- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH +- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH + +These are derived from Anvil's test mnemonic for compatibility. + +### 3. Engine API Integration (`EngineContext`) + +Control canonical block production via Engine API: + +```rust +use base_reth_test_utils::EngineContext; + +#[tokio::test] +async fn test_engine_api() -> eyre::Result<()> { + let node = TestNode::new().await?; + + // Create engine context + let mut engine = EngineContext::new( + node.http_url(), + B256::ZERO, // genesis hash + 1710338135, // initial timestamp + ).await?; + + // Build and finalize a single canonical block + let block_hash = engine.build_and_finalize_block().await?; + + // Advance the chain by multiple blocks + let block_hashes = engine.advance_chain(5).await?; + + // Check current state + let head = engine.head_hash(); + let block_number = engine.block_number(); + + Ok(()) +} +``` + +**Engine Operations:** +- `build_and_finalize_block()` - Create and finalize a single block +- `advance_chain(n)` - Build N blocks sequentially +- `update_forkchoice(...)` - Manual forkchoice updates +- Track current head, block number, and timestamp + +### 4. Flashblocks Integration (`FlashblocksContext`) + +Dummy flashblocks delivery for testing pending state: + +```rust +use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; + +#[tokio::test] +async fn test_flashblocks() -> eyre::Result<()> { + let (fb_ctx, receiver) = FlashblocksContext::new(); + + // Create a base flashblock (first flashblock with base payload) + let flashblock = FlashblockBuilder::new(1, 0) + .as_base(B256::ZERO, 1000) + .with_transaction(tx_bytes, tx_hash, 21000) + .with_balance(address, U256::from(1000)) + .build(); + + // Send flashblock and wait for processing + fb_ctx.send_flashblock(flashblock).await?; + + // Create a delta flashblock (subsequent flashblock) + let delta = FlashblockBuilder::new(1, 1) + .with_transaction(tx_bytes, tx_hash, 21000) + .build(); + + fb_ctx.send_flashblock(delta).await?; + + Ok(()) +} +``` + +**Flashblock Features:** +- Base flashblocks with `ExecutionPayloadBaseV1` +- Delta flashblocks with incremental changes +- Builder pattern for easy construction +- Channel-based delivery (non-WebSocket) + +## Architecture + +The framework is organized into modules: + +``` +test-utils/ +├── src/ +│ ├── lib.rs # Public API and re-exports +│ ├── accounts.rs # Test account definitions +│ ├── node.rs # TestNode implementation +│ ├── engine.rs # Engine API integration +│ └── flashblocks.rs # Flashblocks support +├── assets/ +│ └── genesis.json # Base Sepolia genesis configuration +└── Cargo.toml +``` + +## Usage in Other Crates + +Add `base-reth-test-utils` to your `dev-dependencies`: + +```toml +[dev-dependencies] +base-reth-test-utils.workspace = true +``` + +Then use in your integration tests: + +```rust +use base_reth_test_utils::{TestNode, TestAccounts}; + +#[tokio::test] +async fn my_integration_test() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let node = TestNode::new().await?; + let provider = node.provider().await?; + + // Your test logic here + + Ok(()) +} +``` + +## Design Decisions + +1. **Anvil-Compatible Keys**: Uses the same deterministic mnemonic as Anvil for easy compatibility with other tools +2. **Port Allocation**: Random unused ports enable parallel test execution without conflicts +3. **Isolated Nodes**: Disabled P2P discovery ensures tests don't interfere with each other +4. **Channel-Based Flashblocks**: Non-WebSocket delivery mechanism simplifies testing +5. **Builder Patterns**: Fluent APIs for constructing complex test scenarios + +## Future Enhancements + +This framework is designed to be extended. Planned additions: + +- Transaction builders for common operations +- Smart contract deployment helpers +- Snapshot/restore functionality for test state +- Multi-node network simulation +- Performance benchmarking utilities + +## Testing + +Run the test suite: + +```bash +cargo test -p base-reth-test-utils +``` + +## References + +This framework was inspired by: +- [op-rbuilder test framework](https://github.com/flashbots/op-rbuilder/tree/main/crates/op-rbuilder/src/tests/framework) +- [reth e2e-test-utils](https://github.com/paradigmxyz/reth/tree/main/crates/e2e-test-utils) diff --git a/crates/test-utils/assets/genesis.json b/crates/test-utils/assets/genesis.json new file mode 100644 index 00000000..79ab75e9 --- /dev/null +++ b/crates/test-utils/assets/genesis.json @@ -0,0 +1,106 @@ +{ + "config": { + "chainId": 8453, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "mergeNetsplitBlock": 0, + "bedrockBlock": 0, + "regolithTime": 0, + "canyonTime": 0, + "ecotoneTime": 0, + "fjordTime": 0, + "graniteTime": 0, + "isthmusTime": 0, + "pragueTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "optimism": { + "eip1559Elasticity": 6, + "eip1559Denominator": 50 + } + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x00", + "gasLimit": "0x1c9c380", + "difficulty": "0x0", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "0x14dc79964da2c08b23698b3d3cc7ca32193d9955": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x1cbd3b2770909d4e10f157cabc84c7264073c9ec": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x2546bcd3c84621e976d8185a91a922ae77ecec30": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x71be63f3384f5fb98995898a86b02fb2426c5788": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x90f79bf6eb2c4f870365e785982e1f101e93b906": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x976ea74026e726554db657fa54763abd0c3a0aa9": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc": { + "balance": "0xd3c21bcecceda1000000" + }, + "0x9c41de96b2088cdc640c6182dfcf5491dc574a57": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xa0ee7a142d267c1f36714e4a8f75612f20a79720": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xbcd4042de499d14e55001ccbb24a551f3b954096": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xbda5747bfd65f08deb54cb465eb87d40e51b197e": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xcd3b766ccdd6ae721141f452c550ca635964ce71": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xdd2fd4581271e230360230f9337d5c0430bf44c0": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": { + "balance": "0xd3c21bcecceda1000000" + }, + "0xfabb0ac9d68b0b445fb7357272ff202c5651694a": { + "balance": "0xd3c21bcecceda1000000" + } + }, + "number": "0x0" +} \ No newline at end of file diff --git a/crates/test-utils/src/accounts.rs b/crates/test-utils/src/accounts.rs new file mode 100644 index 00000000..63bf4d8c --- /dev/null +++ b/crates/test-utils/src/accounts.rs @@ -0,0 +1,88 @@ +//! Test accounts with pre-funded balances for integration testing + +use alloy_primitives::{address, Address}; + +/// Hardcoded test account with a fixed private key +#[derive(Debug, Clone)] +pub struct TestAccount { + /// Account name for easy identification + pub name: &'static str, + /// Ethereum address + pub address: Address, + /// Private key (hex string without 0x prefix) + pub private_key: &'static str, +} + +/// Collection of all test accounts +#[derive(Debug, Clone)] +pub struct TestAccounts { + pub alice: TestAccount, + pub bob: TestAccount, + pub charlie: TestAccount, + pub deployer: TestAccount, +} + +impl TestAccounts { + /// Create a new instance with all test accounts + pub fn new() -> Self { + Self { + alice: ALICE, + bob: BOB, + charlie: CHARLIE, + deployer: DEPLOYER, + } + } + + /// Get all accounts as a vector + pub fn all(&self) -> Vec<&TestAccount> { + vec![&self.alice, &self.bob, &self.charlie, &self.deployer] + } + + /// Get account by name + pub fn get(&self, name: &str) -> Option<&TestAccount> { + match name { + "alice" => Some(&self.alice), + "bob" => Some(&self.bob), + "charlie" => Some(&self.charlie), + "deployer" => Some(&self.deployer), + _ => None, + } + } +} + +impl Default for TestAccounts { + fn default() -> Self { + Self::new() + } +} + +// Hardcoded test accounts using Anvil's deterministic keys +// These are derived from the test mnemonic: "test test test test test test test test test test test junk" + +/// Alice - First test account (Anvil account #0) +pub const ALICE: TestAccount = TestAccount { + name: "Alice", + address: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + private_key: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", +}; + +/// Bob - Second test account (Anvil account #1) +pub const BOB: TestAccount = TestAccount { + name: "Bob", + address: address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"), + private_key: "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", +}; + +/// Charlie - Third test account (Anvil account #2) +pub const CHARLIE: TestAccount = TestAccount { + name: "Charlie", + address: address!("3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), + private_key: "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", +}; + +/// Deployer - Account for deploying smart contracts (Anvil account #3) +pub const DEPLOYER: TestAccount = TestAccount { + name: "Deployer", + address: address!("90F79bf6EB2c4f870365E785982E1f101E93b906"), + private_key: "7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", +}; diff --git a/crates/test-utils/src/engine.rs b/crates/test-utils/src/engine.rs new file mode 100644 index 00000000..4c1c19c1 --- /dev/null +++ b/crates/test-utils/src/engine.rs @@ -0,0 +1,183 @@ +//! Engine API integration for canonical block production +//! +//! This module provides a typed, type-safe Engine API client based on +//! reth's OpEngineApiClient trait instead of raw string-based RPC calls. + +use alloy_eips::eip7685::Requests; +use alloy_primitives::B256; +use alloy_rpc_types_engine::{ForkchoiceUpdated, PayloadId, PayloadStatus}; +use eyre::Result; +use jsonrpsee::core::client::SubscriptionClientT; +use op_alloy_rpc_types_engine::OpExecutionPayloadV4; +use reth::api::{EngineTypes, PayloadTypes}; +use reth::rpc::types::engine::ForkchoiceState; +use reth_optimism_node::OpEngineTypes; +use reth_optimism_rpc::engine::OpEngineApiClient; +use reth_rpc_layer::{AuthClientLayer, JwtSecret}; +use reth_tracing::tracing::debug; +use std::marker::PhantomData; +use std::time::Duration; +use url::Url; + +/// Default JWT secret for testing +const DEFAULT_JWT_SECRET: &str = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +#[derive(Clone, Debug)] +pub enum EngineAddress { + Http(Url), + Ipc(String), +} + +pub trait EngineProtocol: Send + Sync { + fn client( + jwt: JwtSecret, + address: EngineAddress, + ) -> impl std::future::Future< + Output = impl jsonrpsee::core::client::SubscriptionClientT + Send + Sync + Unpin + 'static, + > + Send; +} + +pub struct HttpEngine; + +impl EngineProtocol for HttpEngine { + async fn client( + jwt: JwtSecret, + address: EngineAddress, + ) -> impl SubscriptionClientT + Send + Sync + Unpin + 'static { + let EngineAddress::Http(url) = address else { + unreachable!(); + }; + + let secret_layer = AuthClientLayer::new(jwt); + let middleware = tower::ServiceBuilder::default().layer(secret_layer); + + jsonrpsee::http_client::HttpClientBuilder::default() + .request_timeout(Duration::from_secs(10)) + .set_http_middleware(middleware) + .build(url) + .expect("Failed to create http client") + } +} + +pub struct IpcEngine; + +impl EngineProtocol for IpcEngine { + async fn client( + _: JwtSecret, // ipc does not use JWT + address: EngineAddress, + ) -> impl SubscriptionClientT + Send + Sync + Unpin + 'static { + let EngineAddress::Ipc(path) = address else { + unreachable!(); + }; + reth_ipc::client::IpcClientBuilder::default() + .build(&path) + .await + .expect("Failed to create ipc client") + } +} + +pub struct EngineApi { + address: EngineAddress, + jwt_secret: JwtSecret, + _phantom: PhantomData

, +} + +impl EngineApi { + pub fn new(engine_url: String) -> Result { + let url: Url = engine_url.parse()?; + let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; + + Ok(Self { + address: EngineAddress::Http(url), + jwt_secret, + _phantom: PhantomData, + }) + } +} + +impl EngineApi { + pub fn new(path: String) -> Result { + let jwt_secret: JwtSecret = DEFAULT_JWT_SECRET.parse()?; + + Ok(Self { + address: EngineAddress::Ipc(path), + jwt_secret, + _phantom: PhantomData, + }) + } +} + +impl EngineApi

{ + /// Get a client instance + async fn client(&self) -> impl SubscriptionClientT + Send + Sync + Unpin + 'static + use

{ + P::client(self.jwt_secret, self.address.clone()).await + } + + /// Get a payload by ID from the Engine API + pub async fn get_payload( + &self, + payload_id: PayloadId, + ) -> eyre::Result<::ExecutionPayloadEnvelopeV4> { + debug!( + "Fetching payload with id: {} at {}", + payload_id, + chrono::Utc::now() + ); + Ok( + OpEngineApiClient::::get_payload_v4(&self.client().await, payload_id) + .await?, + ) + } + + /// Submit a new payload to the Engine API + pub async fn new_payload( + &self, + payload: OpExecutionPayloadV4, + versioned_hashes: Vec, + parent_beacon_block_root: B256, + execution_requests: Requests, + ) -> eyre::Result { + debug!("Submitting new payload at {}...", chrono::Utc::now()); + Ok(OpEngineApiClient::::new_payload_v4( + &self.client().await, + payload, + versioned_hashes, + parent_beacon_block_root, + execution_requests, + ) + .await?) + } + + /// Update forkchoice on the Engine API + pub async fn update_forkchoice( + &self, + current_head: B256, + new_head: B256, + payload_attributes: Option<::PayloadAttributes>, + ) -> eyre::Result { + debug!( + "Updating forkchoice at {} (current: {}, new: {})", + chrono::Utc::now(), + current_head, + new_head + ); + let result = OpEngineApiClient::::fork_choice_updated_v3( + &self.client().await, + ForkchoiceState { + head_block_hash: new_head, + safe_block_hash: current_head, + finalized_block_hash: current_head, + }, + payload_attributes, + ) + .await; + + match &result { + Ok(fcu) => debug!("Forkchoice updated successfully: {:?}", fcu), + Err(e) => debug!("Forkchoice update failed: {:?}", e), + } + + Ok(result?) + } +} diff --git a/crates/test-utils/src/flashblocks.rs b/crates/test-utils/src/flashblocks.rs new file mode 100644 index 00000000..484c4619 --- /dev/null +++ b/crates/test-utils/src/flashblocks.rs @@ -0,0 +1,156 @@ +//! Dummy flashblocks integration for testing pending state + +use alloy_primitives::{Bytes, TxHash}; +use base_reth_flashblocks_rpc::subscription::Flashblock; +use eyre::Result; +use tokio::sync::{mpsc, oneshot}; + +// Re-export types from flashblocks-rpc +pub use base_reth_flashblocks_rpc::subscription::{Flashblock as FlashblockPayload, Metadata as FlashblockMetadata}; + +/// Context for managing dummy flashblock delivery in tests +/// +/// This provides a non-WebSocket, queue-based mechanism for delivering +/// flashblocks to a test node, similar to how the rpc.rs tests work currently. +pub struct FlashblocksContext { + /// Channel for sending flashblocks to the node + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, +} + +impl FlashblocksContext { + /// Create a new flashblocks context with a channel + /// + /// Returns the context and a receiver that the node should consume + pub fn new() -> (Self, mpsc::Receiver<(Flashblock, oneshot::Sender<()>)>) { + let (sender, receiver) = mpsc::channel(100); + (Self { sender }, receiver) + } + + /// Send a flashblock to the node and wait for processing + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.sender.send((flashblock, tx)).await?; + rx.await?; + Ok(()) + } + + /// Send multiple flashblocks sequentially + pub async fn send_flashblocks(&self, flashblocks: Vec) -> Result<()> { + for flashblock in flashblocks { + self.send_flashblock(flashblock).await?; + } + Ok(()) + } +} + +impl Default for FlashblocksContext { + fn default() -> Self { + Self::new().0 + } +} + +/// Helper to extract transactions from a vec of flashblocks +pub fn extract_transactions_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { + let mut all_txs = Vec::new(); + + for flashblock in flashblocks { + all_txs.extend(flashblock.diff.transactions.clone()); + } + + all_txs +} + +/// Helper to get all transaction hashes from flashblock metadata +pub fn extract_tx_hashes_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { + let mut all_hashes = Vec::new(); + + for flashblock in flashblocks { + all_hashes.extend(flashblock.metadata.receipts.keys().copied()); + } + + all_hashes +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Address, B256, U256}; + use alloy_primitives::map::HashMap; + use alloy_consensus::Receipt; + use alloy_rpc_types_engine::PayloadId; + use base_reth_flashblocks_rpc::subscription::Metadata; + use op_alloy_consensus::OpDepositReceipt; + use reth_optimism_primitives::OpReceipt; + use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + + fn create_test_flashblock() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 0, + base: Some(ExecutionPayloadBaseV1 { + parent_beacon_block_root: B256::default(), + parent_hash: B256::default(), + fee_recipient: Address::ZERO, + prev_randao: B256::default(), + block_number: 1, + gas_limit: 30_000_000, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + }), + diff: ExecutionPayloadFlashblockDeltaV1 { + transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])], + ..Default::default() + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + B256::random(), + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 21000, + logs: vec![], + }, + deposit_nonce: Some(1), + deposit_receipt_version: None, + }), + ); + receipts + }, + new_account_balances: HashMap::default(), + }, + } + } + + #[tokio::test] + async fn test_flashblocks_context() { + let (ctx, mut receiver) = FlashblocksContext::new(); + let flashblock = create_test_flashblock(); + + // Spawn a task to receive and acknowledge + let handle = tokio::spawn(async move { + if let Some((fb, tx)) = receiver.recv().await { + assert_eq!(fb.metadata.block_number, 1); + tx.send(()).unwrap(); + } + }); + + // Send flashblock + ctx.send_flashblock(flashblock).await.unwrap(); + + // Wait for receiver task + handle.await.unwrap(); + } + + #[test] + fn test_extract_transactions() { + let flashblock = create_test_flashblock(); + let txs = extract_transactions_from_flashblocks(&[flashblock]); + + assert_eq!(txs.len(), 1); + assert_eq!(txs[0], Bytes::from(vec![0x01, 0x02, 0x03])); + } +} diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs new file mode 100644 index 00000000..99da7863 --- /dev/null +++ b/crates/test-utils/src/harness.rs @@ -0,0 +1,171 @@ +//! Unified test harness combining node, engine API, and flashblocks functionality + +use crate::accounts::TestAccounts; +use crate::engine::{EngineApi, IpcEngine}; +use crate::node::LocalNode; +use crate::Flashblock; +use alloy_eips::eip7685::Requests; +use alloy_primitives::{Bytes, B256}; +use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_types::BlockNumberOrTag; +use alloy_rpc_types_engine::PayloadAttributes; +use eyre::{eyre, Result}; +use op_alloy_network::Optimism; +use op_alloy_rpc_types_engine::OpPayloadAttributes; +use std::time::Duration; +use tokio::time::sleep; + +const BLOCK_TIME_SECONDS: u64 = 2; +const GAS_LIMIT: u64 = 200_000_000; +const NODE_STARTUP_DELAY_MS: u64 = 500; +const BLOCK_BUILD_DELAY_MS: u64 = 100; + +pub struct TestHarness { + node: LocalNode, + engine: EngineApi, + accounts: TestAccounts, +} + +impl TestHarness { + pub async fn new() -> Result { + let node = LocalNode::new().await?; + let engine = node.engine_api()?; + let accounts = TestAccounts::new(); + + sleep(Duration::from_millis(NODE_STARTUP_DELAY_MS)).await; + + Ok(Self { + node, + engine, + accounts, + }) + } + + pub fn provider(&self) -> RootProvider { + self.node + .provider() + .expect("provider should always be available after node initialization") + } + + pub fn accounts(&self) -> &TestAccounts { + &self.accounts + } + + async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { + let latest_block = self + .provider() + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or_else(|| eyre!("No genesis block found"))?; + + let parent_hash = latest_block.header.hash; + let next_timestamp = latest_block.header.timestamp + BLOCK_TIME_SECONDS; + + let payload_attributes = OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: next_timestamp, + parent_beacon_block_root: Some(B256::ZERO), + withdrawals: Some(vec![]), + ..Default::default() + }, + transactions: Some(transactions), + gas_limit: Some(GAS_LIMIT), + no_tx_pool: Some(true), + ..Default::default() + }; + + let forkchoice_result = self + .engine + .update_forkchoice(parent_hash, parent_hash, Some(payload_attributes)) + .await?; + + let payload_id = forkchoice_result + .payload_id + .ok_or_else(|| eyre!("Forkchoice update did not return payload ID"))?; + + sleep(Duration::from_millis(BLOCK_BUILD_DELAY_MS)).await; + + let payload_envelope = self.engine.get_payload(payload_id).await?; + + let execution_requests = if payload_envelope.execution_requests.is_empty() { + Requests::default() + } else { + Requests::new(payload_envelope.execution_requests) + }; + + let payload_status = self + .engine + .new_payload( + payload_envelope.execution_payload, + vec![], + payload_envelope.parent_beacon_block_root, + execution_requests, + ) + .await?; + + if payload_status.status.is_invalid() { + return Err(eyre!("Engine rejected payload: {:?}", payload_status)); + } + + let new_block_hash = payload_status + .latest_valid_hash + .ok_or_else(|| eyre!("Payload status missing latest_valid_hash"))?; + + self.engine + .update_forkchoice(parent_hash, new_block_hash, None) + .await?; + + Ok(()) + } + + pub async fn advance_chain(&self, n: u64) -> Result<()> { + for _ in 0..n { + self.build_block_from_transactions(vec![]).await?; + } + Ok(()) + } + + pub async fn build_block_from_flashblocks(&self, flashblocks: &[Flashblock]) -> Result<()> { + let transactions: Vec = flashblocks + .iter() + .flat_map(|fb| fb.diff.transactions.iter().cloned()) + .collect(); + self.build_block_from_transactions(transactions).await + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + self.node.send_flashblock(flashblock).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::U256; + use alloy_provider::Provider; + + #[tokio::test] + async fn test_harness_setup() -> Result<()> { + reth_tracing::init_test_tracing(); + let harness = TestHarness::new().await?; + + assert_eq!(harness.accounts().alice.name, "Alice"); + assert_eq!(harness.accounts().bob.name, "Bob"); + + let provider = harness.provider(); + let chain_id = provider.get_chain_id().await?; + assert_eq!(chain_id, crate::node::BASE_CHAIN_ID); + + let alice_balance = provider + .get_balance(harness.accounts().alice.address) + .await?; + assert!(alice_balance > U256::ZERO); + + let block_number = provider.get_block_number().await?; + harness.advance_chain(5).await?; + let new_block_number = provider.get_block_number().await?; + assert_eq!(new_block_number, block_number + 5); + + Ok(()) + } +} diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs new file mode 100644 index 00000000..9237599c --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,44 @@ +//! Common integration test utilities for node-reth crates +//! +//! This crate provides a comprehensive test framework for integration testing. +//! +//! # Quick Start +//! +//! ```no_run +//! use base_reth_test_utils::TestHarness; +//! +//! #[tokio::test] +//! async fn test_example() -> eyre::Result<()> { +//! let harness = TestHarness::new().await?; +//! +//! // Send flashblocks for pending state testing +//! harness.send_flashblock(flashblock).await?; +//! +//! // Access test accounts +//! let alice = harness.alice(); +//! let balance = harness.get_balance(alice.address).await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Components +//! +//! - **TestHarness** - Unified interface combining node, engine API, and flashblocks +//! - **TestNode** - Node setup with Base Sepolia chainspec and flashblocks integration +//! - **EngineContext** - Engine API integration for canonical block production +//! - **TestAccounts** - Pre-funded test accounts (Alice, Bob, Charlie, Deployer - 10,000 ETH each) + +pub mod accounts; +pub mod engine; +pub mod flashblocks; +pub mod harness; +pub mod node; + +// Re-export commonly used types +pub use accounts::{TestAccount, TestAccounts}; +pub use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata as FlashblockMetadata}; +pub use engine::EngineApi; +pub use flashblocks::FlashblocksContext; +pub use harness::TestHarness; +pub use node::LocalNode; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs new file mode 100644 index 00000000..3809eee4 --- /dev/null +++ b/crates/test-utils/src/node.rs @@ -0,0 +1,160 @@ +//! Local node setup with Base Sepolia chainspec + +use crate::engine::EngineApi; +use crate::Flashblock; +use alloy_genesis::Genesis; +use alloy_provider::RootProvider; +use alloy_rpc_client::RpcClient; +use base_reth_flashblocks_rpc::rpc::{EthApiExt, EthApiOverrideServer}; +use base_reth_flashblocks_rpc::state::FlashblocksState; +use base_reth_flashblocks_rpc::subscription::FlashblocksReceiver; +use eyre::Result; +use once_cell::sync::OnceCell; +use op_alloy_network::Optimism; +use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; +use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; +use reth::core::exit::NodeExitFuture; +use reth::tasks::TaskManager; +use reth_exex::ExExEvent; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_node::args::RollupArgs; +use reth_optimism_node::OpNode; +use reth_provider::providers::BlockchainProvider; +use std::any::Any; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::StreamExt; + +pub const BASE_CHAIN_ID: u64 = 8453; + +pub struct LocalNode { + http_api_addr: SocketAddr, + engine_ipc_path: String, + flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + _node_exit_future: NodeExitFuture, + _node: Box, + _task_manager: TaskManager, +} + +impl LocalNode { + pub async fn new() -> Result { + let tasks = TaskManager::current(); + let exec = tasks.executor(); + + let genesis: Genesis = serde_json::from_str(include_str!("../assets/genesis.json"))?; + let chain_spec = Arc::new(OpChainSpec::from_genesis(genesis)); + + let network_config = NetworkArgs { + discovery: DiscoveryArgs { + disable_discovery: true, + ..DiscoveryArgs::default() + }, + ..NetworkArgs::default() + }; + + let node_config = NodeConfig::new(chain_spec.clone()) + .with_network(network_config) + .with_rpc( + RpcServerArgs::default() + .with_unused_ports() + .with_http() + .with_auth_ipc(), + ) + .with_unused_ports(); + + let node = OpNode::new(RollupArgs::default()); + + let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let NodeHandle { + node: node_handle, + node_exit_future, + } = NodeBuilder::new(node_config.clone()) + .testing_node(exec.clone()) + .with_types_and_provider::>() + .with_components(node.components_builder()) + .with_add_ons(node.add_ons()) + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + move |mut ctx| async move { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for b in committed.blocks_iter() { + fb.on_canonical_block_received(b); + } + let _ = ctx + .events + .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + + fb.start(); + + let api_ext = EthApiExt::new( + ctx.registry.eth_api().clone(), + ctx.registry.eth_handlers().filter.clone(), + fb.clone(), + ); + + ctx.modules.replace_configured(api_ext.into_rpc())?; + + // Spawn task to receive flashblocks from the test context + tokio::spawn(async move { + while let Some((payload, tx)) = receiver.recv().await { + fb.on_flashblock_received(payload); + tx.send(()).unwrap(); + } + }); + + Ok(()) + }) + .launch() + .await?; + + let http_api_addr = node_handle + .rpc_server_handle() + .http_local_addr() + .ok_or_else(|| eyre::eyre!("HTTP RPC server failed to bind to address"))?; + + let engine_ipc_path = node_config.rpc.auth_ipc_path; + + Ok(Self { + http_api_addr, + engine_ipc_path, + flashblock_sender: sender, + _node_exit_future: node_exit_future, + _node: Box::new(node_handle), + _task_manager: tasks, + }) + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.flashblock_sender.send((flashblock, tx)).await?; + rx.await?; + Ok(()) + } + + pub fn provider(&self) -> Result> { + let url = format!("http://{}", self.http_api_addr); + let client = RpcClient::builder().http(url.parse()?); + Ok(RootProvider::::new(client)) + } + + pub fn engine_api(&self) -> Result> { + EngineApi::::new(self.engine_ipc_path.clone()) + } +} From 4e8379871d54b3c01236af8b6747c6121f398e5c Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:46:56 -0500 Subject: [PATCH 2/8] wip: initialize foundry project --- .gitmodules | 3 ++ contracts/.github/workflows/test.yml | 37 ++++++++++++++++ contracts/.gitignore | 14 ++++++ contracts/README.md | 66 ++++++++++++++++++++++++++++ contracts/foundry.lock | 8 ++++ contracts/foundry.toml | 6 +++ contracts/lib/forge-std | 1 + contracts/script/Counter.s.sol | 19 ++++++++ contracts/src/Counter.sol | 14 ++++++ contracts/test/Counter.t.sol | 24 ++++++++++ 10 files changed, 192 insertions(+) create mode 100644 .gitmodules create mode 100644 contracts/.github/workflows/test.yml create mode 100644 contracts/.gitignore create mode 100644 contracts/README.md create mode 100644 contracts/foundry.lock create mode 100644 contracts/foundry.toml create mode 160000 contracts/lib/forge-std create mode 100644 contracts/script/Counter.s.sol create mode 100644 contracts/src/Counter.sol create mode 100644 contracts/test/Counter.t.sol diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c65a5965 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/.github/workflows/test.yml b/contracts/.github/workflows/test.yml new file mode 100644 index 00000000..c24b9832 --- /dev/null +++ b/contracts/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: CI + +permissions: + contents: read + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: forge --version + + - name: Run Forge fmt + run: forge fmt --check + + - name: Run Forge build + run: forge build --sizes + + - name: Run Forge tests + run: forge test -vvv diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 00000000..85198aaa --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 00000000..8817d6ab --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 00000000..fee8a957 --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "tag": { + "name": "v1.11.0", + "rev": "8e40513d678f392f398620b3ef2b418648b33e89" + } + } +} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 00000000..25b918f9 --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 00000000..8e40513d --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 diff --git a/contracts/script/Counter.s.sol b/contracts/script/Counter.s.sol new file mode 100644 index 00000000..f01d69c3 --- /dev/null +++ b/contracts/script/Counter.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = new Counter(); + + vm.stopBroadcast(); + } +} diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol new file mode 100644 index 00000000..aded7997 --- /dev/null +++ b/contracts/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol new file mode 100644 index 00000000..48319108 --- /dev/null +++ b/contracts/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} From c145ec7caf2d152584820f6cf1d2f674c2d38372 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:47:34 -0500 Subject: [PATCH 3/8] wip: install solmate --- .gitmodules | 3 +++ contracts/lib/solmate | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/solmate diff --git a/.gitmodules b/.gitmodules index c65a5965..29187eb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/forge-std"] path = contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/solmate"] + path = contracts/lib/solmate + url = https://github.com/transmissions11/solmate diff --git a/contracts/lib/solmate b/contracts/lib/solmate new file mode 160000 index 00000000..89365b88 --- /dev/null +++ b/contracts/lib/solmate @@ -0,0 +1 @@ +Subproject commit 89365b880c4f3c786bdd453d4b8e8fe410344a69 From ca026084080a67fae469e2cc326ace5d571350e6 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 17:50:23 -0500 Subject: [PATCH 4/8] update readme --- crates/test-utils/README.md | 323 +++++++++++++++++++++++------------- 1 file changed, 212 insertions(+), 111 deletions(-) diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md index 08426b3b..630eb2b1 100644 --- a/crates/test-utils/README.md +++ b/crates/test-utils/README.md @@ -6,113 +6,210 @@ A comprehensive integration test framework for node-reth crates. This crate provides reusable testing utilities for integration tests across the node-reth workspace. It includes: -- **Node Setup**: Easy creation of test nodes with Base Sepolia chainspec -- **Engine API Integration**: Control canonical block production and chain advancement -- **Flashblocks Support**: Dummy flashblocks delivery mechanism for testing pending state +- **LocalNode**: Isolated in-process node with Base Sepolia chainspec +- **TestHarness**: Unified orchestration layer combining node, Engine API, and flashblocks +- **EngineApi**: Type-safe Engine API client for CL operations - **Test Accounts**: Pre-funded hardcoded accounts (Alice, Bob, Charlie, Deployer) +- **Flashblocks Support**: Testing pending state with flashblocks delivery -## Features - -### 1. Test Node (`TestNode`) - -Create isolated test nodes with Base Sepolia configuration: +## Quick Start ```rust -use base_reth_test_utils::TestNode; +use base_reth_test_utils::TestHarness; #[tokio::test] async fn test_example() -> eyre::Result<()> { - // Create a test node with Base Sepolia chainspec and pre-funded accounts - let node = TestNode::new().await?; + let harness = TestHarness::new().await?; + + // Advance the chain + harness.advance_chain(5).await?; - // Get an alloy provider - let provider = node.provider().await?; + // Access accounts + let alice = &harness.accounts().alice; - // Access test accounts - let alice = node.alice(); - let balance = node.get_balance(alice.address).await?; + // Get balance via provider + let balance = harness.provider().get_balance(alice.address).await?; Ok(()) } ``` -**Key Features:** -- Automatic port allocation (enables parallel test execution) -- Disabled P2P discovery (isolated testing) -- Pre-funded test accounts -- HTTP RPC server enabled +## Architecture + +The framework follows a three-layer architecture: + +``` +┌─────────────────────────────────────┐ +│ TestHarness │ ← Orchestration layer (tests use this) +│ - Coordinates node + engine │ +│ - Builds blocks from transactions │ +│ - Manages test accounts │ +└─────────────────────────────────────┘ + │ │ + ┌──────┘ └──────┐ + ▼ ▼ +┌─────────┐ ┌──────────┐ +│LocalNode│ │EngineApi │ ← Raw API wrappers +│ (EL) │ │ (CL) │ +└─────────┘ └──────────┘ +``` + +### Component Responsibilities + +- **LocalNode** (EL wrapper): In-process Optimism node with HTTP RPC + Engine API IPC +- **EngineApi** (CL wrapper): Raw Engine API calls (forkchoice, payloads) +- **TestHarness**: Orchestrates block building by fetching latest block headers and calling Engine API + +## Components -### 2. Test Accounts (`TestAccounts`) +### 1. TestHarness -Hardcoded test accounts with deterministic addresses and private keys: +The main entry point for integration tests. Combines node, engine, and accounts into a single interface. ```rust -use base_reth_test_utils::TestAccounts; +use base_reth_test_utils::TestHarness; +use alloy_primitives::Bytes; -let accounts = TestAccounts::new(); +#[tokio::test] +async fn test_harness() -> eyre::Result<()> { + let harness = TestHarness::new().await?; -// Access individual accounts -let alice = &accounts.alice; -let bob = &accounts.bob; -let charlie = &accounts.charlie; -let deployer = &accounts.deployer; + // Access provider + let provider = harness.provider(); + let chain_id = provider.get_chain_id().await?; + + // Access accounts + let alice = &harness.accounts().alice; + let bob = &harness.accounts().bob; + + // Build empty blocks + harness.advance_chain(10).await?; + + // Build block with transactions + let txs: Vec = vec![/* signed transaction bytes */]; + harness.build_block_from_transactions(txs).await?; + + // Build block from flashblocks + harness.build_block_from_flashblocks(&flashblocks).await?; + + // Send flashblocks for pending state testing + harness.send_flashblock(flashblock).await?; -// Each account has: -// - name: Account identifier -// - address: Ethereum address -// - private_key: Private key (hex string) -// - initial_balance_eth: Starting balance in ETH + Ok(()) +} ``` -**Account Details:** -- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH -- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH -- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH -- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH +**Key Methods:** +- `new()` - Create new harness with node, engine, and accounts +- `provider()` - Get Alloy RootProvider for RPC calls +- `accounts()` - Access test accounts +- `advance_chain(n)` - Build N empty blocks +- `build_block_from_transactions(txs)` - Build block with specific transactions +- `build_block_from_flashblocks(&flashblocks)` - Extract txs from flashblocks and build block +- `send_flashblock(fb)` - Send flashblock to node for pending state -These are derived from Anvil's test mnemonic for compatibility. +**Block Building Process:** +1. Fetches latest block header from provider (no local state tracking) +2. Calculates next timestamp (parent + 2 seconds for Base) +3. Calls `engine.update_forkchoice()` with payload attributes +4. Waits for block construction +5. Calls `engine.get_payload()` to retrieve built payload +6. Calls `engine.new_payload()` to validate and submit +7. Calls `engine.update_forkchoice()` again to finalize -### 3. Engine API Integration (`EngineContext`) +### 2. LocalNode -Control canonical block production via Engine API: +In-process Optimism node with Base Sepolia configuration. ```rust -use base_reth_test_utils::EngineContext; +use base_reth_test_utils::LocalNode; #[tokio::test] -async fn test_engine_api() -> eyre::Result<()> { - let node = TestNode::new().await?; - - // Create engine context - let mut engine = EngineContext::new( - node.http_url(), - B256::ZERO, // genesis hash - 1710338135, // initial timestamp - ).await?; +async fn test_node() -> eyre::Result<()> { + let node = LocalNode::new().await?; - // Build and finalize a single canonical block - let block_hash = engine.build_and_finalize_block().await?; + // Get provider + let provider = node.provider()?; - // Advance the chain by multiple blocks - let block_hashes = engine.advance_chain(5).await?; + // Get Engine API + let engine = node.engine_api()?; - // Check current state - let head = engine.head_hash(); - let block_number = engine.block_number(); + // Send flashblocks + node.send_flashblock(flashblock).await?; Ok(()) } ``` -**Engine Operations:** -- `build_and_finalize_block()` - Create and finalize a single block -- `advance_chain(n)` - Build N blocks sequentially -- `update_forkchoice(...)` - Manual forkchoice updates -- Track current head, block number, and timestamp +**Features:** +- Base Sepolia chain configuration +- Disabled P2P discovery (isolated testing) +- Random unused ports (parallel test safety) +- HTTP RPC server at `node.http_api_addr` +- Engine API IPC at `node.engine_ipc_path` +- Flashblocks-canon ExEx integration + +**Note:** Most tests should use `TestHarness` instead of `LocalNode` directly. -### 4. Flashblocks Integration (`FlashblocksContext`) +### 3. EngineApi -Dummy flashblocks delivery for testing pending state: +Type-safe Engine API client wrapping raw CL operations. + +```rust +use base_reth_test_utils::EngineApi; +use alloy_primitives::B256; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +// Usually accessed via TestHarness, but can be used directly +let engine = node.engine_api()?; + +// Raw Engine API calls +let fcu = engine.update_forkchoice(current_head, new_head, Some(attrs)).await?; +let payload = engine.get_payload(payload_id).await?; +let status = engine.new_payload(payload, vec![], parent_root, requests).await?; +``` + +**Methods:** +- `get_payload(payload_id)` - Retrieve built payload by ID +- `new_payload(payload, hashes, root, requests)` - Submit new payload +- `update_forkchoice(current, new, attrs)` - Update forkchoice state + +**Note:** EngineApi is stateless. Block building logic lives in `TestHarness`. + +### 4. Test Accounts + +Hardcoded test accounts with deterministic addresses (Anvil-compatible). + +```rust +use base_reth_test_utils::TestAccounts; + +let accounts = TestAccounts::new(); + +let alice = &accounts.alice; +let bob = &accounts.bob; +let charlie = &accounts.charlie; +let deployer = &accounts.deployer; + +// Access via harness +let harness = TestHarness::new().await?; +let alice = &harness.accounts().alice; +``` + +**Account Details:** +- **Alice**: `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` - 10,000 ETH +- **Bob**: `0x70997970C51812dc3A010C7d01b50e0d17dc79C8` - 10,000 ETH +- **Charlie**: `0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC` - 10,000 ETH +- **Deployer**: `0x90F79bf6EB2c4f870365E785982E1f101E93b906` - 10,000 ETH + +Each account includes: +- `name` - Account identifier +- `address` - Ethereum address +- `private_key` - Private key (hex string) +- `initial_balance_eth` - Starting balance in ETH + +### 5. Flashblocks Support + +Test flashblocks delivery without WebSocket connections. ```rust use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; @@ -121,94 +218,84 @@ use base_reth_test_utils::{FlashblocksContext, FlashblockBuilder}; async fn test_flashblocks() -> eyre::Result<()> { let (fb_ctx, receiver) = FlashblocksContext::new(); - // Create a base flashblock (first flashblock with base payload) + // Create base flashblock let flashblock = FlashblockBuilder::new(1, 0) .as_base(B256::ZERO, 1000) .with_transaction(tx_bytes, tx_hash, 21000) .with_balance(address, U256::from(1000)) .build(); - // Send flashblock and wait for processing fb_ctx.send_flashblock(flashblock).await?; - // Create a delta flashblock (subsequent flashblock) - let delta = FlashblockBuilder::new(1, 1) - .with_transaction(tx_bytes, tx_hash, 21000) - .build(); - - fb_ctx.send_flashblock(delta).await?; - Ok(()) } ``` -**Flashblock Features:** -- Base flashblocks with `ExecutionPayloadBaseV1` -- Delta flashblocks with incremental changes -- Builder pattern for easy construction -- Channel-based delivery (non-WebSocket) +**Via TestHarness:** +```rust +let harness = TestHarness::new().await?; +harness.send_flashblock(flashblock).await?; +``` + +## Configuration Constants -## Architecture +Key constants defined in `harness.rs`: + +```rust +const BLOCK_TIME_SECONDS: u64 = 2; // Base L2 block time +const GAS_LIMIT: u64 = 200_000_000; // Default gas limit +const NODE_STARTUP_DELAY_MS: u64 = 500; // IPC endpoint initialization +const BLOCK_BUILD_DELAY_MS: u64 = 100; // Payload construction wait +``` -The framework is organized into modules: +## File Structure ``` test-utils/ ├── src/ │ ├── lib.rs # Public API and re-exports │ ├── accounts.rs # Test account definitions -│ ├── node.rs # TestNode implementation -│ ├── engine.rs # Engine API integration +│ ├── node.rs # LocalNode (EL wrapper) +│ ├── engine.rs # EngineApi (CL wrapper) +│ ├── harness.rs # TestHarness (orchestration) │ └── flashblocks.rs # Flashblocks support ├── assets/ -│ └── genesis.json # Base Sepolia genesis configuration +│ └── genesis.json # Base Sepolia genesis └── Cargo.toml ``` ## Usage in Other Crates -Add `base-reth-test-utils` to your `dev-dependencies`: +Add to `dev-dependencies`: ```toml [dev-dependencies] base-reth-test-utils.workspace = true ``` -Then use in your integration tests: +Import in tests: ```rust -use base_reth_test_utils::{TestNode, TestAccounts}; +use base_reth_test_utils::TestHarness; #[tokio::test] -async fn my_integration_test() -> eyre::Result<()> { +async fn my_test() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Your test logic here + let harness = TestHarness::new().await?; + // Your test logic Ok(()) } ``` -## Design Decisions +## Design Principles -1. **Anvil-Compatible Keys**: Uses the same deterministic mnemonic as Anvil for easy compatibility with other tools -2. **Port Allocation**: Random unused ports enable parallel test execution without conflicts -3. **Isolated Nodes**: Disabled P2P discovery ensures tests don't interfere with each other -4. **Channel-Based Flashblocks**: Non-WebSocket delivery mechanism simplifies testing -5. **Builder Patterns**: Fluent APIs for constructing complex test scenarios - -## Future Enhancements - -This framework is designed to be extended. Planned additions: - -- Transaction builders for common operations -- Smart contract deployment helpers -- Snapshot/restore functionality for test state -- Multi-node network simulation -- Performance benchmarking utilities +1. **Separation of Concerns**: LocalNode (EL), EngineApi (CL), TestHarness (orchestration) +2. **Stateless Components**: No local state tracking; always fetch from provider +3. **Type Safety**: Use reth's `OpEngineApiClient` trait instead of raw RPC strings +4. **Parallel Testing**: Random ports + isolated nodes enable concurrent tests +5. **Anvil Compatibility**: Same mnemonic as Anvil for tooling compatibility ## Testing @@ -218,8 +305,22 @@ Run the test suite: cargo test -p base-reth-test-utils ``` +Run specific test: + +```bash +cargo test -p base-reth-test-utils test_harness_setup +``` + +## Future Enhancements + +- Transaction builders for common operations +- Smart contract deployment helpers (Foundry integration planned) +- Snapshot/restore functionality +- Multi-node network simulation +- Performance benchmarking utilities + ## References -This framework was inspired by: +Inspired by: - [op-rbuilder test framework](https://github.com/flashbots/op-rbuilder/tree/main/crates/op-rbuilder/src/tests/framework) - [reth e2e-test-utils](https://github.com/paradigmxyz/reth/tree/main/crates/e2e-test-utils) From f2cec69b053c953e53c7d9e0e2ff3f8ed2a7726f Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 23:28:19 -0500 Subject: [PATCH 5/8] wip: generalize test harness, allow for ExEx's and RPC modules to be passed down --- Cargo.lock | 1 - .../src/tests/framework_test.rs | 138 ++-------------- crates/test-utils/Cargo.toml | 3 - crates/test-utils/src/flashblocks.rs | 156 ------------------ crates/test-utils/src/harness.rs | 31 ++-- crates/test-utils/src/lib.rs | 40 ----- crates/test-utils/src/node.rs | 152 +++++++++-------- 7 files changed, 110 insertions(+), 411 deletions(-) delete mode 100644 crates/test-utils/src/flashblocks.rs diff --git a/Cargo.lock b/Cargo.lock index 598767fb..762a6cd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1662,7 +1662,6 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", - "base-reth-flashblocks-rpc", "chrono", "eyre", "futures", diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 5628145f..8bb27456 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -1,135 +1,19 @@ -//! Integration tests using the new test-utils framework -//! -//! These tests demonstrate using the test-utils framework with: -//! - TestNode for node setup -//! - EngineContext for canonical block production -//! - FlashblocksContext for pending state testing -//! - Pre-funded test accounts - -use alloy_eips::BlockNumberOrTag; -use alloy_primitives::U256; -use alloy_provider::Provider; -use base_reth_test_utils::{EngineContext, TestNode}; +use base_reth_test_utils::harness::TestHarness; use eyre::Result; +use reth::api::FullNodeComponents; +use reth_exex::ExExContext; -#[tokio::test] -async fn test_framework_node_setup() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Create test node with Base Sepolia and pre-funded accounts - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Verify chain ID - let chain_id = provider.get_chain_id().await?; - assert_eq!(chain_id, 84532); // Base Sepolia - - // Verify test accounts are funded - let alice_balance = node.get_balance(node.alice().address).await?; - assert!(alice_balance > U256::ZERO, "Alice should have initial balance"); - - let bob_balance = node.get_balance(node.bob().address).await?; - assert!(bob_balance > U256::ZERO, "Bob should have initial balance"); - - Ok(()) -} +use futures_util::{Future, TryStreamExt}; +use reth_exex::{ExExEvent, ExExNotification}; +use reth_tracing::tracing::info; +#[cfg(test)] #[tokio::test] -async fn test_framework_engine_api_block_production() -> Result<()> { - reth_tracing::init_test_tracing(); - - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Get genesis block - let genesis = provider - .get_block_by_number(BlockNumberOrTag::Number(0)) - .await? - .expect("Genesis block should exist"); - - let genesis_hash = genesis.header.hash; - - // Create engine context for canonical block production - let mut engine = EngineContext::new( - node.http_url(), - genesis_hash, - genesis.header.timestamp, - ) - .await?; - - // Build and finalize a single canonical block - let block_1_hash = engine.build_and_finalize_block().await?; - assert_ne!(block_1_hash, genesis_hash); - assert_eq!(engine.block_number(), 1); - - // Verify the block exists - let block_1 = provider - .get_block_by_hash(block_1_hash) - .await? - .expect("Block 1 should exist"); - assert_eq!(block_1.header.number, 1); - - // Advance chain by multiple blocks - let block_hashes = engine.advance_chain(3).await?; - assert_eq!(block_hashes.len(), 3); - assert_eq!(engine.block_number(), 4); - - // Verify latest block - let latest = provider - .get_block_by_number(BlockNumberOrTag::Latest) - .await? - .expect("Latest block should exist"); - assert_eq!(latest.header.number, 4); - assert_eq!(latest.header.hash, engine.head_hash()); - - Ok(()) -} - -#[tokio::test] -async fn test_framework_account_balances() -> Result<()> { - reth_tracing::init_test_tracing(); - - let node = TestNode::new().await?; - let provider = node.provider().await?; - - // Check all test accounts have their initial balances - let accounts = &node.accounts; - - for account in accounts.all() { - let balance = provider.get_balance(account.address).await?; - assert_eq!( - balance, - account.initial_balance_wei(), - "{} should have initial balance", - account.name - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_framework_parallel_nodes() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Launch multiple nodes in parallel to verify isolation - let (node1_result, node2_result) = tokio::join!(TestNode::new(), TestNode::new()); - - let node1 = node1_result?; - let node2 = node2_result?; - - // Verify they have different ports - assert_ne!(node1.http_api_addr, node2.http_api_addr); - - // Verify both are functional - let provider1 = node1.provider().await?; - let provider2 = node2.provider().await?; - - let chain_id_1 = provider1.get_chain_id().await?; - let chain_id_2 = provider2.get_chain_id().await?; +async fn test_framework_test() -> Result<()> { + use base_reth_test_utils::node::default_launcher; - assert_eq!(chain_id_1, 84532); - assert_eq!(chain_id_2, 84532); + let harness = TestHarness::new(default_launcher).await?; + let provider = harness.provider(); Ok(()) } diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index bd01f9ab..be0cc9a6 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -12,9 +12,6 @@ description = "Common integration test utilities for node-reth crates" workspace = true [dependencies] -# internal -base-reth-flashblocks-rpc.workspace = true - # reth reth.workspace = true reth-optimism-node.workspace = true diff --git a/crates/test-utils/src/flashblocks.rs b/crates/test-utils/src/flashblocks.rs deleted file mode 100644 index 484c4619..00000000 --- a/crates/test-utils/src/flashblocks.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Dummy flashblocks integration for testing pending state - -use alloy_primitives::{Bytes, TxHash}; -use base_reth_flashblocks_rpc::subscription::Flashblock; -use eyre::Result; -use tokio::sync::{mpsc, oneshot}; - -// Re-export types from flashblocks-rpc -pub use base_reth_flashblocks_rpc::subscription::{Flashblock as FlashblockPayload, Metadata as FlashblockMetadata}; - -/// Context for managing dummy flashblock delivery in tests -/// -/// This provides a non-WebSocket, queue-based mechanism for delivering -/// flashblocks to a test node, similar to how the rpc.rs tests work currently. -pub struct FlashblocksContext { - /// Channel for sending flashblocks to the node - sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, -} - -impl FlashblocksContext { - /// Create a new flashblocks context with a channel - /// - /// Returns the context and a receiver that the node should consume - pub fn new() -> (Self, mpsc::Receiver<(Flashblock, oneshot::Sender<()>)>) { - let (sender, receiver) = mpsc::channel(100); - (Self { sender }, receiver) - } - - /// Send a flashblock to the node and wait for processing - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.sender.send((flashblock, tx)).await?; - rx.await?; - Ok(()) - } - - /// Send multiple flashblocks sequentially - pub async fn send_flashblocks(&self, flashblocks: Vec) -> Result<()> { - for flashblock in flashblocks { - self.send_flashblock(flashblock).await?; - } - Ok(()) - } -} - -impl Default for FlashblocksContext { - fn default() -> Self { - Self::new().0 - } -} - -/// Helper to extract transactions from a vec of flashblocks -pub fn extract_transactions_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { - let mut all_txs = Vec::new(); - - for flashblock in flashblocks { - all_txs.extend(flashblock.diff.transactions.clone()); - } - - all_txs -} - -/// Helper to get all transaction hashes from flashblock metadata -pub fn extract_tx_hashes_from_flashblocks(flashblocks: &[Flashblock]) -> Vec { - let mut all_hashes = Vec::new(); - - for flashblock in flashblocks { - all_hashes.extend(flashblock.metadata.receipts.keys().copied()); - } - - all_hashes -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Address, B256, U256}; - use alloy_primitives::map::HashMap; - use alloy_consensus::Receipt; - use alloy_rpc_types_engine::PayloadId; - use base_reth_flashblocks_rpc::subscription::Metadata; - use op_alloy_consensus::OpDepositReceipt; - use reth_optimism_primitives::OpReceipt; - use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; - - fn create_test_flashblock() -> Flashblock { - Flashblock { - payload_id: PayloadId::new([0; 8]), - index: 0, - base: Some(ExecutionPayloadBaseV1 { - parent_beacon_block_root: B256::default(), - parent_hash: B256::default(), - fee_recipient: Address::ZERO, - prev_randao: B256::default(), - block_number: 1, - gas_limit: 30_000_000, - timestamp: 0, - extra_data: Bytes::new(), - base_fee_per_gas: U256::ZERO, - }), - diff: ExecutionPayloadFlashblockDeltaV1 { - transactions: vec![Bytes::from(vec![0x01, 0x02, 0x03])], - ..Default::default() - }, - metadata: Metadata { - block_number: 1, - receipts: { - let mut receipts = HashMap::default(); - receipts.insert( - B256::random(), - OpReceipt::Deposit(OpDepositReceipt { - inner: Receipt { - status: true.into(), - cumulative_gas_used: 21000, - logs: vec![], - }, - deposit_nonce: Some(1), - deposit_receipt_version: None, - }), - ); - receipts - }, - new_account_balances: HashMap::default(), - }, - } - } - - #[tokio::test] - async fn test_flashblocks_context() { - let (ctx, mut receiver) = FlashblocksContext::new(); - let flashblock = create_test_flashblock(); - - // Spawn a task to receive and acknowledge - let handle = tokio::spawn(async move { - if let Some((fb, tx)) = receiver.recv().await { - assert_eq!(fb.metadata.block_number, 1); - tx.send(()).unwrap(); - } - }); - - // Send flashblock - ctx.send_flashblock(flashblock).await.unwrap(); - - // Wait for receiver task - handle.await.unwrap(); - } - - #[test] - fn test_extract_transactions() { - let flashblock = create_test_flashblock(); - let txs = extract_transactions_from_flashblocks(&[flashblock]); - - assert_eq!(txs.len(), 1); - assert_eq!(txs[0], Bytes::from(vec![0x01, 0x02, 0x03])); - } -} diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 99da7863..9bb9ba11 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -2,16 +2,19 @@ use crate::accounts::TestAccounts; use crate::engine::{EngineApi, IpcEngine}; -use crate::node::LocalNode; -use crate::Flashblock; +use crate::node::{LocalNode, OpAddOns, OpBuilder}; use alloy_eips::eip7685::Requests; use alloy_primitives::{Bytes, B256}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_types::BlockNumberOrTag; use alloy_rpc_types_engine::PayloadAttributes; use eyre::{eyre, Result}; +use futures_util::Future; use op_alloy_network::Optimism; use op_alloy_rpc_types_engine::OpPayloadAttributes; +use reth::builder::NodeHandle; +use reth_e2e_test_utils::Adapter; +use reth_optimism_node::OpNode; use std::time::Duration; use tokio::time::sleep; @@ -27,8 +30,12 @@ pub struct TestHarness { } impl TestHarness { - pub async fn new() -> Result { - let node = LocalNode::new().await?; + pub async fn new(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { + let node = LocalNode::new(launcher).await?; let engine = node.engine_api()?; let accounts = TestAccounts::new(); @@ -124,22 +131,12 @@ impl TestHarness { } Ok(()) } - - pub async fn build_block_from_flashblocks(&self, flashblocks: &[Flashblock]) -> Result<()> { - let transactions: Vec = flashblocks - .iter() - .flat_map(|fb| fb.diff.transactions.iter().cloned()) - .collect(); - self.build_block_from_transactions(transactions).await - } - - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - self.node.send_flashblock(flashblock).await - } } #[cfg(test)] mod tests { + use crate::node::default_launcher; + use super::*; use alloy_primitives::U256; use alloy_provider::Provider; @@ -147,7 +144,7 @@ mod tests { #[tokio::test] async fn test_harness_setup() -> Result<()> { reth_tracing::init_test_tracing(); - let harness = TestHarness::new().await?; + let harness = TestHarness::new(default_launcher).await?; assert_eq!(harness.accounts().alice.name, "Alice"); assert_eq!(harness.accounts().bob.name, "Bob"); diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 9237599c..62913e26 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,44 +1,4 @@ -//! Common integration test utilities for node-reth crates -//! -//! This crate provides a comprehensive test framework for integration testing. -//! -//! # Quick Start -//! -//! ```no_run -//! use base_reth_test_utils::TestHarness; -//! -//! #[tokio::test] -//! async fn test_example() -> eyre::Result<()> { -//! let harness = TestHarness::new().await?; -//! -//! // Send flashblocks for pending state testing -//! harness.send_flashblock(flashblock).await?; -//! -//! // Access test accounts -//! let alice = harness.alice(); -//! let balance = harness.get_balance(alice.address).await?; -//! -//! Ok(()) -//! } -//! ``` -//! -//! # Components -//! -//! - **TestHarness** - Unified interface combining node, engine API, and flashblocks -//! - **TestNode** - Node setup with Base Sepolia chainspec and flashblocks integration -//! - **EngineContext** - Engine API integration for canonical block production -//! - **TestAccounts** - Pre-funded test accounts (Alice, Bob, Charlie, Deployer - 10,000 ETH each) - pub mod accounts; pub mod engine; -pub mod flashblocks; pub mod harness; pub mod node; - -// Re-export commonly used types -pub use accounts::{TestAccount, TestAccounts}; -pub use base_reth_flashblocks_rpc::subscription::{Flashblock, Metadata as FlashblockMetadata}; -pub use engine::EngineApi; -pub use flashblocks::FlashblocksContext; -pub use harness::TestHarness; -pub use node::LocalNode; diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 3809eee4..8a3a609b 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -1,21 +1,20 @@ //! Local node setup with Base Sepolia chainspec use crate::engine::EngineApi; -use crate::Flashblock; use alloy_genesis::Genesis; use alloy_provider::RootProvider; use alloy_rpc_client::RpcClient; -use base_reth_flashblocks_rpc::rpc::{EthApiExt, EthApiOverrideServer}; -use base_reth_flashblocks_rpc::state::FlashblocksState; -use base_reth_flashblocks_rpc::subscription::FlashblocksReceiver; use eyre::Result; -use once_cell::sync::OnceCell; +use futures_util::Future; use op_alloy_network::Optimism; +use reth::api::{FullNodeTypesAdapter, NodeTypesWithDBAdapter}; use reth::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs}; -use reth::builder::{Node, NodeBuilder, NodeConfig, NodeHandle}; +use reth::builder::{ + Node, NodeBuilder, NodeBuilderWithComponents, NodeConfig, NodeHandle, WithLaunchContext, +}; use reth::core::exit::NodeExitFuture; use reth::tasks::TaskManager; -use reth_exex::ExExEvent; +use reth_e2e_test_utils::{Adapter, TmpDB}; use reth_optimism_chainspec::OpChainSpec; use reth_optimism_node::args::RollupArgs; use reth_optimism_node::OpNode; @@ -23,22 +22,45 @@ use reth_provider::providers::BlockchainProvider; use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; -use tokio::sync::{mpsc, oneshot}; -use tokio_stream::StreamExt; pub const BASE_CHAIN_ID: u64 = 8453; pub struct LocalNode { http_api_addr: SocketAddr, engine_ipc_path: String, - flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + // flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, _node_exit_future: NodeExitFuture, _node: Box, _task_manager: TaskManager, } +// Full node types for OpNode over the TmpDB used in tests +pub type OpTypes = + FullNodeTypesAdapter>>; + +// Components builder for OpNode +pub type OpComponentsBuilder = >::ComponentsBuilder; + +// AddOns (this includes the EthApi type etc.) +pub type OpAddOns = >::AddOns; + +// The builder type we’re going to pass into launch_with_fn +pub type OpBuilder = + WithLaunchContext>; + +pub async fn default_launcher( + builder: OpBuilder, +) -> eyre::Result, OpAddOns>> { + let launcher = builder.engine_api_launcher(); + builder.launch_with(launcher).await +} + impl LocalNode { - pub async fn new() -> Result { + pub async fn new(launcher: L) -> Result + where + L: FnOnce(OpBuilder) -> LRet, + LRet: Future, OpAddOns>>>, + { let tasks = TaskManager::current(); let exec = tasks.executor(); @@ -65,8 +87,9 @@ impl LocalNode { let node = OpNode::new(RollupArgs::default()); - let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); - let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + // let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + // let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + let NodeHandle { node: node_handle, node_exit_future, @@ -75,53 +98,48 @@ impl LocalNode { .with_types_and_provider::>() .with_components(node.components_builder()) .with_add_ons(node.add_ons()) - .install_exex("flashblocks-canon", { - let fb_cell = fb_cell.clone(); - move |mut ctx| async move { - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - .clone(); - Ok(async move { - while let Some(note) = ctx.notifications.try_next().await? { - if let Some(committed) = note.committed_chain() { - for b in committed.blocks_iter() { - fb.on_canonical_block_received(b); - } - let _ = ctx - .events - .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); - } - } - Ok(()) - }) - } - }) - .extend_rpc_modules(move |ctx| { - let fb = fb_cell - .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) - .clone(); - - fb.start(); - - let api_ext = EthApiExt::new( - ctx.registry.eth_api().clone(), - ctx.registry.eth_handlers().filter.clone(), - fb.clone(), - ); - - ctx.modules.replace_configured(api_ext.into_rpc())?; - - // Spawn task to receive flashblocks from the test context - tokio::spawn(async move { - while let Some((payload, tx)) = receiver.recv().await { - fb.on_flashblock_received(payload); - tx.send(()).unwrap(); - } - }); - - Ok(()) - }) - .launch() + // .install_exex("flashblocks-canon", { + // let fb_cell = fb_cell.clone(); + // move |mut ctx| async move { + // let fb = fb_cell + // .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + // .clone(); + // Ok(async move { + // while let Some(note) = ctx.notifications.try_next().await? { + // if let Some(committed) = note.committed_chain() { + // for b in committed.blocks_iter() { + // fb.on_canonical_block_received(b); + // } + // let _ = ctx + // .events + // .send(ExExEvent::FinishedHeight(committed.tip().num_hash())); + // } + // } + // Ok(()) + // }) + // } + // }) + // .extend_rpc_modules(move |ctx| { + // let fb = fb_cell + // .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + // .clone(); + // fb.start(); + // let api_ext = EthApiExt::new( + // ctx.registry.eth_api().clone(), + // ctx.registry.eth_handlers().filter.clone(), + // fb.clone(), + // ); + // ctx.modules.replace_configured(api_ext.into_rpc())?; + // // Spawn task to receive flashblocks from the test context + // tokio::spawn(async move { + // while let Some((payload, tx)) = receiver.recv().await { + // fb.on_flashblock_received(payload); + // tx.send(()).unwrap(); + // } + // }); + // Ok(()) + // }) + .launch_with_fn(launcher) .await?; let http_api_addr = node_handle @@ -134,19 +152,19 @@ impl LocalNode { Ok(Self { http_api_addr, engine_ipc_path, - flashblock_sender: sender, + // flashblock_sender: sender, _node_exit_future: node_exit_future, _node: Box::new(node_handle), _task_manager: tasks, }) } - pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { - let (tx, rx) = oneshot::channel(); - self.flashblock_sender.send((flashblock, tx)).await?; - rx.await?; - Ok(()) - } + // pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + // let (tx, rx) = oneshot::channel(); + // self.flashblock_sender.send((flashblock, tx)).await?; + // rx.await?; + // Ok(()) + // } pub fn provider(&self) -> Result> { let url = format!("http://{}", self.http_api_addr); From 6f6f67a6f6333a298ebf67ffcf3fee695e4a0c2d Mon Sep 17 00:00:00 2001 From: Haardik H Date: Mon, 3 Nov 2025 23:29:18 -0500 Subject: [PATCH 6/8] remove foundry github workflow --- contracts/.github/workflows/test.yml | 37 ---------------------------- 1 file changed, 37 deletions(-) delete mode 100644 contracts/.github/workflows/test.yml diff --git a/contracts/.github/workflows/test.yml b/contracts/.github/workflows/test.yml deleted file mode 100644 index c24b9832..00000000 --- a/contracts/.github/workflows/test.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: CI - -permissions: - contents: read - -on: - push: - pull_request: - workflow_dispatch: - -env: - FOUNDRY_PROFILE: ci - -jobs: - check: - name: Foundry project - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - - name: Show Forge version - run: forge --version - - - name: Run Forge fmt - run: forge fmt --check - - - name: Run Forge build - run: forge build --sizes - - - name: Run Forge tests - run: forge test -vvv From 8d04589e5781c1ee2be16586bb3547e4de3ec92b Mon Sep 17 00:00:00 2001 From: Haardik H Date: Tue, 4 Nov 2025 12:19:39 -0500 Subject: [PATCH 7/8] wip: first test migration --- .../src/tests/framework_test.rs | 181 ++++++++++++++++-- crates/test-utils/src/node.rs | 7 - 2 files changed, 167 insertions(+), 21 deletions(-) diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 8bb27456..3eeac3a3 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -1,19 +1,172 @@ -use base_reth_test_utils::harness::TestHarness; -use eyre::Result; -use reth::api::FullNodeComponents; -use reth_exex::ExExContext; +#[cfg(test)] +mod tests { + use crate::rpc::{EthApiExt, EthApiOverrideServer}; + use crate::state::FlashblocksState; + use crate::subscription::{Flashblock, FlashblocksReceiver, Metadata}; + use crate::tests::{BLOCK_INFO_TXN, BLOCK_INFO_TXN_HASH}; + use alloy_consensus::Receipt; + use alloy_eips::BlockNumberOrTag; + use alloy_primitives::map::HashMap; + use alloy_primitives::{Address, Bytes, B256, U256}; + use alloy_provider::Provider; + use alloy_rpc_types_engine::PayloadId; + use base_reth_test_utils::harness::TestHarness; + use eyre::Result; + use once_cell::sync::OnceCell; + use op_alloy_consensus::OpDepositReceipt; + use reth_exex::ExExEvent; + use reth_optimism_primitives::OpReceipt; + use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + use std::sync::Arc; + use tokio::sync::{mpsc, oneshot}; + use tokio_stream::StreamExt; -use futures_util::{Future, TryStreamExt}; -use reth_exex::{ExExEvent, ExExNotification}; -use reth_tracing::tracing::info; + pub struct TestSetup { + sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, + harness: TestHarness, + } -#[cfg(test)] -#[tokio::test] -async fn test_framework_test() -> Result<()> { - use base_reth_test_utils::node::default_launcher; + impl TestSetup { + pub async fn new() -> Result { + let (sender, mut receiver) = mpsc::channel::<(Flashblock, oneshot::Sender<()>)>(100); + let harness = TestHarness::new(|builder| { + let fb_cell: Arc>>> = Arc::new(OnceCell::new()); + + builder + .install_exex("flashblocks-canon", { + let fb_cell = fb_cell.clone(); + move |mut ctx| async move { + let fb = fb_cell + .get_or_init(|| { + Arc::new(FlashblocksState::new(ctx.provider().clone())) + }) + .clone(); + Ok(async move { + while let Some(note) = ctx.notifications.try_next().await? { + if let Some(committed) = note.committed_chain() { + for b in committed.blocks_iter() { + fb.on_canonical_block_received(b); + } + let _ = ctx.events.send(ExExEvent::FinishedHeight( + committed.tip().num_hash(), + )); + } + } + Ok(()) + }) + } + }) + .extend_rpc_modules(move |ctx| { + let fb = fb_cell + .get_or_init(|| Arc::new(FlashblocksState::new(ctx.provider().clone()))) + .clone(); + + fb.start(); + + let api_ext = EthApiExt::new( + ctx.registry.eth_api().clone(), + ctx.registry.eth_handlers().filter.clone(), + fb.clone(), + ); + + ctx.modules.replace_configured(api_ext.into_rpc())?; + + tokio::spawn(async move { + while let Some((payload, tx)) = receiver.recv().await { + fb.on_flashblock_received(payload); + tx.send(()).unwrap(); + } + }); + + Ok(()) + }) + .launch() + }) + .await?; + + Ok(Self { sender, harness }) + } + + pub async fn send_flashblock(&self, flashblock: Flashblock) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.sender.send((flashblock, tx)).await?; + rx.await?; + Ok(()) + } + } + + fn create_first_payload() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 0, + base: Some(ExecutionPayloadBaseV1 { + parent_beacon_block_root: B256::default(), + parent_hash: B256::default(), + fee_recipient: Address::ZERO, + prev_randao: B256::default(), + block_number: 1, + gas_limit: 30_000_000, + timestamp: 0, + extra_data: Bytes::new(), + base_fee_per_gas: U256::ZERO, + }), + diff: ExecutionPayloadFlashblockDeltaV1 { + transactions: vec![BLOCK_INFO_TXN], + ..Default::default() + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + BLOCK_INFO_TXN_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 10000, + logs: vec![], + }, + deposit_nonce: Some(4012991u64), + deposit_receipt_version: None, + }), + ); + receipts + }, + new_account_balances: HashMap::default(), + }, + } + } + + #[tokio::test] + async fn test_get_pending_block() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + let latest_block = provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .expect("latest block expected"); + assert_eq!(latest_block.number(), 0); + + // Querying pending block when it does not exist yet + let pending_block = provider + .get_block_by_number(BlockNumberOrTag::Pending) + .await?; + assert_eq!(pending_block.is_none(), true); + + let base_payload = create_first_payload(); + setup.send_flashblock(base_payload).await?; + + // Query pending block after sending the base payload with an empty delta + let pending_block = provider + .get_block_by_number(alloy_eips::BlockNumberOrTag::Pending) + .await? + .expect("pending block expected"); - let harness = TestHarness::new(default_launcher).await?; - let provider = harness.provider(); + assert_eq!(pending_block.number(), 1); + assert_eq!(pending_block.transactions.hashes().len(), 1); // L1Info transaction - Ok(()) + Ok(()) + } } diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 8a3a609b..5cf1ec27 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -34,17 +34,10 @@ pub struct LocalNode { _task_manager: TaskManager, } -// Full node types for OpNode over the TmpDB used in tests pub type OpTypes = FullNodeTypesAdapter>>; - -// Components builder for OpNode pub type OpComponentsBuilder = >::ComponentsBuilder; - -// AddOns (this includes the EthApi type etc.) pub type OpAddOns = >::AddOns; - -// The builder type we’re going to pass into launch_with_fn pub type OpBuilder = WithLaunchContext>; From 166e4f196ef2492e24730c178e3f48a319cac5dc Mon Sep 17 00:00:00 2001 From: Haardik H Date: Wed, 5 Nov 2025 11:25:55 -0500 Subject: [PATCH 8/8] migrate most rpc.rs tests --- Cargo.lock | 51 +- .../src/tests/assets/genesis.json | 2 +- .../src/tests/framework_test.rs | 675 +++++++++++++++++- crates/metering/src/tests/assets/genesis.json | 2 +- crates/test-utils/Cargo.toml | 2 + crates/test-utils/assets/genesis.json | 2 +- crates/test-utils/src/accounts.rs | 41 +- crates/test-utils/src/harness.rs | 6 +- crates/test-utils/src/node.rs | 29 +- 9 files changed, 770 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 762a6cd8..fdaac579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b151e38e42f1586a01369ec52a6934702731d07e8509a7307331b09f6c46dc" +checksum = "90d103d3e440ad6f703dd71a5b58a6abd24834563bde8a5fabe706e00242f810" dependencies = [ "alloy-eips", "alloy-primitives", @@ -128,9 +128,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2d5e8668ef6215efdb7dcca6f22277b4e483a5650e05f5de22b2350971f4b8" +checksum = "48ead76c8c84ab3a50c31c56bc2c748c2d64357ad2131c32f9b10ab790a25e1a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5434834adaf64fa20a6fb90877bc1d33214c41b055cc49f82189c98614368cc" +checksum = "7bdbec74583d0067798d77afa43d58f00d93035335d7ceaa5d3f93857d461bb9" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c69f6c9c68a1287c9d5ff903d0010726934de0dac10989be37b75a29190d55" +checksum = "31b67c5a702121e618217f7a86f314918acb2622276d0273490e2d4534490bc0" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -307,9 +307,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf2ae05219e73e0979cb2cf55612aafbab191d130f203079805eaf881cca58" +checksum = "612296e6b723470bb1101420a73c63dfd535aa9bf738ce09951aedbd4ab7292e" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58f4f345cef483eab7374f2b6056973c7419ffe8ad35e994b7a7f5d8e0c7ba4" +checksum = "a0e7918396eecd69d9c907046ec8a93fb09b89e2f325d5e7ea9c4e3929aa0dd2" dependencies = [ "alloy-consensus", "alloy-eips", @@ -554,9 +554,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbde0801a32d21c5f111f037bee7e22874836fba7add34ed4a6919932dd7cf23" +checksum = "cdbf6d1766ca41e90ac21c4bc5cbc5e9e965978a25873c3f90b3992d905db4cb" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -617,9 +617,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361cd87ead4ba7659bda8127902eda92d17fa7ceb18aba1676f7be10f7222487" +checksum = "a15e4831b71eea9d20126a411c1c09facf1d01d5cac84fd51d532d3c429cfc26" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -680,9 +680,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64600fc6c312b7e0ba76f73a381059af044f4f21f43e07f51f1fa76c868fe302" +checksum = "751d1887f7d202514a82c5b3caf28ee8bd4a2ad9549e4f498b6f0bff99b52add" dependencies = [ "alloy-primitives", "arbitrary", @@ -692,9 +692,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5772858492b26f780468ae693405f895d6a27dea6e3eab2c36b6217de47c2647" +checksum = "9cf0b42ffbf558badfecf1dde0c3c5ed91f29bb7e97876d0bed008c3d5d67171" dependencies = [ "alloy-primitives", "async-trait", @@ -707,9 +707,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4195b803d0a992d8dbaab2ca1986fc86533d4bc80967c0cce7668b26ad99ef9" +checksum = "3e7d555ee5f27be29af4ae312be014b57c6cff9acb23fe2cf008500be6ca7e33" dependencies = [ "alloy-consensus", "alloy-network", @@ -893,11 +893,10 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.41" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e52276fdb553d3c11563afad2898f4085165e4093604afe3d78b69afbf408f" +checksum = "cd7ce8ed34106acd6e21942022b6a15be6454c2c3ead4d76811d3bdcd63cf771" dependencies = [ - "alloy-primitives", "darling 0.21.3", "proc-macro2", "quote", @@ -1662,6 +1661,8 @@ dependencies = [ "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", + "alloy-signer", + "alloy-signer-local", "chrono", "eyre", "futures", diff --git a/crates/flashblocks-rpc/src/tests/assets/genesis.json b/crates/flashblocks-rpc/src/tests/assets/genesis.json index 79ab75e9..b3099c33 100644 --- a/crates/flashblocks-rpc/src/tests/assets/genesis.json +++ b/crates/flashblocks-rpc/src/tests/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, diff --git a/crates/flashblocks-rpc/src/tests/framework_test.rs b/crates/flashblocks-rpc/src/tests/framework_test.rs index 3eeac3a3..14a4cc59 100644 --- a/crates/flashblocks-rpc/src/tests/framework_test.rs +++ b/crates/flashblocks-rpc/src/tests/framework_test.rs @@ -7,16 +7,24 @@ mod tests { use alloy_consensus::Receipt; use alloy_eips::BlockNumberOrTag; use alloy_primitives::map::HashMap; - use alloy_primitives::{Address, Bytes, B256, U256}; + use alloy_primitives::{address, b256, bytes, Address, Bytes, LogData, TxHash, B256, U256}; use alloy_provider::Provider; + use alloy_rpc_client::RpcClient; + use alloy_rpc_types::simulate::{SimBlock, SimulatePayload}; use alloy_rpc_types_engine::PayloadId; + use alloy_rpc_types_eth::error::EthRpcErrorCode; + use alloy_rpc_types_eth::TransactionInput; use base_reth_test_utils::harness::TestHarness; use eyre::Result; use once_cell::sync::OnceCell; use op_alloy_consensus::OpDepositReceipt; + use op_alloy_network::{Optimism, ReceiptResponse, TransactionResponse}; + use op_alloy_rpc_types::OpTransactionRequest; use reth_exex::ExExEvent; use reth_optimism_primitives::OpReceipt; + use reth_rpc_eth_api::RpcReceipt; use rollup_boost::{ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1}; + use std::str::FromStr; use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; use tokio_stream::StreamExt; @@ -93,6 +101,91 @@ mod tests { rx.await?; Ok(()) } + + pub async fn send_test_payloads(&self) -> Result<()> { + let base_payload = create_first_payload(); + self.send_flashblock(base_payload).await?; + + let second_payload = create_second_payload(); + self.send_flashblock(second_payload).await?; + + Ok(()) + } + + pub async fn send_raw_transaction_sync( + &self, + tx: Bytes, + timeout_ms: Option, + ) -> Result> { + let url = self.harness.rpc_url(); + let client = RpcClient::new_http(url.parse()?); + + let receipt = client + .request::<_, RpcReceipt>("eth_sendRawTransactionSync", (tx, timeout_ms)) + .await?; + + Ok(receipt) + } + } + + // Test constants + const TEST_ADDRESS: Address = address!("0x1234567890123456789012345678901234567890"); + const PENDING_BALANCE: u64 = 4660; + + const DEPOSIT_SENDER: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001"); + const TX_SENDER: Address = address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"); + + const DEPOSIT_TX_HASH: TxHash = + b256!("0x2be2e6f8b01b03b87ae9f0ebca8bbd420f174bef0fbcc18c7802c5378b78f548"); + const TRANSFER_ETH_HASH: TxHash = + b256!("0xbb079fbde7d12fd01664483cd810e91014113e405247479e5615974ebca93e4a"); + + const DEPLOYMENT_HASH: TxHash = + b256!("0x2b14d58c13406f25a78cfb802fb711c0d2c27bf9eccaec2d1847dc4392918f63"); + + const INCREMENT_HASH: TxHash = + b256!("0x993ad6a332752f6748636ce899b3791e4a33f7eece82c0db4556c7339c1b2929"); + const INCREMENT2_HASH: TxHash = + b256!("0x617a3673399647d12bb82ec8eba2ca3fc468e99894bcf1c67eb50ef38ee615cb"); + + const COUNTER_ADDRESS: Address = address!("0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"); + + // Test log topics - these represent common events + const TEST_LOG_TOPIC_0: B256 = + b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); // Transfer event + const TEST_LOG_TOPIC_1: B256 = + b256!("0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"); // From address + const TEST_LOG_TOPIC_2: B256 = + b256!("0x0000000000000000000000001234567890123456789012345678901234567890"); // To address + + // Transaction bytes + const DEPOSIT_TX: Bytes = bytes!("0x7ef8f8a042a8ae5ec231af3d0f90f68543ec8bca1da4f7edd712d5b51b490688355a6db794deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000044d000a118b00000000000000040000000067cb7cb0000000000077dbd4000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014edd27304108914dd6503b19b9eeb9956982ef197febbeeed8a9eac3dbaaabdf000000000000000000000000fc56e7272eebbba5bc6c544e159483c4a38f8ba3"); + const TRANSFER_ETH_TX: Bytes = bytes!("0x02f87383014a3480808449504f80830186a094deaddeaddeaddeaddeaddeaddeaddeaddead00018ad3c21bcb3f6efc39800080c0019f5a6fe2065583f4f3730e82e5725f651cbbaf11dc1f82c8d29ba1f3f99e5383a061e0bf5dfff4a9bc521ad426eee593d3653c5c330ae8a65fad3175d30f291d31"); + const DEPLOYMENT_TX: Bytes = bytes!("0x02f9029483014a3401808449504f80830493e08080b9023c608060405260015f55600180553480156016575f80fd5b50610218806100245f395ff3fe608060405234801561000f575f80fd5b5060043610610060575f3560e01c80631d63e24d146100645780637477f70014610082578063a87d942c146100a0578063ab57b128146100be578063d09de08a146100c8578063d631c639146100d2575b5f80fd5b61006c6100f0565b6040516100799190610155565b60405180910390f35b61008a6100f6565b6040516100979190610155565b60405180910390f35b6100a86100fb565b6040516100b59190610155565b60405180910390f35b6100c6610103565b005b6100d061011c565b005b6100da610134565b6040516100e79190610155565b60405180910390f35b60015481565b5f5481565b5f8054905090565b60015f8154809291906101159061019b565b9190505550565b5f8081548092919061012d9061019b565b9190505550565b5f600154905090565b5f819050919050565b61014f8161013d565b82525050565b5f6020820190506101685f830184610146565b92915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f6101a58261013d565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036101d7576101d661016e565b5b60018201905091905056fea264697066735822122025c7e02ddf460dece9c1e52a3f9ff042055b58005168e7825d7f6c426288c27164736f6c63430008190033c001a02f196658032e0b003bcd234349d63081f5d6c2785264c6fec6b25ad877ae326aa0290c9f96f4501439b07a7b5e8e938f15fc30a9c15db3fc5e654d44e1f522060c"); + const INCREMENT_TX: Bytes = bytes!("0x02f86d83014a3402808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084d09de08ac080a0a9c1a565668084d4052bbd9bc3abce8555a06aed6651c82c2756ac8a83a79fa2a03427f440ce4910a5227ea0cedb60b06cf0bea2dbbac93bd37efa91a474c29d89"); + const INCREMENT2_TX: Bytes = bytes!("0x02f86d83014a3403808449504f8082abe094e7f1725e7734ce288f8367e1bb143e90bb3f05128084ab57b128c001a03a155b8c81165fc8193aa739522c2a9e432e274adea7f0b90ef2b5078737f153a0288d7fad4a3b0d1e7eaf7fab63b298393a5020bf11d91ff8df13b235410799e2"); + + fn create_test_logs() -> Vec { + vec![ + alloy_primitives::Log { + address: COUNTER_ADDRESS, + data: LogData::new( + vec![TEST_LOG_TOPIC_0, TEST_LOG_TOPIC_1, TEST_LOG_TOPIC_2], + bytes!("0x0000000000000000000000000000000000000000000000000de0b6b3a7640000") + .into(), // 1 ETH in wei + ) + .unwrap(), + }, + alloy_primitives::Log { + address: TEST_ADDRESS, + data: LogData::new( + vec![TEST_LOG_TOPIC_0], + bytes!("0x0000000000000000000000000000000000000000000000000000000000000001") + .into(), // Value: 1 + ) + .unwrap(), + }, + ] } fn create_first_payload() -> Flashblock { @@ -137,6 +230,87 @@ mod tests { } } + fn create_second_payload() -> Flashblock { + Flashblock { + payload_id: PayloadId::new([0; 8]), + index: 1, + base: None, + diff: ExecutionPayloadFlashblockDeltaV1 { + state_root: B256::default(), + receipts_root: B256::default(), + gas_used: 0, + block_hash: B256::default(), + transactions: vec![ + DEPOSIT_TX, + TRANSFER_ETH_TX, + DEPLOYMENT_TX, + INCREMENT_TX, + INCREMENT2_TX, + ], + withdrawals: Vec::new(), + logs_bloom: Default::default(), + withdrawals_root: Default::default(), + }, + metadata: Metadata { + block_number: 1, + receipts: { + let mut receipts = HashMap::default(); + receipts.insert( + DEPOSIT_TX_HASH, + OpReceipt::Deposit(OpDepositReceipt { + inner: Receipt { + status: true.into(), + cumulative_gas_used: 31000, + logs: vec![], + }, + deposit_nonce: Some(4012992u64), + deposit_receipt_version: None, + }), + ); + receipts.insert( + TRANSFER_ETH_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 55000, + logs: vec![], + }), + ); + receipts.insert( + DEPLOYMENT_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279, + logs: vec![], + }), + ); + receipts.insert( + INCREMENT_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279 + 44000, + logs: create_test_logs(), + }), + ); + receipts.insert( + INCREMENT2_HASH, + OpReceipt::Legacy(Receipt { + status: true.into(), + cumulative_gas_used: 272279 + 44000 + 44000, + logs: vec![], + }), + ); + receipts + }, + new_account_balances: { + let mut map = HashMap::default(); + map.insert(TEST_ADDRESS, U256::from(PENDING_BALANCE)); + map.insert(COUNTER_ADDRESS, U256::from(0)); + map + }, + }, + } + } + #[tokio::test] async fn test_get_pending_block() -> Result<()> { reth_tracing::init_test_tracing(); @@ -169,4 +343,503 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_get_balance_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + let balance = provider.get_balance(TEST_ADDRESS).await?; + assert_eq!(balance, U256::ZERO); + + let pending_balance = provider.get_balance(TEST_ADDRESS).pending().await?; + assert_eq!(pending_balance, U256::from(PENDING_BALANCE)); + Ok(()) + } + + #[tokio::test] + async fn test_get_transaction_by_hash_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + assert!(provider + .get_transaction_by_hash(DEPOSIT_TX_HASH) + .await? + .is_none()); + assert!(provider + .get_transaction_by_hash(TRANSFER_ETH_HASH) + .await? + .is_none()); + + setup.send_test_payloads().await?; + + let tx1 = provider + .get_transaction_by_hash(DEPOSIT_TX_HASH) + .await? + .expect("tx1 expected"); + assert_eq!(tx1.tx_hash(), DEPOSIT_TX_HASH); + assert_eq!(tx1.from(), DEPOSIT_SENDER); + + let tx2 = provider + .get_transaction_by_hash(TRANSFER_ETH_HASH) + .await? + .expect("tx2 expected"); + assert_eq!(tx2.tx_hash(), TRANSFER_ETH_HASH); + assert_eq!(tx2.from(), TX_SENDER); + + Ok(()) + } + + #[tokio::test] + async fn test_get_transaction_receipt_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + let receipt = provider.get_transaction_receipt(DEPOSIT_TX_HASH).await?; + assert_eq!(receipt.is_none(), true); + + setup.send_test_payloads().await?; + + let receipt = provider + .get_transaction_receipt(DEPOSIT_TX_HASH) + .await? + .expect("receipt expected"); + assert_eq!(receipt.gas_used(), 21000); + + let receipt = provider + .get_transaction_receipt(TRANSFER_ETH_HASH) + .await? + .expect("receipt expected"); + assert_eq!(receipt.gas_used(), 24000); // 45000 - 21000 + + Ok(()) + } + + #[tokio::test] + async fn test_get_transaction_count() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); + assert_eq!( + provider.get_transaction_count(TX_SENDER).pending().await?, + 0 + ); + + setup.send_test_payloads().await?; + + assert_eq!(provider.get_transaction_count(DEPOSIT_SENDER).await?, 0); + assert_eq!( + provider.get_transaction_count(TX_SENDER).pending().await?, + 4 + ); + + Ok(()) + } + + #[tokio::test] + async fn test_eth_call() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // We ensure that eth_call will succeed because we are on plain state + let send_eth_call = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(200000) + .nonce(1) + .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) + .value(U256::from(9999999999849942300000u128)) + .input(TransactionInput::new(bytes!("0x"))); + + let res = provider + .call(send_eth_call.clone()) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_ok()); + + setup.send_test_payloads().await?; + + // We included a heavy spending transaction and now don't have enough funds for this request, so + // this eth_call with fail + let res = provider + .call(send_eth_call.nonce(4)) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_err()); + assert!(res + .unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas")); + + // read count1 from counter contract + let eth_call_count1 = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(20000000) + .nonce(5) + .to(COUNTER_ADDRESS) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))); + let res_count1 = provider.call(eth_call_count1).await; + assert!(res_count1.is_ok()); + assert_eq!( + U256::from_str(res_count1.unwrap().to_string().as_str()).unwrap(), + U256::from(2) + ); + + // read count2 from counter contract + let eth_call_count2 = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(20000000) + .nonce(6) + .to(COUNTER_ADDRESS) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xd631c639"))); + let res_count2 = provider.call(eth_call_count2).await; + assert!(res_count2.is_ok()); + assert_eq!( + U256::from_str(res_count2.unwrap().to_string().as_str()).unwrap(), + U256::from(2) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_eth_estimate_gas() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // We ensure that eth_estimate_gas will succeed because we are on plain state + let send_estimate_gas = OpTransactionRequest::default() + .from(TX_SENDER) + .transaction_type(0) + .gas_limit(200000) + .nonce(1) + .to(address!("0xf39635f2adf40608255779ff742afe13de31f577")) + .value(U256::from(9999999999849942300000u128)) + .input(TransactionInput::new(bytes!("0x"))); + + let res = provider + .estimate_gas(send_estimate_gas.clone()) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_ok()); + + setup.send_test_payloads().await?; + + // We included a heavy spending transaction and now don't have enough funds for this request, so + // this eth_estimate_gas with fail + let res = provider + .estimate_gas(send_estimate_gas.nonce(4)) + .block(BlockNumberOrTag::Pending.into()) + .await; + + assert!(res.is_err()); + assert!(res + .unwrap_err() + .as_error_resp() + .unwrap() + .message + .contains("insufficient funds for gas")); + + Ok(()) + } + + #[tokio::test] + async fn test_eth_simulate_v1() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + setup.send_test_payloads().await?; + + let simulate_call = SimulatePayload { + block_state_calls: vec![SimBlock { + calls: vec![ + // read number from counter contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))) + .into(), + // increment() value in contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .input(TransactionInput::new(bytes!("0xd09de08a"))) + .into(), + // read number from counter contract + OpTransactionRequest::default() + .from(address!("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")) + .transaction_type(0) + .gas_limit(200000) + .to(address!("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512")) + .value(U256::ZERO) + .input(TransactionInput::new(bytes!("0xa87d942c"))) + .into(), + ], + block_overrides: None, + state_overrides: None, + }], + trace_transfers: false, + validation: true, + return_full_transactions: true, + }; + let simulate_res = provider + .simulate(&simulate_call) + .block_id(BlockNumberOrTag::Pending.into()) + .await; + assert!(simulate_res.is_ok()); + let block = simulate_res.unwrap(); + assert_eq!(block.len(), 1); + assert_eq!(block[0].calls.len(), 3); + assert_eq!( + block[0].calls[0].return_data, + bytes!("0x0000000000000000000000000000000000000000000000000000000000000002") + ); + assert_eq!(block[0].calls[1].return_data, bytes!("0x")); + assert_eq!( + block[0].calls[2].return_data, + bytes!("0x0000000000000000000000000000000000000000000000000000000000000003") + ); + + Ok(()) + } + + #[tokio::test] + async fn test_send_raw_transaction_sync() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + + setup.send_flashblock(create_first_payload()).await?; + + // run the Tx sync and, in parallel, deliver the payload that contains the Tx + let (receipt_result, payload_result) = tokio::join!( + setup.send_raw_transaction_sync(TRANSFER_ETH_TX, None), + async { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + setup.send_flashblock(create_second_payload()).await + } + ); + + payload_result?; + let receipt = receipt_result?; + + assert_eq!(receipt.transaction_hash(), TRANSFER_ETH_HASH); + Ok(()) + } + + #[tokio::test] + async fn test_send_raw_transaction_sync_timeout() { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await.unwrap(); + + // fail request immediately by passing a timeout of 0 ms + let receipt_result = setup + .send_raw_transaction_sync(TRANSFER_ETH_TX, Some(0)) + .await; + + let error_code = EthRpcErrorCode::TransactionConfirmationTimeout.code(); + assert!(receipt_result + .err() + .unwrap() + .to_string() + .contains(format!("{}", error_code).as_str())); + } + + #[tokio::test] + async fn test_get_logs_pending() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + // Test no logs when no flashblocks sent + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .select(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + assert_eq!(logs.len(), 0); + + // Send payloads with transactions + setup.send_test_payloads().await?; + + // Test getting pending logs - must use both fromBlock and toBlock as "pending" + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // We should now have 2 logs from the INCREMENT_TX transaction + assert_eq!(logs.len(), 2); + + // Verify the first log is from COUNTER_ADDRESS + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].topics()[0], TEST_LOG_TOPIC_0); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + // Verify the second log is from TEST_ADDRESS + assert_eq!(logs[1].address(), TEST_ADDRESS); + assert_eq!(logs[1].topics()[0], TEST_LOG_TOPIC_0); + assert_eq!(logs[1].transaction_hash, Some(INCREMENT_HASH)); + + Ok(()) + } + + #[tokio::test] + async fn test_get_logs_filter_by_address() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test filtering by a specific address (COUNTER_ADDRESS) + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .address(COUNTER_ADDRESS) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should get only 1 log from COUNTER_ADDRESS + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + // Test filtering by TEST_ADDRESS + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .address(TEST_ADDRESS) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should get only 1 log from TEST_ADDRESS + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), TEST_ADDRESS); + assert_eq!(logs[0].transaction_hash, Some(INCREMENT_HASH)); + + Ok(()) + } + + #[tokio::test] + async fn test_get_logs_topic_filtering() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test filtering by topic - should match both logs + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .event_signature(TEST_LOG_TOPIC_0) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + assert_eq!(logs.len(), 2); + assert!(logs.iter().all(|log| log.topics()[0] == TEST_LOG_TOPIC_0)); + + // Test filtering by specific topic combination - should match only the first log + let filter = alloy_rpc_types_eth::Filter::default() + .topic1(TEST_LOG_TOPIC_1) + .from_block(alloy_eips::BlockNumberOrTag::Pending) + .to_block(alloy_eips::BlockNumberOrTag::Pending); + + let logs = provider.get_logs(&filter).await?; + + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].address(), COUNTER_ADDRESS); + assert_eq!(logs[0].topics()[1], TEST_LOG_TOPIC_1); + + Ok(()) + } + + #[tokio::test] + async fn test_get_logs_mixed_block_ranges() -> Result<()> { + reth_tracing::init_test_tracing(); + let setup = TestSetup::new().await?; + let provider = setup.harness.provider(); + + setup.send_test_payloads().await?; + + // Test fromBlock: 0, toBlock: pending (should include both historical and pending) + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(0) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should now include pending logs (2 logs from our test setup) + assert_eq!(logs.len(), 2); + assert!(logs + .iter() + .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + // Test fromBlock: latest, toBlock: pending + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Latest) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should include pending logs (historical part is empty in our test setup) + assert_eq!(logs.len(), 2); + assert!(logs + .iter() + .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + // Test fromBlock: earliest, toBlock: pending + let logs = provider + .get_logs( + &alloy_rpc_types_eth::Filter::default() + .from_block(alloy_eips::BlockNumberOrTag::Earliest) + .to_block(alloy_eips::BlockNumberOrTag::Pending), + ) + .await?; + + // Should include pending logs (historical part is empty in our test setup) + assert_eq!(logs.len(), 2); + assert!(logs + .iter() + .all(|log| log.transaction_hash == Some(INCREMENT_HASH))); + + Ok(()) + } } diff --git a/crates/metering/src/tests/assets/genesis.json b/crates/metering/src/tests/assets/genesis.json index 4d703497..dbdbfe69 100644 --- a/crates/metering/src/tests/assets/genesis.json +++ b/crates/metering/src/tests/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index be0cc9a6..56469561 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -42,6 +42,8 @@ alloy-consensus.workspace = true alloy-provider.workspace = true alloy-rpc-client.workspace = true alloy-serde.workspace = true +alloy-signer = "1.0" +alloy-signer-local = "1.1.0" # op-alloy op-alloy-rpc-types.workspace = true diff --git a/crates/test-utils/assets/genesis.json b/crates/test-utils/assets/genesis.json index 79ab75e9..b3099c33 100644 --- a/crates/test-utils/assets/genesis.json +++ b/crates/test-utils/assets/genesis.json @@ -1,6 +1,6 @@ { "config": { - "chainId": 8453, + "chainId": 84532, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, diff --git a/crates/test-utils/src/accounts.rs b/crates/test-utils/src/accounts.rs index 63bf4d8c..65ac2ccc 100644 --- a/crates/test-utils/src/accounts.rs +++ b/crates/test-utils/src/accounts.rs @@ -1,10 +1,15 @@ //! Test accounts with pre-funded balances for integration testing -use alloy_primitives::{address, Address}; +use alloy_consensus::{SignableTransaction, TxLegacy}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{address, hex, Address, Bytes, FixedBytes, U256}; +use alloy_signer::SignerSync; +use alloy_signer_local::PrivateKeySigner; +use eyre::Result; /// Hardcoded test account with a fixed private key #[derive(Debug, Clone)] -pub struct TestAccount { +pub struct Account { /// Account name for easy identification pub name: &'static str, /// Ethereum address @@ -13,6 +18,38 @@ pub struct TestAccount { pub private_key: &'static str, } +impl Account { + /// Sign a simple ETH transfer transaction and return the signed bytes + pub fn sign_transaction_bytes( + &self, + to: Address, + value: U256, + nonce: u64, + chain_id: u64, + ) -> Result { + let key_bytes = hex::decode(self.private_key)?; + let key_fixed: FixedBytes<32> = FixedBytes::from_slice(&key_bytes); + let signer = PrivateKeySigner::from_bytes(&key_fixed)?; + + let tx = TxLegacy { + chain_id: Some(chain_id), + nonce, + gas_price: 200, + gas_limit: 21_000, + to: alloy_primitives::TxKind::Call(to), + value, + input: Bytes::new(), + }; + + let signature = signer.sign_hash_sync(&tx.signature_hash())?; + let signed_tx = tx.into_signed(signature); + + Ok(signed_tx.encoded_2718().into()) + } +} + +pub type TestAccount = Account; + /// Collection of all test accounts #[derive(Debug, Clone)] pub struct TestAccounts { diff --git a/crates/test-utils/src/harness.rs b/crates/test-utils/src/harness.rs index 9bb9ba11..854e679a 100644 --- a/crates/test-utils/src/harness.rs +++ b/crates/test-utils/src/harness.rs @@ -58,7 +58,11 @@ impl TestHarness { &self.accounts } - async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { + pub fn rpc_url(&self) -> String { + format!("http://{}", self.node.http_api_addr) + } + + pub async fn build_block_from_transactions(&self, transactions: Vec) -> Result<()> { let latest_block = self .provider() .get_block_by_number(BlockNumberOrTag::Latest) diff --git a/crates/test-utils/src/node.rs b/crates/test-utils/src/node.rs index 5cf1ec27..7c4f9d91 100644 --- a/crates/test-utils/src/node.rs +++ b/crates/test-utils/src/node.rs @@ -23,10 +23,10 @@ use std::any::Any; use std::net::SocketAddr; use std::sync::Arc; -pub const BASE_CHAIN_ID: u64 = 8453; +pub const BASE_CHAIN_ID: u64 = 84532; pub struct LocalNode { - http_api_addr: SocketAddr, + pub(crate) http_api_addr: SocketAddr, engine_ipc_path: String, // flashblock_sender: mpsc::Sender<(Flashblock, oneshot::Sender<()>)>, _node_exit_future: NodeExitFuture, @@ -68,14 +68,27 @@ impl LocalNode { ..NetworkArgs::default() }; + // Generate unique IPC path for this test instance to avoid conflicts + // Use timestamp + thread ID + process ID for uniqueness + let unique_ipc_path = format!( + "/tmp/reth_engine_api_{}_{}_{:?}.ipc", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(), + std::process::id(), + std::thread::current().id() + ); + + let mut rpc_args = RpcServerArgs::default() + .with_unused_ports() + .with_http() + .with_auth_ipc(); + rpc_args.auth_ipc_path = unique_ipc_path; + let node_config = NodeConfig::new(chain_spec.clone()) .with_network(network_config) - .with_rpc( - RpcServerArgs::default() - .with_unused_ports() - .with_http() - .with_auth_ipc(), - ) + .with_rpc(rpc_args) .with_unused_ports(); let node = OpNode::new(RollupArgs::default());