Skip to content
Draft
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
956 changes: 532 additions & 424 deletions Cargo.lock

Large diffs are not rendered by default.

47 changes: 34 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@ readme = "README.md"
keywords = ["ethereum", "reth", "optimism", "arkiv"]

[workspace.dependencies]
# Op-Reth (from ethereum-optimism monorepo)
reth-optimism-node = { git = "https://github.com/ethereum-optimism/optimism.git", branch = "develop" }
reth-optimism-cli = { git = "https://github.com/ethereum-optimism/optimism.git", branch = "develop" }
reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism.git", branch = "develop" }
reth-optimism-primitives = { git = "https://github.com/ethereum-optimism/optimism.git", branch = "develop" }
# Op-Reth (from ethereum-optimism monorepo, pinned to op-reth/v2.2.0)
reth-optimism-node = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0" }
reth-optimism-cli = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0" }
reth-optimism-chainspec = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0" }
reth-optimism-primitives = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0" }

# Reth (pinned to same rev as op-reth)
reth-exex = { git = "https://github.com/paradigmxyz/reth", rev = "552d896f9c4b75201def55969d3c23bcc990dd80" }
reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "552d896f9c4b75201def55969d3c23bcc990dd80" }
reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "552d896f9c4b75201def55969d3c23bcc990dd80" }
reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "552d896f9c4b75201def55969d3c23bcc990dd80" }
# Reth (pinned to same rev as op-reth/v2.2.0)
reth-exex = { git = "https://github.com/paradigmxyz/reth", rev = "27bfddeada3953edc22759080a3659ccea62ca1f" }
reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "27bfddeada3953edc22759080a3659ccea62ca1f" }
reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "27bfddeada3953edc22759080a3659ccea62ca1f" }
reth-node-builder = { git = "https://github.com/paradigmxyz/reth", rev = "27bfddeada3953edc22759080a3659ccea62ca1f" }
reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "27bfddeada3953edc22759080a3659ccea62ca1f" }
reth-primitives-traits = "0.3"
reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "27bfddeada3953edc22759080a3659ccea62ca1f" }

# Arkiv bindings (from arkiv-contracts repo)
arkiv-bindings = { git = "https://github.com/Arkiv-Network/arkiv-contracts.git", rev = "ff30cab" }
arkiv-bindings = { git = "https://github.com/Arkiv-Network/arkiv-contracts.git", rev = "d4bf0d59" }

# Arkiv genesis primitives (in-tree)
arkiv-genesis = { path = "crates/arkiv-genesis" }
Expand All @@ -42,9 +45,27 @@ alloy-signer-local = { version = "2.0", features = ["mnemonic"] }
alloy-network = "2.0"
alloy-contract = "2.0"
alloy-rpc-types = "2.0"
alloy-rpc-types-engine = "2.0"
alloy-rpc-types-eth = "2.0"
alloy-eips = "2.0"
alloy-hardforks = "0.4"

# EVM
revm = { version = "36.0.0", default-features = false, features = ["std"] }
# EVM. Pinned to `38.0.0` to match what `alloy-op-evm` / `op-revm` /
# `revm-precompile 34` pull in transitively. Earlier this workspace had
# `revm = "36.0.0"` (used only by `arkiv-genesis`), which silently
# produced parallel `revm 36` and `revm 38` graphs and meant types from
# the two were incompatible. Keeping a single version simplifies the
# precompile wiring — see `docs/custom-precompile.md` §9.E.
revm = { version = "38.0.0", default-features = false, features = ["std"] }

# Custom-precompile wiring (POC).
# alloy-op-evm must come from the optimism git tag, not crates.io, so its
# `revm 38` line aligns with op-revm/op-reth.
alloy-evm = { version = "0.33", default-features = false, features = ["std"] }
alloy-op-evm = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0", default-features = false, features = ["std"] }
op-alloy-consensus = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0", default-features = false, features = ["std"] }
op-revm = { git = "https://github.com/ethereum-optimism/optimism.git", tag = "op-reth/v2.2.0", default-features = false, features = ["std"] }
revm-precompile = { version = "34", default-features = false, features = ["std"] }

