|
| 1 | +//! In this example, we demonstrate how we wrap the `Provider` trait over reth-db by |
| 2 | +//! leveraging `ProviderCall`. |
| 3 | +//! |
| 4 | +//! `ProviderCall` enables the alloy-provider to fetch results of a rpc request from arbitrary |
| 5 | +//! sources. These arbitray sources could be a RPC call over the network, a local database, or even |
| 6 | +//! a synchronous function call. |
| 7 | +//! |
| 8 | +//! `ProviderCall` is the final future in the flow of an rpc request and is used by the |
| 9 | +//! `RpcWithBlock` and `EthCall` types under the hood to give flexibility to the user to use |
| 10 | +//! their own implementation of the `Provider` trait and fetch results from any source. |
| 11 | +//! |
| 12 | +//! Learn more about `ProviderCall` [here](https://github.com/alloy-rs/alloy/pull/788). |
| 13 | +use std::{marker::PhantomData, path::PathBuf, sync::Arc}; |
| 14 | + |
| 15 | +use alloy::{ |
| 16 | + eips::{BlockId, BlockNumberOrTag}, |
| 17 | + node_bindings::{utils::run_with_tempdir, Reth}, |
| 18 | + primitives::{address, Address, U64}, |
| 19 | + providers::{ |
| 20 | + Provider, ProviderBuilder, ProviderCall, ProviderLayer, RootProvider, RpcWithBlock, |
| 21 | + }, |
| 22 | + rpc::client::NoParams, |
| 23 | + transports::{Transport, TransportErrorKind}, |
| 24 | +}; |
| 25 | +use eyre::Result; |
| 26 | + |
| 27 | +use reth_chainspec::ChainSpecBuilder; |
| 28 | +use reth_db::{open_db_read_only, DatabaseEnv}; |
| 29 | +use reth_node_ethereum::EthereumNode; |
| 30 | +use reth_node_types::NodeTypesWithDBAdapter; |
| 31 | +use reth_provider::{ |
| 32 | + providers::StaticFileProvider, BlockNumReader, DatabaseProviderFactory, ProviderError, |
| 33 | + ProviderFactory, StateProvider, TryIntoHistoricalStateProvider, |
| 34 | +}; |
| 35 | +mod reth_db_layer; |
| 36 | +use reth_db_layer::RethDbLayer; |
| 37 | + |
| 38 | +#[tokio::main] |
| 39 | +async fn main() -> Result<()> { |
| 40 | + run_with_tempdir("provider-call-reth-db", |data_dir| async move { |
| 41 | + // Initializing reth with a tmp data directory. |
| 42 | + // We use a tmp directory for the purposes of this example. |
| 43 | + // This would actually use an existing reth datadir specified by `--datadir` when starting |
| 44 | + // your reth node. |
| 45 | + let reth = Reth::new() |
| 46 | + .dev() |
| 47 | + .disable_discovery() |
| 48 | + .block_time("1s") |
| 49 | + .data_dir(data_dir.clone()) |
| 50 | + .spawn(); |
| 51 | + |
| 52 | + let db_path = data_dir.join("db"); |
| 53 | + |
| 54 | + // Initialize the provider with the reth-db layer. The reth-db layer intercepts the rpc |
| 55 | + // requests and returns the results from the reth-db database. |
| 56 | + // Any RPC method that is not implemented in the RethDbProvider gracefully falls back to the |
| 57 | + // RPC provider specified in the `on_http` method. |
| 58 | + let provider = |
| 59 | + ProviderBuilder::new().layer(RethDbLayer::new(db_path)).on_http(reth.endpoint_url()); |
| 60 | + |
| 61 | + // Initialize the RPC provider to compare the time taken to fetch the results. |
| 62 | + let rpc_provider = ProviderBuilder::new().on_http(reth.endpoint_url()); |
| 63 | + |
| 64 | + println!("--------get_block_number---------"); |
| 65 | + |
| 66 | + let start_t = std::time::Instant::now(); |
| 67 | + let latest_block_db = provider.get_block_number().await.unwrap(); |
| 68 | + println!("via reth-db: {:?}", start_t.elapsed()); |
| 69 | + |
| 70 | + let start_t = std::time::Instant::now(); |
| 71 | + let latest_block_rpc = rpc_provider.get_block_number().await.unwrap(); |
| 72 | + println!("via rpc: {:?}\n", start_t.elapsed()); |
| 73 | + |
| 74 | + assert_eq!(latest_block_db, latest_block_rpc); |
| 75 | + |
| 76 | + println!("------get_transaction_count------"); |
| 77 | + |
| 78 | + let alice = address!("14dC79964da2C08b23698B3D3cc7Ca32193d9955"); |
| 79 | + |
| 80 | + let start_t = std::time::Instant::now(); |
| 81 | + let nonce_db = |
| 82 | + provider.get_transaction_count(alice).block_id(BlockId::latest()).await.unwrap(); |
| 83 | + println!("via reth-db: {:?}", start_t.elapsed()); |
| 84 | + |
| 85 | + let start_t = std::time::Instant::now(); |
| 86 | + let nonce_rpc = |
| 87 | + rpc_provider.get_transaction_count(alice).block_id(BlockId::latest()).await.unwrap(); |
| 88 | + println!("via rpc: {:?}\n", start_t.elapsed()); |
| 89 | + |
| 90 | + assert_eq!(nonce_db, nonce_rpc); |
| 91 | + }) |
| 92 | + .await; |
| 93 | + |
| 94 | + Ok(()) |
| 95 | +} |
| 96 | + |
| 97 | +/// Implement the `ProviderLayer` trait for the `RethDBLayer` struct. |
| 98 | +impl<P, T> ProviderLayer<P, T> for RethDbLayer |
| 99 | +where |
| 100 | + P: Provider<T>, |
| 101 | + T: Transport + Clone, |
| 102 | +{ |
| 103 | + type Provider = RethDbProvider<P, T>; |
| 104 | + |
| 105 | + fn layer(&self, inner: P) -> Self::Provider { |
| 106 | + RethDbProvider::new(inner, self.db_path().clone()) |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +/// A provider that overrides the vanilla `Provider` trait to get results from the reth-db. |
| 111 | +/// |
| 112 | +/// It holds the `reth_provider::ProviderFactory` that enables read-only access to the database |
| 113 | +/// tables and static files. |
| 114 | +#[derive(Clone, Debug)] |
| 115 | +pub struct RethDbProvider<P, T> { |
| 116 | + inner: P, |
| 117 | + db_path: PathBuf, |
| 118 | + provider_factory: DbAccessor, |
| 119 | + _pd: PhantomData<T>, |
| 120 | +} |
| 121 | + |
| 122 | +impl<P, T> RethDbProvider<P, T> { |
| 123 | + /// Create a new `RethDbProvider` instance. |
| 124 | + pub fn new(inner: P, db_path: PathBuf) -> Self { |
| 125 | + let db = open_db_read_only(&db_path, Default::default()).unwrap(); |
| 126 | + let chain_spec = ChainSpecBuilder::mainnet().build(); |
| 127 | + let static_file_provider = |
| 128 | + StaticFileProvider::read_only(db_path.join("static_files"), false).unwrap(); |
| 129 | + |
| 130 | + let provider_factory = |
| 131 | + ProviderFactory::new(db.into(), chain_spec.into(), static_file_provider); |
| 132 | + |
| 133 | + let db_accessor: DbAccessor< |
| 134 | + ProviderFactory<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>, |
| 135 | + > = DbAccessor::new(provider_factory); |
| 136 | + Self { inner, db_path, provider_factory: db_accessor, _pd: PhantomData } |
| 137 | + } |
| 138 | + |
| 139 | + const fn factory(&self) -> &DbAccessor { |
| 140 | + &self.provider_factory |
| 141 | + } |
| 142 | + |
| 143 | + /// Get the DB Path |
| 144 | + pub fn db_path(&self) -> PathBuf { |
| 145 | + self.db_path.clone() |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +/// Implement the `Provider` trait for the `RethDbProvider` struct. |
| 150 | +/// |
| 151 | +/// This is where we override specific RPC methods to fetch from the reth-db. |
| 152 | +impl<P, T> Provider<T> for RethDbProvider<P, T> |
| 153 | +where |
| 154 | + P: Provider<T>, |
| 155 | + T: Transport + Clone, |
| 156 | +{ |
| 157 | + fn root(&self) -> &RootProvider<T> { |
| 158 | + self.inner.root() |
| 159 | + } |
| 160 | + |
| 161 | + /// Override the `get_block_number` method to fetch the latest block number from the reth-db. |
| 162 | + fn get_block_number(&self) -> ProviderCall<T, NoParams, U64, u64> { |
| 163 | + let provider = self.factory().provider().map_err(TransportErrorKind::custom).unwrap(); |
| 164 | + |
| 165 | + let best = provider.best_block_number().map_err(TransportErrorKind::custom); |
| 166 | + |
| 167 | + ProviderCall::ready(best) |
| 168 | + } |
| 169 | + |
| 170 | + /// Override the `get_transaction_count` method to fetch the transaction count of an address. |
| 171 | + /// |
| 172 | + /// `RpcWithBlock` uses `ProviderCall` under the hood. |
| 173 | + fn get_transaction_count(&self, address: Address) -> RpcWithBlock<T, Address, U64, u64> { |
| 174 | + let this = self.factory().clone(); |
| 175 | + RpcWithBlock::new_provider(move |block_id| { |
| 176 | + let provider = this.provider_at(block_id).map_err(TransportErrorKind::custom).unwrap(); |
| 177 | + |
| 178 | + let maybe_acc = |
| 179 | + provider.basic_account(address).map_err(TransportErrorKind::custom).unwrap(); |
| 180 | + |
| 181 | + let nonce = maybe_acc.map(|acc| acc.nonce).unwrap_or_default(); |
| 182 | + |
| 183 | + ProviderCall::ready(Ok(nonce)) |
| 184 | + }) |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +/// A helper type to get the appropriate DB provider. |
| 189 | +#[derive(Debug, Clone)] |
| 190 | +struct DbAccessor<DB = ProviderFactory<NodeTypesWithDBAdapter<EthereumNode, Arc<DatabaseEnv>>>> |
| 191 | +where |
| 192 | + DB: DatabaseProviderFactory<Provider: TryIntoHistoricalStateProvider + BlockNumReader>, |
| 193 | +{ |
| 194 | + inner: DB, |
| 195 | +} |
| 196 | + |
| 197 | +impl<DB> DbAccessor<DB> |
| 198 | +where |
| 199 | + DB: DatabaseProviderFactory<Provider: TryIntoHistoricalStateProvider + BlockNumReader>, |
| 200 | +{ |
| 201 | + const fn new(inner: DB) -> Self { |
| 202 | + Self { inner } |
| 203 | + } |
| 204 | + |
| 205 | + fn provider(&self) -> Result<DB::Provider, ProviderError> { |
| 206 | + self.inner.database_provider_ro() |
| 207 | + } |
| 208 | + |
| 209 | + fn provider_at(&self, block_id: BlockId) -> Result<Box<dyn StateProvider>, ProviderError> { |
| 210 | + let provider = self.inner.database_provider_ro()?; |
| 211 | + |
| 212 | + let block_number = match block_id { |
| 213 | + BlockId::Hash(hash) => { |
| 214 | + if let Some(num) = provider.block_number(hash.into())? { |
| 215 | + num |
| 216 | + } else { |
| 217 | + return Err(ProviderError::BlockHashNotFound(hash.into())); |
| 218 | + } |
| 219 | + } |
| 220 | + BlockId::Number(BlockNumberOrTag::Number(num)) => num, |
| 221 | + _ => provider.best_block_number()?, |
| 222 | + }; |
| 223 | + |
| 224 | + provider.try_into_history_at_block(block_number) |
| 225 | + } |
| 226 | +} |
0 commit comments