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
2,696 changes: 2,465 additions & 231 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ alloy-consensus = { version = "1.0.37", default-features = false }
alloy-eips = { version = "1.0.37", default-features = false }
alloy-json-rpc = { version = "1.0.37", default-features = false }
alloy-network = { version = "1.0.37", default-features = false }
alloy-node-bindings = { version = "1.0.37", default-features = false }
alloy-primitives = { version = "1.4.1", default-features = false }
alloy-provider = { version = "1.0.37", default-features = false }
alloy-rpc-client = { version = "1.0.37", default-features = false }
Expand Down
2 changes: 2 additions & 0 deletions crates/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ aws-sdk-kms = "1.76.0"

# test-utils
alloy-eips = { workspace = true, optional = true }
anvil = { git = "https://github.com/foundry-rs/foundry.git", rev = "2c84e1c970d11ef5023a77d8002a1cb70b143888", default-features = false, optional = true }
alloy-rpc-types-eth = { workspace = true, optional = true }
alloy-rpc-types-engine = { workspace = true, optional = true }
reth-e2e-test-utils = { workspace = true, optional = true }
Expand Down Expand Up @@ -155,6 +156,7 @@ test-utils = [
"rollup-node-chain-orchestrator/test-utils",
"scroll-network/test-utils",
"alloy-eips",
"anvil",
"reth-storage-api",
"alloy-rpc-types-eth",
]
2 changes: 1 addition & 1 deletion crates/node/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ impl ScrollRollupNodeConfig {
let consensus = self.consensus_args.consensus(authorized_signer)?;

let (l1_notification_tx, l1_notification_rx): (Option<Sender<Arc<L1Notification>>>, _) =
if let Some(provider) = l1_provider.filter(|_| !self.test) {
if let Some(provider) = l1_provider.filter(|_| !self.test||self.blob_provider_args.anvil_url.is_some()) {
tracing::info!(target: "scroll::node::args", ?l1_block_startup_info, "Starting L1 watcher");
(
None,
Expand Down
162 changes: 160 additions & 2 deletions crates/node/src/test_utils/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,31 @@ use std::{
use tokio::sync::{mpsc, Mutex};

/// Main test fixture providing a high-level interface for testing rollup nodes.
#[derive(Debug)]
pub struct TestFixture {
/// The list of nodes in the test setup.
pub nodes: Vec<NodeHandle>,
/// Shared wallet for generating transactions.
pub wallet: Arc<Mutex<Wallet>>,
/// Chain spec used by the nodes.
pub chain_spec: Arc<<ScrollRollupNode as NodeTypes>::ChainSpec>,
/// Optional Anvil instance for L1 simulation.
pub anvil: Option<anvil::NodeHandle>,
/// The task manager. Held in order to avoid dropping the node.
_tasks: TaskManager,
}

impl Debug for TestFixture {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestFixture")
.field("nodes", &self.nodes)
.field("wallet", &"<Mutex<Wallet>>")
.field("chain_spec", &self.chain_spec)
.field("anvil", &self.anvil.is_some())
.field("_tasks", &"<TaskManager>")
.finish()
}
}

/// The network handle to the Scroll network.
pub type ScrollNetworkHandle =
NetworkHandle<BasicNetworkPrimitives<ScrollPrimitives, ScrollPooledTransaction>>;
Expand Down Expand Up @@ -201,6 +214,45 @@ impl TestFixture {
) -> eyre::Result<rollup_node_chain_orchestrator::ChainOrchestratorStatus> {
self.get_status(0).await
}

/// Get the Anvil instance if one was started.
pub const fn anvil(&self) -> Option<&anvil::NodeHandle> {
self.anvil.as_ref()
}

/// Get the Anvil HTTP endpoint if Anvil was started.
pub fn anvil_endpoint(&self) -> Option<String> {
self.anvil.as_ref().map(|a| a.http_endpoint())
}

/// Check if Anvil is running.
pub const fn has_anvil(&self) -> bool {
self.anvil.is_some()
}

/// Send a raw transaction to Anvil.
pub async fn anvil_send_raw_transaction(
&self,
raw_tx: impl Into<alloy_primitives::Bytes>,
) -> eyre::Result<alloy_primitives::B256> {
use alloy_provider::{Provider, ProviderBuilder};

// Ensure Anvil is running
let anvil_endpoint =
self.anvil_endpoint().ok_or_else(|| eyre::eyre!("Anvil is not running"))?;

// Create provider
let provider = ProviderBuilder::new().connect_http(anvil_endpoint.parse()?);

// Send raw transaction
let raw_tx_bytes = raw_tx.into();
let pending_tx = provider.send_raw_transaction(&raw_tx_bytes).await?;

let tx_hash = *pending_tx.tx_hash();
tracing::info!("Sent raw transaction to Anvil: {:?}", tx_hash);

Ok(tx_hash)
}
}

/// Builder for creating test fixtures with a fluent API.
Expand All @@ -211,6 +263,10 @@ pub struct TestFixtureBuilder {
chain_spec: Option<Arc<<ScrollRollupNode as NodeTypes>::ChainSpec>>,
is_dev: bool,
no_local_transactions_propagation: bool,
enable_anvil: bool,
anvil_state_path: Option<PathBuf>,
anvil_chain_id: Option<u64>,
anvil_block_time: Option<u64>,
}

impl Default for TestFixtureBuilder {
Expand All @@ -228,6 +284,10 @@ impl TestFixtureBuilder {
chain_spec: None,
is_dev: false,
no_local_transactions_propagation: false,
enable_anvil: false,
anvil_state_path: None,
anvil_chain_id: None,
anvil_block_time: None,
}
}

Expand Down Expand Up @@ -421,11 +481,69 @@ impl TestFixtureBuilder {
&mut self.config
}

/// Enable Anvil with default settings.
pub const fn with_anvil(mut self) -> Self {
self.enable_anvil = true;
self
}

/// Enable Anvil with the default state file (`tests/anvil_state.json`).
pub fn with_anvil_default_state(mut self) -> Self {
self.enable_anvil = true;
self.anvil_state_path = Some(PathBuf::from("./tests/testdata/anvil_state.json"));
self
}

/// Enable Anvil with a custom state file.
pub fn with_anvil_state(mut self, path: impl Into<PathBuf>) -> Self {
self.enable_anvil = true;
self.anvil_state_path = Some(path.into());
self
}

/// Set the chain ID for Anvil.
pub const fn with_anvil_chain_id(mut self, chain_id: u64) -> Self {
self.anvil_chain_id = Some(chain_id);
self
}

/// Set the block time for Anvil (in seconds).
pub const fn with_anvil_block_time(mut self, block_time: u64) -> Self {
self.anvil_block_time = Some(block_time);
self
}

/// Build the test fixture.
pub async fn build(self) -> eyre::Result<TestFixture> {
let config = self.config;
let mut config = self.config;
let chain_spec = self.chain_spec.unwrap_or_else(|| SCROLL_DEV.clone());

// Start Anvil if requested
let anvil = if self.enable_anvil {
let handle = Self::spawn_anvil(
self.anvil_state_path.as_deref(),
self.anvil_chain_id,
self.anvil_block_time,
)
.await?;

// Parse endpoint URL once and reuse
let endpoint_url = handle
.http_endpoint()
.parse::<reqwest::Url>()
.map_err(|e| eyre::eyre!("Failed to parse Anvil endpoint URL: {}", e))?;

// Configure L1 provider and blob provider to use Anvil
config.l1_provider_args.url = Some(endpoint_url.clone());
config.l1_provider_args.logs_query_block_range = 500;
config.blob_provider_args.anvil_url = Some(endpoint_url);
config.blob_provider_args.mock = false;

Some(handle)
} else {
None
};

let (nodes, _tasks, wallet) = setup_engine(
config.clone(),
self.num_nodes,
Expand Down Expand Up @@ -475,6 +593,46 @@ impl TestFixtureBuilder {
wallet: Arc::new(Mutex::new(wallet)),
chain_spec,
_tasks,
anvil,
})
}

/// Spawn an Anvil instance with the given configuration.
async fn spawn_anvil(
state_path: Option<&std::path::Path>,
chain_id: Option<u64>,
block_time: Option<u64>,
) -> eyre::Result<anvil::NodeHandle> {
let mut config = anvil::NodeConfig::default();

// Configure chain ID
if let Some(id) = chain_id {
config.chain_id = Some(id);
}

config.port = 8544;

// Configure block time
if let Some(time) = block_time {
config.block_time = Some(std::time::Duration::from_secs(time));
}

// Load state from file if provided
if let Some(path) = state_path {
let state = anvil::eth::backend::db::SerializableState::load(path)
.map_err(|e| {
eyre::eyre!(
"Failed to load Anvil state from {}: {:?}",
path.display(),
e
)
})?;
tracing::info!("Loaded Anvil state from: {}", path.display());
config.init_state = Some(state);
}

// Spawn Anvil and return the NodeHandle
let (_api, handle) = anvil::spawn(config).await;
Ok(handle)
}
}
Loading
Loading