diff --git a/Cargo.toml b/Cargo.toml index 6316522..19b824f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.14.1" +version = "0.14.2" edition = "2024" rust-version = "1.88" authors = ["init4"] diff --git a/crates/node-tests/src/context.rs b/crates/node-tests/src/context.rs index 1cd1927..95f46cf 100644 --- a/crates/node-tests/src/context.rs +++ b/crates/node-tests/src/context.rs @@ -24,7 +24,7 @@ use reth_db::{PlainAccountState, transaction::DbTxMut}; use reth_exex_test_utils::{Adapter, TestExExHandle, TmpDB as TmpDb}; use reth_node_api::FullNodeComponents; use signet_db::DbProviderExt; -use signet_node::SignetNode; +use signet_node::SignetNodeBuilder; use signet_node_config::test_utils::test_config; use signet_node_types::{NodeStatus, SignetNodeTypes}; use signet_test_utils::contracts::counter::COUNTER_DEPLOY_CODE; @@ -104,15 +104,12 @@ impl SignetTestContext { let alias_oracle: Arc>> = Arc::new(Mutex::new(HashSet::default())); - // instantiate Signet Node, booting rpc - let (node, mut node_status) = SignetNode::new( - ctx, - cfg.clone(), - factory.clone(), - Arc::clone(&alias_oracle), - Default::default(), - ) - .unwrap(); + let (node, mut node_status) = SignetNodeBuilder::new(cfg.clone()) + .with_ctx(ctx) + .with_factory(factory.clone()) + .with_alias_oracle(Arc::clone(&alias_oracle)) + .build() + .unwrap(); // Spawn the node, and wait for it to indicate RPC readiness. let node = tokio::spawn(node.start()); diff --git a/crates/node-tests/tests/db.rs b/crates/node-tests/tests/db.rs index c83633e..84a527b 100644 --- a/crates/node-tests/tests/db.rs +++ b/crates/node-tests/tests/db.rs @@ -1,7 +1,7 @@ -use alloy::primitives::{Address, hex, map::HashSet}; +use alloy::primitives::hex; use reth::providers::BlockReader; use serial_test::serial; -use signet_node::SignetNode; +use signet_node::SignetNodeBuilder; use signet_node_config::test_utils::test_config; use signet_node_tests::utils::create_test_provider_factory_with_chain_spec; use std::sync::Arc; @@ -17,14 +17,11 @@ async fn test_genesis() { assert_eq!(chain_spec.genesis().config.chain_id, consts.unwrap().ru_chain_id()); let factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); - let (_, _) = SignetNode::<_, _, HashSet
>::new( - ctx, - cfg.clone(), - factory.clone(), - Default::default(), - Default::default(), - ) - .unwrap(); + let (_, _) = SignetNodeBuilder::new(cfg.clone()) + .with_ctx(ctx) + .with_factory(factory.clone()) + .build() + .unwrap(); let genesis_block = factory.provider().unwrap().block_by_number(0).unwrap().unwrap(); diff --git a/crates/node/src/builder.rs b/crates/node/src/builder.rs new file mode 100644 index 0000000..eec7599 --- /dev/null +++ b/crates/node/src/builder.rs @@ -0,0 +1,323 @@ +#![allow(clippy::type_complexity)] + +use crate::{GENESIS_JOURNAL_HASH, SignetNode}; +use eyre::OptionExt; +use reth::{ + primitives::EthPrimitives, + providers::{BlockHashReader, ProviderFactory, StateProviderFactory}, +}; +use reth_db::transaction::DbTxMut; +use reth_db_common::init; +use reth_exex::ExExContext; +use reth_node_api::{FullNodeComponents, NodeTypes}; +use signet_block_processor::AliasOracleFactory; +use signet_db::DbProviderExt; +use signet_node_config::SignetNodeConfig; +use signet_node_types::{NodeStatus, NodeTypesDbTrait, SignetNodeTypes}; +use std::sync::Arc; + +/// A type that does not implement [`AliasOracleFactory`]. +#[derive(Debug, Clone, Copy)] +pub struct NotAnAof; + +/// A type that does not implement [`NodeTypesDbTrait`]. +#[derive(Debug, Clone, Copy)] +pub struct NotADb; + +/// Builder for [`SignetNode`]. This is the main way to create a signet node. +/// +/// The builder requires the following components to be set before building: +/// - An [`ExExContext`], via [`Self::with_ctx`]. +/// - A [`ProviderFactory`] for the signet node's database. +/// - This can be provided directly via [`Self::with_factory`]. +/// - Or created from a database implementing [`NodeTypesDbTrait`] via +/// [`Self::with_db`]. +/// - If not set directly, can be created from the config via +/// [`Self::with_config_db`]. +/// - An [`AliasOracleFactory`], via [`Self::with_alias_oracle`]. +/// - If not set, a default one will be created from the [`ExExContext`]'s +/// provider. +/// - A `reqwest::Client`, via [`Self::with_client`]. +/// - If not set, a default client will be created. +pub struct SignetNodeBuilder { + config: SignetNodeConfig, + alias_oracle: Option, + ctx: Option, + factory: Option, + client: Option, +} + +impl core::fmt::Debug for SignetNodeBuilder { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SignetNodeBuilder").finish_non_exhaustive() + } +} + +impl SignetNodeBuilder { + /// Create a new SignetNodeBuilder instance. + pub const fn new(config: SignetNodeConfig) -> Self { + Self { config, alias_oracle: None, ctx: None, factory: None, client: None } + } +} + +impl SignetNodeBuilder { + /// Set the DB for the signet node. + pub fn with_db( + self, + db: NewDb, + ) -> eyre::Result>, Aof>> { + let factory = ProviderFactory::new( + db, + self.config.chain_spec().clone(), + self.config.static_file_rw()?, + ); + + Ok(SignetNodeBuilder { + config: self.config, + alias_oracle: self.alias_oracle, + ctx: self.ctx, + factory: Some(factory), + client: self.client, + }) + } + + /// Set the DB for the signet node from config, opening the mdbx database. + pub fn with_config_db( + self, + ) -> eyre::Result< + SignetNodeBuilder>>, Aof>, + > { + let factory = ProviderFactory::new_with_database_path( + self.config.database_path(), + self.config.chain_spec().clone(), + reth_db::mdbx::DatabaseArguments::default(), + self.config.static_file_rw().unwrap(), + )?; + Ok(SignetNodeBuilder { + config: self.config, + alias_oracle: self.alias_oracle, + ctx: self.ctx, + factory: Some(factory), + client: self.client, + }) + } + + /// Set the provider factory for the signet node. + /// + /// This is an alternative to [`Self::with_db`] and + /// [`Self::with_config_db`]. + pub fn with_factory( + self, + factory: ProviderFactory>, + ) -> SignetNodeBuilder>, Aof> + where + NewDb: NodeTypesDbTrait, + { + SignetNodeBuilder { + config: self.config, + alias_oracle: self.alias_oracle, + ctx: self.ctx, + factory: Some(factory), + client: self.client, + } + } + + /// Set the [`ExExContext`] for the signet node. + pub fn with_ctx( + self, + ctx: ExExContext, + ) -> SignetNodeBuilder, Db, Aof> + where + NewHost: FullNodeComponents, + NewHost::Types: NodeTypes, + { + SignetNodeBuilder { + config: self.config, + alias_oracle: self.alias_oracle, + ctx: Some(ctx), + factory: self.factory, + client: self.client, + } + } + + /// Set the [`AliasOracleFactory`] for the signet node. + pub fn with_alias_oracle( + self, + alias_oracle: NewAof, + ) -> SignetNodeBuilder { + SignetNodeBuilder { + config: self.config, + alias_oracle: Some(alias_oracle), + ctx: self.ctx, + factory: self.factory, + client: self.client, + } + } + + /// Set the reqwest client for the signet node. + pub fn with_client(mut self, client: reqwest::Client) -> SignetNodeBuilder { + self.client = Some(client); + self + } +} + +impl SignetNodeBuilder, ProviderFactory>, Aof> +where + Host: FullNodeComponents, + Host::Types: NodeTypes, + Db: NodeTypesDbTrait, +{ + /// Prebuild checks for the signet node builder. Shared by all build + /// commands. + fn prebuild(&mut self) -> eyre::Result<()> { + self.client.get_or_insert_default(); + self.ctx.as_ref().ok_or_eyre("Launch context must be set")?; + let factory = self.factory.as_ref().ok_or_eyre("Provider factory must be set")?; + + // This check appears redundant with the same check made in + // `init_genesis`, but is not. We init the genesis DB state but then we + // drop some of it, and reuse those tables for our own nefarious + // purposes. If we attempt to drop those tables AFTER we have reused + // them, we will get a key deser error (as the tables will contain keys + // the old schema does not permit). This check ensures we only attempt + // to drop the tables once. + if matches!( + factory.block_hash(0), + Ok(None) + | Err(reth::providers::ProviderError::MissingStaticFileBlock( + reth::primitives::StaticFileSegment::Headers, + 0 + )) + ) { + init::init_genesis(factory)?; + + factory.provider_rw()?.update( + |writer: &mut reth::providers::DatabaseProviderRW>| { + writer.tx_mut().clear::()?; + writer.tx_mut().clear::()?; + writer.tx_mut().clear::()?; + + writer.tx_ref().put::(0, GENESIS_JOURNAL_HASH)?; + // we do not need to pre-populate the `ZenithHeaders` or + // `SignetEvents` tables, as missing data is legal in those + // tables + + Ok(()) + }, + )?; + } + + Ok(()) + } +} + +impl SignetNodeBuilder, NotADb, NotAnAof> +where + Host: FullNodeComponents, + Host::Types: NodeTypes, +{ + /// Build the node. This performs the following steps: + /// + /// - Runs prebuild checks. + /// - Inits the rollup DB from genesis if needed. + /// - Creates a default `AliasOracleFactory` from the host DB. + /// + /// # Panics + /// + /// If called outside a tokio runtime. + pub fn build( + self, + ) -> eyre::Result<( + SignetNode, Box>, + tokio::sync::watch::Receiver, + )> { + self.with_config_db()?.build() + } +} + +impl SignetNodeBuilder, NotADb, Aof> +where + Host: FullNodeComponents, + Host::Types: NodeTypes, + Aof: AliasOracleFactory, +{ + /// Build the node. This performs the following steps: + /// + /// - Runs prebuild checks. + /// - Inits the rollup DB from genesis if needed. + /// + /// # Panics + /// + /// If called outside a tokio runtime. + pub fn build( + self, + ) -> eyre::Result<( + SignetNode, Aof>, + tokio::sync::watch::Receiver, + )> { + self.with_config_db()?.build() + } +} + +impl SignetNodeBuilder, ProviderFactory>, NotAnAof> +where + Host: FullNodeComponents, + Host::Types: NodeTypes, + Db: NodeTypesDbTrait, +{ + /// Build the node. This performs the following steps: + /// + /// - Runs prebuild checks. + /// - Inits the rollup DB from genesis if needed. + /// - Creates a default `AliasOracleFactory` from the host DB. + /// + /// # Panics + /// + /// If called outside a tokio runtime. + pub fn build( + mut self, + ) -> eyre::Result<(SignetNode, tokio::sync::watch::Receiver)> { + self.prebuild()?; + // This allows the node to look up contract status. + let ctx = self.ctx.unwrap(); + let provider = ctx.provider().clone(); + let alias_oracle: Box = Box::new(provider); + + SignetNode::new_unsafe( + ctx, + self.config, + self.factory.unwrap(), + alias_oracle, + self.client.unwrap(), + ) + } +} + +impl SignetNodeBuilder, ProviderFactory>, Aof> +where + Host: FullNodeComponents, + Host::Types: NodeTypes, + Db: NodeTypesDbTrait, + Aof: AliasOracleFactory, +{ + /// Build the node. This performs the following steps: + /// + /// - Runs prebuild checks. + /// - Inits the rollup DB from genesis if needed. + /// + /// # Panics + /// + /// If called outside a tokio runtime. + pub fn build( + mut self, + ) -> eyre::Result<(SignetNode, tokio::sync::watch::Receiver)> { + self.prebuild()?; + SignetNode::new_unsafe( + self.ctx.unwrap(), + self.config, + self.factory.unwrap(), + self.alias_oracle.unwrap(), + self.client.unwrap(), + ) + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 325913f..92b97f1 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -11,6 +11,9 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +mod builder; +pub use builder::SignetNodeBuilder; + mod metrics; mod node; diff --git a/crates/node/src/node.rs b/crates/node/src/node.rs index 2a9fc8b..125730a 100644 --- a/crates/node/src/node.rs +++ b/crates/node/src/node.rs @@ -9,16 +9,13 @@ use futures_util::StreamExt; use reth::{ primitives::EthPrimitives, providers::{ - BlockHashReader, BlockIdReader, BlockNumReader, BlockReader, CanonChainTracker, - CanonStateNotification, CanonStateNotifications, CanonStateSubscriptions, HeaderProvider, - NodePrimitivesProvider, ProviderFactory, StateProviderFactory, - providers::BlockchainProvider, + BlockIdReader, BlockNumReader, BlockReader, CanonChainTracker, CanonStateNotification, + CanonStateNotifications, CanonStateSubscriptions, HeaderProvider, NodePrimitivesProvider, + ProviderFactory, StateProviderFactory, providers::BlockchainProvider, }, rpc::types::engine::ForkchoiceState, }; use reth_chainspec::EthChainSpec; -use reth_db::transaction::DbTxMut; -use reth_db_common::init; use reth_exex::{ExExContext, ExExEvent, ExExHead, ExExNotificationsStream}; use reth_node_api::{FullNodeComponents, FullNodeTypes, NodeTypes}; use signet_blobber::BlobFetcher; @@ -117,12 +114,20 @@ where Db: NodeTypesDbTrait, AliasOracle: AliasOracleFactory, { - /// Create a new Signet instance. + /// Create a new Signet instance. It is strongly recommend that you use the + /// [`SignetNodeBuilder`] instead of this function. + /// + /// This function does NOT initialize the genesis state. As such it is NOT + /// safe to use directly. The genesis state in the `factory` MUST be + /// initialized BEFORE calling this function. /// /// # Panics /// /// If invoked outside a tokio runtime. - pub fn new( + /// + /// [`SignetNodeBuilder`]: crate::builder::SignetNodeBuilder + #[doc(hidden)] + pub fn new_unsafe( ctx: ExExContext, config: SignetNodeConfig, factory: ProviderFactory>, @@ -132,39 +137,6 @@ where let constants = config.constants().wrap_err("failed to load signet constants from genesis")?; - // This check appears redundant with the same check made in - // `init_genesis`, but is not. We init the genesis DB state but then we - // drop some of it, and reuse those tables for our own nefarious - // purposes. If we attempt to drop those tables AFTER we have reused - // them, we will get a key deser error (as the tables will contain keys - // the old schema does not permit). This check ensures we only attempt - // to drop the tables once. - if matches!( - factory.block_hash(0), - Ok(None) - | Err(reth::providers::ProviderError::MissingStaticFileBlock( - reth::primitives::StaticFileSegment::Headers, - 0 - )) - ) { - init::init_genesis(&factory)?; - - factory.provider_rw()?.update( - |writer: &mut reth::providers::DatabaseProviderRW>| { - writer.tx_mut().clear::()?; - writer.tx_mut().clear::()?; - writer.tx_mut().clear::()?; - - writer.tx_ref().put::(0, GENESIS_JOURNAL_HASH)?; - // we do not need to pre-populate the `ZenithHeaders` or - // `SignetEvents` tables, as missing data is legal in those - // tables - - Ok(()) - }, - )?; - } - let bp: BlockchainProvider> = BlockchainProvider::new(factory.clone())?; let (status, receiver) = tokio::sync::watch::channel(NodeStatus::Booting);