feat: introduce ProviderFactoryBuilder (#13989)

This commit is contained in:
Matthias Seitz
2025-01-28 20:34:03 +01:00
committed by GitHub
parent 4653d3dd3a
commit 9bc07cc5bd
6 changed files with 362 additions and 31 deletions

View File

@ -22,7 +22,7 @@ use reth_node_builder::{
BuilderContext, Node, NodeAdapter, NodeComponentsBuilder, PayloadTypes,
};
use reth_primitives::{EthPrimitives, PooledTransaction};
use reth_provider::{CanonStateSubscriptions, EthStorage};
use reth_provider::{providers::ProviderFactoryBuilder, CanonStateSubscriptions, EthStorage};
use reth_rpc::EthApi;
use reth_tracing::tracing::{debug, info};
use reth_transaction_pool::{
@ -63,6 +63,41 @@ impl EthereumNode {
.executor(EthereumExecutorBuilder::default())
.consensus(EthereumConsensusBuilder::default())
}
/// Instantiates the [`ProviderFactoryBuilder`] for an ethereum node.
///
/// # Open a Providerfactory in read-only mode from a datadir
///
/// See also: [`ProviderFactoryBuilder`] and
/// [`ReadOnlyConfig`](reth_provider::providers::ReadOnlyConfig).
///
/// ```no_run
/// use reth_chainspec::MAINNET;
/// use reth_node_ethereum::EthereumNode;
///
/// let factory = EthereumNode::provider_factory_builder()
/// .open_read_only(MAINNET.clone(), "datadir")
/// .unwrap();
/// ```
///
/// # Open a Providerfactory manually with with all required componets
///
/// ```no_run
/// use reth_chainspec::ChainSpecBuilder;
/// use reth_db::open_db_read_only;
/// use reth_node_ethereum::EthereumNode;
/// use reth_provider::providers::StaticFileProvider;
/// use std::{path::Path, sync::Arc};
///
/// let factory = EthereumNode::provider_factory_builder()
/// .db(Arc::new(open_db_read_only(Path::new("db"), Default::default()).unwrap()))
/// .chainspec(ChainSpecBuilder::mainnet().build().into())
/// .static_file(StaticFileProvider::read_only("db/static_files", false).unwrap())
/// .build_provider_factory();
/// ```
pub fn provider_factory_builder() -> ProviderFactoryBuilder<Self> {
ProviderFactoryBuilder::default()
}
}
impl NodeTypes for EthereumNode {

View File

@ -58,6 +58,7 @@ notify = { workspace = true, default-features = false, features = ["macos_fseven
parking_lot.workspace = true
dashmap = { workspace = true, features = ["inline"] }
strum.workspace = true
eyre.workspace = true
# test-utils
reth-ethereum-engine-primitives = { workspace = true, optional = true }

View File

@ -0,0 +1,280 @@
//! Helper builder entrypoint to instantiate a [`ProviderFactory`].
//!
//! This also includes general purpose staging types that provide builder style functions that lead
//! up to the intended build target.
use crate::{providers::StaticFileProvider, ProviderFactory};
use reth_db::{mdbx::DatabaseArguments, open_db_read_only, DatabaseEnv};
use reth_db_api::{database_metrics::DatabaseMetrics, Database};
use reth_node_types::{NodeTypes, NodeTypesWithDBAdapter};
use std::{
marker::PhantomData,
path::{Path, PathBuf},
sync::Arc,
};
/// Helper type to create a [`ProviderFactory`].
///
/// This type is the entry point for a stage based builder.
///
/// The intended staging is:
/// 1. Configure the database: [`ProviderFactoryBuilder::db`]
/// 2. Configure the chainspec: `chainspec`
/// 3. Configure the [`StaticFileProvider`]: `static_file`
#[derive(Debug)]
pub struct ProviderFactoryBuilder<N> {
_types: PhantomData<N>,
}
impl<N> ProviderFactoryBuilder<N> {
/// Maps the [`NodeTypes`] of this builder.
pub fn types<T>(self) -> ProviderFactoryBuilder<T> {
ProviderFactoryBuilder::default()
}
/// Configures the database.
pub fn db<DB>(self, db: DB) -> TypesAnd1<N, DB> {
TypesAnd1::new(db)
}
/// Opens the database with the given chainspec and [`ReadOnlyConfig`].
///
/// # Open a monitored instance
///
/// This is recommended when the new read-only instance is used with an active node.
///
/// ```no_run
/// use reth_chainspec::MAINNET;
/// use reth_node_types::NodeTypes;
/// use reth_provider::providers::ProviderFactoryBuilder;
///
/// fn demo<N: NodeTypes<ChainSpec = reth_chainspec::ChainSpec>>() {
/// let provider_factory = ProviderFactoryBuilder::<N>::default()
/// .open_read_only(MAINNET.clone(), "datadir")
/// .unwrap();
/// }
/// ```
///
/// # Open an unmonitored instace
///
/// This is recommended when no changes to the static files are expected (e.g. no active node)
///
/// ```no_run
/// use reth_chainspec::MAINNET;
/// use reth_node_types::NodeTypes;
/// ///
/// use reth_provider::providers::{ProviderFactoryBuilder, ReadOnlyConfig};
///
/// fn demo<N: NodeTypes<ChainSpec = reth_chainspec::ChainSpec>>() {
/// let provider_factory = ProviderFactoryBuilder::<N>::default()
/// .open_read_only(MAINNET.clone(), ReadOnlyConfig::from_datadir("datadir").no_watch())
/// .unwrap();
/// }
/// ```
pub fn open_read_only(
self,
chainspec: Arc<N::ChainSpec>,
config: impl Into<ReadOnlyConfig>,
) -> eyre::Result<ProviderFactory<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>>
where
N: NodeTypes,
{
let ReadOnlyConfig { db_dir, db_args, static_files_dir, watch_static_files } =
config.into();
Ok(self
.db(Arc::new(open_db_read_only(db_dir, db_args)?))
.chainspec(chainspec)
.static_file(StaticFileProvider::read_only(static_files_dir, watch_static_files)?)
.build_provider_factory())
}
}
impl<N> Default for ProviderFactoryBuilder<N> {
fn default() -> Self {
Self { _types: Default::default() }
}
}
/// Settings for how to open the database and static files.
///
/// The default derivation from a path assumes the path is the datadir:
/// [`ReadOnlyConfig::from_datadir`]
#[derive(Debug, Clone)]
pub struct ReadOnlyConfig {
/// The path to the database directory.
pub db_dir: PathBuf,
/// How to open the database
pub db_args: DatabaseArguments,
/// The path to the static file dir
pub static_files_dir: PathBuf,
/// Whether the static files should be watched for changes.
pub watch_static_files: bool,
}
impl ReadOnlyConfig {
/// Derives the [`ReadOnlyConfig`] from the datadir.
///
/// By default this assumes the following datadir layour:
///
/// ```text
/// -`datadir`
/// |__db
/// |__static_files
/// ```
///
/// By default this watches the static file directory for changes, see also
/// [`StaticFileProvider::read_only`]
pub fn from_datadir(datadir: impl AsRef<Path>) -> Self {
let datadir = datadir.as_ref();
let db_path = datadir.join("db");
Self::from_db_dir(db_path)
}
/// Derives the [`ReadOnlyConfig`] from the database dir.
///
/// By default this assumes the following datadir layour:
///
/// ```text
/// - db
/// |__static_files
/// ```
///
/// By default this watches the static file directory for changes, see also
/// [`StaticFileProvider::read_only`]
pub fn from_db_dir(db_dir: impl AsRef<Path>) -> Self {
let db_dir = db_dir.as_ref();
Self {
static_files_dir: db_dir.join("static_files"),
db_dir: db_dir.into(),
db_args: Default::default(),
watch_static_files: true,
}
}
/// Configures the db arguments used when opening the database.
pub fn with_db_args(mut self, db_args: impl Into<DatabaseArguments>) -> Self {
self.db_args = db_args.into();
self
}
/// Configures the db directory.
pub fn with_db_dir(mut self, db_dir: impl Into<PathBuf>) -> Self {
self.db_dir = db_dir.into();
self
}
/// Configures the static file directory.
pub fn with_static_file_dir(mut self, static_file_dir: impl Into<PathBuf>) -> Self {
self.static_files_dir = static_file_dir.into();
self
}
/// Whether the static file directory should be watches for changes, see also
/// [`StaticFileProvider::read_only`]
pub fn set_watch_static_files(&mut self, watch_static_files: bool) {
self.watch_static_files = watch_static_files;
}
/// Don't watch the static files for changes.
///
/// This is only recommended if this is used without a running node instance that modifies
/// static files.
pub fn no_watch(mut self) -> Self {
self.set_watch_static_files(false);
self
}
}
impl<T> From<T> for ReadOnlyConfig
where
T: AsRef<Path>,
{
fn from(value: T) -> Self {
Self::from_datadir(value.as_ref())
}
}
/// This is staging type that contains the configured types and _one_ value.
#[derive(Debug)]
pub struct TypesAnd1<N, Val1> {
_types: PhantomData<N>,
val_1: Val1,
}
impl<N, Val1> TypesAnd1<N, Val1> {
/// Creates a new instance with the given types and one value.
pub fn new(val_1: Val1) -> Self {
Self { _types: Default::default(), val_1 }
}
/// Configures the chainspec.
pub fn chainspec<C>(self, chainspec: Arc<C>) -> TypesAnd2<N, Val1, Arc<C>> {
TypesAnd2::new(self.val_1, chainspec)
}
}
/// This is staging type that contains the configured types and _two_ values.
#[derive(Debug)]
pub struct TypesAnd2<N, Val1, Val2> {
_types: PhantomData<N>,
val_1: Val1,
val_2: Val2,
}
impl<N, Val1, Val2> TypesAnd2<N, Val1, Val2> {
/// Creates a new instance with the given types and two values.
pub fn new(val_1: Val1, val_2: Val2) -> Self {
Self { _types: Default::default(), val_1, val_2 }
}
/// Returns the first value.
pub const fn val_1(&self) -> &Val1 {
&self.val_1
}
/// Returns the second value.
pub const fn val_2(&self) -> &Val2 {
&self.val_2
}
/// Configures the [`StaticFileProvider`].
pub fn static_file(
self,
static_file_provider: StaticFileProvider<N::Primitives>,
) -> TypesAnd3<N, Val1, Val2, StaticFileProvider<N::Primitives>>
where
N: NodeTypes,
{
TypesAnd3::new(self.val_1, self.val_2, static_file_provider)
}
// TODO: add helper fns for opening static file provider
}
/// This is staging type that contains the configured types and _three_ values.
#[derive(Debug)]
pub struct TypesAnd3<N, Val1, Val2, Val3> {
_types: PhantomData<N>,
val_1: Val1,
val_2: Val2,
val_3: Val3,
}
impl<N, Val1, Val2, Val3> TypesAnd3<N, Val1, Val2, Val3> {
/// Creates a new instance with the given types and three values.
pub fn new(val_1: Val1, val_2: Val2, val_3: Val3) -> Self {
Self { _types: Default::default(), val_1, val_2, val_3 }
}
}
impl<N, DB> TypesAnd3<N, DB, Arc<N::ChainSpec>, StaticFileProvider<N::Primitives>>
where
N: NodeTypes,
DB: Database + DatabaseMetrics + Clone + Unpin + 'static,
{
/// Creates the [`ProviderFactory`].
pub fn build_provider_factory(self) -> ProviderFactory<NodeTypesWithDBAdapter<N, DB>> {
let Self { _types, val_1, val_2, val_3 } = self;
ProviderFactory::new(val_1, val_2, val_3)
}
}

View File

@ -15,7 +15,9 @@ use reth_chainspec::{ChainInfo, EthereumHardforks};
use reth_db::{init_db, mdbx::DatabaseArguments, DatabaseEnv};
use reth_db_api::{database::Database, models::StoredBlockBodyIndices};
use reth_errors::{RethError, RethResult};
use reth_node_types::{BlockTy, HeaderTy, NodeTypesWithDB, ReceiptTy, TxTy};
use reth_node_types::{
BlockTy, HeaderTy, NodeTypes, NodeTypesWithDB, NodeTypesWithDBAdapter, ReceiptTy, TxTy,
};
use reth_primitives::{RecoveredBlock, SealedBlock, SealedHeader, StaticFileSegment};
use reth_prune_types::{PruneCheckpoint, PruneModes, PruneSegment};
use reth_stages_types::{StageCheckpoint, StageId};
@ -40,6 +42,9 @@ pub use provider::{DatabaseProvider, DatabaseProviderRO, DatabaseProviderRW};
use super::ProviderNodeTypes;
mod builder;
pub use builder::{ProviderFactoryBuilder, ReadOnlyConfig};
mod metrics;
mod chain;
@ -49,7 +54,7 @@ pub use chain::*;
///
/// This provider implements most provider or provider factory traits.
pub struct ProviderFactory<N: NodeTypesWithDB> {
/// Database
/// Database instance
db: N::DB,
/// Chain spec
chain_spec: Arc<N::ChainSpec>,
@ -61,19 +66,10 @@ pub struct ProviderFactory<N: NodeTypesWithDB> {
storage: Arc<N::Storage>,
}
impl<N> fmt::Debug for ProviderFactory<N>
where
N: NodeTypesWithDB<DB: fmt::Debug, ChainSpec: fmt::Debug, Storage: fmt::Debug>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { db, chain_spec, static_file_provider, prune_modes, storage } = self;
f.debug_struct("ProviderFactory")
.field("db", &db)
.field("chain_spec", &chain_spec)
.field("static_file_provider", &static_file_provider)
.field("prune_modes", &prune_modes)
.field("storage", &storage)
.finish()
impl<N: NodeTypes> ProviderFactory<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>> {
/// Instantiates the builder for this type
pub fn builder() -> ProviderFactoryBuilder<N> {
ProviderFactoryBuilder::default()
}
}
@ -630,6 +626,22 @@ impl<N: ProviderNodeTypes> HashedPostStateProvider for ProviderFactory<N> {
}
}
impl<N> fmt::Debug for ProviderFactory<N>
where
N: NodeTypesWithDB<DB: fmt::Debug, ChainSpec: fmt::Debug, Storage: fmt::Debug>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { db, chain_spec, static_file_provider, prune_modes, storage } = self;
f.debug_struct("ProviderFactory")
.field("db", &db)
.field("chain_spec", &chain_spec)
.field("static_file_provider", &static_file_provider)
.field("prune_modes", &prune_modes)
.field("storage", &storage)
.finish()
}
}
impl<N: NodeTypesWithDB> Clone for ProviderFactory<N> {
fn clone(&self) -> Self {
Self {

View File

@ -79,6 +79,13 @@ impl StaticFileAccess {
}
/// [`StaticFileProvider`] manages all existing [`StaticFileJarProvider`].
///
/// "Static files" contain immutable chain history data, such as:
/// - transactions
/// - headers
/// - receipts
///
/// This provider type is responsible for reading and writing to static files.
#[derive(Debug)]
pub struct StaticFileProvider<N>(pub(crate) Arc<StaticFileProviderInner<N>>);
@ -89,7 +96,7 @@ impl<N> Clone for StaticFileProvider<N> {
}
impl<N: NodePrimitives> StaticFileProvider<N> {
/// Creates a new [`StaticFileProvider`].
/// Creates a new [`StaticFileProvider`] with the given [`StaticFileAccess`].
fn new(path: impl AsRef<Path>, access: StaticFileAccess) -> ProviderResult<Self> {
let provider = Self(Arc::new(StaticFileProviderInner::new(path, access)?));
provider.initialize_index()?;
@ -100,6 +107,11 @@ impl<N: NodePrimitives> StaticFileProvider<N> {
///
/// Set `watch_directory` to `true` to track the most recent changes in static files. Otherwise,
/// new data won't be detected or queryable.
///
/// Watching is recommended if the read-only provider is used on a directory that an active node
/// instance is modifying.
///
/// See also [`StaticFileProvider::watch_directory`].
pub fn read_only(path: impl AsRef<Path>, watch_directory: bool) -> ProviderResult<Self> {
let provider = Self::new(path, StaticFileAccess::RO)?;

View File

@ -2,16 +2,13 @@ use alloy_consensus::BlockHeader;
use alloy_primitives::{Address, B256};
use alloy_rpc_types_eth::{Filter, FilteredParams};
use reth_chainspec::ChainSpecBuilder;
use reth_db::{open_db_read_only, DatabaseEnv};
use reth_node_ethereum::EthereumNode;
use reth_node_types::NodeTypesWithDBAdapter;
use reth_primitives::{SealedBlock, SealedHeader, TransactionSigned};
use reth_primitives_traits::transaction::signed::SignedTransaction;
use reth_provider::{
providers::StaticFileProvider, AccountReader, BlockReader, BlockSource, HeaderProvider,
ProviderFactory, ReceiptProvider, StateProvider, TransactionsProvider,
providers::ReadOnlyConfig, AccountReader, BlockReader, BlockSource, HeaderProvider,
ReceiptProvider, StateProvider, TransactionsProvider,
};
use std::{path::Path, sync::Arc};
// Providers are zero cost abstractions on top of an opened MDBX Transaction
// exposing a familiar API to query the chain's information without requiring knowledge
@ -22,17 +19,11 @@ use std::{path::Path, sync::Arc};
fn main() -> eyre::Result<()> {
// Opens a RO handle to the database file.
let db_path = std::env::var("RETH_DB_PATH")?;
let db_path = Path::new(&db_path);
let db = open_db_read_only(db_path.join("db").as_path(), Default::default())?;
// Instantiate a provider factory for Ethereum mainnet using the provided DB.
// TODO: Should the DB version include the spec so that you do not need to specify it here?
// Instantiate a provider factory for Ethereum mainnet using the provided DB path.
let spec = ChainSpecBuilder::mainnet().build();
let factory = ProviderFactory::<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>::new(
db.into(),
spec.into(),
StaticFileProvider::read_only(db_path.join("static_files"), true)?,
);
let factory = EthereumNode::provider_factory_builder()
.open_read_only(spec.into(), ReadOnlyConfig::from_db_dir(db_path))?;
// This call opens a RO transaction on the database. To write to the DB you'd need to call
// the `provider_rw` function and look for the `Writer` variants of the traits.