# Util
clap = { version = "4", features = ["derive"] }
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,9 @@ Working today:
- ExEx detects the predeploy by chainspec content and activates on
explicit operator opt-in (`--arkiv.db-url` or `--arkiv.debug`).
- ExEx → EntityDB JSON-RPC v2 wire format is complete and documented.
- `arkiv_query` JSON-RPC proxy method (registered when `--arkiv.db-url`
is set; transparent passthrough to EntityDB).
- `arkiv_*` JSON-RPC namespace (registered when `--arkiv.db-url` is set;
transparent passthrough to EntityDB). Currently: `arkiv_query`,
`arkiv_getEntityCount`, `arkiv_getBlockTiming`.
- Operator CLI covers all six entity-operation types plus batched submission
with cross-references between ops.
- Storage backends: `LoggingStore` (tracing) and `JsonRpcStore`
Expand Down
16 changes: 16 additions & 0 deletions crates/arkiv-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true

[lib]
path = "src/lib.rs"

[[bin]]
name = "arkiv-node"
path = "src/main.rs"
Expand All @@ -16,16 +19,29 @@ reth-optimism-chainspec.workspace = true
reth-optimism-primitives.workspace = true

reth-exex.workspace = true
reth-evm.workspace = true
reth-node-api.workspace = true
reth-node-builder.workspace = true
reth-execution-types.workspace = true
reth-primitives-traits.workspace = true
reth-storage-api.workspace = true

arkiv-bindings.workspace = true
arkiv-genesis.workspace = true
alloy-primitives.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-hardforks.workspace = true
alloy-rpc-types-engine.workspace = true
alloy-rpc-types-eth.workspace = true
alloy-sol-types.workspace = true

alloy-evm.workspace = true
alloy-op-evm.workspace = true
op-alloy-consensus.workspace = true
op-revm.workspace = true
revm-precompile.workspace = true

clap.workspace = true
eyre.workspace = true
reqwest.workspace = true
Expand Down
30 changes: 30 additions & 0 deletions crates/arkiv-node/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Arkiv-specific clap args. Designed to be `#[command(flatten)]`-ed into
//! a host CLI so downstream binaries can compose Arkiv onto their own
//! argument surface.

use reth_optimism_node::args::RollupArgs;

/// CLI extension over [`RollupArgs`]. Adds Arkiv-specific flags.
#[derive(Debug, clap::Args)]
pub struct ArkivExt {
/// EntityDB JSON-RPC URL. On an Arkiv chainspec, enables the ExEx
/// (forwarding to EntityDB) and the `arkiv_query` JSON-RPC method.
#[arg(long = "arkiv.db-url", env = "ARKIV_ENTITYDB_URL")]
pub arkiv_db_url: Option<String>,

/// Debug mode: run the ExEx with the in-process `LoggingStore` backend
/// (decoded ops are emitted as tracing events). Useful for local dev
/// without a running EntityDB. The `arkiv_*` RPC namespace is not
/// installed in this mode.
#[arg(long = "arkiv.debug", conflicts_with = "arkiv_db_url")]
pub arkiv_debug: bool,

/// Install the Arkiv EntityDB-write precompile (POC). Requires
/// `--arkiv.db-url`; the precompile reuses the same EntityDB client.
/// Off by default; the precompile is independent of the ExEx.
#[arg(long = "arkiv.precompile", requires = "arkiv_db_url")]
pub arkiv_precompile: bool,

#[command(flatten)]
pub rollup: RollupArgs,
}
29 changes: 29 additions & 0 deletions crates/arkiv-node/src/genesis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Predeploy detection for Arkiv chainspecs.

use alloy_primitives::keccak256;
use reth_optimism_chainspec::OpChainSpec;

