diff --git a/bin/node/README.md b/bin/node/README.md index 055b54ff91..e63b5360bb 100644 --- a/bin/node/README.md +++ b/bin/node/README.md @@ -78,8 +78,10 @@ kona-node \ Many configuration options can be set via environment variables: - `KONA_NODE_L1_ETH_RPC` - L1 execution client RPC URL +- `KONA_NODE_L1_TRUST_RPC` - Whether to trust the L1 RPC without verification (default: true) - `KONA_NODE_L1_BEACON` - L1 beacon API URL - `KONA_NODE_L2_ENGINE_RPC` - L2 engine API URL +- `KONA_NODE_L2_TRUST_RPC` - Whether to trust the L2 RPC without verification (default: true) - `KONA_NODE_L2_ENGINE_AUTH` - Path to L2 engine JWT secret file - `KONA_NODE_MODE` - Node operation mode (default: validator) - `RUST_LOG` - Logging configuration @@ -143,4 +145,26 @@ kona-node info --help ## Advanced Configuration +### RPC Trust Configuration + +By default, Kona trusts RPC providers and does not perform additional block hash verification, optimizing for performance. This can be configured using trust flags: + +```bash +# For untrusted/public RPC providers (adds verification) +kona-node node \ + --l1 https://public-rpc-endpoint.com \ + --l1-trust-rpc false \ + --l2 https://another-public-rpc.com \ + --l2-trust-rpc false \ + # ... other options +``` + +**Security Considerations:** +- Default behavior (`true`): No additional verification, assumes RPC is trustworthy +- Verification mode (`false`): All block hashes are verified against requested hashes +- Use verification (`false`) for public or third-party RPC endpoints +- Default trust (`true`) is suitable for local nodes and trusted infrastructure + +### Production Deployments + For production deployments and advanced configurations, refer to the docker recipe in the main repository at `docker/recipes/kona-node/` which provides a complete setup example with monitoring and multiple services. diff --git a/bin/node/src/commands/node.rs b/bin/node/src/commands/node.rs index 10fb565aa0..316591bf93 100644 --- a/bin/node/src/commands/node.rs +++ b/bin/node/src/commands/node.rs @@ -82,12 +82,30 @@ pub struct NodeCommand { /// URL of the L1 execution client RPC API. #[arg(long, visible_alias = "l1", env = "KONA_NODE_L1_ETH_RPC")] pub l1_eth_rpc: Url, + /// Whether to trust the L1 RPC. + /// If false, block hash verification is performed for all retrieved blocks. + #[arg( + long, + visible_alias = "l1.trust-rpc", + env = "KONA_NODE_L1_TRUST_RPC", + default_value = "true" + )] + pub l1_trust_rpc: bool, /// URL of the L1 beacon API. #[arg(long, visible_alias = "l1.beacon", env = "KONA_NODE_L1_BEACON")] pub l1_beacon: Url, /// URL of the engine API endpoint of an L2 execution client. #[arg(long, visible_alias = "l2", env = "KONA_NODE_L2_ENGINE_RPC")] pub l2_engine_rpc: Url, + /// Whether to trust the L2 RPC. + /// If false, block hash verification is performed for all retrieved blocks. + #[arg( + long, + visible_alias = "l2.trust-rpc", + env = "KONA_NODE_L2_TRUST_RPC", + default_value = "true" + )] + pub l2_trust_rpc: bool, /// JWT secret for the auth-rpc endpoint of the execution client. /// This MUST be a valid path to a file containing the hex-encoded JWT secret. #[arg(long, visible_alias = "l2.jwt-secret", env = "KONA_NODE_L2_ENGINE_AUTH")] @@ -111,8 +129,10 @@ impl Default for NodeCommand { fn default() -> Self { Self { l1_eth_rpc: Url::parse("http://localhost:8545").unwrap(), + l1_trust_rpc: true, l1_beacon: Url::parse("http://localhost:5052").unwrap(), l2_engine_rpc: Url::parse("http://localhost:8551").unwrap(), + l2_trust_rpc: true, l2_engine_jwt_secret: None, l2_config_file: None, node_mode: NodeMode::Validator, @@ -264,8 +284,10 @@ impl NodeCommand { .with_mode(self.node_mode) .with_jwt_secret(jwt_secret) .with_l1_provider_rpc_url(self.l1_eth_rpc) + .with_l1_trust_rpc(self.l1_trust_rpc) .with_l1_beacon_api_url(self.l1_beacon) .with_l2_engine_rpc_url(self.l2_engine_rpc) + .with_l2_trust_rpc(self.l2_trust_rpc) .with_p2p_config(p2p_config) .with_rpc_config(rpc_config) .with_sequencer_config(self.sequencer_flags.config()) diff --git a/crates/node/service/src/actors/derivation.rs b/crates/node/service/src/actors/derivation.rs index ad5e4869ba..198f95d0bd 100644 --- a/crates/node/service/src/actors/derivation.rs +++ b/crates/node/service/src/actors/derivation.rs @@ -98,10 +98,14 @@ pub trait PipelineBuilder: Send + Sync + 'static { pub struct DerivationBuilder { /// The L1 provider. pub l1_provider: RootProvider, + /// Whether to trust the L1 RPC. + pub l1_trust_rpc: bool, /// The L1 beacon client. pub l1_beacon: OnlineBeaconClient, /// The L2 provider. pub l2_provider: RootProvider, + /// Whether to trust the L2 RPC. + pub l2_trust_rpc: bool, /// The rollup config. pub rollup_config: Arc, /// The interop mode. @@ -114,12 +118,16 @@ impl PipelineBuilder for DerivationBuilder { async fn build(self) -> DerivationState { // Create the caching L1/L2 EL providers for derivation. - let l1_derivation_provider = - AlloyChainProvider::new(self.l1_provider.clone(), DERIVATION_PROVIDER_CACHE_SIZE); - let l2_derivation_provider = AlloyL2ChainProvider::new( + let l1_derivation_provider = AlloyChainProvider::new_with_trust( + self.l1_provider.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l1_trust_rpc, + ); + let l2_derivation_provider = AlloyL2ChainProvider::new_with_trust( self.l2_provider.clone(), self.rollup_config.clone(), DERIVATION_PROVIDER_CACHE_SIZE, + self.l2_trust_rpc, ); let pipeline = match self.interop_mode { diff --git a/crates/node/service/src/actors/sequencer/actor.rs b/crates/node/service/src/actors/sequencer/actor.rs index 68abe68239..7ffc1082eb 100644 --- a/crates/node/service/src/actors/sequencer/actor.rs +++ b/crates/node/service/src/actors/sequencer/actor.rs @@ -119,20 +119,28 @@ pub struct SequencerBuilder { pub rollup_cfg: Arc, /// The L1 provider. pub l1_provider: RootProvider, + /// Whether to trust the L1 RPC. + pub l1_trust_rpc: bool, /// The L2 provider. pub l2_provider: RootProvider, + /// Whether to trust the L2 RPC. + pub l2_trust_rpc: bool, } impl AttributesBuilderConfig for SequencerBuilder { type AB = StatefulAttributesBuilder; fn build(self) -> Self::AB { - let l1_derivation_provider = - AlloyChainProvider::new(self.l1_provider.clone(), DERIVATION_PROVIDER_CACHE_SIZE); - let l2_derivation_provider = AlloyL2ChainProvider::new( + let l1_derivation_provider = AlloyChainProvider::new_with_trust( + self.l1_provider.clone(), + DERIVATION_PROVIDER_CACHE_SIZE, + self.l1_trust_rpc, + ); + let l2_derivation_provider = AlloyL2ChainProvider::new_with_trust( self.l2_provider.clone(), self.rollup_cfg.clone(), DERIVATION_PROVIDER_CACHE_SIZE, + self.l2_trust_rpc, ); StatefulAttributesBuilder::new( self.rollup_cfg, diff --git a/crates/node/service/src/service/standard/builder.rs b/crates/node/service/src/service/standard/builder.rs index 993a94a3c4..d35233248e 100644 --- a/crates/node/service/src/service/standard/builder.rs +++ b/crates/node/service/src/service/standard/builder.rs @@ -26,10 +26,14 @@ pub struct RollupNodeBuilder { config: RollupConfig, /// The L1 EL provider RPC URL. l1_provider_rpc_url: Option, + /// Whether to trust the L1 RPC. + l1_trust_rpc: bool, /// The L1 beacon API URL. l1_beacon_api_url: Option, /// The L2 engine RPC URL. l2_engine_rpc_url: Option, + /// Whether to trust the L2 RPC. + l2_trust_rpc: bool, /// The JWT secret. jwt_secret: Option, /// The [`NetworkConfig`]. @@ -60,6 +64,11 @@ impl RollupNodeBuilder { Self { l1_provider_rpc_url: Some(l1_provider_rpc_url), ..self } } + /// Sets whether to trust the L1 RPC. + pub fn with_l1_trust_rpc(self, l1_trust_rpc: bool) -> Self { + Self { l1_trust_rpc, ..self } + } + /// Appends an L1 beacon API URL to the builder. pub fn with_l1_beacon_api_url(self, l1_beacon_api_url: Url) -> Self { Self { l1_beacon_api_url: Some(l1_beacon_api_url), ..self } @@ -70,6 +79,11 @@ impl RollupNodeBuilder { Self { l2_engine_rpc_url: Some(l2_engine_rpc_url), ..self } } + /// Sets whether to trust the L2 RPC. + pub fn with_l2_trust_rpc(self, l2_trust_rpc: bool) -> Self { + Self { l2_trust_rpc, ..self } + } + /// Appends a JWT secret to the builder. pub fn with_jwt_secret(self, jwt_secret: JwtSecret) -> Self { Self { jwt_secret: Some(jwt_secret), ..self } @@ -136,8 +150,10 @@ impl RollupNodeBuilder { config: rollup_config, interop_mode: self.interop_mode, l1_provider, + l1_trust_rpc: self.l1_trust_rpc, l1_beacon, l2_provider, + l2_trust_rpc: self.l2_trust_rpc, engine_builder, rpc_builder: self.rpc_config, p2p_config, diff --git a/crates/node/service/src/service/standard/node.rs b/crates/node/service/src/service/standard/node.rs index 2979076fc2..c612bee291 100644 --- a/crates/node/service/src/service/standard/node.rs +++ b/crates/node/service/src/service/standard/node.rs @@ -27,10 +27,14 @@ pub struct RollupNode { pub(crate) interop_mode: InteropMode, /// The L1 EL provider. pub(crate) l1_provider: RootProvider, + /// Whether to trust the L1 RPC. + pub(crate) l1_trust_rpc: bool, /// The L1 beacon API. pub(crate) l1_beacon: OnlineBeaconClient, /// The L2 EL provider. pub(crate) l2_provider: RootProvider, + /// Whether to trust the L2 RPC. + pub(crate) l2_trust_rpc: bool, /// The [`EngineBuilder`] for the node. pub(crate) engine_builder: EngineBuilder, /// The [`RpcBuilder`] for the node. @@ -79,7 +83,9 @@ impl RollupNodeService for RollupNode { seq_cfg: self.sequencer_config.clone(), rollup_cfg: self.config.clone(), l1_provider: self.l1_provider.clone(), + l1_trust_rpc: self.l1_trust_rpc, l2_provider: self.l2_provider.clone(), + l2_trust_rpc: self.l2_trust_rpc, } } @@ -94,8 +100,10 @@ impl RollupNodeService for RollupNode { fn derivation_builder(&self) -> DerivationBuilder { DerivationBuilder { l1_provider: self.l1_provider.clone(), + l1_trust_rpc: self.l1_trust_rpc, l1_beacon: self.l1_beacon.clone(), l2_provider: self.l2_provider.clone(), + l2_trust_rpc: self.l2_trust_rpc, rollup_config: self.config.clone(), interop_mode: self.interop_mode, } diff --git a/crates/providers/providers-alloy/src/chain_provider.rs b/crates/providers/providers-alloy/src/chain_provider.rs index cf0beffb5d..82e580e936 100644 --- a/crates/providers/providers-alloy/src/chain_provider.rs +++ b/crates/providers/providers-alloy/src/chain_provider.rs @@ -19,6 +19,8 @@ use std::{boxed::Box, num::NonZeroUsize, vec::Vec}; pub struct AlloyChainProvider { /// The inner Ethereum JSON-RPC provider. pub inner: RootProvider, + /// Whether to trust the RPC without verification. + pub trust_rpc: bool, /// `header_by_hash` LRU cache. header_by_hash_cache: LruCache, /// `receipts_by_hash_cache` LRU cache. @@ -33,8 +35,17 @@ impl AlloyChainProvider { /// ## Panics /// - Panics if `cache_size` is zero. pub fn new(inner: RootProvider, cache_size: usize) -> Self { + Self::new_with_trust(inner, cache_size, true) + } + + /// Creates a new [AlloyChainProvider] with the given alloy provider and trust setting. + /// + /// ## Panics + /// - Panics if `cache_size` is zero. + pub fn new_with_trust(inner: RootProvider, cache_size: usize, trust_rpc: bool) -> Self { Self { inner, + trust_rpc, header_by_hash_cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()), receipts_by_hash_cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()), block_info_and_transactions_by_hash_cache: LruCache::new( @@ -67,6 +78,31 @@ impl AlloyChainProvider { pub async fn chain_id(&mut self) -> Result> { self.inner.get_chain_id().await } + + /// Verifies that a header's hash matches the expected hash when trust_rpc is false. + fn verify_header_hash( + &self, + header: &Header, + expected_hash: B256, + ) -> Result<(), AlloyChainProviderError> { + if self.trust_rpc { + return Ok(()); + } + + let actual_hash = header.hash_slow(); + if actual_hash != expected_hash { + return Err(AlloyChainProviderError::Transport(RpcError::Transport( + TransportErrorKind::Custom( + format!( + "Header hash mismatch: expected {expected_hash:?}, got {actual_hash:?}" + ) + .into(), + ), + ))); + } + + Ok(()) + } } /// An error for the [AlloyChainProvider]. @@ -126,6 +162,9 @@ impl ChainProvider for AlloyChainProvider { .ok_or(AlloyChainProviderError::BlockNotFound(hash.into()))?; let header = block.header.into_consensus(); + // Verify the header hash matches what we requested + self.verify_header_hash(&header, hash)?; + self.header_by_hash_cache.put(hash, header.clone()); kona_macros::inc!(gauge, Metrics::CACHE_ENTRIES, "cache" => "header_by_hash"); @@ -212,6 +251,9 @@ impl ChainProvider for AlloyChainProvider { .into_consensus() .map_transactions(|t| t.inner.into_inner()); + // Verify the block hash matches what we requested + self.verify_header_hash(&block.header, hash)?; + let block_info = BlockInfo { hash: block.header.hash_slow(), number: block.header.number, diff --git a/crates/providers/providers-alloy/src/l2_chain_provider.rs b/crates/providers/providers-alloy/src/l2_chain_provider.rs index f1307766f4..cbdb3a2916 100644 --- a/crates/providers/providers-alloy/src/l2_chain_provider.rs +++ b/crates/providers/providers-alloy/src/l2_chain_provider.rs @@ -3,7 +3,7 @@ #[cfg(feature = "metrics")] use crate::Metrics; use alloy_eips::BlockId; -use alloy_primitives::Bytes; +use alloy_primitives::{B256, Bytes}; use alloy_provider::{Provider, RootProvider}; use alloy_rpc_client::RpcClient; use alloy_rpc_types_engine::JwtSecret; @@ -29,6 +29,8 @@ use tower::ServiceBuilder; pub struct AlloyL2ChainProvider { /// The inner Ethereum JSON-RPC provider. inner: RootProvider, + /// Whether to trust the RPC without verification. + trust_rpc: bool, /// The rollup configuration. rollup_config: Arc, /// The `block_by_number` LRU cache. @@ -44,9 +46,24 @@ impl AlloyL2ChainProvider { inner: RootProvider, rollup_config: Arc, cache_size: usize, + ) -> Self { + Self::new_with_trust(inner, rollup_config, cache_size, true) + } + + /// Creates a new [AlloyL2ChainProvider] with the given alloy provider, [RollupConfig], and + /// trust setting. + /// + /// ## Panics + /// - Panics if `cache_size` is zero. + pub fn new_with_trust( + inner: RootProvider, + rollup_config: Arc, + cache_size: usize, + trust_rpc: bool, ) -> Self { Self { inner, + trust_rpc, rollup_config, block_by_number_cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()), } @@ -62,6 +79,25 @@ impl AlloyL2ChainProvider { self.inner.get_block_number().await } + /// Verifies that a block's hash matches the expected hash when trust_rpc is false. + fn verify_block_hash( + &self, + block_hash: B256, + expected_hash: B256, + ) -> Result<(), RpcError> { + if self.trust_rpc { + return Ok(()); + } + + if block_hash != expected_hash { + return Err(RpcError::local_usage_str(&format!( + "Block hash mismatch: expected {expected_hash:?}, got {block_hash:?}" + ))); + } + + Ok(()) + } + /// Returns the [L2BlockInfo] for the given [BlockId]. [None] is returned if the block /// does not exist. pub async fn block_info_by_id( @@ -79,7 +115,16 @@ impl AlloyL2ChainProvider { let result = async { let block = match id { BlockId::Number(num) => self.inner.get_block_by_number(num).full().await?, - BlockId::Hash(hash) => self.inner.get_block_by_hash(hash.block_hash).full().await?, + BlockId::Hash(hash) => { + let block = self.inner.get_block_by_hash(hash.block_hash).full().await?; + + // Verify block hash matches if we fetched by hash + if let Some(ref b) = block { + self.verify_block_hash(b.header.hash, hash.block_hash)?; + } + + block + } }; match block { diff --git a/docs/docs/pages/node/configuration.mdx b/docs/docs/pages/node/configuration.mdx index 9abac11ff3..3f94a38c7b 100644 --- a/docs/docs/pages/node/configuration.mdx +++ b/docs/docs/pages/node/configuration.mdx @@ -23,8 +23,10 @@ For more details on each flag, see the inline help (`kona-node node --help`) or |------|-----|-------------|----------|---------| | `--mode ` | `KONA_NODE_MODE` | Mode of operation for the node | Yes | `verifier` | | `--l1-eth-rpc ` | `KONA_NODE_L1_ETH_RPC` | URL of the L1 execution client RPC API | Yes | - | +| `--l1-trust-rpc ` | `KONA_NODE_L1_TRUST_RPC` | Whether to trust the L1 RPC without verification | No | `true` | | `--l1-beacon ` | `KONA_NODE_L1_BEACON` | URL of the L1 beacon API | Yes | - | | `--l2-engine-rpc ` | `KONA_NODE_L2_ENGINE_RPC` | URL of the engine API endpoint of an L2 execution client | Yes | - | +| `--l2-trust-rpc ` | `KONA_NODE_L2_TRUST_RPC` | Whether to trust the L2 RPC without verification | No | `true` | | `--l2-engine-jwt-secret ` | `KONA_NODE_L2_ENGINE_AUTH` | Path to file containing the hex-encoded JWT secret for the execution client | No | - | | `--l2-config-file ` | `KONA_NODE_ROLLUP_CONFIG` | Path to a custom L2 rollup configuration file | No | - | | `--l1-runtime-config-reload-interval ` | `KONA_NODE_L1_RUNTIME_CONFIG_RELOAD_INTERVAL` | Poll interval for reloading runtime config | No | `600` | @@ -128,3 +130,56 @@ Supported chain names include all those recognized by `alloy_chains` (e.g., `opt | `--supervisor.jwt.secret ` | `KONA_NODE_SUPERVISOR_JWT_SECRET` | JWT secret for supervisor websocket authentication | - | | `--supervisor.jwt.secret.file ` | `KONA_NODE_SUPERVISOR_JWT_SECRET_FILE` | Path to file containing JWT secret | - | +## RPC Trust Configuration + +The `--l1-trust-rpc` and `--l2-trust-rpc` flags control whether Kona performs additional verification on RPC responses to protect against malicious or faulty RPC providers. + +### Trust Modes + +**Default Behavior (trust enabled, `true`):** +- No additional block hash verification is performed +- Optimized for performance +- Suitable for local nodes and trusted infrastructure +- Assumes the RPC provider is reliable and honest + +**Verification Mode (trust disabled, `false`):** +- All fetched blocks have their hashes verified against the requested hashes +- Protects against malicious RPC providers returning incorrect blocks +- Recommended for public or third-party RPC endpoints +- Small performance overhead due to hash verification + +### Examples + +**Using trusted local RPCs (default):** +```bash +kona-node node \ + --l1-eth-rpc http://localhost:8545 \ + --l2-engine-rpc http://localhost:8551 \ + # trust-rpc defaults to true, no need to specify +``` + +**Using untrusted public RPCs:** +```bash +kona-node node \ + --l1-eth-rpc https://public-eth-rpc.com \ + --l1-trust-rpc false \ + --l2-engine-rpc https://public-l2-rpc.com \ + --l2-trust-rpc false +``` + +**Mixed trust configuration:** +```bash +kona-node node \ + --l1-eth-rpc https://public-eth-rpc.com \ + --l1-trust-rpc false \ + --l2-engine-rpc http://localhost:8551 \ + # L2 trust-rpc defaults to true for local engine +``` + +### Security Recommendations + +1. **Local Infrastructure**: Keep the default `true` setting for RPCs you control +2. **Public RPCs**: Always set `--trust-rpc false` when using third-party endpoints +3. **Shared Infrastructure**: Consider setting `--trust-rpc false` as a precaution +4. **Performance Testing**: The verification overhead is minimal but can be measured in high-throughput scenarios + diff --git a/docs/docs/pages/node/run/binary.mdx b/docs/docs/pages/node/run/binary.mdx index e678436202..76d3173b79 100644 --- a/docs/docs/pages/node/run/binary.mdx +++ b/docs/docs/pages/node/run/binary.mdx @@ -100,6 +100,24 @@ kona-node node \ That's it! Your node should connect to P2P and start syncing quickly. +#### RPC Trust Configuration + +By default, `kona-node` trusts RPC providers and doesn't perform additional verification. +This is suitable for local nodes but should be changed when using public RPC endpoints: + +``` +kona-node node \ + --chain 8453 \ + --l1-eth-rpc https://public-eth-rpc.com \ + --l1-trust-rpc false \ + --l1-beacon https://public-beacon-api.com \ + --l2-engine-rpc http://127.0.0.1:9551 \ + # L2 trust-rpc defaults to true for local engine +``` + +The `--l1-trust-rpc false` flag enables block hash verification for the L1 RPC, +protecting against malicious or faulty public RPC providers. + #### Debugging diff --git a/docs/docs/pages/node/run/docker.mdx b/docs/docs/pages/node/run/docker.mdx index 721eb807c4..5d9d628e23 100644 --- a/docs/docs/pages/node/run/docker.mdx +++ b/docs/docs/pages/node/run/docker.mdx @@ -89,6 +89,31 @@ By default, the recipe is configured for **OP Sepolia**. To sync a different OP - Update `op-reth --rollup.sequencer-http` endpoint - Update `kona-node --chain` parameter +### RPC Trust Configuration + +By default, `kona-node` trusts RPC providers (both L1 and L2). When using public or untrusted RPC endpoints, you should disable trust to enable block hash verification: + +```bash +# In cfg.env or as environment variables: +KONA_NODE_L1_TRUST_RPC=false +KONA_NODE_L2_TRUST_RPC=false +``` + +Or modify the docker-compose.yaml command: +```yaml +kona-node: + command: | + node + --chain op-sepolia + --l1-eth-rpc ${L1_PROVIDER_RPC} + --l1-beacon ${L1_BEACON_API} + --l1-trust-rpc false # Add this for untrusted L1 RPCs + --l2-engine-rpc ws://op-reth:8551 + --l2-trust-rpc false # Add this for untrusted L2 RPCs +``` + +See the [configuration guide](/node/configuration#rpc-trust-configuration) for more details on RPC trust settings. + ### Port Configuration All host ports can be customized via environment variables in `cfg.env`: