diff --git a/crates/node-types/src/lib.rs b/crates/node-types/src/lib.rs index 251d995..71fb7bf 100644 --- a/crates/node-types/src/lib.rs +++ b/crates/node-types/src/lib.rs @@ -11,6 +11,9 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +mod utils; +pub use utils::{NodeTypesDbTrait, Pnt}; + use reth::{ primitives::EthPrimitives, providers::{ @@ -96,21 +99,6 @@ where type DB = Db; } -/// Convenience trait to aggregate the DB requirements -pub trait NodeTypesDbTrait: - reth_db::database::Database + reth_db::database_metrics::DatabaseMetrics + Clone + Unpin + 'static -{ -} - -impl NodeTypesDbTrait for T where - T: reth_db::database::Database - + reth_db::database_metrics::DatabaseMetrics - + Clone - + Unpin - + 'static -{ -} - /// Shim to impl [`CanonStateSubscriptions`] #[derive(Debug, Clone)] pub struct SharedCanonState { @@ -172,3 +160,15 @@ where self.sender.subscribe() } } + +#[cfg(test)] +mod test { + use super::*; + + #[allow(dead_code)] + fn compile_check() { + fn inner() {} + + inner::>>(); + } +} diff --git a/crates/node-types/src/utils.rs b/crates/node-types/src/utils.rs new file mode 100644 index 0000000..fa5484c --- /dev/null +++ b/crates/node-types/src/utils.rs @@ -0,0 +1,36 @@ +use reth::{primitives::EthPrimitives, providers::providers::ProviderNodeTypes}; +use reth_chainspec::ChainSpec; +use reth_db::mdbx; +use std::sync::Arc; + +/// Convenience trait for specifying the [`ProviderNodeTypes`] implementation +/// required for Signet functionality. This is used to condense many trait +/// bounds. +pub trait Pnt: + ProviderNodeTypes> +{ +} + +impl Pnt for T where + T: ProviderNodeTypes< + ChainSpec = ChainSpec, + Primitives = EthPrimitives, + DB = Arc, + > +{ +} + +/// Convenience trait to aggregate the DB requirements +pub trait NodeTypesDbTrait: + reth_db::database::Database + reth_db::database_metrics::DatabaseMetrics + Clone + Unpin + 'static +{ +} + +impl NodeTypesDbTrait for T where + T: reth_db::database::Database + + reth_db::database_metrics::DatabaseMetrics + + Clone + + Unpin + + 'static +{ +} diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 54e9833..166a018 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -9,10 +9,12 @@ homepage.workspace = true repository.workspace = true [dependencies] +signet-node-types.workspace = true + signet-bundle.workspace = true signet-evm.workspace = true -signet-types.workspace = true signet-tx-cache.workspace = true +signet-types.workspace = true ajj.workspace = true trevm.workspace = true @@ -20,6 +22,8 @@ trevm.workspace = true alloy.workspace = true reth.workspace = true reth-chainspec.workspace = true +reth-db.workspace = true +reth-db-common.workspace = true reth-evm-ethereum.workspace = true reth-node-api.workspace = true reth-rpc-eth-api.workspace = true @@ -35,6 +39,7 @@ tokio = { workspace = true, features = ["macros"] } tokio-util = "0.7.13" tower-http = { version = "0.6.2", features = ["cors"] } tracing.workspace = true +serde_json.workspace = true [dev-dependencies] signet-zenith.workspace = true diff --git a/crates/rpc/src/config.rs b/crates/rpc/src/config.rs index dc8510b..7dbc579 100644 --- a/crates/rpc/src/config.rs +++ b/crates/rpc/src/config.rs @@ -1,10 +1,9 @@ +use crate::utils::{serve_axum, serve_ipc, serve_ws}; use ajj::{Router, pubsub::ServerShutdown}; use reth::{args::RpcServerArgs, tasks::TaskExecutor}; use std::net::SocketAddr; use tokio::task::JoinHandle; -use crate::util::{serve_axum, serve_ipc, serve_ws}; - /// Guard to shutdown the RPC servers. When dropped, this will shutdown all /// running servers #[derive(Default)] diff --git a/crates/rpc/src/ctx.rs b/crates/rpc/src/ctx.rs index 0c80313..80a77c7 100644 --- a/crates/rpc/src/ctx.rs +++ b/crates/rpc/src/ctx.rs @@ -1,9 +1,8 @@ use crate::{ - Pnt, eth::EthError, interest::{ActiveFilter, FilterManager, FilterOutput, SubscriptionManager}, receipts::build_signet_receipt, - util::BlockRangeInclusiveIter, + utils::BlockRangeInclusiveIter, }; use alloy::{ consensus::{BlockHeader, Header, Signed, Transaction, TxEnvelope}, @@ -14,12 +13,11 @@ use alloy::{ }; use reth::{ core::primitives::SignerRecoverable, - primitives::{Block, EthPrimitives, Receipt, Recovered, RecoveredBlock, TransactionSigned}, + primitives::{Block, Receipt, Recovered, RecoveredBlock, TransactionSigned}, providers::{ BlockHashReader, BlockIdReader, BlockNumReader, CanonStateSubscriptions, HeaderProvider, - ProviderBlock, ProviderError, ProviderReceipt, ReceiptProvider, StateProviderFactory, - TransactionsProvider, - providers::{BlockchainProvider, ProviderNodeTypes}, + ProviderBlock, ProviderError, ProviderFactory, ProviderReceipt, ProviderResult, + ReceiptProvider, StateProviderFactory, TransactionsProvider, providers::BlockchainProvider, }, revm::{database::StateProviderDatabase, primitives::hardfork::SpecId}, rpc::{ @@ -40,6 +38,7 @@ use reth_chainspec::{BaseFeeParams, ChainSpec, ChainSpecProvider}; use reth_node_api::{BlockBody, FullNodeComponents}; use reth_rpc_eth_api::{RpcBlock, RpcConvert, RpcReceipt, RpcTransaction}; use signet_evm::EvmNeedsTx; +use signet_node_types::Pnt; use signet_tx_cache::client::TxCache; use signet_types::{MagicSig, constants::SignetSystemConstants}; use std::{marker::PhantomData, sync::Arc}; @@ -80,17 +79,16 @@ where pub fn new( host: Host, constants: SignetSystemConstants, - provider: BlockchainProvider, + factory: ProviderFactory, eth_config: EthConfig, tx_cache: Option, spawner: Tasks, - ) -> Self + ) -> ProviderResult where Tasks: TaskSpawner + Clone + 'static, { - let inner = RpcCtxInner::new(host, constants, provider, eth_config, tx_cache, spawner); - - Self { inner: Arc::new(inner) } + RpcCtxInner::new(host, constants, factory, eth_config, tx_cache, spawner) + .map(|inner| Self { inner: Arc::new(inner) }) } } @@ -136,16 +134,16 @@ where pub fn new( host: Host, constants: SignetSystemConstants, - provider: BlockchainProvider, + factory: ProviderFactory, eth_config: EthConfig, tx_cache: Option, spawner: Tasks, - ) -> Self + ) -> ProviderResult where Tasks: TaskSpawner + Clone + 'static, { - let signet = SignetCtx::new(constants, provider, eth_config, tx_cache, spawner); - Self { host, signet } + SignetCtx::new(constants, factory, eth_config, tx_cache, spawner) + .map(|signet| Self { host, signet }) } pub const fn host(&self) -> &Host { @@ -194,6 +192,7 @@ where eth_config: EthConfig, // State stuff + factory: ProviderFactory, provider: BlockchainProvider, cache: EthStateCache< ProviderBlock>, @@ -217,20 +216,22 @@ where impl SignetCtx where - Inner: ProviderNodeTypes, + Inner: Pnt, { /// Instantiate a new `SignetCtx`, spawning necessary tasks to keep the /// relevant caches up to date. pub fn new( constants: SignetSystemConstants, - provider: BlockchainProvider, + factory: ProviderFactory, eth_config: EthConfig, tx_cache: Option, spawner: Tasks, - ) -> Self + ) -> ProviderResult where Tasks: TaskSpawner + Clone + 'static, { + let provider = BlockchainProvider::new(factory.clone())?; + let cache = EthStateCache::spawn_with(provider.clone(), eth_config.cache, spawner.clone()); let gas_oracle = GasPriceOracle::new(provider.clone(), eth_config.gas_oracle, cache.clone()); @@ -249,8 +250,9 @@ where let subs = SubscriptionManager::new(provider.clone(), eth_config.stale_filter_ttl); - Self { + Ok(Self { constants, + factory, provider, eth_config, cache, @@ -260,7 +262,7 @@ where filters, subs, _pd: PhantomData, - } + }) } /// Access the signet constants @@ -268,6 +270,11 @@ where &self.constants } + /// Access the signet [`ProviderFactory`]. + pub const fn factory(&self) -> &ProviderFactory { + &self.factory + } + /// Access the signet DB pub const fn provider(&self) -> &BlockchainProvider { &self.provider diff --git a/crates/rpc/src/eth/endpoints.rs b/crates/rpc/src/eth/endpoints.rs index 6705699..d223418 100644 --- a/crates/rpc/src/eth/endpoints.rs +++ b/crates/rpc/src/eth/endpoints.rs @@ -1,10 +1,9 @@ use crate::{ - Pnt, ctx::RpcCtx, eth::{CallErrorData, EthError}, interest::{FilterOutput, InterestKind}, receipts::build_signet_receipt, - util::{await_jh_option, await_jh_option_response, response_tri}, + utils::{await_jh_option, await_jh_option_response, response_tri}, }; use ajj::{HandlerCtx, ResponsePayload}; use alloy::{ @@ -28,6 +27,7 @@ use reth_node_api::FullNodeComponents; use reth_rpc_eth_api::{RpcBlock, RpcHeader, RpcReceipt, RpcTransaction}; use serde::Deserialize; use signet_evm::EvmErrored; +use signet_node_types::Pnt; use std::borrow::Cow; use tracing::{Instrument, debug, trace_span}; use trevm::revm::context::result::ExecutionResult; diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index c8fc25b..0d8c131 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -7,9 +7,10 @@ pub use error::EthError; mod helpers; pub use helpers::CallErrorData; -use crate::{Pnt, ctx::RpcCtx}; +use crate::ctx::RpcCtx; use alloy::{eips::BlockNumberOrTag, primitives::B256}; use reth_node_api::FullNodeComponents; +use signet_node_types::Pnt; /// Instantiate the `eth` API router. pub fn eth() -> ajj::Router> diff --git a/crates/rpc/src/inspect/db.rs b/crates/rpc/src/inspect/db.rs new file mode 100644 index 0000000..429d421 --- /dev/null +++ b/crates/rpc/src/inspect/db.rs @@ -0,0 +1,162 @@ +use ajj::serde_json; +use eyre::WrapErr; +use reth::providers::ProviderFactory; +use reth_db::{Database, TableViewer, table::Table}; +use reth_db_common::{DbTool, ListFilter}; +use signet_node_types::Pnt; +use std::sync::OnceLock; +use tracing::instrument; + +/// Modeled on the `Command` struct from `reth/crates/cli/commands/src/db/list.rs` +#[derive(Debug, serde::Deserialize)] +pub(crate) struct DbArgs( + /// The table name + String, // 0 + /// Skip first N entries + #[serde(default)] + usize, // 1 + /// How many items to take from the walker + #[serde(default)] + Option, // 2 + /// Search parameter for both keys and values. Prefix it with `0x` to search for binary data, + /// and text otherwise. + /// + /// ATTENTION! For compressed tables (`Transactions` and `Receipts`), there might be + /// missing results since the search uses the raw uncompressed value from the database. + #[serde(default)] + Option, // 3 +); + +impl DbArgs { + /// Get the table name. + pub(crate) fn table_name(&self) -> &str { + &self.0 + } + + /// Parse the table name into a [`reth_db::Tables`] enum. + pub(crate) fn table(&self) -> Result { + self.table_name().parse() + } + + /// Get the skip value. + pub(crate) const fn skip(&self) -> usize { + self.1 + } + + /// Get the length value. + pub(crate) fn len(&self) -> usize { + self.2.unwrap_or(5) + } + + /// Get the search value. + pub(crate) fn search(&self) -> Vec { + self.3 + .as_ref() + .map(|search| { + if let Some(search) = search.strip_prefix("0x") { + return alloy::primitives::hex::decode(search).unwrap(); + } + search.as_bytes().to_vec() + }) + .unwrap_or_default() + } + + /// Generate [`ListFilter`] from command. + pub(crate) fn list_filter(&self) -> ListFilter { + ListFilter { + skip: self.skip(), + len: self.len(), + search: self.search(), + min_row_size: 0, + min_key_size: 0, + min_value_size: 0, + reverse: false, + only_count: false, + } + } +} + +pub(crate) struct ListTableViewer<'a, 'b, N: Pnt> { + pub(crate) factory: &'b ProviderFactory, + pub(crate) args: &'a DbArgs, + + pub(crate) output: OnceLock>, +} + +impl<'a, 'b, N: Pnt> ListTableViewer<'a, 'b, N> { + /// Create a new `ListTableViewer`. + pub(crate) fn new(factory: &'b ProviderFactory, args: &'a DbArgs) -> Self { + Self { factory, args, output: Default::default() } + } + + /// Take the output if it has been initialized, otherwise return `None`. + pub(crate) fn take_output(self) -> Option> { + self.output.into_inner() + } +} + +impl TableViewer<()> for ListTableViewer<'_, '_, N> { + type Error = eyre::Report; + + #[instrument(skip(self), err)] + fn view(&self) -> eyre::Result<()> { + let tool = DbTool { provider_factory: self.factory.clone() }; + + self.factory.db_ref().view(|tx| { + let table_db = + tx.inner.open_db(Some(self.args.table_name())).wrap_err("Could not open db.")?; + let stats = tx + .inner + .db_stat(&table_db) + .wrap_err(format!("Could not find table: {}", stringify!($table)))?; + let total_entries = stats.entries(); + let final_entry_idx = total_entries.saturating_sub(1); + eyre::ensure!( + self.args.skip() >= final_entry_idx, + "Skip value {} is greater than total entries {}", + self.args.skip(), + total_entries + ); + + let list_filter = self.args.list_filter(); + + let (list, _) = tool.list::(&list_filter)?; + + let json = + serde_json::value::to_raw_value(&list).wrap_err("Failed to serialize list")?; + + self.output.get_or_init(|| json); + + Ok(()) + })??; + + Ok(()) + } +} + +// Some code in this file is adapted from github.com/paradigmxyz/reth. +// +// Particularly the `reth/crates/cli/commands/src/db/list.rs` file. It is +// reproduced here under the terms of the MIT license, +// +// The MIT License (MIT) +// +// Copyright (c) 2022-2025 Reth Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. diff --git a/crates/rpc/src/inspect/endpoints.rs b/crates/rpc/src/inspect/endpoints.rs new file mode 100644 index 0000000..8de1e41 --- /dev/null +++ b/crates/rpc/src/inspect/endpoints.rs @@ -0,0 +1,37 @@ +use crate::{ + RpcCtx, + inspect::db::{DbArgs, ListTableViewer}, + utils::{await_jh_option_response, response_tri}, +}; +use ajj::{HandlerCtx, ResponsePayload}; +use reth_node_api::FullNodeComponents; +use signet_node_types::Pnt; + +/// Handler for the `db` endpoint in the `inspect` module. +pub(super) async fn db( + hctx: HandlerCtx, + args: DbArgs, + ctx: RpcCtx, +) -> ResponsePayload, String> +where + Host: FullNodeComponents, + Signet: Pnt, +{ + let task = async move { + let table: reth_db::Tables = response_tri!(args.table(), "invalid table name"); + + let viewer = ListTableViewer::new(ctx.signet().factory(), &args); + + response_tri!(table.view(&viewer), "Failed to view table"); + + let Some(output) = viewer.take_output() else { + return ResponsePayload::internal_error_message( + "No output generated. The task may have panicked or been cancelled. This is a bug, please report it.".into(), + ); + }; + + ResponsePayload::Success(output) + }; + + await_jh_option_response!(hctx.spawn_blocking(task)) +} diff --git a/crates/rpc/src/inspect/mod.rs b/crates/rpc/src/inspect/mod.rs new file mode 100644 index 0000000..ad4982e --- /dev/null +++ b/crates/rpc/src/inspect/mod.rs @@ -0,0 +1,16 @@ +pub(crate) mod db; + +mod endpoints; + +use crate::RpcCtx; +use reth_node_api::FullNodeComponents; +use signet_node_types::Pnt; + +/// Instantiate the `inspect` API router. +pub fn inspect() -> ajj::Router> +where + Host: FullNodeComponents, + Signet: Pnt, +{ + ajj::Router::new().route("db", endpoints::db::) +} diff --git a/crates/rpc/src/interest/subs.rs b/crates/rpc/src/interest/subs.rs index b36215d..36583d9 100644 --- a/crates/rpc/src/interest/subs.rs +++ b/crates/rpc/src/interest/subs.rs @@ -1,4 +1,4 @@ -use crate::{Pnt, interest::InterestKind}; +use crate::interest::InterestKind; use ajj::{HandlerCtx, serde_json}; use alloy::{primitives::U64, rpc::types::Log}; use dashmap::DashMap; @@ -6,6 +6,7 @@ use reth::{ providers::{CanonStateNotifications, CanonStateSubscriptions, providers::BlockchainProvider}, rpc::types::Header, }; +use signet_node_types::Pnt; use std::{ cmp::min, collections::VecDeque, diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index 4ae2687..543fd98 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -9,7 +9,8 @@ //! ## Usage Example //! //! ```rust -//! # use signet_rpc::{Pnt, RpcCtx}; +//! # use signet_rpc::{RpcCtx}; +//! # use signet_node_types::Pnt; //! # use reth_node_api::FullNodeComponents; //! # use reth::tasks::TaskExecutor; //! use signet_rpc::{router, ServeConfig}; @@ -60,19 +61,22 @@ pub use eth::{CallErrorData, EthError, eth}; mod signet; pub use signet::{error::SignetError, signet}; +mod inspect; +pub use inspect::inspect; + mod interest; pub mod receipts; /// Utils and simple serve functions. -pub mod util; -pub use util::Pnt; +pub mod utils; /// Re-exported for convenience pub use ::ajj; use ajj::Router; use reth_node_api::FullNodeComponents; +use signet_node_types::Pnt; /// Create a new router with the given host and signet types. pub fn router() -> Router> @@ -82,3 +86,12 @@ where { ajj::Router::new().nest("eth", eth::()).nest("signet", signet::()) } + +/// Create a new hazmat router that exposes the `inspect` API. +pub fn hazmat_router() -> Router> +where + Host: FullNodeComponents, + Signet: Pnt, +{ + ajj::Router::new().nest("inspect", inspect::inspect::()) +} diff --git a/crates/rpc/src/signet/endpoints.rs b/crates/rpc/src/signet/endpoints.rs index 7c545af..4dc988b 100644 --- a/crates/rpc/src/signet/endpoints.rs +++ b/crates/rpc/src/signet/endpoints.rs @@ -1,14 +1,14 @@ use crate::{ - Pnt, ctx::RpcCtx, signet::error::SignetError, - util::{await_jh_option, await_jh_option_response, response_tri}, + utils::{await_jh_option, await_jh_option_response, response_tri}, }; use ajj::{HandlerCtx, ResponsePayload}; use reth_node_api::FullNodeComponents; use signet_bundle::{ SignetBundleDriver, SignetCallBundle, SignetCallBundleResponse, SignetEthBundle, }; +use signet_node_types::Pnt; use signet_types::SignedOrder; use std::time::Duration; use tokio::select; diff --git a/crates/rpc/src/signet/mod.rs b/crates/rpc/src/signet/mod.rs index 6a9ef14..015139b 100644 --- a/crates/rpc/src/signet/mod.rs +++ b/crates/rpc/src/signet/mod.rs @@ -5,8 +5,9 @@ use endpoints::*; pub(crate) mod error; -use crate::{Pnt, ctx::RpcCtx}; +use crate::ctx::RpcCtx; use reth_node_api::FullNodeComponents; +use signet_node_types::Pnt; /// Instantiate a `signet` API router. pub fn signet() -> ajj::Router> diff --git a/crates/rpc/src/util.rs b/crates/rpc/src/utils.rs similarity index 93% rename from crates/rpc/src/util.rs rename to crates/rpc/src/utils.rs index 7483fa8..34a8ec1 100644 --- a/crates/rpc/src/util.rs +++ b/crates/rpc/src/utils.rs @@ -5,11 +5,7 @@ use ajj::{ use axum::http::HeaderValue; use interprocess::local_socket as ls; use reqwest::Method; -use reth::{ - primitives::EthPrimitives, providers::providers::ProviderNodeTypes, - rpc::builder::CorsDomainError, tasks::TaskExecutor, -}; -use reth_chainspec::ChainSpec; +use reth::{rpc::builder::CorsDomainError, tasks::TaskExecutor}; use std::{future::IntoFuture, iter::StepBy, net::SocketAddr, ops::RangeInclusive}; use tokio::task::JoinHandle; use tower_http::cors::{AllowOrigin, Any, CorsLayer}; @@ -73,13 +69,8 @@ macro_rules! response_tri { } }; } -pub(crate) use response_tri; -/// Convenience trait for specifying the [`ProviderNodeTypes`] implementation -/// required for Signet RPC functionality. -pub trait Pnt: ProviderNodeTypes {} - -impl Pnt for T where T: ProviderNodeTypes {} +pub(crate) use response_tri; /// An iterator that yields _inclusive_ block ranges of a given step size #[derive(Debug)]