/// Returns `true` iff the chainspec's genesis alloc contains the Arkiv
/// EntityRegistry predeploy at the canonical address with bytecode that
/// matches the runtime form for this chain's chain_id.
///
/// The bytecode hash check (rather than mere address presence) guards
/// against squatting at `0x44…0044` with unrelated code.
pub fn has_arkiv_predeploy(chain: &OpChainSpec) -> bool {
let chain_id = chain.inner.chain.id();
let Some(account) = chain
.inner
.genesis
.alloc
.get(&arkiv_genesis::ENTITY_REGISTRY_ADDRESS)
else {
return false;
};
let Some(code) = &account.code else {
return false;
};
let Ok(expected) = arkiv_genesis::deploy_creation_code(chain_id) else {
return false;
};
keccak256(code) == keccak256(&expected)
}
150 changes: 150 additions & 0 deletions crates/arkiv-node/src/install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Mode resolution + installation of Arkiv onto an op-stack node builder.
//!
//! The split is deliberate:
//!
//! - [`resolve_mode`] is pure validation + a network health check. No
//! reth/builder generics. Embedders can call it directly, or skip it
//! entirely and construct an [`ArkivMode`] themselves.
//! - [`install`] is the only function that touches `reth-node-builder`
//! generics. It mirrors op-reth's `launch_node_with_proof_history`
//! pattern: take the post-`.node()` builder, call `install_exex` and
//! (conditionally) `extend_rpc_modules`, return the builder.

use std::sync::Arc;

use eyre::{Result, WrapErr, bail};
use reth_node_builder::{
FullNodeTypes, NodeAdapter, NodeBuilderWithComponents, NodeComponentsBuilder, NodeTypes,
WithLaunchContext, rpc::RethRpcAddOns,
};
use reth_optimism_chainspec::OpChainSpec;
use reth_optimism_primitives::OpPrimitives;

use crate::exex;
use crate::genesis::has_arkiv_predeploy;
use crate::rpc::{ArkivApiServer, ArkivRpc};
use crate::storage::{EntityDbClient, JsonRpcStore, Storage, logging::LoggingStore};

/// Resolved Arkiv configuration. Decouples "what was decided" from how it
/// was decided (CLI flags, programmatic config, …).
#[derive(Clone)]
pub enum ArkivMode {
/// No Arkiv extensions; behave as plain op-reth.
Disabled,
/// In-process [`LoggingStore`] backend; no RPC namespace.
Debug,
/// Forward to EntityDB; install `arkiv_*` RPC.
EntityDb { client: Arc<EntityDbClient> },
}

/// Extract the EntityDB client to bind to the custom precompile, if and
/// only if the `--arkiv.precompile` flag is set. Mirrors the clap
/// `requires = "arkiv_db_url"` rule: if the flag is set we expect to be
/// in [`ArkivMode::EntityDb`].
pub fn precompile_client(
mode: &ArkivMode,
enabled: bool,
) -> Result<Option<Arc<EntityDbClient>>> {
if !enabled {
tracing::info!("Arkiv precompile: disabled (pass --arkiv.precompile to enable)");
return Ok(None);
}
match mode {
ArkivMode::EntityDb { client } => {
tracing::info!(
address = %crate::precompile::ARKIV_PRECOMPILE_ADDRESS,
"Arkiv precompile: enabled; calls will forward to EntityDB"
);
Ok(Some(client.clone()))
}
_ => bail!("--arkiv.precompile requires --arkiv.db-url"),
}
}

