Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/optimism/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ reth-optimism-primitives.workspace = true
reth-optimism-forks.workspace = true
reth-optimism-exex.workspace = true
reth-optimism-trie.workspace = true
reth-node-builder.workspace = true
reth-db-api.workspace = true
reth-chainspec.workspace = true
reth-db.workspace = true

clap = { workspace = true, features = ["derive", "env"] }
tracing.workspace = true
Expand Down
84 changes: 49 additions & 35 deletions crates/optimism/bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#![allow(missing_docs, rustdoc::missing_crate_level_docs)]

use clap::{builder::ArgPredicate, Parser};
use eyre::ErrReport;
use futures_util::FutureExt;
use reth_db::DatabaseEnv;
use reth_node_builder::{NodeBuilder, WithLaunchContext};
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_cli::{chainspec::OpChainSpecParser, Cli};
use reth_optimism_exex::OpProofsExEx;
use reth_optimism_node::{args::RollupArgs, OpNode};
use reth_optimism_trie::{db::MdbxProofsStorage, InMemoryProofsStorage};
use reth_optimism_rpc::eth::proofs::{EthApiExt, EthApiOverrideServer};
use reth_optimism_trie::{db::MdbxProofsStorage, InMemoryProofsStorage, OpProofsStorage};
use tracing::info;

use std::{path::PathBuf, sync::Arc};
Expand Down Expand Up @@ -60,6 +65,33 @@ struct Args {
pub proofs_history_window: u64,
}

async fn launch_node_with_storage<S>(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this particular design we need to initialize Proof Storage regardless of proof history is enabled or not. Because we are checking the proofs_history_enabled inside this function.

Copy link
Copy Markdown

@emhane emhane Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you pls open a debt issue for improving this design permalinking to this code @sadiq1971

builder: WithLaunchContext<NodeBuilder<Arc<DatabaseEnv>, OpChainSpec>>,
args: Args,
storage: S,
) -> eyre::Result<(), ErrReport>
where
S: OpProofsStorage + Clone + 'static,
{
let storage_clone = storage.clone();
let proofs_history_enabled = args.proofs_history;
let handle = builder
.node(OpNode::new(args.rollup_args))
.install_exex_if(proofs_history_enabled, "proofs-history", async move |exex_context| {
Ok(OpProofsExEx::new(exex_context, storage, args.proofs_history_window).run().boxed())
})
.extend_rpc_modules(move |ctx| {
if proofs_history_enabled {
let api_ext = EthApiExt::new(ctx.registry.eth_api().clone(), storage_clone);
ctx.modules.replace_configured(api_ext.into_rpc())?;
}
Ok(())
})
.launch_with_debug_capabilities()
.await?;
handle.node_exit_future.await
}

