Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Cargo.lock

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

15 changes: 14 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repository = "https://github.com/base/node-reth"

[workspace]
resolver = "2"
members = ["bin/*", "crates/client/*", "crates/shared/*"]
members = ["bin/*", "crates/client/*", "crates/shared/*", "testing/*"]
default-members = ["bin/node"]

[workspace.metadata.cargo-udeps.ignore]
Expand Down Expand Up @@ -63,6 +63,8 @@ base-txpool = { path = "crates/client/txpool" }
base-reth-runner = { path = "crates/client/runner" }
base-reth-test-utils = { path = "crates/client/test-utils" }
base-reth-flashblocks = { path = "crates/client/flashblocks" }
# Testing
base-testing-e2e = { path = "testing/e2e" }

# reth
reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
Expand Down Expand Up @@ -170,3 +172,14 @@ serde_json = "1.0.145"
metrics-derive = "0.1.0"
tracing-subscriber = "0.3.22"
thiserror = "2.0"

# Additional alloy crates
alloy-transport = "1.0.41"
alloy-transport-http = "1.0.41"
alloy-signer = "1.0.41"
alloy-network = "1.0.41"

# Testing misc
colored = "2.1"
dotenvy = "0.15"
hex = "0.4"
28 changes: 28 additions & 0 deletions bin/flashblocks-e2e/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "flashblocks-e2e"
description = "End-to-end regression testing tool for node-reth flashblocks RPC"

version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[lints]
workspace = true

[dependencies]
# internal
base-testing-e2e.workspace = true

# alloy
alloy-primitives.workspace = true

# misc
clap = { workspace = true, features = ["derive"] }
dotenvy.workspace = true
eyre.workspace = true
tokio = { workspace = true, features = ["full"] }
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
50 changes: 50 additions & 0 deletions bin/flashblocks-e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# flashblocks-e2e

End-to-end regression testing tool for node-reth flashblocks RPC.

## Overview

This tool runs a comprehensive suite of tests against a live node to validate
the flashblocks RPC implementation including state visibility, eth_call,
transaction receipts, WebSocket subscriptions, and metering endpoints.

## Usage

```bash
# Run all tests against a local node
flashblocks-e2e --rpc-url http://localhost:8545 --flashblocks-ws-url wss://localhost:8546/ws

# Run with a filter
flashblocks-e2e --filter "flashblock*"

# List available tests
flashblocks-e2e --list

# Continue running after failures
flashblocks-e2e --keep-going

# Verbose output
flashblocks-e2e -v

# JSON output for CI
flashblocks-e2e --format json
```

## Environment Variables

- `PRIVATE_KEY`: Hex-encoded private key for transaction-sending tests (optional)

## Test Categories

- **blocks**: Block retrieval and pending state visibility
- **call**: `eth_call` and `eth_estimateGas` tests
- **receipts**: Transaction receipt retrieval
- **logs**: `eth_getLogs` including pending logs
- **sanity**: Flashblocks WebSocket endpoint validation
- **metering**: `base_meterBundle` and `base_meteredPriorityFeePerGas` endpoints
- **contracts**: Contract deployment and interaction tests

## Exit Codes

- `0`: All tests passed
- `1`: One or more tests failed
66 changes: 66 additions & 0 deletions bin/flashblocks-e2e/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! CLI argument parsing and configuration.

use clap::Parser;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};

/// End-to-end regression testing for node-reth flashblocks RPC.
#[derive(Parser, Debug)]
#[command(name = "flashblocks-e2e")]
#[command(about = "End-to-end regression testing for node-reth flashblocks RPC")]
pub(crate) struct Args {
/// HTTP RPC endpoint URL for the node being tested.
#[arg(long, default_value = "http://localhost:8545")]
pub rpc_url: String,

/// Flashblocks WebSocket URL (sequencer/builder endpoint).
#[arg(long, default_value = "wss://mainnet.flashblocks.base.org/ws")]
pub flashblocks_ws_url: String,

/// Recipient address for ETH transfers in tests.
/// Required when PRIVATE_KEY is set.
#[arg(long)]
pub recipient: Option<String>,

/// Run only tests matching this filter (supports glob patterns).
#[arg(long, short)]
pub filter: Option<String>,

/// List available tests without running them.
#[arg(long)]
pub list: bool,

/// Continue running tests even after failures.
#[arg(long, default_value = "false")]
pub keep_going: bool,

/// Verbose output (can be repeated for more verbosity).
#[arg(long, short, action = clap::ArgAction::Count)]
pub verbose: u8,

/// Output format: text, json.
#[arg(long, default_value = "text")]
pub format: OutputFormat,
}

/// Output format for test results.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum OutputFormat {
/// Human-readable text output with colors.
Text,
/// JSON output for CI integration.
Json,
}

/// Initialize tracing with the specified verbosity level.
pub(crate) fn init_tracing(verbose: u8) {
let filter = match verbose {
0 => "flashblocks_e2e=info,base_testing_e2e=info",
1 => "flashblocks_e2e=debug,base_testing_e2e=debug",
_ => "flashblocks_e2e=trace,base_testing_e2e=trace",
};

tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)))
.init();
}
84 changes: 84 additions & 0 deletions bin/flashblocks-e2e/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#![doc = include_str!("../README.md")]
#![doc(issue_tracker_base_url = "https://github.com/base/node-reth/issues/")]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

//! Flashblocks E2E - End-to-end regression testing for node-reth flashblocks RPC.

mod cli;

use alloy_primitives::Address;
use base_testing_e2e::{
TestClient, build_test_suite, list_tests, print_results_json, print_results_text, run_tests,
};
use clap::Parser;
use cli::{Args, OutputFormat};
use eyre::{Result, WrapErr};

#[tokio::main]
async fn main() -> Result<()> {
// Load .env file if present (ignores errors if file doesn't exist)
let _ = dotenvy::dotenv();

let args = Args::parse();

// Initialize tracing
cli::init_tracing(args.verbose);

// Get private key from environment (optional)
let private_key = std::env::var("PRIVATE_KEY").ok();

if private_key.is_some() {
tracing::info!("Private key loaded from environment");
} else {
tracing::warn!("No PRIVATE_KEY set - tests requiring transaction signing will be skipped");
}

// Parse recipient address if provided
let recipient: Option<Address> = args
.recipient
.as_ref()
.map(|s| s.parse())
.transpose()
.wrap_err("Invalid recipient address")?;

if recipient.is_none() {
tracing::warn!("No --recipient set - tests requiring ETH transfers will be skipped");
}

// Create test client (chain ID is fetched from RPC)
let client =
TestClient::new(&args.rpc_url, &args.flashblocks_ws_url, private_key.as_deref(), recipient)
.await?;

if let Some(addr) = client.signer_address() {
tracing::info!(address = ?addr, "Signer configured");
}
if let Some(addr) = client.recipient() {
tracing::info!(address = ?addr, "Recipient configured");
}

// Build test suite
let suite = build_test_suite();

if args.list {
list_tests(&suite);
return Ok(());
}

// Run tests
let results = run_tests(&client, &suite, args.filter.as_deref(), args.keep_going).await;

// Output results
match args.format {
OutputFormat::Text => print_results_text(&results),
OutputFormat::Json => print_results_json(&results)?,
}

// Exit with error code if any tests failed
if results.iter().any(|r| !r.passed) {
std::process::exit(1);
}

Ok(())
}
Loading