feat: add signet-rpc-storage crate with ETH RPC endpoints#75
feat: add signet-rpc-storage crate with ETH RPC endpoints#75
Conversation
|
[Claude Code] Review SummaryWhat's here
Issues flagged (inline comments)
Codebase health
|
Add the foundational scaffolding for the signet-rpc-storage crate, which provides an Ethereum JSON-RPC server backed by signet-storage's unified storage backend, independent of reth's FullNodeComponents. This includes: - Workspace dependency additions (signet-storage, signet-cold, signet-hot, signet-storage-types) - StorageRpcCtx context struct with Arc<Inner> pattern - BlockTags atomic block tag tracker for Latest/Safe/Finalized - Block ID and block tag resolution utilities - Stub eth module (endpoints to be added in follow-up) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement all ETH namespace JSON-RPC endpoints backed by cold/hot storage instead of reth. Converts eth.rs placeholder into eth/ directory module with error types, helpers, and 24 supported endpoint handlers: - Simple queries: blockNumber, chainId - Block queries: getBlockByHash/Number, getBlockReceipts, headers - Transaction queries: getTransactionByHash, getTransactionReceipt, raw txs - Account state (hot storage): getBalance, getStorageAt, getCode, getTransactionCount - EVM execution: call, estimateGas (via signet-evm/trevm) - Transaction submission: sendRawTransaction (via TxCache) - Logs: getLogs with bloom filter matching Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 23 integration tests covering all endpoint categories: simple queries, block/transaction lookups, account state, logs, and error cases. Tests exercise the router through the axum service layer using tower's oneshot(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- send_raw_transaction: log warning on forwarding failure instead of silently discarding the error - get_logs: reject reversed block ranges (from > to) with an explicit error instead of silently returning empty results - build_receipt_envelope: remove catch-all arm so new TxType variants from alloy produce a compile error instead of a runtime panic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Regular txs execute before system txs, not the other way around. Drive-by from #74 (comment) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4341320 to
601c156
Compare
- Extract `resolve_evm_block` method on `StorageRpcCtx` to deduplicate the block resolution + header fetch + revm db creation shared by `call()` and `estimate_gas()`. Resolves headers directly (by hash or by tag→number) to avoid redundant cold storage lookups. - Replace glob import `use endpoints::*` with explicit imports. - Remove unused `revm_state()` method from `StorageRpcCtx`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ounds - Move `resolve_block_id` and `resolve_block_number_or_tag` from free functions in resolve.rs to `resolve_block_id` and `resolve_block_tag` methods on `StorageRpcCtx`. This eliminates repeated `ctx.tags()` and `ctx.cold()` threading at every call site. - `resolve_block_tag` returns `u64` directly (infallible) instead of `Result`, simplifying callers like `get_logs`. - Remove `H::RoTx: Send + Sync + 'static` bounds from all endpoint functions, router, and ctx methods — the trait already provides these. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the bare `rpc_gas_cap` constructor parameter with a `StorageRpcConfig` struct that bundles all RPC configuration. This moves `max_blocks_per_filter` from a hard-coded constant to a configurable value, adds `max_logs_per_response` enforcement in `eth_getLogs`, and pre-creates a tracing semaphore for future debug endpoint concurrency limiting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add gas oracle, filters, subscriptions, debug tracing, and signet namespaces to signet-rpc-storage. Port 15 endpoints from the old reth-backed signet-rpc crate to the storage-backed architecture. New modules: - gas_oracle: cold-storage gas price oracle (suggest_tip_cap) - interest/: filter manager, subscription manager, block notifications - debug/: traceBlockByNumber, traceBlockByHash, traceTransaction - signet/: sendOrder, callBundle Wired eth endpoints: gasPrice, maxPriorityFeePerGas, feeHistory, newFilter, newBlockFilter, uninstallFilter, getFilterChanges, getFilterLogs, subscribe, unsubscribe. Integration tests cover gas/fee queries, filter lifecycle, and debug tracing with noop tracer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use `SealedHeader` with `.hash()` / `.into_inner()` instead of `header.hash_slow()` - Use `RecoveredTx` (pre-recovered sender) instead of manual `recover_sender` calls - Use `ColdReceipt` with per-tx `gas_used` instead of computing deltas from cumulative gas - Delegate `get_logs` to cold storage instead of manual bloom filtering and block iteration - Remove `BlockRangeInclusiveIter`, `collect_matching_logs`, `build_receipt_from_parts`, and `recover_sender` helpers - Simplify `build_rpc_transaction` and `build_receipt` to be infallible Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses PR review feedback: - resolve_block_id now uses hot HotDbRead::get_header_number instead of cold storage, making it synchronous and avoiding async overhead - Add resolve_header for direct header fetches from hot storage, eliminating the double header lookup in header_by - Change not_supported() to return method_not_found() (JSON-RPC -32601) instead of internal_error (-32603) - Update ResolveError to use Storage/Db variants instead of Cold - Update tests to write headers to both hot and cold storage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swap return types to use RpcBlock/RpcReceipt/RpcTransaction/RpcHeader type aliases, rename tx_by_block_and_index to match rpc naming, fix not_supported error message, and split call into run_call + call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align rpc-storage behavioral semantics with the rpc crate: - subscribe: require filter for Logs, reject filter for NewHeads via TryFrom impl - send_raw_transaction: return Result from spawned task instead of swallowing errors - uninstall_filter/unsubscribe: add HandlerCtx and wrap in spawn_blocking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…to 0.5 resolve_evm_block previously mapped Pending to Latest without modifying the header, causing eth_estimateGas (which defaults to Pending) and eth_call with explicit Pending to see wrong block.number, timestamp, and base_fee. Now synthesizes a next-block header matching signet-rpc's block_cfg() behavior. Also refactors callBundle to use resolve_evm_block instead of duplicating the pending header logic inline, and passes max_logs to cold.get_logs() for early termination (signet-cold 0.5 API). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
prestwich
left a comment
There was a problem hiding this comment.
are there ways that we can DRY endpoint logic or definitions?
Move response/serialization types (EmptyArray, BlockTransactions, RpcBlock, LazyReceipts) and type aliases from eth/endpoints.rs into eth/types.rs, and parameter types (TraceBlockParams, TraceTransactionParams) from debug/endpoints.rs into debug/types.rs. Consolidate duplicate RpcTransaction/RpcReceipt aliases from eth/helpers.rs into eth/types.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the one-notification-per-iteration drain pattern with permit_many batching for better throughput and fairness. Bumps ajj to 0.6.2 for permit_many/notification_capacity support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tracking Move NodeStatus from signet-node-types to signet-node, move ServeConfig/RpcServerGuard from signet-rpc to signet-node, and remove the journal hash tracking system (JournalHashes table, journal module, GENESIS_JOURNAL_HASH, and all related parameters/methods). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
prestwich
left a comment
There was a problem hiding this comment.
[Claude Code]
PR Review: signet-rpc-storage crate
The crate is well-organized and test coverage is solid. However, there are two likely bugs (leaked thread in SubCleanerTask, unsubscribe is a no-op), a conformance gap (eth_syncing), and a design issue with spawn_blocking being used for async work. See inline comments for details.
Design-level observations (not tied to a specific line):
StorageRpcConfig should have a builder — Per project conventions: "A struct is considered complex enough to warrant a builder if it has more than 4 fields." StorageRpcConfig has 11 fields.
Duplicated Either enum — filters.rs and subs.rs both define an Either enum with nearly identical semantics but different variants. Should be unified or renamed (FilterItem / SubscriptionItem).
Duplicated buffer/output types — FilterOutput and SubscriptionBuffer are structurally identical VecDeque-backed enums with Log/Block variants, len(), is_empty(), extend(), pop_front(). This could be a single generic type.
Error handling via String is inconsistent — Most endpoints return Result<T, String>, losing structured error info, while run_call/estimate_gas/create_access_list return ResponsePayload<T, CallErrorData>, and debug endpoints return ResponsePayload<T, DebugError>. The crate defines three good error types (EthError, DebugError, SignetError) but most endpoints discard them with .map_err(|e| e.to_string()).
- Fix SubCleanerTask thread leak (missing break on Weak upgrade failure) - Fix missing tasks.insert in subscribe() preventing unsubscribe - Implement eth_syncing with concrete SyncingResponse type - Fix reward percentile cursor overflow in fee_history - Switch cold-storage endpoints from spawn_blocking to spawn - Add StorageRpcConfigBuilder for 11-field config struct - Unify FilterOutput/SubscriptionBuffer into generic EventBuffer<B> - Replace hard-coded 12s block time with slot_duration() from constants - Add EthError::TransactionMissing variant - Remove dead code (Either enums, unused methods, allow(dead_code)) - Fix license comment typos, min() style, mut rebinding nits Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rewrite block processor to use signet-hot storage Replace the reth ProviderFactory-based storage with HotKv for rollup state reads. The processor becomes a stateless executor that reads from hot storage, runs the EVM, and returns ExecutedBlock. Extraction is moved to the node (PR3) to avoid lifetime issues with borrowed Extracts. - Replace Db: NodeTypesDbTrait generic with H: HotKv - Replace state_provider_database() with revm_state() using RevmRead - Remove on_host_commit() and commit_evm_results() - Add process_block() returning ExecutedBlock - Add build_executed_block() for type conversion - Remove signet-db, signet-node-types, reth-exex, reth-node-api deps - Add signet-hot, signet-storage-types deps - Remove Chain/PrimitivesOf/ExExNotification type aliases from lib.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: rewrite signet-node to use unified storage and rpc-storage Replace reth's ProviderFactory/BlockchainProvider with UnifiedStorage and swap signet-rpc for signet-rpc-storage. The node now holds Arc<UnifiedStorage<H>> and shares state with the RPC context through BlockTags (atomic block tag tracking) and broadcast::Sender (new block notifications). Key changes: - StorageRpcCtx accepts Arc<UnifiedStorage<H>> for shared ownership - Node struct uses HotKv generic instead of NodeTypesDbTrait - Block processing uses signet-extract's Extractor + ExtractableChainShim - Genesis loading via HistoryWrite::load_genesis + UnsafeDbWrite::commit - Fix metrics bug: record_notification_received now correctly increments the received counter instead of the processed counter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: rewrite node-tests to use unified storage and align gas oracle Replace TmpDb/ProviderFactory with MemKv/UnifiedStorage in all node test infrastructure. Update signet-storage crates to v0.6.2 to fix MemKv intra-transaction read visibility. Align the cold-storage gas oracle with reth's GasPriceOracle by adding default_gas_price (1 Gwei), ignore_price (2 wei), and max_price (500 Gwei) to StorageRpcConfig. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: delete signet-db, signet-node-types, and signet-rpc These crates are fully replaced by signet-storage, signet-hot, and signet-rpc-storage respectively. Remove 6,700+ lines of dead code and clean up 22 unused workspace dependencies. Replace reth-db tempdir_path with tempfile in signet-node-config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: rename signet-rpc-storage to signet-rpc Reclaim the signet-rpc name now that the old reth-backed crate is deleted. Rename directory, package, and all import paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: restore RUN EVM ascii art banner Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve clippy empty_line_after_doc_comments warning in ascii banner Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review — bugs, style, and test infra - Fix log_index per-receipt → per-block (kind.rs) - Fix hash/height mismatch in update_highest_processed_height (node.rs) - Fix serve_ipc panic: expect → ? (serve.rs) - Use seal_unchecked to avoid redundant header re-hashing (processor.rs) - Refactor imperative event loop → functional chain (processor.rs) - Add #[instrument(skip_all)] to run_evm and build_executed_block - Add TODO for two-reader consistency risk (processor.rs) - Group signet_hot imports across 6 RPC files - Move supported methods above "Unsupported" comment (eth/mod.rs) - Hoist function-scoped imports to module level (eth_rpc.rs) - Fix stale signet_rpc_storage reference (eth_rpc.rs) - Differentiate HTTP/WS doc comments, fix Result type (serve.rs) - Wrap cold storage polling loop in 30s timeout (context.rs) - Remove dead signet_events_in_range method (context.rs) - Update README to reflect new API types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Remove `not_supported` pattern; unregistered methods return `method_not_found` by default in ajj (threads 14, 15) - Simplify `SubscriptionNotification`: hardcode `jsonrpc`/`method` in manual `Serialize` impl instead of storing as fields (threads 18, 19) - Change `send_order` return type to `ResponsePayload<(), SignetError>` and add fire-and-forget doc comment (threads 5, 7) - Use `ctx.resolve_header()` (hot storage) instead of `cold.get_header_by_number()` in debug endpoints (threads 12, 13) - Replace `DebugError::Cold(String)` / `Hot(String)` with concrete `#[from]` error types; remove `Clone` derive and `into_string` (thread 10) - Parallelize gas oracle transaction reads with `JoinSet` (thread 21) - Add `tokio-stream` and `tokio-util` to workspace deps (thread 22) - Add blob gas vec compatibility comment (thread 23) - Add `State<Db>` / `DatabaseCommit` documentation (thread 20) - Update README with namespace overview and unsupported methods (thread 16) - Add blank lines in lib.rs between module groups (thread 2) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| .map_err(Into::<eyre::Report>::into)? | ||
| .wrap_err("parent ru block not present in DB") | ||
| .inspect_err(|e| error!(%e))?; | ||
| let parent_header = signet_types::primitives::SealedHeader::new(parent_header.into_inner()); |
There was a problem hiding this comment.
what is this type conversion doing? why can't we use it without converting. add a table of types to the comment response
There was a problem hiding this comment.
[Claude Code]
Here is the type table:
| Expression | Type | Source |
|---|---|---|
get_header() returns |
signet_storage_types::SealedHeader = alloy::consensus::Sealed<Header> |
alloy sealed wrapper |
.into_inner() yields |
alloy::consensus::Header |
unwraps alloy seal |
signet_types::primitives::SealedHeader::new(...) |
signet_types::primitives::SealedHeader |
re-wraps with OnceLock<BlockHash> |
The conversion is not redundant — it bridges two different SealedHeader types. SignetDriver::new() requires signet_types::primitives::SealedHeader. The precomputed hash from storage is discarded, but the signet type lazily recomputes it via OnceLock.
… Paris `EthereumHardfork` is a bitflags type where each hardfork is an independent bit. All callers of `load_genesis()` were passing only `EthereumHardfork::Paris`, so `genesis_header()` never set `base_fee_per_gas` (a London feature) because the London bit was unset. The test context worked around this by patching the header after the fact. This adds `genesis_hardforks()` to signet-genesis, which derives the correct set of active hardfork flags by inspecting the chain config's activation blocks and timestamps. Per-network LazyLock statics and a `GenesisSpec::genesis_hardforks()` method are also provided. The workaround in the test context is removed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… PairedHeights - Unify BlockTags + notification sender into ChainNotifier shared struct - Replace log-only cold_lag check with proper storage reconciliation (unwind to min of hot/cold tips on startup) - Fix PairedHeights 0/0 fallback to use genesis heights with let-else - Remove TODO about reader snapshots and redundant map_err - Replace fully-qualified HotDbRead/HistoryRead syntax with method calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
||
| /// Get the active spec id at the given timestamp. | ||
| fn spec_id(&self, timestamp: u64) -> SpecId { | ||
| crate::revm_spec(&self.chain_spec, timestamp) |
There was a problem hiding this comment.
remove this in favor of holding an EthereumHardforks instance
There was a problem hiding this comment.
[Claude Code]
Done. The processor is now instantiated per-block with an EthereumHardfork bitflags value computed by the caller. Deleted utils.rs (containing revm_spec and assert_in_range) and removed reth-chainspec from the block processor. The node constructs a fresh processor for each block in process_committed_chain via EthereumHardfork::active_hardforks(&config, block_number, timestamp). Only alias_oracle and blob_cacher moved to SignetNode fields — the rest are already accessible from existing fields.
| .signet_events(signet_events) | ||
| .zenith_header(zenith_header) | ||
| .build() | ||
| .map_err(|e| eyre::eyre!("failed to build ExecutedBlock: {e}")) |
There was a problem hiding this comment.
this should be wrap_err. under what circumstances could this fail?
There was a problem hiding this comment.
[Claude Code]
Done — changed to wrap_err("failed to build ExecutedBlock"). This can only fail if a required builder field is missing (MissingFieldError). All four fields are set above so this is purely defensive.
| /// Build an [`ExecutedBlock`] from processor outputs. | ||
| #[instrument(skip_all)] | ||
| fn commit_evm_results<'a>( | ||
| fn build_executed_block( |
There was a problem hiding this comment.
this is invoked on the output of run_evm.
could we refactor to havre run_evm return this? suggest improved flows that minimize passing args
There was a problem hiding this comment.
[Claude Code]
Done — build_executed_block is merged into run_evm, which now returns eyre::Result<ExecutedBlock> directly. Metrics recording moved inside run_evm. process_block simplifies to record_extracts + run_evm.
The builder previously only loaded genesis into hot storage. Cold-backed RPC queries for block 0 would fail. Now prebuild checks cold independently and writes the genesis block if absent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/node/src/node.rs
Outdated
| if let Some(chain) = notification.reverted_chain() { | ||
| reverted = self.on_host_revert(&chain).wrap_err("error encountered during revert")?; | ||
| self.on_host_revert(&chain).wrap_err("error encountered during revert")?; | ||
| changed = true; |
There was a problem hiding this comment.
this is not necessarily correct, as notifications may conceivably be no-ops
There was a problem hiding this comment.
[Claude Code]
Fixed — both on_host_revert and process_committed_chain now return bool. on_host_revert returns false when the chain tip is at or below the rollup genesis (early return, no unwind). process_committed_chain returns false when no blocks pass the height filter.
crates/node/src/node.rs
Outdated
| self.process_committed_chain(&chain) | ||
| .await | ||
| .wrap_err("error encountered during commit")?; | ||
| changed = true; |
There was a problem hiding this comment.
[Claude Code]
Addressed with the same fix — see reply above.
crates/node/src/node.rs
Outdated
|
|
||
| // Update the RPC's block information | ||
| self.update_canon_heights(ru_height)?; | ||
| let reader = self.storage.reader()?; |
There was a problem hiding this comment.
this is a repeated pattern. load, check last, drop. DRY it
There was a problem hiding this comment.
[Claude Code]
Done — added a last_rollup_block(&self) -> eyre::Result<u64> helper that encapsulates the reader + last_block_number + drop pattern. Replaced all four occurrences (in start, start_inner, process_committed_chain, and update_status).
|
|
||
| use alloy::{consensus::Transaction, primitives::U256}; | ||
| use signet_cold::{ColdStorageError, ColdStorageReadHandle, HeaderSpecifier}; | ||
|
|
There was a problem hiding this comment.
[Claude Code]
Fixed — removed the blank line between imports and let the formatter re-order them.
prestwich
left a comment
There was a problem hiding this comment.
how is the storage backend configured in node config?
… merge gas cache - Bump signet-sdk crates from 0.16.0-rc.8 to 0.16.0-rc.11 - Migrate to new block primitives (Sealed<Header>, SealedBlock<T>) - Replace ChainSpec with EthereumHardfork bitflags in block processor - Restructure processor to be instantiated per-block instead of held - Merge build_executed_block into run_evm, change map_err to wrap_err - Fix changed flag: on_host_revert/process_committed_chain return bool - DRY reader/last_block/drop pattern into last_rollup_block() helper - Remove blank line between imports in gas_oracle.rs - Merge PR #80: add lock-free GasOracleCache for tip suggestions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Summary
signet-rpc-storagecrate providing Ethereum JSON-RPC endpoints backed bysignet-storage(hot + cold), independent of reth'sFullNodeComponentsStorageRpcCtxwrapsUnifiedStorage,BlockTags, system constants, optionalTxCache, and RPC gas capBlockTagsprovides atomic latest/safe/finalized block tracking with sync/async resolutionblockNumber,chainIdgetBlockByHash/Number,getBlockTransactionCount*,getBlockReceipts,getBlockHeader*getTransactionByHash,getRawTransactionByHash,*ByBlockAndIndex,getTransactionReceiptgetBalance,getStorageAt,getTransactionCount,getCodecall,estimateGasviasignet-evm/trevmsendRawTransactionviaTxCachegetLogswith bloom filter matchingsignet-storage0.3.0 from crates.ioTest plan
cargo clippy -p signet-rpc-storage --all-features --all-targets— cleancargo clippy -p signet-rpc-storage --no-default-features --all-targets— cleancargo +nightly fmt— cleancargo t -p signet-rpc-storage— 33 tests passcargo doc -p signet-rpc-storage— docs build successfully🤖 Generated with Claude Code