/// Validate the given Arkiv flags against the loaded chainspec and, in
/// the EntityDB case, run a health check. Mirrors the original `match` in
/// `main` 1:1; only the shape is different.
pub async fn resolve_mode(
arkiv_db_url: Option<String>,
arkiv_debug: bool,
chain: &OpChainSpec,
) -> Result<ArkivMode> {
let predeploy = has_arkiv_predeploy(chain);

match (predeploy, arkiv_db_url, arkiv_debug) {
(false, None, false) => {
tracing::info!("EntityRegistry predeploy not detected; running as plain op-reth");
Ok(ArkivMode::Disabled)
}
(false, _, _) => {
bail!(
"Arkiv flags set but the loaded chainspec does not contain the \
EntityRegistry predeploy at {}",
arkiv_genesis::ENTITY_REGISTRY_ADDRESS,
);
}
(true, None, false) => {
bail!(
"EntityRegistry predeploy detected; either --arkiv.db-url (or \
ARKIV_ENTITYDB_URL) or --arkiv.debug is required",
);
}
(true, None, true) => {
tracing::info!("Arkiv: predeploy detected; installing ExEx with LoggingStore (debug)");
Ok(ArkivMode::Debug)
}
(true, Some(url), false) => {
let client = Arc::new(EntityDbClient::new(url.clone()));
client
.health_check()
.await
.wrap_err_with(|| format!("EntityDB unreachable at {url}"))?;
tracing::info!(%url, "Arkiv: predeploy + EntityDB OK; installing ExEx + arkiv_* RPC");
Ok(ArkivMode::EntityDb { client })
}
(true, Some(_), true) => {
// Mirrors `clap::conflicts_with` for callers that bypass clap.
bail!("--arkiv.db-url and --arkiv.debug are mutually exclusive");
}
}
}

/// Install the Arkiv ExEx (and, in [`ArkivMode::EntityDb`], the `arkiv_*`
/// RPC namespace) on an op-stack node builder. No-op for
/// [`ArkivMode::Disabled`].
///
/// The bounds match what the underlying `install_exex` /
/// `extend_rpc_modules` calls require, plus `Primitives = OpPrimitives`
/// (the ExEx assumes op-stack primitives).
pub fn install<T, CB, AO>(
node: WithLaunchContext<NodeBuilderWithComponents<T, CB, AO>>,
mode: ArkivMode,
) -> WithLaunchContext<NodeBuilderWithComponents<T, CB, AO>>
where
T: FullNodeTypes,
T::Types: NodeTypes<Primitives = OpPrimitives>,
CB: NodeComponentsBuilder<T>,
AO: RethRpcAddOns<NodeAdapter<T, CB::Components>>,
{
match mode {
ArkivMode::Disabled => node,
ArkivMode::Debug => {
let store: Arc<dyn Storage> = Arc::new(LoggingStore::new());
node.install_exex("arkiv", move |ctx| async move {
Ok(exex::arkiv_exex(ctx, store))
})
}
ArkivMode::EntityDb { client } => {
let store: Arc<dyn Storage> = Arc::new(JsonRpcStore::from_client(client.clone()));
let rpc_client = client;
node.install_exex("arkiv", move |ctx| async move {
Ok(exex::arkiv_exex(ctx, store))
})
.extend_rpc_modules(move |ctx| {
ctx.modules
.merge_configured(ArkivRpc::new(rpc_client).into_rpc())?;
Ok(())
})
}
}
}
29 changes: 29 additions & 0 deletions crates/arkiv-node/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Arkiv node library.
//!
//! This crate exposes the building blocks the `arkiv-node` binary uses to
//! turn an op-stack node builder into an Arkiv node:
//!
//! - [`ArkivExt`] — clap args (`--arkiv.db-url`, `--arkiv.debug`).
//! - [`ArkivMode`] — resolved configuration (off / debug / EntityDB).
//! - [`resolve_mode`] — validates flags against the loaded chainspec and
//! performs the EntityDB health check. Returns an [`ArkivMode`].
//! - [`install`] — wires the ExEx (and the `arkiv_*` RPC namespace, when
//! applicable) onto an op-stack [`NodeBuilderWithComponents`].
//! - [`has_arkiv_predeploy`] — bytecode-equality check for the
//! EntityRegistry predeploy in a chainspec's genesis alloc.
//!
//! Consumers compose these the same way op-reth's
//! `launch_node_with_proof_history` composes its ExEx onto `OpNode`.

pub mod exex;
pub mod precompile;
pub mod rpc;
pub mod storage;

mod cli;
mod genesis;
mod install;

pub use cli::ArkivExt;
pub use genesis::has_arkiv_predeploy;
pub use install::{ArkivMode, install, precompile_client, resolve_mode};
Loading