diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a82d876a8..134f9d8b7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -168,6 +168,8 @@ jobs: with: key: "eth-tests-1c23e3c" path: crates/evm/ethereum-tests + - name: Build citrea + run: make build - name: Run coverage run: make coverage env: @@ -351,6 +353,8 @@ jobs: run: cargo risczero install --version r0.1.79.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build citrea + run: make build # `cargo-nextest` is much faster than standard `cargo test`. - uses: taiki-e/install-action@nextest - name: Cache ethereum-tests diff --git a/Cargo.lock b/Cargo.lock index 75b6f939b..863cece43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1381,6 +1381,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bollard" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http 1.1.0", + "http-body-util", + "hyper 1.4.1", + "hyper-named-pipe", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.45.0-rc.26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "bonsai-sdk" version = "0.9.0" @@ -1642,7 +1686,10 @@ dependencies = [ "anyhow", "async-trait", "bincode", + "bitcoin", "bitcoin-da", + "bitcoincore-rpc", + "bollard", "borsh", "citrea-evm", "citrea-fullnode", @@ -1653,11 +1700,13 @@ dependencies = [ "citrea-stf", "clap", "ethereum-rpc", + "futures", "hex", "jsonrpsee", "log", "log-panics", "proptest", + "rand 0.8.5", "regex", "reqwest 0.12.5", "reth-primitives", @@ -1685,6 +1734,7 @@ dependencies = [ "sov-stf-runner", "tempfile", "tokio", + "toml", "tracing", "tracing-subscriber 0.3.18", ] @@ -3298,6 +3348,21 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -3351,6 +3416,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -6955,6 +7035,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "serde_spanned" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index d3c9e6881..f8302882b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,6 +162,8 @@ tower-http = { version = "0.5.0", features = ["full"] } tower = { version = "0.4.13", features = ["full"] } hyper = { version = "1.4.0" } +bollard = { version = "0.17.1" } + [patch.'https://github.com/eigerco/celestia-node-rs.git'] # Uncomment to apply local changes # celestia-proto = { path = "../celestia-node-rs/proto" } diff --git a/Makefile b/Makefile index 7af7de0bb..260e66013 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ EF_TESTS_DIR := crates/evm/ethereum-tests help: ## Display this help message @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) +.PHONY: build build: ## Build the project @cargo build @@ -24,7 +25,7 @@ clean-node: ## Cleans local dbs needed for sequencer and nodes test-legacy: ## Runs test suite with output from tests printed @cargo test -- --nocapture -Zunstable-options --report-time -test: $(EF_TESTS_DIR) ## Runs test suite using next test +test: build $(EF_TESTS_DIR) ## Runs test suite using next test @cargo nextest run --workspace --all-features --no-fail-fast $(filter-out $@,$(MAKECMDGOALS)) install-dev-tools: ## Installs all necessary cargo helpers @@ -75,7 +76,7 @@ find-unused-deps: ## Prints unused dependencies for project. Note: requires nigh find-flaky-tests: ## Runs tests over and over to find if there's flaky tests flaky-finder -j16 -r320 --continue "cargo test -- --nocapture" -coverage: $(EF_TESTS_DIR) ## Coverage in lcov format +coverage: build $(EF_TESTS_DIR) ## Coverage in lcov format cargo llvm-cov --locked --lcov --output-path lcov.info nextest --workspace --all-features coverage-html: ## Coverage in HTML format diff --git a/bin/citrea/Cargo.toml b/bin/citrea/Cargo.toml index cfad128fa..a640aee71 100644 --- a/bin/citrea/Cargo.toml +++ b/bin/citrea/Cargo.toml @@ -82,6 +82,14 @@ log = "0.4" regex = "1.10" rustc_version_runtime = { workspace = true } +# bitcoin-e2e dependencies +bitcoin.workspace = true +bitcoincore-rpc.workspace = true +bollard.workspace = true +futures.workspace = true +rand.workspace = true +toml.workspace = true + [features] default = [] # Deviate from convention by making the "native" feature active by default. This aligns with how this package is meant to be used (as a binary first, library second). diff --git a/bin/citrea/tests/all_tests.rs b/bin/citrea/tests/all_tests.rs index 373e30e7e..92240734e 100644 --- a/bin/citrea/tests/all_tests.rs +++ b/bin/citrea/tests/all_tests.rs @@ -1,6 +1,9 @@ // disable bank module tests due to needing a big rewrite to make it work // mod bank; mod e2e; + +mod bitcoin_e2e; + mod evm; mod mempool; mod sequencer_commitments; diff --git a/bin/citrea/tests/bitcoin_e2e/bitcoin.rs b/bin/citrea/tests/bitcoin_e2e/bitcoin.rs new file mode 100644 index 000000000..71c0522f9 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/bitcoin.rs @@ -0,0 +1,255 @@ +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +use anyhow::{bail, Context}; +use async_trait::async_trait; +use bitcoin::Address; +use bitcoincore_rpc::{Auth, Client, RpcApi}; +use tokio::process::Command; +use tokio::time::sleep; + +use super::config::BitcoinConfig; +use super::docker::DockerEnv; +use super::framework::TestContext; +use super::node::{Node, SpawnOutput}; +use super::Result; +use crate::bitcoin_e2e::node::NodeKind; + +pub struct BitcoinNode { + spawn_output: SpawnOutput, + pub config: BitcoinConfig, + client: Client, + gen_addr: Address, +} + +impl BitcoinNode { + pub async fn new(config: &BitcoinConfig, docker: &Option) -> Result { + let spawn_output = match docker { + Some(docker) => docker.spawn(config.into()).await?, + None => Self::spawn(config, &PathBuf::default()).await?, + }; + + let rpc_url = format!( + "http://127.0.0.1:{}/wallet/{}", + config.rpc_port, + NodeKind::Bitcoin + ); + let client = Client::new( + &rpc_url, + Auth::UserPass(config.rpc_user.clone(), config.rpc_password.clone()), + ) + .await + .context("Failed to create RPC client")?; + + wait_for_rpc_ready(&client, Duration::from_secs(30)).await?; + println!("bitcoin RPC is ready"); + + client + .create_wallet(&NodeKind::Sequencer.to_string(), None, None, None, None) + .await?; + client + .create_wallet(&NodeKind::Prover.to_string(), None, None, None, None) + .await?; + client + .create_wallet(&NodeKind::Bitcoin.to_string(), None, None, None, None) + .await?; + + let gen_addr = client.get_new_address(None, None).await?.assume_checked(); + Ok(Self { + spawn_output, + config: config.clone(), + client, + gen_addr, + }) + } + + pub fn get_log_path(&self) -> PathBuf { + self.config.data_dir.join("regtest").join("debug.log") + } + + pub async fn wait_mempool_len( + &self, + target_len: usize, + timeout: Option, + ) -> Result<()> { + let timeout = timeout.unwrap_or(Duration::from_secs(120)); + let start = Instant::now(); + while start.elapsed() < timeout { + let mempool_len = self.get_raw_mempool().await?.len(); + if mempool_len >= target_len { + return Ok(()); + } + sleep(Duration::from_millis(500)).await; + } + bail!("Timeout waiting for mempool to reach length {}", target_len) + } + + pub async fn fund_wallet(&self, name: String) -> Result<()> { + let rpc_url = format!("http://127.0.0.1:{}/wallet/{}", self.config.rpc_port, name); + let client = Client::new( + &rpc_url, + Auth::UserPass( + self.config.rpc_user.clone(), + self.config.rpc_password.clone(), + ), + ) + .await + .context("Failed to create RPC client")?; + + let gen_addr = client.get_new_address(None, None).await?.assume_checked(); + client.generate_to_address(25, &gen_addr).await?; + Ok(()) + } +} + +#[async_trait] +impl RpcApi for BitcoinNode { + async fn call serde::de::Deserialize<'a>>( + &self, + cmd: &str, + args: &[serde_json::Value], + ) -> bitcoincore_rpc::Result { + self.client.call(cmd, args).await + } + + // Override deprecated generate method. + // Uses node gen address and forward to `generate_to_address` + async fn generate( + &self, + block_num: u64, + _maxtries: Option, + ) -> bitcoincore_rpc::Result> { + self.generate_to_address(block_num, &self.gen_addr).await + } +} + +impl Node for BitcoinNode { + type Config = BitcoinConfig; + type Client = Client; + + async fn spawn(config: &Self::Config, _dir: &Path) -> Result { + let mut args = vec![ + "-regtest".to_string(), + format!("-datadir={}", config.data_dir.display()), + format!("-port={}", config.p2p_port), + format!("-rpcport={}", config.rpc_port), + format!("-rpcuser={}", config.rpc_user), + format!("-rpcpassword={}", config.rpc_password), + "-server".to_string(), + "-daemon".to_string(), + ]; + println!("Running bitcoind with args : {args:?}"); + + args.extend(config.extra_args.iter().cloned()); + Command::new("bitcoind") + .args(&args) + .kill_on_drop(true) + .spawn() + .context("Failed to spawn bitcoind process") + .map(SpawnOutput::Child) + } + + fn spawn_output(&mut self) -> &mut SpawnOutput { + &mut self.spawn_output + } + + async fn wait_for_ready(&self, timeout: Duration) -> Result<()> { + println!("Waiting for ready"); + let start = Instant::now(); + while start.elapsed() < timeout { + if wait_for_rpc_ready(&self.client, timeout).await.is_ok() { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + anyhow::bail!("Node failed to become ready within the specified timeout") + } + + fn client(&self) -> &Self::Client { + &self.client + } +} + +pub struct BitcoinNodeCluster { + inner: Vec, +} + +impl BitcoinNodeCluster { + pub async fn new(ctx: &TestContext) -> Result { + let n_nodes = ctx.config.test_case.num_nodes; + let mut cluster = Self { + inner: Vec::with_capacity(n_nodes), + }; + for config in ctx.config.bitcoin.iter() { + let node = BitcoinNode::new(config, &ctx.docker).await?; + cluster.inner.push(node) + } + + Ok(cluster) + } + + pub async fn stop_all(&mut self) -> Result<()> { + for node in &mut self.inner { + node.stop().await?; + } + Ok(()) + } + + pub async fn wait_for_sync(&self, timeout: Duration) -> Result<()> { + let start = Instant::now(); + while start.elapsed() < timeout { + let mut heights = HashSet::new(); + for node in &self.inner { + let height = node.get_block_count().await?; + println!("height : {height}"); + heights.insert(height); + } + + if heights.len() == 1 { + return Ok(()); + } + + sleep(Duration::from_secs(1)).await; + } + bail!("Nodes failed to sync within the specified timeout") + } + + // Connect all bitcoin nodes between them + pub async fn connect_nodes(&self) -> Result<()> { + for (i, from_node) in self.inner.iter().enumerate() { + for (j, to_node) in self.inner.iter().enumerate() { + if i != j { + let ip = match &to_node.spawn_output { + SpawnOutput::Container(container) => container.ip.clone(), + _ => "127.0.0.1".to_string(), + }; + + let add_node_arg = format!("{}:{}", ip, to_node.config.p2p_port); + from_node.add_node(&add_node_arg).await?; + } + } + } + Ok(()) + } + + pub fn get(&self, index: usize) -> Option<&BitcoinNode> { + self.inner.get(index) + } + + #[allow(unused)] + pub fn get_mut(&mut self, index: usize) -> Option<&mut BitcoinNode> { + self.inner.get_mut(index) + } +} + +async fn wait_for_rpc_ready(client: &Client, timeout: Duration) -> Result<()> { + let start = Instant::now(); + while start.elapsed() < timeout { + match client.get_blockchain_info().await { + Ok(_) => return Ok(()), + Err(_) => sleep(Duration::from_millis(500)).await, + } + } + Err(anyhow::anyhow!("Timeout waiting for RPC to be ready")) +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/bitcoin.rs b/bin/citrea/tests/bitcoin_e2e/config/bitcoin.rs new file mode 100644 index 000000000..fc98b3746 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/bitcoin.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +use bitcoin::Network; +use serde::{Deserialize, Serialize}; +use tempfile::TempDir; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BitcoinConfig { + pub p2p_port: u16, + pub rpc_port: u16, + pub rpc_user: String, + pub rpc_password: String, + pub data_dir: PathBuf, + pub extra_args: Vec, + pub network: Network, + pub docker_image: Option, +} + +impl Default for BitcoinConfig { + fn default() -> Self { + Self { + p2p_port: 0, + rpc_port: 0, + rpc_user: "user".to_string(), + rpc_password: "password".to_string(), + data_dir: TempDir::new() + .expect("Failed to create temporary directory") + .into_path(), + extra_args: vec![], + network: Network::Regtest, + docker_image: Some("ruimarinho/bitcoin-core:latest".to_string()), + } + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/docker.rs b/bin/citrea/tests/bitcoin_e2e/config/docker.rs new file mode 100644 index 000000000..7baddcafd --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/docker.rs @@ -0,0 +1,64 @@ +use super::{BitcoinConfig, FullSequencerConfig}; +use crate::bitcoin_e2e::utils::get_genesis_path; + +pub struct DockerConfig { + pub ports: Vec, + pub image: String, + pub cmd: Vec, + pub dir: String, +} + +impl From<&BitcoinConfig> for DockerConfig { + fn from(v: &BitcoinConfig) -> Self { + let mut args = vec![ + "-regtest".to_string(), + format!("-datadir=/bitcoin/data"), + format!("-port={}", v.p2p_port), + format!("-rpcport={}", v.rpc_port), + format!("-rpcuser={}", v.rpc_user), + format!("-rpcpassword={}", v.rpc_password), + "-server".to_string(), + "-rpcallowip=0.0.0.0/0".to_string(), + "-rpcbind=0.0.0.0".to_string(), + ]; + println!("Running bitcoind with args : {args:?}"); + + args.extend(v.extra_args.iter().cloned()); + Self { + ports: vec![v.rpc_port, v.p2p_port], + image: v + .docker_image + .clone() + .unwrap_or_else(|| "ruimarinho/bitcoin-core:latest".to_string()), + cmd: args, + dir: format!("{}:/bitcoin/data", v.data_dir.display()), + } + } +} + +impl From<&FullSequencerConfig> for DockerConfig { + fn from(v: &FullSequencerConfig) -> Self { + let args = vec![ + "--da-layer".to_string(), + "bitcoin".to_string(), + "--rollup-config-path".to_string(), + "sequencer_rollup_config.toml".to_string(), + "--sequencer-config-path".to_string(), + "sequencer_config.toml".to_string(), + "--genesis-paths".to_string(), + get_genesis_path(v.dir.parent().expect("Couldn't get parent dir")) + .display() + .to_string(), + ]; + + Self { + ports: vec![v.rollup.rpc.bind_port], + image: v + .docker_image + .clone() + .unwrap_or_else(|| "citrea:latest".to_string()), // Default to local image + cmd: args, + dir: format!("{}:/sequencer/data", v.dir.display()), + } + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/mod.rs b/bin/citrea/tests/bitcoin_e2e/config/mod.rs new file mode 100644 index 000000000..03b2abf4e --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/mod.rs @@ -0,0 +1,17 @@ +mod bitcoin; +mod docker; +mod rollup; +mod sequencer; +mod test; +mod test_case; +mod utils; + +pub use bitcoin::BitcoinConfig; +pub use citrea_sequencer::SequencerConfig; +pub use docker::DockerConfig; +pub use rollup::{default_rollup_config, RollupConfig}; +pub use sequencer::FullSequencerConfig; +pub use sov_stf_runner::ProverConfig; +pub use test::TestConfig; +pub use test_case::TestCaseConfig; +pub use utils::config_to_file; diff --git a/bin/citrea/tests/bitcoin_e2e/config/rollup.rs b/bin/citrea/tests/bitcoin_e2e/config/rollup.rs new file mode 100644 index 000000000..e09511691 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/rollup.rs @@ -0,0 +1,67 @@ +use bitcoin_da::service::BitcoinServiceConfig; +use sov_stf_runner::{FullNodeConfig, RollupPublicKeys, RpcConfig, StorageConfig}; +use tempfile::TempDir; + +use super::BitcoinConfig; +pub type RollupConfig = FullNodeConfig; + +pub fn default_rollup_config() -> RollupConfig { + RollupConfig { + rpc: RpcConfig { + bind_host: "127.0.0.1".into(), + bind_port: 0, + max_connections: 100, + max_request_body_size: 10 * 1024 * 1024, + max_response_body_size: 10 * 1024 * 1024, + batch_requests_limit: 50, + enable_subscriptions: true, + max_subscriptions_per_connection: 100, + }, + storage: StorageConfig { + path: TempDir::new() + .expect("Failed to create temporary directory") + .into_path(), + }, + runner: None, + da: BitcoinServiceConfig { + node_url: String::new(), + node_username: String::from("user"), + node_password: String::from("password"), + network: bitcoin::Network::Regtest, + da_private_key: None, + fee_rates_to_avg: None, + }, + public_keys: RollupPublicKeys { + sequencer_public_key: vec![ + 32, 64, 64, 227, 100, 193, 15, 43, 236, 156, 31, 229, 0, 161, 205, 76, 36, 124, + 137, 214, 80, 160, 30, 215, 232, 44, 171, 168, 103, 135, 124, 33, + ], + // private key [4, 95, 252, 129, 163, 193, 253, 179, 175, 19, 89, 219, 242, 209, 20, 176, 179, 239, 191, 127, 41, 204, 156, 93, 160, 18, 103, 170, 57, 210, 199, 141] + // Private Key (WIF): KwNDSCvKqZqFWLWN1cUzvMiJQ7ck6ZKqR6XBqVKyftPZtvmbE6YD + sequencer_da_pub_key: vec![ + 3, 136, 195, 18, 11, 187, 25, 37, 38, 109, 184, 237, 247, 208, 131, 219, 162, 70, + 35, 174, 234, 47, 239, 247, 60, 51, 174, 242, 247, 112, 186, 222, 30, + ], + // private key [117, 186, 249, 100, 208, 116, 89, 70, 0, 54, 110, 91, 17, 26, 29, 168, 248, 107, 46, 254, 45, 34, 218, 81, 200, 216, 33, 38, 160, 252, 172, 114] + // Private Key (WIF): L1AZdJXzDGGENBBPZGSL7dKJnwn5xSKqzszgK6CDwiBGThYQEVTo + prover_da_pub_key: vec![ + 2, 138, 232, 157, 214, 46, 7, 210, 235, 33, 105, 239, 71, 169, 105, 233, 239, 84, + 172, 112, 13, 54, 9, 206, 106, 138, 251, 218, 15, 28, 137, 112, 127, + ], + }, + sync_blocks_count: 10, + } +} + +impl From for BitcoinServiceConfig { + fn from(v: BitcoinConfig) -> Self { + Self { + node_url: format!("127.0.0.1:{}", v.rpc_port), + node_username: v.rpc_user, + node_password: v.rpc_password, + network: v.network, + da_private_key: None, + fee_rates_to_avg: None, + } + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/sequencer.rs b/bin/citrea/tests/bitcoin_e2e/config/sequencer.rs new file mode 100644 index 000000000..35da38e95 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/sequencer.rs @@ -0,0 +1,13 @@ +use std::path::PathBuf; + +use citrea_sequencer::SequencerConfig; + +use super::rollup::RollupConfig; + +#[derive(Clone, Debug)] +pub struct FullSequencerConfig { + pub sequencer: SequencerConfig, + pub rollup: RollupConfig, + pub docker_image: Option, + pub dir: PathBuf, +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/test.rs b/bin/citrea/tests/bitcoin_e2e/config/test.rs new file mode 100644 index 000000000..b8e01f50d --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/test.rs @@ -0,0 +1,16 @@ +use sov_stf_runner::ProverConfig; + +use super::bitcoin::BitcoinConfig; +use super::rollup::RollupConfig; +use super::test_case::TestCaseConfig; +use super::FullSequencerConfig; + +#[derive(Clone)] +pub struct TestConfig { + pub test_case: TestCaseConfig, + pub bitcoin: Vec, + pub sequencer: FullSequencerConfig, + pub prover: ProverConfig, + pub prover_rollup: RollupConfig, + pub full_node_rollup: RollupConfig, +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/test_case.rs b/bin/citrea/tests/bitcoin_e2e/config/test_case.rs new file mode 100644 index 000000000..aabf808f1 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/test_case.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; +use std::time::Duration; + +use tempfile::TempDir; + +#[derive(Clone)] +pub struct TestCaseConfig { + pub num_nodes: usize, + pub with_sequencer: bool, + pub with_full_node: bool, + pub with_prover: bool, + #[allow(unused)] + pub timeout: Duration, + pub dir: PathBuf, + pub docker: bool, + // Either a relative dir from workspace root, i.e. "./resources/genesis/devnet" + // Or an absolute path. + // Defaults to resources/genesis/bitcoin-regtest + pub genesis_dir: Option, +} + +impl Default for TestCaseConfig { + fn default() -> Self { + TestCaseConfig { + num_nodes: 1, + with_sequencer: true, + with_prover: false, + with_full_node: false, + timeout: Duration::from_secs(60), + dir: TempDir::new() + .expect("Failed to create temporary directory") + .into_path(), + docker: true, + genesis_dir: None, + } + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/config/utils.rs b/bin/citrea/tests/bitcoin_e2e/config/utils.rs new file mode 100644 index 000000000..49beb1563 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/config/utils.rs @@ -0,0 +1,14 @@ +use std::path::Path; + +use serde::Serialize; + +pub fn config_to_file(config: &C, path: &P) -> std::io::Result<()> +where + C: Serialize, + P: AsRef, +{ + let toml = + toml::to_string(config).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + std::fs::write(path, toml)?; + Ok(()) +} diff --git a/bin/citrea/tests/bitcoin_e2e/docker.rs b/bin/citrea/tests/bitcoin_e2e/docker.rs new file mode 100644 index 000000000..d40ed748a --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/docker.rs @@ -0,0 +1,185 @@ +use std::collections::HashMap; +use std::io::{stdout, Write}; + +use anyhow::{anyhow, Context, Result}; +use bollard::container::{Config, NetworkingConfig}; +use bollard::image::CreateImageOptions; +use bollard::models::{EndpointSettings, PortBinding}; +use bollard::network::CreateNetworkOptions; +use bollard::service::HostConfig; +use bollard::Docker; +use futures::StreamExt; + +use super::config::DockerConfig; +use super::node::SpawnOutput; +use super::utils::generate_test_id; +use crate::bitcoin_e2e::node::ContainerSpawnOutput; + +pub struct DockerEnv { + pub docker: Docker, + pub network_id: String, + pub network_name: String, +} + +impl DockerEnv { + pub async fn new() -> Result { + let docker = + Docker::connect_with_local_defaults().context("Failed to connect to Docker")?; + let test_id = generate_test_id(); + let (network_id, network_name) = Self::create_network(&docker, &test_id).await?; + Ok(Self { + docker, + network_id, + network_name, + }) + } + + async fn create_network(docker: &Docker, test_case_id: &str) -> Result<(String, String)> { + let network_name = format!("test_network_{}", test_case_id); + let options = CreateNetworkOptions { + name: network_name.clone(), + check_duplicate: true, + driver: "bridge".to_string(), + ..Default::default() + }; + + let id = docker + .create_network(options) + .await? + .id + .context("Error getting network id")?; + Ok((id, network_name)) + } + + pub async fn spawn(&self, config: DockerConfig) -> Result { + let exposed_ports: HashMap> = config + .ports + .iter() + .map(|port| (format!("{}/tcp", port), HashMap::new())) + .collect(); + + let port_bindings: HashMap>> = config + .ports + .iter() + .map(|port| { + ( + format!("{}/tcp", port), + Some(vec![PortBinding { + host_ip: Some("0.0.0.0".to_string()), + host_port: Some(port.to_string()), + }]), + ) + }) + .collect(); + + let mut network_config = HashMap::new(); + network_config.insert(self.network_id.clone(), EndpointSettings::default()); + + let config = Config { + image: Some(config.image), + cmd: Some(config.cmd), + exposed_ports: Some(exposed_ports), + host_config: Some(HostConfig { + port_bindings: Some(port_bindings), + binds: Some(vec![config.dir]), + ..Default::default() + }), + networking_config: Some(NetworkingConfig { + endpoints_config: network_config, + }), + tty: Some(true), + ..Default::default() + }; + + let image = config + .image + .as_ref() + .context("Image not specified in config")?; + self.ensure_image_exists(image).await?; + + // println!("options :{options:?}"); + // println!("config :{config:?}"); + + let container = self + .docker + .create_container::(None, config) + .await + .map_err(|e| anyhow!("Failed to create Docker container {e}"))?; + + self.docker + .start_container::(&container.id, None) + .await + .context("Failed to start Docker container")?; + + let inspect_result = self.docker.inspect_container(&container.id, None).await?; + let ip_address = inspect_result + .network_settings + .and_then(|ns| ns.networks) + .and_then(|networks| { + networks + .values() + .next() + .and_then(|network| network.ip_address.clone()) + }) + .context("Failed to get container IP address")?; + + Ok(SpawnOutput::Container(ContainerSpawnOutput { + id: container.id, + ip: ip_address, + })) + } + + async fn ensure_image_exists(&self, image: &str) -> Result<()> { + let images = self + .docker + .list_images::(None) + .await + .context("Failed to list Docker images")?; + if images + .iter() + .any(|img| img.repo_tags.contains(&image.to_string())) + { + return Ok(()); + } + + println!("Pulling image: {}", image); + let options = Some(CreateImageOptions { + from_image: image, + ..Default::default() + }); + + let mut stream = self.docker.create_image(options, None, None); + while let Some(result) = stream.next().await { + match result { + Ok(info) => { + if let (Some(status), Some(progress)) = (info.status, info.progress) { + print!("\r{}: {} ", status, progress); + stdout().flush().unwrap(); + } + } + Err(e) => return Err(anyhow::anyhow!("Failed to pull image: {}", e)), + } + } + println!("Image succesfully pulled"); + + Ok(()) + } + + pub async fn cleanup(&self) -> Result<()> { + let containers = self.docker.list_containers::(None).await?; + for container in containers { + if let (Some(id), Some(networks)) = ( + container.id, + container.network_settings.and_then(|ns| ns.networks), + ) { + if networks.contains_key(&self.network_name) { + self.docker.stop_container(&id, None).await?; + self.docker.remove_container(&id, None).await?; + } + } + } + + self.docker.remove_network(&self.network_name).await?; + Ok(()) + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/framework.rs b/bin/citrea/tests/bitcoin_e2e/framework.rs new file mode 100644 index 000000000..d6849e800 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/framework.rs @@ -0,0 +1,133 @@ +use std::future::Future; + +use super::bitcoin::BitcoinNodeCluster; +use super::config::TestConfig; +use super::docker::DockerEnv; +use super::full_node::FullNode; +use super::node::Node; +use super::sequencer::Sequencer; +use super::Result; +use crate::bitcoin_e2e::prover::Prover; +use crate::bitcoin_e2e::utils::get_stdout_path; + +pub struct TestContext { + pub config: TestConfig, + pub docker: Option, +} + +pub struct TestFramework { + ctx: TestContext, + pub bitcoin_nodes: BitcoinNodeCluster, + pub sequencer: Option, + pub prover: Option, + pub full_node: Option, + show_logs: bool, +} + +async fn create_optional(pred: bool, f: impl Future>) -> Result> { + if pred { + Ok(Some(f.await?)) + } else { + Ok(None) + } +} + +impl TestFramework { + pub async fn new(config: TestConfig) -> Result { + anyhow::ensure!( + config.test_case.num_nodes > 0, + "At least one bitcoin node has to be running" + ); + + let docker = if config.test_case.docker { + Some(DockerEnv::new().await?) + } else { + None + }; + + let ctx = TestContext { config, docker }; + + let bitcoin_nodes = BitcoinNodeCluster::new(&ctx).await?; + + let sequencer = + create_optional(ctx.config.test_case.with_sequencer, Sequencer::new(&ctx)).await?; + + let (prover, full_node) = tokio::try_join!( + create_optional(ctx.config.test_case.with_prover, Prover::new(&ctx)), + create_optional(ctx.config.test_case.with_full_node, FullNode::new(&ctx)), + )?; + + Ok(Self { + bitcoin_nodes, + sequencer, + prover, + full_node, + ctx, + show_logs: true, + }) + } + + pub async fn stop(&mut self) -> Result<()> { + println!("Stopping framework..."); + + if let Some(docker) = &self.ctx.docker { + let _ = docker.cleanup().await; + println!("Successfully cleaned docker"); + } + + if let Some(sequencer) = &mut self.sequencer { + let _ = sequencer.stop().await; + println!("Successfully stopped sequencer"); + } + + if let Some(prover) = &mut self.prover { + let _ = prover.stop().await; + println!("Successfully stopped prover"); + } + + if let Some(full_node) = &mut self.full_node { + let _ = full_node.stop().await; + println!("Successfully stopped full_node"); + } + + let _ = self.bitcoin_nodes.stop_all().await; + println!("Successfully stopped bitcoin nodes"); + + if self.show_logs { + println!( + "Logs available at {}", + self.ctx.config.test_case.dir.display() + ); + + if let Some(bitcoin_node) = self.bitcoin_nodes.get(0) { + println!( + "Bitcoin logs available at : {}", + bitcoin_node.get_log_path().display() + ); + } + + if let Some(sequencer) = &self.sequencer { + println!( + "Sequencer logs available at {}", + get_stdout_path(sequencer.dir()).display() + ); + } + + if let Some(full_node) = &self.full_node { + println!( + "Full node logs available at {}", + get_stdout_path(&full_node.dir).display() + ); + } + + if let Some(prover) = &self.prover { + println!( + "Prover logs available at {}", + get_stdout_path(&prover.dir).display() + ); + } + } + + Ok(()) + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/full_node.rs b/bin/citrea/tests/bitcoin_e2e/full_node.rs new file mode 100644 index 000000000..e4d6d2287 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/full_node.rs @@ -0,0 +1,116 @@ +use std::fs::File; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; + +use anyhow::{bail, Context}; +use tokio::process::Command; +use tokio::time::{sleep, Duration, Instant}; + +use super::config::{config_to_file, TestConfig}; +use super::framework::TestContext; +use super::node::{L2Node, Node, SpawnOutput}; +use super::utils::{get_citrea_path, get_stderr_path, get_stdout_path, retry}; +use super::Result; +use crate::bitcoin_e2e::config::RollupConfig; +use crate::bitcoin_e2e::utils::get_genesis_path; +use crate::evm::make_test_client; +use crate::test_client::TestClient; + +#[allow(unused)] +pub struct FullNode { + spawn_output: SpawnOutput, + config: RollupConfig, + pub dir: PathBuf, + pub client: Box, +} + +impl FullNode { + pub async fn new(ctx: &TestContext) -> Result { + let TestConfig { + test_case, + full_node_rollup: rollup_config, + .. + } = &ctx.config; + + let dir = test_case.dir.join("full-node"); + + let spawn_output = Self::spawn(rollup_config, &dir).await?; + + let socket_addr = SocketAddr::new( + rollup_config + .rpc + .bind_host + .parse() + .context("Failed to parse bind host")?, + rollup_config.rpc.bind_port, + ); + let client = retry(|| async { make_test_client(socket_addr).await }, None).await?; + + Ok(Self { + spawn_output, + config: rollup_config.clone(), + dir, + client, + }) + } +} + +impl Node for FullNode { + type Config = RollupConfig; + type Client = TestClient; + + async fn spawn(config: &Self::Config, dir: &Path) -> Result { + let citrea = get_citrea_path(); + + let stdout_file = + File::create(get_stdout_path(dir)).context("Failed to create stdout file")?; + let stderr_file = + File::create(get_stderr_path(dir)).context("Failed to create stderr file")?; + + let rollup_config_path = dir.join("full_node_rollup_config.toml"); + config_to_file(&config, &rollup_config_path)?; + + Command::new(citrea) + .arg("--da-layer") + .arg("bitcoin") + .arg("--rollup-config-path") + .arg(rollup_config_path) + .arg("--genesis-paths") + .arg(get_genesis_path( + dir.parent().expect("Couldn't get parent dir"), + )) + .stdout(Stdio::from(stdout_file)) + .stderr(Stdio::from(stderr_file)) + .kill_on_drop(true) + .spawn() + .context("Failed to spawn citrea process") + .map(SpawnOutput::Child) + } + + fn spawn_output(&mut self) -> &mut SpawnOutput { + &mut self.spawn_output + } + + async fn wait_for_ready(&self, timeout: Duration) -> Result<()> { + let start = Instant::now(); + while start.elapsed() < timeout { + if self + .client + .ledger_get_head_soft_confirmation() + .await + .is_ok() + { + return Ok(()); + } + sleep(Duration::from_millis(500)).await; + } + bail!("FullNode failed to become ready within the specified timeout") + } + + fn client(&self) -> &Self::Client { + &self.client + } +} + +impl L2Node for FullNode {} diff --git a/bin/citrea/tests/bitcoin_e2e/mod.rs b/bin/citrea/tests/bitcoin_e2e/mod.rs new file mode 100644 index 000000000..9cf7f4d7b --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/mod.rs @@ -0,0 +1,15 @@ +mod bitcoin; +pub mod config; +mod docker; +pub mod framework; +mod full_node; +pub mod node; +mod prover; +mod sequencer; +pub mod test_case; + +mod tests; + +mod utils; + +pub(crate) type Result = anyhow::Result; diff --git a/bin/citrea/tests/bitcoin_e2e/node.rs b/bin/citrea/tests/bitcoin_e2e/node.rs new file mode 100644 index 000000000..fd97d89ca --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/node.rs @@ -0,0 +1,91 @@ +use std::fmt; +use std::path::Path; +use std::time::Duration; + +use anyhow::Context; +use bollard::container::StopContainerOptions; +use bollard::Docker; +use tokio::process::Child; + +use super::Result; +use crate::test_client::TestClient; +use crate::test_helpers::wait_for_l2_block; + +#[derive(Debug)] +pub enum NodeKind { + Bitcoin, + Prover, + Sequencer, + FullNode, +} + +impl fmt::Display for NodeKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NodeKind::Bitcoin => write!(f, "bitcoin"), + NodeKind::Prover => write!(f, "prover"), + NodeKind::Sequencer => write!(f, "sequencer"), + NodeKind::FullNode => write!(f, "full-node"), + } + } +} + +#[derive(Debug)] +pub struct ContainerSpawnOutput { + pub id: String, + pub ip: String, +} + +#[derive(Debug)] +pub enum SpawnOutput { + Child(Child), + Container(ContainerSpawnOutput), +} +/// The Node trait defines the common interface shared between +/// BitcoinNode, Prover, Sequencer and FullNode +pub(crate) trait Node { + type Config; + type Client; + + /// Spawn a new node with specific config and return its child + async fn spawn(test_config: &Self::Config, node_dir: &Path) -> Result; + fn spawn_output(&mut self) -> &mut SpawnOutput; + + /// Stops the running node + async fn stop(&mut self) -> Result<()> { + match self.spawn_output() { + SpawnOutput::Child(process) => { + process + .kill() + .await + .context("Failed to kill child process")?; + Ok(()) + } + SpawnOutput::Container(ContainerSpawnOutput { id, .. }) => { + println!("Removing container {id}"); + let docker = + Docker::connect_with_local_defaults().context("Failed to connect to Docker")?; + docker + .stop_container(id, Some(StopContainerOptions { t: 10 })) + .await + .context("Failed to stop Docker container")?; + docker + .remove_container(id, None) + .await + .context("Failed to remove Docker container")?; + Ok(()) + } + } + } + + /// Wait for the node to be reachable by its client. + async fn wait_for_ready(&self, timeout: Duration) -> Result<()>; + + fn client(&self) -> &Self::Client; +} + +pub trait L2Node: Node { + async fn wait_for_l2_height(&self, height: u64, timeout: Option) { + wait_for_l2_block(self.client(), height, timeout).await + } +} diff --git a/bin/citrea/tests/bitcoin_e2e/prover.rs b/bin/citrea/tests/bitcoin_e2e/prover.rs new file mode 100644 index 000000000..d1af6339b --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/prover.rs @@ -0,0 +1,131 @@ +use std::fs::File; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; + +use anyhow::Context; +use tokio::process::Command; +use tokio::time::{sleep, Duration, Instant}; + +use super::config::{config_to_file, RollupConfig, TestConfig}; +use super::framework::TestContext; +use super::node::{L2Node, Node, SpawnOutput}; +use super::utils::{get_citrea_path, get_stderr_path, get_stdout_path, retry}; +use super::Result; +use crate::bitcoin_e2e::config::ProverConfig; +use crate::bitcoin_e2e::utils::get_genesis_path; +use crate::evm::make_test_client; +use crate::test_client::TestClient; +use crate::test_helpers::wait_for_prover_l1_height; + +#[allow(unused)] +pub struct Prover { + spawn_output: SpawnOutput, + config: ProverConfig, + rollup_config: RollupConfig, + pub dir: PathBuf, + pub client: Box, +} + +impl Prover { + pub async fn new(ctx: &TestContext) -> Result { + let TestConfig { + prover: prover_config, + test_case, + prover_rollup: rollup_config, + .. + } = &ctx.config; + + let dir = test_case.dir.join("prover"); + + let spawn_output = + Self::spawn(&(prover_config.clone(), rollup_config.clone()), &dir).await?; + + let socket_addr = SocketAddr::new( + rollup_config + .rpc + .bind_host + .parse() + .context("Failed to parse bind host")?, + rollup_config.rpc.bind_port, + ); + let client = retry(|| async { make_test_client(socket_addr).await }, None).await?; + + Ok(Self { + spawn_output, + config: prover_config.to_owned(), + dir, + rollup_config: rollup_config.to_owned(), + client, + }) + } + + pub async fn wait_for_l1_height(&self, height: u64, timeout: Option) { + wait_for_prover_l1_height(&self.client, height, timeout).await + } +} + +impl Node for Prover { + type Config = (ProverConfig, RollupConfig); + type Client = TestClient; + + async fn spawn(config: &Self::Config, dir: &Path) -> Result { + let citrea = get_citrea_path(); + + let stdout_file = + File::create(get_stdout_path(dir)).context("Failed to create stdout file")?; + let stderr_file = + File::create(get_stderr_path(dir)).context("Failed to create stderr file")?; + + let (prover_config, rollup_config) = config; + let config_path = dir.join("prover_config.toml"); + config_to_file(&prover_config, &config_path)?; + + let rollup_config_path = dir.join("prover_rollup_config.toml"); + config_to_file(&rollup_config, &rollup_config_path)?; + + Command::new(citrea) + .arg("--da-layer") + .arg("bitcoin") + .arg("--rollup-config-path") + .arg(rollup_config_path) + .arg("--prover-config-path") + .arg(config_path) + .arg("--genesis-paths") + .arg(get_genesis_path( + dir.parent().expect("Couldn't get parent dir"), + )) + .stdout(Stdio::from(stdout_file)) + .stderr(Stdio::from(stderr_file)) + .kill_on_drop(true) + .spawn() + .context("Failed to spawn citrea process") + .map(SpawnOutput::Child) + } + + fn spawn_output(&mut self) -> &mut SpawnOutput { + &mut self.spawn_output + } + + async fn wait_for_ready(&self, timeout: Duration) -> Result<()> { + let start = Instant::now(); + while start.elapsed() < timeout { + if self + .client + .ledger_get_head_soft_confirmation() + .await + .is_ok() + { + return Ok(()); + } + sleep(Duration::from_millis(500)).await; + } + anyhow::bail!("Prover failed to become ready within the specified timeout") + } + + fn client(&self) -> &Self::Client { + &self.client + } +} + +impl L2Node for Prover {} diff --git a/bin/citrea/tests/bitcoin_e2e/sequencer.rs b/bin/citrea/tests/bitcoin_e2e/sequencer.rs new file mode 100644 index 000000000..555139e1e --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/sequencer.rs @@ -0,0 +1,119 @@ +use std::fs::File; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::process::Stdio; + +use anyhow::Context; +use tokio::process::Command; +use tokio::time::{sleep, Duration, Instant}; + +use super::config::{config_to_file, FullSequencerConfig, TestConfig}; +use super::framework::TestContext; +use super::node::{L2Node, Node, SpawnOutput}; +use super::utils::{get_citrea_path, get_stderr_path, get_stdout_path, retry}; +use super::Result; +use crate::bitcoin_e2e::utils::get_genesis_path; +use crate::evm::make_test_client; +use crate::test_client::TestClient; + +#[allow(unused)] +pub struct Sequencer { + spawn_output: SpawnOutput, + config: FullSequencerConfig, + pub client: Box, +} + +impl Sequencer { + pub async fn new(ctx: &TestContext) -> Result { + let TestConfig { + sequencer: config, .. + } = &ctx.config; + + let spawn_output = Self::spawn(config, &config.dir).await?; + + let socket_addr = SocketAddr::new( + config + .rollup + .rpc + .bind_host + .parse() + .context("Failed to parse bind host")?, + config.rollup.rpc.bind_port, + ); + + let client = retry(|| async { make_test_client(socket_addr).await }, None).await?; + + Ok(Self { + spawn_output, + config: config.clone(), + client, + }) + } + + pub fn dir(&self) -> &PathBuf { + &self.config.dir + } +} + +impl Node for Sequencer { + type Config = FullSequencerConfig; + type Client = TestClient; + + async fn spawn(config: &Self::Config, dir: &Path) -> Result { + let citrea = get_citrea_path(); + + let stdout_file = + File::create(get_stdout_path(dir)).context("Failed to create stdout file")?; + let stderr_file = + File::create(get_stderr_path(dir)).context("Failed to create stderr file")?; + + let config_path = dir.join("sequencer_config.toml"); + config_to_file(&config.sequencer, &config_path)?; + + let rollup_config_path = dir.join("sequencer_rollup_config.toml"); + config_to_file(&config.rollup, &rollup_config_path)?; + + Command::new(citrea) + .arg("--da-layer") + .arg("bitcoin") + .arg("--rollup-config-path") + .arg(rollup_config_path) + .arg("--sequencer-config-path") + .arg(config_path) + .arg("--genesis-paths") + .arg(get_genesis_path( + dir.parent().expect("Couldn't get parent dir"), + )) + .stdout(Stdio::from(stdout_file)) + .stderr(Stdio::from(stderr_file)) + .kill_on_drop(true) + .spawn() + .context("Failed to spawn citrea process") + .map(SpawnOutput::Child) + } + + fn spawn_output(&mut self) -> &mut SpawnOutput { + &mut self.spawn_output + } + + async fn wait_for_ready(&self, timeout: Duration) -> Result<()> { + let start = Instant::now(); + while start.elapsed() < timeout { + if self + .client + .ledger_get_head_soft_confirmation() + .await + .is_ok() + { + return Ok(()); + } + sleep(Duration::from_millis(500)).await; + } + anyhow::bail!("Sequencer failed to become ready within the specified timeout") + } + fn client(&self) -> &Self::Client { + &self.client + } +} + +impl L2Node for Sequencer {} diff --git a/bin/citrea/tests/bitcoin_e2e/test_case.rs b/bin/citrea/tests/bitcoin_e2e/test_case.rs new file mode 100644 index 000000000..ff63c1c5c --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/test_case.rs @@ -0,0 +1,300 @@ +//! This module provides the TestCaseRunner and TestCase trait for running and defining test cases. +//! It handles setup, execution, and cleanup of test environments. + +use std::panic::{self}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use bitcoin_da::service::BitcoinServiceConfig; +use citrea_sequencer::SequencerConfig; +use sov_stf_runner::{ProverConfig, RpcConfig, RunnerConfig, StorageConfig}; +use tokio::task; + +use super::config::{ + default_rollup_config, BitcoinConfig, FullSequencerConfig, RollupConfig, TestCaseConfig, + TestConfig, +}; +use super::framework::TestFramework; +use super::node::NodeKind; +use super::utils::{copy_directory, get_available_port}; +use super::Result; +use crate::bitcoin_e2e::node::Node; +use crate::bitcoin_e2e::utils::{get_default_genesis_path, get_workspace_root}; + +// TestCaseRunner manages the lifecycle of a test case, including setup, execution, and cleanup. +/// It creates a test framework with the associated configs, spawns required nodes, connects them, +/// runs the test case, and performs cleanup afterwards. The `run` method handles any panics that +/// might occur during test execution and takes care of cleaning up and stopping the child processes. +pub struct TestCaseRunner(Arc); + +impl TestCaseRunner { + /// Creates a new TestCaseRunner with the given test case. + pub fn new(test_case: T) -> Self { + Self(Arc::new(test_case)) + } + + pub async fn setup(&self, f: &TestFramework) -> Result<()> { + let bitcoin_node = f.bitcoin_nodes.get(0).unwrap(); + if f.sequencer.is_some() { + bitcoin_node + .fund_wallet(NodeKind::Sequencer.to_string()) + .await?; + } + + if f.prover.is_some() { + bitcoin_node + .fund_wallet(NodeKind::Prover.to_string()) + .await?; + } + Ok(()) + } + + /// Internal method to set up connect the nodes, wait for the nodes to be ready and run the test. + async fn run_test_case(&self, f: &mut TestFramework) -> Result<()> { + f.bitcoin_nodes.connect_nodes().await?; + + if let Some(sequencer) = &f.sequencer { + sequencer.wait_for_ready(Duration::from_secs(5)).await?; + } + + self.0.run_test(f).await + } + + /// Executes the test case, handling any panics and performing cleanup. + /// + /// This method spawns a blocking task to run the test, sets up the framework, + /// executes the test, and ensures cleanup is performed even if a panic occurs. + pub async fn run(self) -> Result<()> { + let result = task::spawn_blocking(move || { + let mut framework = None; + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + futures::executor::block_on(async { + framework = Some(TestFramework::new(Self::generate_test_config()?).await?); + self.setup(framework.as_ref().unwrap()).await?; + self.run_test_case(framework.as_mut().unwrap()).await + }) + })); + + // Always attempt to stop the framework, even if a panic occurred + if let Some(mut f) = framework { + let _ = futures::executor::block_on(f.stop()); + } + result + }) + .await + .expect("Task panicked"); + + match result { + Ok(Ok(())) => Ok(()), + Ok(Err(e)) => Err(e), + Err(panic_error) => { + let panic_msg = panic_error + .downcast_ref::() + .map(|s| s.to_string()) + .unwrap_or_else(|| "Unknown panic".to_string()); + Err(anyhow!("Test panicked: {}", panic_msg)) + } + } + } + + fn generate_test_config() -> Result { + let test_case = T::test_config(); + let bitcoin = T::bitcoin_config(); + let prover = T::prover_config(); + let sequencer = T::sequencer_config(); + let sequencer_rollup = default_rollup_config(); + let prover_rollup = default_rollup_config(); + let full_node_rollup = default_rollup_config(); + + let [bitcoin_dir, dbs_dir, _prover_dir, sequencer_dir, _full_node_dir, genesis_dir] = + create_dirs(&test_case.dir)?; + + copy_genesis_dir(&test_case.genesis_dir, &genesis_dir)?; + + let mut bitcoin_confs = vec![]; + for i in 0..test_case.num_nodes { + let data_dir = bitcoin_dir.join(i.to_string()); + std::fs::create_dir_all(&data_dir) + .with_context(|| format!("Failed to create {} directory", data_dir.display()))?; + + let p2p_port = get_available_port()?; + let rpc_port = get_available_port()?; + + bitcoin_confs.push(BitcoinConfig { + p2p_port, + rpc_port, + data_dir, + ..bitcoin.clone() + }) + } + + // Target first bitcoin node as DA for now + let da_config: BitcoinServiceConfig = bitcoin_confs[0].clone().into(); + + let sequencer_rollup = { + let bind_port = get_available_port()?; + let node_kind = NodeKind::Sequencer.to_string(); + RollupConfig { + da: BitcoinServiceConfig { + da_private_key: Some( + "045FFC81A3C1FDB3AF1359DBF2D114B0B3EFBF7F29CC9C5DA01267AA39D2C78D" + .to_string(), + ), + node_url: format!("{}/wallet/{}", da_config.node_url, node_kind), + ..da_config.clone() + }, + storage: StorageConfig { + path: dbs_dir.join(format!("{}-db", node_kind)), + }, + rpc: RpcConfig { + bind_port, + ..sequencer_rollup.rpc + }, + ..sequencer_rollup + } + }; + + let runner_config = Some(RunnerConfig { + sequencer_client_url: format!( + "http://{}:{}", + sequencer_rollup.rpc.bind_host, sequencer_rollup.rpc.bind_port + ), + include_tx_body: true, + accept_public_input_as_proven: None, + }); + + let prover_rollup = { + let bind_port = get_available_port()?; + let node_kind = NodeKind::Prover.to_string(); + RollupConfig { + da: BitcoinServiceConfig { + da_private_key: Some( + "75BAF964D074594600366E5B111A1DA8F86B2EFE2D22DA51C8D82126A0FCAC72" + .to_string(), + ), + node_url: format!("{}/wallet/{}", da_config.node_url, node_kind), + ..da_config.clone() + }, + storage: StorageConfig { + path: dbs_dir.join(format!("{}-db", node_kind)), + }, + rpc: RpcConfig { + bind_port, + ..prover_rollup.rpc + }, + runner: runner_config.clone(), + ..prover_rollup + } + }; + + let full_node_rollup = { + let bind_port = get_available_port()?; + let node_kind = NodeKind::FullNode.to_string(); + RollupConfig { + da: da_config.clone(), + storage: StorageConfig { + path: dbs_dir.join(format!("{}-db", node_kind)), + }, + rpc: RpcConfig { + bind_port, + ..full_node_rollup.rpc + }, + runner: runner_config.clone(), + ..full_node_rollup + } + }; + + Ok(TestConfig { + bitcoin: bitcoin_confs, + sequencer: FullSequencerConfig { + rollup: sequencer_rollup, + dir: sequencer_dir, + docker_image: None, + sequencer, + }, + prover, + prover_rollup, + full_node_rollup, + test_case, + }) + } +} + +/// Defines the interface for implementing test cases. +/// +/// This trait should be implemented by every test case to define the configuration +/// and inner test logic. It provides default configurations that should be sane for most test cases, +/// which can be overridden by implementing the associated methods. +#[async_trait] +pub trait TestCase: Send + Sync + 'static { + /// Returns the test case configuration. + /// Override this method to provide custom test configurations. + fn test_config() -> TestCaseConfig { + TestCaseConfig::default() + } + + /// Returns the Bitcoin configuration for the test. + /// Override this method to provide a custom Bitcoin configuration. + fn bitcoin_config() -> BitcoinConfig { + BitcoinConfig::default() + } + + /// Returns the sequencer configuration for the test. + /// Override this method to provide a custom sequencer configuration. + fn sequencer_config() -> SequencerConfig { + SequencerConfig::default() + } + + /// Returns the prover configuration for the test. + /// Override this method to provide a custom prover configuration. + fn prover_config() -> ProverConfig { + ProverConfig::default() + } + + /// Implements the actual test logic. + /// + /// This method is where the test case should be implemented. It receives + /// a reference to the TestFramework, which provides access to the test environment. + /// + /// # Arguments + /// * `framework` - A reference to the TestFramework instance + async fn run_test(&self, framework: &TestFramework) -> Result<()>; +} + +fn create_dirs(base_dir: &Path) -> Result<[PathBuf; 6]> { + let paths = [ + NodeKind::Bitcoin.to_string(), + "dbs".to_string(), + NodeKind::Prover.to_string(), + NodeKind::Sequencer.to_string(), + NodeKind::FullNode.to_string(), + "genesis".to_string(), + ] + .map(|dir| base_dir.join(dir)); + + for path in &paths { + std::fs::create_dir_all(path) + .with_context(|| format!("Failed to create {} directory", path.display()))?; + } + + Ok(paths) +} + +fn copy_genesis_dir(genesis_dir: &Option, target_dir: &Path) -> std::io::Result<()> { + let genesis_dir = + genesis_dir + .as_ref() + .map(PathBuf::from) + .map_or_else(get_default_genesis_path, |dir| { + if dir.is_absolute() { + dir + } else { + get_workspace_root().join(dir) + } + }); + + copy_directory(genesis_dir, target_dir) +} diff --git a/bin/citrea/tests/bitcoin_e2e/tests/mod.rs b/bin/citrea/tests/bitcoin_e2e/tests/mod.rs new file mode 100644 index 000000000..450081ef5 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/tests/mod.rs @@ -0,0 +1,3 @@ +pub mod prover_test; +pub mod sequencer_test; +pub mod sync_test; diff --git a/bin/citrea/tests/bitcoin_e2e/tests/prover_test.rs b/bin/citrea/tests/bitcoin_e2e/tests/prover_test.rs new file mode 100644 index 000000000..239083af1 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/tests/prover_test.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +use anyhow::bail; +use async_trait::async_trait; +use bitcoin_da::service::FINALITY_DEPTH; +use bitcoincore_rpc::RpcApi; + +use crate::bitcoin_e2e::config::{ProverConfig, SequencerConfig, TestCaseConfig}; +use crate::bitcoin_e2e::framework::TestFramework; +use crate::bitcoin_e2e::test_case::{TestCase, TestCaseRunner}; +use crate::bitcoin_e2e::Result; + +/// This is a basic prover test showcasing spawning a bitcoin node as DA, a sequencer and a prover. +/// It generates soft confirmations and wait until it reaches the first commitment. +/// It asserts that the blob inscribe txs have been sent. +/// This catches regression to the default prover flow, such as the one introduced by [#942](https://github.com/chainwayxyz/citrea/pull/942) and [#973](https://github.com/chainwayxyz/citrea/pull/973) +struct BasicProverTest; + +#[async_trait] +impl TestCase for BasicProverTest { + fn test_config() -> TestCaseConfig { + TestCaseConfig { + with_prover: true, + ..Default::default() + } + } + + fn prover_config() -> ProverConfig { + ProverConfig { + proof_sampling_number: 0, + ..Default::default() + } + } + + fn sequencer_config() -> SequencerConfig { + SequencerConfig { + min_soft_confirmations_per_commitment: 10, + test_mode: true, + ..Default::default() + } + } + + async fn run_test(&self, f: &TestFramework) -> Result<()> { + let Some(sequencer) = &f.sequencer else { + bail!("Sequencer not running. Set TestCaseConfig with_sequencer to true") + }; + + let Some(prover) = &f.prover else { + bail!("Prover not running. Set TestCaseConfig with_prover to true") + }; + + let Some(da) = f.bitcoin_nodes.get(0) else { + bail!("bitcoind not running. Test cannot run with bitcoind running as DA") + }; + + // Generate confirmed UTXOs + da.generate(120, None).await?; + + let seq_height0 = sequencer.client.eth_block_number().await; + assert_eq!(seq_height0, 0); + + for _ in 0..10 { + sequencer.client.send_publish_batch_request().await; + } + + da.generate(5, None).await?; + + // Wait for blob inscribe tx to be in mempool + da.wait_mempool_len(1, None).await?; + + da.generate(5, None).await?; + let height = da.get_block_count().await?; + prover + .wait_for_l1_height(height - FINALITY_DEPTH, Some(Duration::from_secs(600))) + .await; + + Ok(()) + } +} + +#[tokio::test] +async fn basic_prover_test() -> Result<()> { + TestCaseRunner::new(BasicProverTest).run().await +} diff --git a/bin/citrea/tests/bitcoin_e2e/tests/sequencer_test.rs b/bin/citrea/tests/bitcoin_e2e/tests/sequencer_test.rs new file mode 100644 index 000000000..eb15693e2 --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/tests/sequencer_test.rs @@ -0,0 +1,40 @@ +use anyhow::bail; +use async_trait::async_trait; +use bitcoincore_rpc::RpcApi; + +use crate::bitcoin_e2e::framework::TestFramework; +use crate::bitcoin_e2e::node::L2Node; +use crate::bitcoin_e2e::test_case::{TestCase, TestCaseRunner}; +use crate::bitcoin_e2e::Result; + +struct BasicSequencerTest; + +#[async_trait] +impl TestCase for BasicSequencerTest { + async fn run_test(&self, f: &TestFramework) -> Result<()> { + let Some(sequencer) = &f.sequencer else { + anyhow::bail!("Sequencer not running. Set TestCaseConfig with_sequencer to true") + }; + + let Some(da) = f.bitcoin_nodes.get(0) else { + bail!("bitcoind not running. Test cannot run with bitcoind runnign as DA") + }; + + let seq_height0 = sequencer.client.eth_block_number().await; + assert_eq!(seq_height0, 0); + + sequencer.client.send_publish_batch_request().await; + da.generate(1, None).await?; + + sequencer.wait_for_l2_height(1, None).await; + let seq_height1 = sequencer.client.eth_block_number().await; + assert_eq!(seq_height1, 1); + + Ok(()) + } +} + +#[tokio::test] +async fn basic_sequencer_test() -> Result<()> { + TestCaseRunner::new(BasicSequencerTest).run().await +} diff --git a/bin/citrea/tests/bitcoin_e2e/tests/sync_test.rs b/bin/citrea/tests/bitcoin_e2e/tests/sync_test.rs new file mode 100644 index 000000000..e0a20771f --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/tests/sync_test.rs @@ -0,0 +1,57 @@ +use std::time::Duration; + +use anyhow::bail; +use async_trait::async_trait; +use bitcoincore_rpc::RpcApi; + +use crate::bitcoin_e2e::config::TestCaseConfig; +use crate::bitcoin_e2e::framework::TestFramework; +use crate::bitcoin_e2e::test_case::{TestCase, TestCaseRunner}; +use crate::bitcoin_e2e::Result; + +struct BasicSyncTest; + +#[async_trait] +impl TestCase for BasicSyncTest { + fn test_config() -> TestCaseConfig { + TestCaseConfig { + num_nodes: 2, + timeout: Duration::from_secs(60), + ..Default::default() + } + } + + async fn run_test(&self, f: &TestFramework) -> Result<()> { + let (Some(da0), Some(da1)) = (f.bitcoin_nodes.get(0), f.bitcoin_nodes.get(1)) else { + bail!("bitcoind not running. Test should run with two da nodes") + }; + let initial_height = da0.get_block_count().await?; + + // Generate some blocks on node0 + da0.generate(5, None).await?; + + let height0 = da0.get_block_count().await?; + let height1 = da1.get_block_count().await?; + + // Nodes are now out of sync + assert_eq!(height0, initial_height + 5); + assert_eq!(height1, 0); + + // Sync both nodes + f.bitcoin_nodes + .wait_for_sync(Duration::from_secs(30)) + .await?; + + let height0 = da0.get_block_count().await?; + let height1 = da1.get_block_count().await?; + + // Assert that nodes are in sync + assert_eq!(height0, height1, "Block heights don't match"); + Ok(()) + } +} + +#[tokio::test] +async fn basic_sync_test() -> Result<()> { + TestCaseRunner::new(BasicSyncTest).run().await +} diff --git a/bin/citrea/tests/bitcoin_e2e/utils.rs b/bin/citrea/tests/bitcoin_e2e/utils.rs new file mode 100644 index 000000000..278d13e3c --- /dev/null +++ b/bin/citrea/tests/bitcoin_e2e/utils.rs @@ -0,0 +1,118 @@ +use std::future::Future; +use std::net::TcpListener; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +use anyhow::bail; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use tokio::time::{sleep, Duration, Instant}; + +use super::Result; + +pub fn get_available_port() -> Result { + let listener = TcpListener::bind("127.0.0.1:0")?; + Ok(listener.local_addr()?.port()) +} + +pub fn get_workspace_root() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .ancestors() + .nth(2) + .expect("Failed to find workspace root") + .to_path_buf() +} + +/// Get citrea path from CITREA env or resolves to debug build. +pub fn get_citrea_path() -> PathBuf { + std::env::var("CITREA").map_or_else( + |_| { + let workspace_root = get_workspace_root(); + let mut path = workspace_root.to_path_buf(); + path.push("target"); + path.push("debug"); + path.push("citrea"); + path + }, + PathBuf::from, + ) +} + +pub fn get_stdout_path(dir: &Path) -> PathBuf { + dir.join("stdout.log") +} + +pub fn get_stderr_path(dir: &Path) -> PathBuf { + dir.join("stderr.log") +} + +/// Get genesis path from resources +/// TODO: assess need for customable genesis path in e2e tests +pub fn get_default_genesis_path() -> PathBuf { + let workspace_root = get_workspace_root(); + let mut path = workspace_root.to_path_buf(); + path.push("resources"); + path.push("genesis"); + path.push("bitcoin-regtest"); + path +} + +pub fn get_genesis_path(dir: &Path) -> PathBuf { + dir.join("genesis") +} + +pub fn generate_test_id() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect() +} + +pub fn copy_directory(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + let src = src.as_ref(); + let dst = dst.as_ref(); + + if !dst.exists() { + fs::create_dir_all(dst)?; + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let file_name = entry.file_name(); + let src_path = src.join(&file_name); + let dst_path = dst.join(&file_name); + + if ty.is_dir() { + copy_directory(&src_path, &dst_path)?; + } else { + fs::copy(&src_path, &dst_path)?; + } + } + + Ok(()) +} + +pub(crate) async fn retry(f: F, timeout: Option) -> Result +where + F: Fn() -> Fut, + Fut: Future>, +{ + let start = Instant::now(); + let timeout = start + timeout.unwrap_or_else(|| Duration::from_secs(5)); + + loop { + match tokio::time::timeout_at(timeout, f()).await { + Ok(Ok(result)) => return Ok(result), + Ok(Err(e)) => { + if Instant::now() >= timeout { + return Err(e); + } + sleep(Duration::from_millis(500)).await; + } + Err(elapsed) => bail!("Timeout expired {elapsed}"), + } + } +} diff --git a/bin/citrea/tests/e2e/mod.rs b/bin/citrea/tests/e2e/mod.rs index 67d82d638..bdd394b00 100644 --- a/bin/citrea/tests/e2e/mod.rs +++ b/bin/citrea/tests/e2e/mod.rs @@ -29,7 +29,7 @@ use crate::test_helpers::{ }; use crate::{ DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, DEFAULT_MIN_SOFT_CONFIRMATIONS_PER_COMMITMENT, - DEFAULT_PROOF_WAIT_DURATION, TEST_DATA_GENESIS_PATH, + TEST_DATA_GENESIS_PATH, }; struct TestConfig { @@ -84,7 +84,7 @@ async fn test_all_flow() { }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); let da_service = MockDaService::new(MockAddress::from([0; 32]), &da_db_dir); let (prover_node_port_tx, prover_node_port_rx) = tokio::sync::oneshot::channel(); @@ -113,7 +113,7 @@ async fn test_all_flow() { let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await.unwrap(); let (full_node_port_tx, full_node_port_rx) = tokio::sync::oneshot::channel(); @@ -139,7 +139,7 @@ async fn test_all_flow() { let addr = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92265").unwrap(); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await.unwrap(); da_service.publish_test_block().await.unwrap(); wait_for_l1_block(&da_service, 2, None).await; @@ -173,12 +173,7 @@ async fn test_all_flow() { wait_for_l1_block(&da_service, 3, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 4, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 4, None).await; let commitments = prover_node_test_client .ledger_get_sequencer_commitments_on_slot_by_number(3) @@ -289,12 +284,7 @@ async fn test_all_flow() { wait_for_l1_block(&da_service, 5, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 5, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 5, None).await; let commitments = prover_node_test_client .ledger_get_sequencer_commitments_on_slot_by_number(5) @@ -473,7 +463,7 @@ async fn initialize_test( }); let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await.unwrap(); let (full_node_port_tx, full_node_port_rx) = tokio::sync::oneshot::channel(); @@ -497,7 +487,7 @@ async fn initialize_test( }); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await.unwrap(); ( seq_test_client, diff --git a/bin/citrea/tests/e2e/proving.rs b/bin/citrea/tests/e2e/proving.rs index e6dce4965..6b28d02fc 100644 --- a/bin/citrea/tests/e2e/proving.rs +++ b/bin/citrea/tests/e2e/proving.rs @@ -12,9 +12,7 @@ use crate::test_helpers::{ start_rollup, tempdir_with_children, wait_for_l1_block, wait_for_l2_block, wait_for_proof, wait_for_prover_l1_height, NodeMode, }; -use crate::{ - DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, DEFAULT_PROOF_WAIT_DURATION, TEST_DATA_GENESIS_PATH, -}; +use crate::{DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, TEST_DATA_GENESIS_PATH}; /// Run the sequencer, prover and full node. /// Trigger proof production. @@ -51,7 +49,7 @@ async fn full_node_verify_proof_and_store() { }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); let da_service = MockDaService::new(MockAddress::from([0; 32]), &da_db_dir); @@ -81,7 +79,7 @@ async fn full_node_verify_proof_and_store() { let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await.unwrap(); let (full_node_port_tx, full_node_port_rx) = tokio::sync::oneshot::channel(); @@ -105,7 +103,7 @@ async fn full_node_verify_proof_and_store() { }); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await.unwrap(); da_service.publish_test_block().await.unwrap(); wait_for_l1_block(&da_service, 2, None).await; @@ -124,12 +122,7 @@ async fn full_node_verify_proof_and_store() { wait_for_l2_block(&full_node_test_client, 5, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 4, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 4, None).await; let commitments = prover_node_test_client .ledger_get_sequencer_commitments_on_slot_by_number(3) diff --git a/bin/citrea/tests/e2e/reopen.rs b/bin/citrea/tests/e2e/reopen.rs index 5949473b1..dd65899b3 100644 --- a/bin/citrea/tests/e2e/reopen.rs +++ b/bin/citrea/tests/e2e/reopen.rs @@ -19,7 +19,7 @@ use crate::test_helpers::{ }; use crate::{ DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, DEFAULT_MIN_SOFT_CONFIRMATIONS_PER_COMMITMENT, - DEFAULT_PROOF_WAIT_DURATION, TEST_DATA_GENESIS_PATH, + TEST_DATA_GENESIS_PATH, }; #[tokio::test(flavor = "multi_thread")] @@ -158,7 +158,7 @@ async fn test_reopen_full_node() -> Result<(), anyhow::Error> { let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await?; wait_for_l2_block(&seq_test_client, 110, None).await; wait_for_l2_block(&full_node_test_client, 110, None).await; @@ -264,7 +264,7 @@ async fn test_reopen_sequencer() -> Result<(), anyhow::Error> { let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await?; let seq_last_block = seq_test_client .eth_get_block_by_number(Some(BlockNumberOrTag::Latest)) @@ -330,7 +330,7 @@ async fn test_reopen_prover() -> Result<(), anyhow::Error> { }); let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await?; let (prover_node_port_tx, prover_node_port_rx) = tokio::sync::oneshot::channel(); let (thread_kill_sender, thread_kill_receiver) = std::sync::mpsc::channel(); @@ -363,7 +363,7 @@ async fn test_reopen_prover() -> Result<(), anyhow::Error> { }); let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await?; // prover should not have any blocks saved assert_eq!(prover_node_test_client.eth_block_number().await, 0); @@ -385,12 +385,7 @@ async fn test_reopen_prover() -> Result<(), anyhow::Error> { wait_for_l1_block(&da_service, 4, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 5, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 5, None).await; // Contains the proof wait_for_l1_block(&da_service, 5, None).await; @@ -438,7 +433,7 @@ async fn test_reopen_prover() -> Result<(), anyhow::Error> { }); let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await?; seq_test_client.send_publish_batch_request().await; wait_for_l2_block(&seq_test_client, 6, None).await; @@ -491,7 +486,7 @@ async fn test_reopen_prover() -> Result<(), anyhow::Error> { }); let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await?; sleep(Duration::from_secs(2)).await; // Publish a DA to force prover to process new blocks da_service.publish_test_block().await.unwrap(); @@ -510,12 +505,7 @@ async fn test_reopen_prover() -> Result<(), anyhow::Error> { // Commitment is sent wait_for_l1_block(&da_service, 8, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 9, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 9, None).await; // Should now have 8 blocks = 2 commitments of blocks 1-4 and 5-8 // there is an extra soft confirmation due to the prover publishing a proof. This causes diff --git a/bin/citrea/tests/e2e/sequencer_behaviour.rs b/bin/citrea/tests/e2e/sequencer_behaviour.rs index 6f4359ec8..5439f0943 100644 --- a/bin/citrea/tests/e2e/sequencer_behaviour.rs +++ b/bin/citrea/tests/e2e/sequencer_behaviour.rs @@ -264,7 +264,7 @@ async fn transaction_failing_on_l1_is_removed_from_mempool() -> Result<(), anyho random_wallet_address, seq_test_client.rpc_addr, ) - .await; + .await?; let tx = random_test_client .send_eth_with_gas( @@ -374,7 +374,7 @@ async fn test_gas_limit_too_high() { }); let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await.unwrap(); let (full_node_port_tx, full_node_port_rx) = tokio::sync::oneshot::channel(); @@ -398,7 +398,7 @@ async fn test_gas_limit_too_high() { }); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await.unwrap(); let mut tx_hashes = vec![]; // Loop until tx_count. @@ -515,7 +515,7 @@ async fn test_system_tx_effect_on_block_gas_limit() -> Result<(), anyhow::Error> }); let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await?; // sys tx use L1BlockHash(50751 + 80720) + Bridge(261215) = 392686 gas // the block gas limit is 1_500_000 because the system txs gas limit is 1_500_000 (decided with @eyusufatik and @okkothejawa as bridge init takes 1M gas) diff --git a/bin/citrea/tests/e2e/sequencer_replacement.rs b/bin/citrea/tests/e2e/sequencer_replacement.rs index 127a33ace..b2a60e99e 100644 --- a/bin/citrea/tests/e2e/sequencer_replacement.rs +++ b/bin/citrea/tests/e2e/sequencer_replacement.rs @@ -19,10 +19,7 @@ use crate::test_helpers::{ create_default_sequencer_config, start_rollup, tempdir_with_children, wait_for_commitment, wait_for_l1_block, wait_for_l2_block, NodeMode, }; -use crate::{ - DEFAULT_MIN_SOFT_CONFIRMATIONS_PER_COMMITMENT, DEFAULT_PROOF_WAIT_DURATION, - TEST_DATA_GENESIS_PATH, -}; +use crate::{DEFAULT_MIN_SOFT_CONFIRMATIONS_PER_COMMITMENT, TEST_DATA_GENESIS_PATH}; /// Run the sequencer and the full node. /// After publishing some blocks, the sequencer crashes. @@ -157,7 +154,7 @@ async fn test_sequencer_crash_and_replace_full_node() -> Result<(), anyhow::Erro let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await?; assert_eq!(seq_test_client.eth_block_number().await as u64, 5); @@ -168,12 +165,7 @@ async fn test_sequencer_crash_and_replace_full_node() -> Result<(), anyhow::Erro wait_for_l1_block(&da_service, 3, None).await; - let commitments = wait_for_commitment( - &da_service, - 3, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + let commitments = wait_for_commitment(&da_service, 3, None).await; assert_eq!(commitments.len(), 1); assert_eq!(commitments[0].l2_start_block_number, 5); @@ -436,7 +428,7 @@ async fn test_soft_confirmation_save() -> Result<(), anyhow::Error> { }); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await?; let (full_node_port_tx_2, full_node_port_rx_2) = tokio::sync::oneshot::channel(); @@ -460,7 +452,7 @@ async fn test_soft_confirmation_save() -> Result<(), anyhow::Error> { }); let full_node_port_2 = full_node_port_rx_2.await.unwrap(); - let full_node_test_client_2 = make_test_client(full_node_port_2).await; + let full_node_test_client_2 = make_test_client(full_node_port_2).await?; let _ = execute_blocks(&seq_test_client, &full_node_test_client, &da_db_dir.clone()).await; diff --git a/bin/citrea/tests/e2e/syncing.rs b/bin/citrea/tests/e2e/syncing.rs index 473a0b7fe..cc850af7e 100644 --- a/bin/citrea/tests/e2e/syncing.rs +++ b/bin/citrea/tests/e2e/syncing.rs @@ -19,7 +19,7 @@ use crate::test_helpers::{ }; use crate::{ DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, DEFAULT_MIN_SOFT_CONFIRMATIONS_PER_COMMITMENT, - DEFAULT_PROOF_WAIT_DURATION, TEST_DATA_GENESIS_PATH, + TEST_DATA_GENESIS_PATH, }; /// Run the sequencer. @@ -93,7 +93,7 @@ async fn test_delayed_sync_ten_blocks() -> Result<(), anyhow::Error> { }); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await?; wait_for_l2_block(&full_node_test_client, 10, None).await; @@ -292,7 +292,7 @@ async fn test_prover_sync_with_commitments() -> Result<(), anyhow::Error> { }); let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await?; let (prover_node_port_tx, prover_node_port_rx) = tokio::sync::oneshot::channel(); @@ -319,7 +319,7 @@ async fn test_prover_sync_with_commitments() -> Result<(), anyhow::Error> { }); let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await?; // prover should not have any blocks saved assert_eq!(prover_node_test_client.eth_block_number().await, 0); @@ -338,12 +338,7 @@ async fn test_prover_sync_with_commitments() -> Result<(), anyhow::Error> { seq_test_client.send_publish_batch_request().await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 3, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 3, None).await; // Submit an L2 block to prevent sequencer from falling behind. seq_test_client.send_publish_batch_request().await; @@ -369,12 +364,7 @@ async fn test_prover_sync_with_commitments() -> Result<(), anyhow::Error> { wait_for_l1_block(&da_service, 4, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 4, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 4, None).await; // Should now have 8 blocks = 2 commitments of blocks 1-4 and 5-9 // there is an extra soft confirmation due to the prover publishing a proof. This causes @@ -474,7 +464,7 @@ async fn test_full_node_sync_status() { }); let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await.unwrap(); wait_for_l2_block(&full_node_test_client, 5, Some(Duration::from_secs(60))).await; diff --git a/bin/citrea/tests/evm/gas_price.rs b/bin/citrea/tests/evm/gas_price.rs index 1f610483f..6289af9ca 100644 --- a/bin/citrea/tests/evm/gas_price.rs +++ b/bin/citrea/tests/evm/gas_price.rs @@ -114,7 +114,7 @@ async fn execute( // send 15 transactions from each wallet for wallet in wallets { let address = wallet.address(); - let wallet_client = TestClient::new(client.chain_id, wallet, address, port).await; + let wallet_client = TestClient::new(client.chain_id, wallet, address, port).await?; for i in 0..tx_count_from_single_address { let _pending = wallet_client .contract_transaction(contract_address, contract.set_call_data(i), None) diff --git a/bin/citrea/tests/evm/mod.rs b/bin/citrea/tests/evm/mod.rs index d454b2f71..45ded415b 100644 --- a/bin/citrea/tests/evm/mod.rs +++ b/bin/citrea/tests/evm/mod.rs @@ -55,7 +55,7 @@ async fn web3_rpc_tests() -> Result<(), anyhow::Error> { // Wait for rollup task to start: let port = port_rx.await.unwrap(); - let test_client = make_test_client(port).await; + let test_client = make_test_client(port).await?; let arch = std::env::consts::ARCH; @@ -188,7 +188,7 @@ async fn test_genesis_contract_call() -> Result<(), Box> }); let seq_port = seq_port_rx.await.unwrap(); - let seq_test_client = make_test_client(seq_port).await; + let seq_test_client = make_test_client(seq_port).await?; // call the contract with address 0x3100000000000000000000000000000000000001 let contract_address = Address::from_str("0x3100000000000000000000000000000000000001").unwrap(); @@ -536,7 +536,7 @@ async fn execute(client: &Box) -> Result<(), Box Box { - let test_client = make_test_client(rpc_address).await; + let test_client = make_test_client(rpc_address).await.unwrap(); let etc_accounts = test_client.eth_accounts().await; assert_eq!( @@ -559,7 +559,7 @@ pub async fn init_test_rollup(rpc_address: SocketAddr) -> Box { } #[allow(clippy::borrowed_box)] -pub async fn make_test_client(rpc_address: SocketAddr) -> Box { +pub async fn make_test_client(rpc_address: SocketAddr) -> anyhow::Result> { let chain_id: u64 = 5655; let key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" .parse::() @@ -568,5 +568,7 @@ pub async fn make_test_client(rpc_address: SocketAddr) -> Box { let from_addr = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap(); - Box::new(TestClient::new(chain_id, key, from_addr, rpc_address).await) + Ok(Box::new( + TestClient::new(chain_id, key, from_addr, rpc_address).await?, + )) } diff --git a/bin/citrea/tests/evm/subscription.rs b/bin/citrea/tests/evm/subscription.rs index 821cff622..15f12dd2a 100644 --- a/bin/citrea/tests/evm/subscription.rs +++ b/bin/citrea/tests/evm/subscription.rs @@ -49,7 +49,7 @@ async fn test_eth_subscriptions() -> Result<(), Box> { // Wait for rollup task to start: let port = port_rx.await.unwrap(); - let test_client = make_test_client(port).await; + let test_client = make_test_client(port).await?; test_client.send_publish_batch_request().await; wait_for_l2_block(&test_client, 1, None).await; diff --git a/bin/citrea/tests/evm/tracing.rs b/bin/citrea/tests/evm/tracing.rs index 5c360a7fa..f427965c9 100644 --- a/bin/citrea/tests/evm/tracing.rs +++ b/bin/citrea/tests/evm/tracing.rs @@ -48,7 +48,7 @@ async fn tracing_tests() -> Result<(), Box> { // Wait for rollup task to start: let port = port_rx.await.unwrap(); - let test_client = make_test_client(port).await; + let test_client = make_test_client(port).await?; // ss is short for simple storage in this context let (caller_contract_address, caller_contract, ss_contract_address, _ss_contract) = { diff --git a/bin/citrea/tests/mempool/mod.rs b/bin/citrea/tests/mempool/mod.rs index 5206b0ed2..d13a2f1d4 100644 --- a/bin/citrea/tests/mempool/mod.rs +++ b/bin/citrea/tests/mempool/mod.rs @@ -40,7 +40,7 @@ async fn initialize_test( }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); (seq_task, test_client) } @@ -161,7 +161,9 @@ async fn test_order_by_fee() { .with_chain_id(Some(chain_id)); let poor_addr = key.address(); - let poor_test_client = TestClient::new(chain_id, key, poor_addr, test_client.rpc_addr).await; + let poor_test_client = TestClient::new(chain_id, key, poor_addr, test_client.rpc_addr) + .await + .unwrap(); let _addr = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap(); diff --git a/bin/citrea/tests/sequencer_commitments/mod.rs b/bin/citrea/tests/sequencer_commitments/mod.rs index a0d764e23..e56965109 100644 --- a/bin/citrea/tests/sequencer_commitments/mod.rs +++ b/bin/citrea/tests/sequencer_commitments/mod.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use borsh::BorshDeserialize; use citrea_stf::genesis_config::GenesisPaths; use rs_merkle::algorithms::Sha256; @@ -16,9 +14,7 @@ use crate::test_helpers::{ start_rollup, tempdir_with_children, wait_for_l1_block, wait_for_l2_block, wait_for_prover_l1_height, NodeMode, }; -use crate::{ - DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, DEFAULT_PROOF_WAIT_DURATION, TEST_DATA_GENESIS_PATH, -}; +use crate::{DEFAULT_DEPOSIT_MEMPOOL_FETCH_LIMIT, TEST_DATA_GENESIS_PATH}; #[tokio::test(flavor = "multi_thread")] async fn sequencer_sends_commitments_to_da_layer() { @@ -50,7 +46,7 @@ async fn sequencer_sends_commitments_to_da_layer() { }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); let da_service = MockDaService::new(MockAddress::from([0; 32]), &da_db_dir); @@ -213,7 +209,7 @@ async fn test_ledger_get_commitments_on_slot() { }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); let da_service = MockDaService::new(MockAddress::from([0; 32]), &da_db_dir); let (full_node_port_tx, full_node_port_rx) = tokio::sync::oneshot::channel(); @@ -238,7 +234,7 @@ async fn test_ledger_get_commitments_on_slot() { let full_node_port = full_node_port_rx.await.unwrap(); - let full_node_test_client = make_test_client(full_node_port).await; + let full_node_test_client = make_test_client(full_node_port).await.unwrap(); da_service.publish_test_block().await.unwrap(); wait_for_l1_block(&da_service, 2, None).await; @@ -309,7 +305,7 @@ async fn test_ledger_get_commitments_on_slot_prover() { }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); let da_service = MockDaService::new(MockAddress::from([0; 32]), &da_db_dir); let (prover_node_port_tx, prover_node_port_rx) = tokio::sync::oneshot::channel(); @@ -337,7 +333,7 @@ async fn test_ledger_get_commitments_on_slot_prover() { let prover_node_port = prover_node_port_rx.await.unwrap(); - let prover_node_test_client = make_test_client(prover_node_port).await; + let prover_node_test_client = make_test_client(prover_node_port).await.unwrap(); da_service.publish_test_block().await.unwrap(); wait_for_l1_block(&da_service, 2, None).await; @@ -351,12 +347,7 @@ async fn test_ledger_get_commitments_on_slot_prover() { wait_for_l1_block(&da_service, 3, None).await; // wait here until we see from prover's rpc that it finished proving - wait_for_prover_l1_height( - &prover_node_test_client, - 4, - Some(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)), - ) - .await; + wait_for_prover_l1_height(&prover_node_test_client, 4, None).await; let commitments = prover_node_test_client .ledger_get_sequencer_commitments_on_slot_by_number(3) diff --git a/bin/citrea/tests/soft_confirmation_rule_enforcer/mod.rs b/bin/citrea/tests/soft_confirmation_rule_enforcer/mod.rs index ff0d11aaa..61fbe5ab2 100644 --- a/bin/citrea/tests/soft_confirmation_rule_enforcer/mod.rs +++ b/bin/citrea/tests/soft_confirmation_rule_enforcer/mod.rs @@ -43,7 +43,7 @@ async fn too_many_l2_block_per_l1_block() { .await; }); let seq_port = seq_port_rx.await.unwrap(); - let test_client = make_test_client(seq_port).await; + let test_client = make_test_client(seq_port).await.unwrap(); let max_l2_blocks_per_l1 = test_client.get_max_l2_blocks_per_l1().await; let da_service = MockDaService::new(MockAddress::from([0; 32]), &da_db_dir); diff --git a/bin/citrea/tests/test_client/mod.rs b/bin/citrea/tests/test_client/mod.rs index abd75a02d..4d64c7b36 100644 --- a/bin/citrea/tests/test_client/mod.rs +++ b/bin/citrea/tests/test_client/mod.rs @@ -43,7 +43,7 @@ impl TestClient { key: PrivateKeySigner, from_addr: Address, rpc_addr: std::net::SocketAddr, - ) -> Self { + ) -> anyhow::Result { let http_host = format!("http://localhost:{}", rpc_addr.port()); let ws_host = format!("ws://localhost:{}", rpc_addr.port()); @@ -56,14 +56,12 @@ impl TestClient { let http_client = HttpClientBuilder::default() .request_timeout(Duration::from_secs(120)) - .build(http_host) - .unwrap(); + .build(http_host)?; let ws_client = WsClientBuilder::default() .enable_ws_ping(PingConfig::default().inactive_limit(Duration::from_secs(10))) .build(ws_host) - .await - .unwrap(); + .await?; let client = Self { chain_id, @@ -75,7 +73,7 @@ impl TestClient { rpc_addr, }; client.sync_nonce().await; - client + Ok(client) } pub(crate) async fn spam_publish_batch_request( diff --git a/bin/citrea/tests/test_helpers/mod.rs b/bin/citrea/tests/test_helpers/mod.rs index 85ca7e089..854fc4a37 100644 --- a/bin/citrea/tests/test_helpers/mod.rs +++ b/bin/citrea/tests/test_helpers/mod.rs @@ -22,6 +22,7 @@ use tokio::time::sleep; use tracing::{debug, info_span, instrument, warn, Instrument}; use crate::test_client::TestClient; +use crate::DEFAULT_PROOF_WAIT_DURATION; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NodeMode { @@ -225,7 +226,7 @@ pub async fn wait_for_prover_l1_height( timeout: Option, ) { let start = SystemTime::now(); - let timeout = timeout.unwrap_or(Duration::from_secs(30)); // Default 30 seconds timeout + let timeout = timeout.unwrap_or(Duration::from_secs(DEFAULT_PROOF_WAIT_DURATION)); // Default 300 seconds timeout loop { debug!("Waiting for prover height {}", num); let latest_block = prover_client.prover_get_last_scanned_l1_height().await; diff --git a/crates/bitcoin-da/src/service.rs b/crates/bitcoin-da/src/service.rs index cea6326b1..36cff54af 100644 --- a/crates/bitcoin-da/src/service.rs +++ b/crates/bitcoin-da/src/service.rs @@ -75,7 +75,7 @@ pub struct BitcoinServiceConfig { pub fee_rates_to_avg: Option, } -const FINALITY_DEPTH: u64 = 4; // blocks +pub const FINALITY_DEPTH: u64 = 4; // blocks const POLLING_INTERVAL: u64 = 10; // seconds impl BitcoinService { @@ -555,7 +555,7 @@ impl DaService for BitcoinService { let finalized_blockhash = self .client - .get_block_hash(block_count - FINALITY_DEPTH) + .get_block_hash(block_count.saturating_sub(FINALITY_DEPTH)) .await?; let finalized_block_header = self.get_block_by_hash(finalized_blockhash).await?; diff --git a/crates/sequencer/src/config.rs b/crates/sequencer/src/config.rs index a0039e7f6..00de34ad8 100644 --- a/crates/sequencer/src/config.rs +++ b/crates/sequencer/src/config.rs @@ -1,7 +1,7 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; /// Rollup Configuration -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SequencerConfig { /// Private key of the sequencer pub private_key: String, @@ -19,9 +19,24 @@ pub struct SequencerConfig { pub block_production_interval_ms: u64, } +impl Default for SequencerConfig { + fn default() -> Self { + SequencerConfig { + private_key: "1212121212121212121212121212121212121212121212121212121212121212" + .to_string(), + min_soft_confirmations_per_commitment: 10, + test_mode: true, + deposit_mempool_fetch_limit: 10, + block_production_interval_ms: 1000, + da_update_interval_ms: 2000, + mempool_conf: Default::default(), + } + } +} + /// Mempool Config for the sequencer /// Read: https://github.com/ledgerwatch/erigon/wiki/Transaction-Pool-Design -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SequencerMempoolConfig { /// Max number of transactions in the pending sub-pool pub pending_tx_limit: u64, diff --git a/crates/sovereign-sdk/full-node/sov-stf-runner/src/config.rs b/crates/sovereign-sdk/full-node/sov-stf-runner/src/config.rs index f1a0ac3c3..c0bbaf00e 100644 --- a/crates/sovereign-sdk/full-node/sov-stf-runner/src/config.rs +++ b/crates/sovereign-sdk/full-node/sov-stf-runner/src/config.rs @@ -3,12 +3,12 @@ use std::io::Read; use std::path::{Path, PathBuf}; use serde::de::DeserializeOwned; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::ProverGuestRunConfig; /// Runner configuration. -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct RunnerConfig { /// Sequencer client configuration. pub sequencer_client_url: String, @@ -19,7 +19,7 @@ pub struct RunnerConfig { } /// RPC configuration. -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Default, Serialize)] pub struct RpcConfig { /// RPC host. pub bind_host: String, @@ -82,14 +82,14 @@ const fn default_max_subscriptions_per_connection() -> u32 { } /// Simple storage configuration -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct StorageConfig { /// Path that can be utilized by concrete rollup implementation pub path: PathBuf, } /// Important public keys for the rollup -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct RollupPublicKeys { /// Soft confirmation signing public key of the Sequencer #[serde(with = "hex::serde")] @@ -105,7 +105,7 @@ pub struct RollupPublicKeys { } /// Rollup Configuration -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct FullNodeConfig { /// RPC configuration pub rpc: RpcConfig, @@ -123,7 +123,7 @@ pub struct FullNodeConfig { } /// Prover configuration -#[derive(Debug, Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ProverConfig { /// Prover run mode pub proving_mode: ProverGuestRunConfig, diff --git a/crates/sovereign-sdk/full-node/sov-stf-runner/src/prover_service/mod.rs b/crates/sovereign-sdk/full-node/sov-stf-runner/src/prover_service/mod.rs index 2293bf659..42b8d58d0 100644 --- a/crates/sovereign-sdk/full-node/sov-stf-runner/src/prover_service/mod.rs +++ b/crates/sovereign-sdk/full-node/sov-stf-runner/src/prover_service/mod.rs @@ -9,7 +9,8 @@ use sov_rollup_interface::zk::{Proof, StateTransitionData}; use thiserror::Error; /// The possible configurations of the prover. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] pub enum ProverGuestRunConfig { /// Skip proving. Skip,