fn main() {
reth_cli_util::sigsegv_handler::install();

Expand All @@ -73,42 +105,24 @@ fn main() {
if let Err(err) = Cli::<OpChainSpecParser, Args>::parse().run(async move |builder, args| {
info!(target: "reth::cli", "Launching node");

let rollup_args = args.rollup_args;
if args.proofs_history_storage_in_mem {
let storage = Arc::new(InMemoryProofsStorage::new());
launch_node_with_storage(builder, args.clone(), storage).await?;
} else {
let path = args
.proofs_history_storage_path
.clone()
.expect("Path must be provided if not using in-memory storage");
info!(target: "reth::cli", "Using on-disk storage for proofs history");

let handle = builder
.node(OpNode::new(rollup_args))
.install_exex_if(args.proofs_history, "proofs-history", async move |exex_context| {
if args.proofs_history_storage_in_mem {
info!(target: "reth::cli", "Using in-memory storage for proofs history");

let storage = InMemoryProofsStorage::new();
Ok(OpProofsExEx::new(
exex_context,
Arc::new(storage),
args.proofs_history_window,
)
.run()
.boxed())
} else {
let path = args
.proofs_history_storage_path
.expect("Path must be provided if not using in-memory storage");
info!(target: "reth::cli", "Using on-disk storage for proofs history");
let storage = Arc::new(
MdbxProofsStorage::new(&path)
.map_err(|e| eyre::eyre!("Failed to create MdbxProofsStorage: {e}"))?,
);
launch_node_with_storage(builder, args.clone(), storage).await?;
}

let storage = MdbxProofsStorage::new(&path)
.map_err(|e| eyre::eyre!("Failed to create MdbxProofsStorage: {e}"))?;
Ok(OpProofsExEx::new(
exex_context,
Arc::new(storage),
args.proofs_history_window,
)
.run()
.boxed())
}
})
.launch_with_debug_capabilities()
.await?;
handle.node_exit_future.await
Ok(())
}) {
eprintln!("Error: {err:?}");
std::process::exit(1);
Expand Down
7 changes: 3 additions & 4 deletions crates/optimism/exex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use reth_node_api::{FullNodeComponents, NodePrimitives};
use reth_node_types::NodeTypes;
use reth_optimism_trie::{BackfillJob, OpProofsStorage};
use reth_provider::{BlockNumReader, DBProvider, DatabaseProviderFactory};
use std::sync::Arc;

/// OP Proofs ExEx - processes blocks and tracks state changes within fault proof window.
///
Expand All @@ -19,13 +18,13 @@ use std::sync::Arc;
pub struct OpProofsExEx<Node, S>
where
Node: FullNodeComponents,
S: OpProofsStorage,
S: OpProofsStorage + Clone,
{
/// The ExEx context containing the node related utilities e.g. provider, notifications,
/// events.
ctx: ExExContext<Node>,
/// The type of storage DB.
storage: Arc<S>,
storage: S,
/// The window to span blocks for proofs history. Value is the number of blocks, received as
/// cli arg.
#[expect(dead_code)]
Expand All @@ -36,7 +35,7 @@ impl<Node, S, Primitives> OpProofsExEx<Node, S>
where
Node: FullNodeComponents<Types: NodeTypes<Primitives = Primitives>>,
Primitives: NodePrimitives,
S: OpProofsStorage,
S: OpProofsStorage + Clone,
{
/// Main execution loop for the ExEx
pub async fn run(mut self) -> eyre::Result<()> {
Expand Down
3 changes: 3 additions & 0 deletions crates/optimism/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ reth-chainspec.workspace = true
reth-chain-state.workspace = true
reth-rpc-engine-api.workspace = true
reth-rpc-convert.workspace = true
reth-provider.workspace = true

# op-reth
reth-optimism-evm.workspace = true
Expand All @@ -38,6 +39,7 @@ reth-optimism-txpool.workspace = true
# TODO remove node-builder import
reth-optimism-primitives = { workspace = true, features = ["reth-codec", "serde-bincode-compat", "serde"] }
reth-optimism-forks.workspace = true
reth-optimism-trie.workspace = true

# ethereum
alloy-eips.workspace = true
Expand All @@ -46,6 +48,7 @@ alloy-primitives.workspace = true
alloy-rpc-client.workspace = true
alloy-rpc-types-eth.workspace = true
alloy-rpc-types-debug.workspace = true
alloy-serde.workspace = true
alloy-transport.workspace = true
alloy-transport-http.workspace = true
alloy-consensus.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/optimism/rpc/src/eth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! OP-Reth `eth_` endpoint implementation.

pub mod ext;
pub mod proofs;
pub mod receipt;
pub mod transaction;

Expand Down
105 changes: 105 additions & 0 deletions crates/optimism/rpc/src/eth/proofs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! Historical proofs RPC server implementation.

use alloy_eips::BlockId;
use alloy_primitives::Address;
use alloy_rpc_types_eth::EIP1186AccountProofResponse;
use alloy_serde::JsonStorageKey;
use async_trait::async_trait;
use derive_more::Constructor;
use jsonrpsee::proc_macros::rpc;
use jsonrpsee_core::RpcResult;
use jsonrpsee_types::error::ErrorObject;
use reth_optimism_trie::{provider::OpProofsStateProviderRef, OpProofsStorage};
use reth_provider::{BlockIdReader, ProviderError, ProviderResult, StateProofProvider};
use reth_rpc_api::eth::helpers::FullEthApi;
use reth_rpc_eth_types::EthApiError;

#[cfg_attr(not(test), rpc(server, namespace = "eth"))]
#[cfg_attr(test, rpc(server, client, namespace = "eth"))]
pub trait EthApiOverride {
/// Returns the account and storage values of the specified account including the Merkle-proof.
/// This call can be used to verify that the data you are pulling from is not tampered with.
#[method(name = "getProof")]
async fn get_proof(
&self,
address: Address,
keys: Vec<JsonStorageKey>,
block_number: Option<BlockId>,
) -> RpcResult<EIP1186AccountProofResponse>;
}

#[derive(Debug, Constructor)]
/// Overrides applied to the `eth_` namespace of the RPC API for historical proofs ExEx.
pub struct EthApiExt<Eth, P> {
eth_api: Eth,
preimage_store: P,
}

impl<Eth, P> EthApiExt<Eth, P>
where
Eth: FullEthApi + Send + Sync + 'static,
ErrorObject<'static>: From<Eth::Error>,
P: OpProofsStorage + Clone + 'static,
{
async fn state_provider(
&self,
block_id: Option<BlockId>,
) -> ProviderResult<impl StateProofProvider> {
let block_id = block_id.unwrap_or_default();
// Check whether the distance to the block exceeds the maximum configured window.
let block_number = self
.eth_api
.provider()
.block_number_for_id(block_id)?
.ok_or(EthApiError::HeaderNotFound(block_id))
.map_err(ProviderError::other)?;

let historical_provider =
self.eth_api.state_at_block_id(block_id).await.map_err(ProviderError::other)?;

let (Some((latest_block_number, _)), Some((earliest_block_number, _))) = (
self.preimage_store
.get_latest_block_number()
.await
.map_err(|e| ProviderError::Database(e.into()))?,
self.preimage_store
.get_earliest_block_number()
.await
.map_err(|e| ProviderError::Database(e.into()))?,
) else {
// if no earliest block, db is empty - use historical provider
return Ok(historical_provider as Box<dyn StateProofProvider>);
};

if block_number < earliest_block_number || block_number > latest_block_number {
return Ok(historical_provider as Box<dyn StateProofProvider>);
}

let external_overlay_provider =
OpProofsStateProviderRef::new(historical_provider, &self.preimage_store, block_number);

Ok(Box::new(external_overlay_provider))
}
}

#[async_trait]
impl<Eth, P> EthApiOverrideServer for EthApiExt<Eth, P>
where
Eth: FullEthApi + Send + Sync + 'static,
ErrorObject<'static>: From<Eth::Error>,
P: OpProofsStorage + Clone + 'static,
{
async fn get_proof(
&self,
address: Address,
keys: Vec<JsonStorageKey>,
block_number: Option<BlockId>,
) -> RpcResult<EIP1186AccountProofResponse> {
let state = self.state_provider(block_number).await.map_err(Into::into)?;
let storage_keys = keys.iter().map(|key| key.as_b256()).collect::<Vec<_>>();

let proof = state.proof(Default::default(), address, &storage_keys).map_err(Into::into)?;

Comment on lines +101 to +102
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proof is being computed with a default (zero) state root, which will yield incorrect proofs. Retrieve the state root for the requested BlockId and pass it to state.proof; for example, fetch the header for block_id and use header.state_root so the Merkle proof corresponds to the correct block.

Suggested change
let proof = state.proof(Default::default(), address, &storage_keys).map_err(Into::into)?;
// Fetch the block header to get the correct state root
let block_id = block_number.unwrap_or_default();
let header = self.eth_api.provider().header_by_id(block_id)
.map_err(Into::into)?
.ok_or(EthApiError::HeaderNotFound(block_id))
.map_err(Into::into)?;
let state_root = header.state_root;
let proof = state.proof(state_root, address, &storage_keys).map_err(Into::into)?;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@itschaindev itschaindev Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this suggestion..

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@itschaindev Default::default() is not the state root - it's the hashed post state which is the state we apply on top of the current state, not the current state root (I think Copilot is hallucinating, or doesn't have enough context). We don't want any state applied on top of this.

return Ok(proof.into_eip1186_response(keys));
}
}