diff --git a/src/clementine/mod.rs b/src/clementine/mod.rs new file mode 100644 index 0000000..492bc84 --- /dev/null +++ b/src/clementine/mod.rs @@ -0,0 +1 @@ +pub mod node; diff --git a/src/clementine/node.rs b/src/clementine/node.rs new file mode 100644 index 0000000..7cba182 --- /dev/null +++ b/src/clementine/node.rs @@ -0,0 +1,149 @@ +//! # Clementine Node +use std::{fs::File, process::Stdio, time::Duration}; + +use anyhow::Context; +use async_trait::async_trait; +use tokio::process::Command; +use tracing::info; + +use crate::{ + client::Client, + config::config_to_file, + log_provider::LogPathProvider, + node::Config, + traits::{NodeT, Restart, SpawnOutput}, + utils::get_clementine_path, + Result, +}; + +pub struct ClementineNode { + spawn_output: SpawnOutput, + config: C, + pub client: Client, +} + +impl ClementineNode { + pub async fn new(config: &C) -> Result { + let spawn_output = Self::spawn(config)?; + + let client = Client::new(config.rpc_bind_host(), config.rpc_bind_port())?; + Ok(Self { + spawn_output, + config: config.clone(), + client, + }) + } + + fn spawn(config: &C) -> Result { + let clementine = get_clementine_path()?; + let dir = config.dir(); + + let kind = C::node_kind(); + + let stdout_path = config.log_path(); + let stdout_file = File::create(&stdout_path).context("Failed to create stdout file")?; + info!( + "{} stdout logs available at : {}", + kind, + stdout_path.display() + ); + + let stderr_path = config.stderr_path(); + let stderr_file = File::create(stderr_path).context("Failed to create stderr file")?; + + let server_arg = match kind { + crate::node::NodeKind::Verifier => "--verifier-server", + _ => panic!("Wrong kind {}", kind), + }; + + let config_path = dir.join(format!("{kind}_config.toml")); + config_to_file(&config.clementine_config(), &config_path)?; + + Command::new(clementine) + .arg(server_arg) + .envs(config.env()) + .stdout(Stdio::from(stdout_file)) + .stderr(Stdio::from(stderr_file)) + .kill_on_drop(true) + .spawn() + .context(format!("Failed to spawn {kind} process")) + .map(SpawnOutput::Child) + } +} + +#[async_trait] +impl NodeT for ClementineNode +where + C: Config + LogPathProvider + Send + Sync, +{ + type Config = C; + type Client = Client; + + fn spawn(config: &Self::Config) -> Result { + Self::spawn(config) + } + + fn spawn_output(&mut self) -> &mut SpawnOutput { + &mut self.spawn_output + } + + async fn wait_for_ready(&self, _timeout: Option) -> Result<()> { + // let start = Instant::now(); + // let timeout = timeout.unwrap_or(Duration::from_secs(30)); + // 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!( + // "{} failed to become ready within the specified timeout", + // C::node_kind() + // ) + } + + fn client(&self) -> &Self::Client { + &self.client + } + + fn env(&self) -> Vec<(&'static str, &'static str)> { + self.config.env() + } + + fn config_mut(&mut self) -> &mut Self::Config { + &mut self.config + } + + fn config(&self) -> &Self::Config { + &self.config + } +} + +#[async_trait] +impl Restart for ClementineNode +where + C: Config + LogPathProvider + Send + Sync, +{ + async fn wait_until_stopped(&mut self) -> Result<()> { + self.stop().await?; + match &mut self.spawn_output { + SpawnOutput::Child(pid) => pid.wait().await?, + SpawnOutput::Container(_) => unimplemented!("L2 nodes don't run in docker yet"), + }; + Ok(()) + } + + async fn start(&mut self, new_config: Option) -> Result<()> { + let config = self.config_mut(); + if let Some(new_config) = new_config { + *config = new_config; + } + *self.spawn_output() = Self::spawn(config)?; + self.wait_for_ready(None).await + } +} diff --git a/src/config/clementine.rs b/src/config/clementine.rs new file mode 100644 index 0000000..9517dcd --- /dev/null +++ b/src/config/clementine.rs @@ -0,0 +1,119 @@ +//! # Clementine Configuration Options + +use std::path::PathBuf; + +use bitcoin::{address::NetworkUnchecked, secp256k1, Amount, Network}; +use serde::{Deserialize, Serialize}; + +/// Clementine's configuration options. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClementineClient { + /// Host of the operator or the verifier + pub host: String, + /// Port of the operator or the verifier + pub port: u16, + /// Bitcoin network to work on. + pub network: Network, + /// Secret key for the operator or the verifier. + pub secret_key: secp256k1::SecretKey, + /// Verifiers public keys. + pub verifiers_public_keys: Vec, + /// Number of verifiers. + pub num_verifiers: usize, + /// Operators x-only public keys. + pub operators_xonly_pks: Vec, + /// Operators wallet addresses. + pub operator_wallet_addresses: Vec>, + /// Number of operators. + pub num_operators: usize, + /// Operator's fee for withdrawal, in satoshis. + pub operator_withdrawal_fee_sats: Option, + /// Number of blocks after which user can take deposit back if deposit request fails. + pub user_takes_after: u32, + /// Number of blocks after which operator can take reimburse the bridge fund if they are honest. + pub operator_takes_after: u32, + /// Bridge amount in satoshis. + pub bridge_amount_sats: Amount, + /// Operator: number of kickoff UTXOs per funding transaction. + pub operator_num_kickoff_utxos_per_tx: usize, + /// Threshold for confirmation. + pub confirmation_threshold: u32, + /// Bitcoin remote procedure call URL. + pub bitcoin_rpc_url: String, + /// Bitcoin RPC user. + pub bitcoin_rpc_user: String, + /// Bitcoin RPC user password. + pub bitcoin_rpc_password: String, + /// All Secret keys. Just for testing purposes. + pub all_verifiers_secret_keys: Option>, + /// All Secret keys. Just for testing purposes. + pub all_operators_secret_keys: Option>, + /// Verifier endpoints. + pub verifier_endpoints: Option>, + /// PostgreSQL database host address. + pub db_host: String, + /// PostgreSQL database port. + pub db_port: usize, + /// PostgreSQL database user name. + pub db_user: String, + /// PostgreSQL database user password. + pub db_password: String, + /// PostgreSQL database name. + pub db_name: String, + /// Citrea RPC URL. + pub citrea_rpc_url: String, + /// Bridge contract address. + pub bridge_contract_address: String, +} + +impl Default for ClementineClient { + fn default() -> Self { + Self { + host: "127.0.0.1".to_string(), + port: 3030, + secret_key: secp256k1::SecretKey::new(&mut secp256k1::rand::thread_rng()), + verifiers_public_keys: vec![], + num_verifiers: 7, + operators_xonly_pks: vec![], + operator_wallet_addresses: vec![], + num_operators: 3, + operator_withdrawal_fee_sats: None, + user_takes_after: 5, + operator_takes_after: 5, + bridge_amount_sats: Amount::from_sat(100_000_000), + operator_num_kickoff_utxos_per_tx: 10, + confirmation_threshold: 1, + network: Network::Regtest, + bitcoin_rpc_url: "http://127.0.0.1:18443".to_string(), + bitcoin_rpc_user: "admin".to_string(), + bitcoin_rpc_password: "admin".to_string(), + all_verifiers_secret_keys: None, + all_operators_secret_keys: None, + verifier_endpoints: None, + db_host: "127.0.0.1".to_string(), + db_port: 5432, + db_user: "postgres".to_string(), + db_password: "postgres".to_string(), + db_name: "postgres".to_string(), + citrea_rpc_url: "http://127.0.0.1:12345".to_string(), + bridge_contract_address: "3100000000000000000000000000000000000002".to_string(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClementineConfig { + pub client: ClementineClient, + pub docker_image: Option, + pub data_dir: PathBuf, +} + +impl Default for ClementineConfig { + fn default() -> Self { + Self { + client: ClementineClient::default(), + docker_image: None, + data_dir: PathBuf::from("bridge_backend"), + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index b069209..7f7348e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,4 +1,5 @@ mod bitcoin; +mod clementine; mod docker; mod rollup; mod test; @@ -8,6 +9,7 @@ mod utils; use std::path::PathBuf; pub use bitcoin::BitcoinConfig; +pub use clementine::{ClementineClient, ClementineConfig}; pub use docker::DockerConfig; pub use rollup::{default_rollup_config, RollupConfig}; use serde::Serialize; @@ -98,6 +100,10 @@ where fn rollup_config(&self) -> &RollupConfig { &self.rollup } + + fn clementine_config(&self) -> &ClementineConfig { + unimplemented!() + } } impl LogPathProvider for FullL2NodeConfig @@ -116,3 +122,78 @@ where self.dir().join("stderr.log") } } + +#[derive(Clone, Debug)] +pub struct ClementineNodeConfig { + pub node: T, + pub config: ClementineConfig, + pub docker_image: Option, + pub dir: PathBuf, + pub env: Vec<(&'static str, &'static str)>, +} + +pub type FullVerifierConfig = ClementineNodeConfig; + +impl NodeKindMarker for FullVerifierConfig { + const KIND: NodeKind = NodeKind::Verifier; +} + +impl Config for ClementineNodeConfig +where + ClementineNodeConfig: NodeKindMarker, +{ + type NodeConfig = T; + + fn dir(&self) -> &PathBuf { + &self.dir + } + + fn rpc_bind_host(&self) -> &str { + &self.config.client.host + } + + fn rpc_bind_port(&self) -> u16 { + self.config.client.port + } + + fn env(&self) -> Vec<(&'static str, &'static str)> { + self.env.clone() + } + + fn node_config(&self) -> Option<&Self::NodeConfig> { + if std::mem::size_of::() == 0 { + None + } else { + Some(&self.node) + } + } + + fn node_kind() -> NodeKind { + ::KIND + } + + fn rollup_config(&self) -> &RollupConfig { + unimplemented!() + } + + fn clementine_config(&self) -> &ClementineConfig { + &self.config + } +} + +impl LogPathProvider for ClementineNodeConfig +where + ClementineNodeConfig: Config, +{ + fn kind() -> NodeKind { + Self::node_kind() + } + + fn log_path(&self) -> PathBuf { + self.dir().join("stdout.log") + } + + fn stderr_path(&self) -> PathBuf { + self.dir().join("stderr.log") + } +} diff --git a/src/config/test.rs b/src/config/test.rs index f67755d..8d0027d 100644 --- a/src/config/test.rs +++ b/src/config/test.rs @@ -1,6 +1,6 @@ use super::{ bitcoin::BitcoinConfig, test_case::TestCaseConfig, FullBatchProverConfig, FullFullNodeConfig, - FullLightClientProverConfig, FullSequencerConfig, + FullLightClientProverConfig, FullSequencerConfig, FullVerifierConfig, }; #[derive(Clone)] @@ -10,5 +10,6 @@ pub struct TestConfig { pub sequencer: FullSequencerConfig, pub batch_prover: FullBatchProverConfig, pub light_client_prover: FullLightClientProverConfig, + pub verifier: FullVerifierConfig, pub full_node: FullFullNodeConfig, } diff --git a/src/config/test_case.rs b/src/config/test_case.rs index bc081a8..3325c94 100644 --- a/src/config/test_case.rs +++ b/src/config/test_case.rs @@ -10,6 +10,7 @@ pub struct TestCaseEnv { pub batch_prover: Vec<(&'static str, &'static str)>, pub light_client_prover: Vec<(&'static str, &'static str)>, pub bitcoin: Vec<(&'static str, &'static str)>, + pub verifier: Vec<(&'static str, &'static str)>, } impl TestCaseEnv { @@ -41,6 +42,10 @@ impl TestCaseEnv { pub fn bitcoin(&self) -> Vec<(&'static str, &'static str)> { [self.test_env(), self.bitcoin.clone()].concat() } + + pub fn verifier(&self) -> Vec<(&'static str, &'static str)> { + [self.test_env(), self.verifier.clone()].concat() + } } #[derive(Clone)] @@ -50,6 +55,7 @@ pub struct TestCaseConfig { pub with_full_node: bool, pub with_batch_prover: bool, pub with_light_client_prover: bool, + pub with_verifier: bool, pub timeout: Duration, pub dir: PathBuf, pub docker: bool, @@ -67,6 +73,7 @@ impl Default for TestCaseConfig { with_batch_prover: false, with_light_client_prover: false, with_full_node: false, + with_verifier: false, timeout: Duration::from_secs(60), dir: TempDir::new() .expect("Failed to create temporary directory") diff --git a/src/framework.rs b/src/framework.rs index 5e66d87..8f88e73 100644 --- a/src/framework.rs +++ b/src/framework.rs @@ -16,6 +16,7 @@ use crate::{ light_client_prover::LightClientProver, log_provider::{LogPathProvider, LogPathProviderErased}, utils::tail_file, + verifier::Verifier, }; pub struct TestContext { @@ -44,6 +45,7 @@ pub struct TestFramework { pub batch_prover: Option, pub light_client_prover: Option, pub full_node: Option, + pub verifier: Option, pub initial_da_height: u64, } @@ -74,6 +76,7 @@ impl TestFramework { batch_prover: None, light_client_prover: None, full_node: None, + verifier: None, ctx, initial_da_height: 0, }) @@ -87,7 +90,12 @@ impl TestFramework { ) .await?; - (self.batch_prover, self.light_client_prover, self.full_node) = tokio::try_join!( + ( + self.batch_prover, + self.light_client_prover, + self.full_node, + self.verifier, + ) = tokio::try_join!( create_optional( self.ctx.config.test_case.with_batch_prover, BatchProver::new(&self.ctx.config.batch_prover) @@ -100,6 +108,10 @@ impl TestFramework { self.ctx.config.test_case.with_full_node, FullNode::new(&self.ctx.config.full_node) ), + create_optional( + self.ctx.config.test_case.with_verifier, + Verifier::new(&self.ctx.config.verifier) + ), )?; Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 0108d76..8321ddc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod batch_prover; pub mod bitcoin; mod citrea_config; +pub mod clementine; mod client; pub mod config; mod docker; @@ -13,5 +14,6 @@ pub mod sequencer; pub mod test_case; pub mod traits; mod utils; +pub mod verifier; pub type Result = anyhow::Result; diff --git a/src/node.rs b/src/node.rs index a11d76c..7c0dc35 100644 --- a/src/node.rs +++ b/src/node.rs @@ -17,7 +17,7 @@ use tracing::{info, trace}; use crate::{ client::Client, - config::{config_to_file, RollupConfig}, + config::{config_to_file, ClementineConfig, RollupConfig}, log_provider::LogPathProvider, traits::{NodeT, Restart, SpawnOutput}, utils::{get_citrea_path, get_genesis_path}, @@ -31,6 +31,7 @@ pub enum NodeKind { LightClientProver, Sequencer, FullNode, + Verifier, } impl fmt::Display for NodeKind { @@ -41,6 +42,7 @@ impl fmt::Display for NodeKind { NodeKind::LightClientProver => write!(f, "light-client-prover"), NodeKind::Sequencer => write!(f, "sequencer"), NodeKind::FullNode => write!(f, "full-node"), + NodeKind::Verifier => write!(f, "verifier"), } } } @@ -55,6 +57,7 @@ pub trait Config: Clone { fn node_config(&self) -> Option<&Self::NodeConfig>; fn node_kind() -> NodeKind; fn rollup_config(&self) -> &RollupConfig; + fn clementine_config(&self) -> &ClementineConfig; } pub struct Node { diff --git a/src/test_case.rs b/src/test_case.rs index dc04c67..9686852 100644 --- a/src/test_case.rs +++ b/src/test_case.rs @@ -23,8 +23,9 @@ use super::{ }; use crate::{ config::{ - BatchProverConfig, BitcoinServiceConfig, FullLightClientProverConfig, - LightClientProverConfig, RpcConfig, RunnerConfig, SequencerConfig, StorageConfig, + BatchProverConfig, BitcoinServiceConfig, ClementineConfig, FullLightClientProverConfig, + FullVerifierConfig, LightClientProverConfig, RpcConfig, RunnerConfig, SequencerConfig, + StorageConfig, }, traits::NodeT, utils::{get_default_genesis_path, get_workspace_root}, @@ -129,8 +130,9 @@ impl TestCaseRunner { let batch_prover_rollup = default_rollup_config(); let light_client_prover_rollup = default_rollup_config(); let full_node_rollup = default_rollup_config(); + let clementine_config = T::clementine_config(); - let [bitcoin_dir, dbs_dir, batch_prover_dir, light_client_prover_dir, sequencer_dir, full_node_dir, genesis_dir, tx_backup_dir] = + let [bitcoin_dir, dbs_dir, batch_prover_dir, light_client_prover_dir, sequencer_dir, full_node_dir, genesis_dir, tx_backup_dir, verifier_dir] = create_dirs(&test_case.dir)?; copy_genesis_dir(&test_case.genesis_dir, &genesis_dir)?; @@ -298,6 +300,13 @@ impl TestCaseRunner { node: (), env: env.full_node(), }, + verifier: FullVerifierConfig { + config: ClementineConfig::default(), + docker_image: None, + env: env.verifier(), + node: clementine_config, + dir: verifier_dir, + }, test_case, }) } @@ -346,6 +355,12 @@ pub trait TestCase: Send + Sync + 'static { LightClientProverConfig::default() } + /// Returns the light clementine configuration for the test. + /// Override this method to provide a custom clementine configuration. + fn clementine_config() -> ClementineConfig { + ClementineConfig::default() + } + /// Returns the test setup /// Override this method to add custom initialization logic async fn setup(&self, _framework: &mut TestFramework) -> Result<()> { @@ -366,7 +381,7 @@ pub trait TestCase: Send + Sync + 'static { } } -fn create_dirs(base_dir: &Path) -> Result<[PathBuf; 8]> { +fn create_dirs(base_dir: &Path) -> Result<[PathBuf; 9]> { let paths = [ NodeKind::Bitcoin.to_string(), "dbs".to_string(), @@ -374,6 +389,7 @@ fn create_dirs(base_dir: &Path) -> Result<[PathBuf; 8]> { NodeKind::LightClientProver.to_string(), NodeKind::Sequencer.to_string(), NodeKind::FullNode.to_string(), + NodeKind::Verifier.to_string(), "genesis".to_string(), "inscription_txs".to_string(), ] diff --git a/src/utils.rs b/src/utils.rs index e913c9d..66e4825 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -33,6 +33,14 @@ pub fn get_citrea_path() -> Result { .map(PathBuf::from) .map_err(|_| anyhow!("CITREA_E2E_TEST_BINARY is not set. Cannot resolve citrea path")) } +/// Get clementine path from `CLEMENTINE_E2E_TEST_BINARY` env +pub fn get_clementine_path() -> Result { + std::env::var("CLEMENTINE_E2E_TEST_BINARY") + .map(PathBuf::from) + .map_err(|_| { + anyhow!("CLEMENTINE_E2E_TEST_BINARY is not set. Cannot resolve clementine path") + }) +} /// Get genesis path from resources /// TODO: assess need for customable genesis path in e2e tests diff --git a/src/verifier.rs b/src/verifier.rs new file mode 100644 index 0000000..e0b1522 --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +use crate::{clementine, config::FullVerifierConfig, traits::NodeT}; + +pub type Verifier = clementine::node::ClementineNode; + +impl Verifier { + pub fn dir(&self) -> &PathBuf { + &self.config().dir + } +} diff --git a/tests/clementine.rs b/tests/clementine.rs new file mode 100644 index 0000000..f04711a --- /dev/null +++ b/tests/clementine.rs @@ -0,0 +1,37 @@ +use anyhow::bail; +use async_trait::async_trait; + +use citrea_e2e::{ + config::TestCaseConfig, + framework::TestFramework, + test_case::{TestCase, TestCaseRunner}, +}; + +struct BasicClementineTest; + +#[async_trait] +impl TestCase for BasicClementineTest { + fn test_config() -> TestCaseConfig { + TestCaseConfig { + with_verifier: true, + with_sequencer: false, + ..Default::default() + } + } + + async fn run_test(&mut self, f: &mut TestFramework) -> citrea_e2e::Result<()> { + let Some(_da) = f.bitcoin_nodes.get(0) else { + bail!("bitcoind not running!") + }; + let Some(_verifier) = &f.verifier else { + bail!("Verifier is not running!") + }; + + Ok(()) + } +} + +#[tokio::test] +async fn basic_clementine_test() -> citrea_e2e::Result<()> { + TestCaseRunner::new(BasicClementineTest).run().await +}