feat(rpc): add eth_capabilities method for routing capability discovery#11438
Conversation
This commit introduces the `eth_capabilities` method to the Eth RPC module, which returns the node's historical data availability and head block information. It includes the new `EthCapabilitiesResult` class and associated classes (`CapabilityHead`, `CapabilityResource`, and `CapabilityDeleteStrategy`) to structure the response. Additionally, tests for the new functionality have been added to ensure correct behavior across various configurations, including archive and pruned nodes.
|
@claude review |
|
Claude finished @LukaszRozmej's task in 5m 43s —— View job Code Review
SummaryThe overall structure is clean — the DI wiring is correct ( Three correctness issues with the boundary calculations warrant attention before merge:
|
…tate reporting This commit updates the `eth_capabilities` method in the Eth RPC module to correctly report the availability of blocks and receipts based on the configured pruning mode. It ensures that the `OldestBlock` field is set to null for full pruning scenarios, preventing misleading information. Additionally, tests have been added to verify the behavior of the method across different configurations, including archive nodes and various pruning settings, ensuring compliance with the expected JSON structure.
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 3m 35s —— View job Re-Review
SummaryAll three Medium and all three Low findings from the prior review have been addressed in commit
The test suite grew from 4 to 8 tests, covering: archive, memory-pruned, full-pruned, no-receipts, archive-receipts-genesis, null-configs, JSON key names, and all-resources-present. Two new observations (both Low, non-blocking):
Verdict: ready to merge. No Medium or higher findings remain unresolved. |
This commit introduces the `EthCapabilitiesProvider` class, which encapsulates the logic for retrieving the capabilities of the Ethereum node, including block and receipt availability based on the current sync and pruning configurations. The `eth_capabilities` method in the `EthRpcModule` is refactored to utilize this new provider, streamlining the code and improving maintainability. Additionally, the interface for the `eth_capabilities` method is updated to include an example response for better clarity in usage.
LukaszRozmej
left a comment
There was a problem hiding this comment.
one last thing - cleanup json type
| [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
| public string? OldestBlock { get; init; } |
There was a problem hiding this comment.
don't format in the method, keep it as long. Json serialization will format it without allocating strings.
There was a problem hiding this comment.
Done — OldestBlock is now long?. Removed all .ToHexString() calls from EthCapabilitiesProvider; NullableLongConverter (globally registered in EthereumJsonSerializer) handles hex encoding on the wire.
| /// Response for eth_capabilities — describes the head block and the historical data availability of each resource type. | ||
| /// Follows ethereum/execution-apis#755. | ||
| /// </summary> | ||
| public class EthCapabilitiesResult |
There was a problem hiding this comment.
readonly record struct? Or record class
There was a problem hiding this comment.
@claude from performance perspective, better to make it struct or keeping this one class?
|
@claude which ones to make structs for optimal performance? |
|
Claude finished @LukaszRozmej's task in 1m 25s —— View job Struct vs Class Analysis for
|
| Type | Recommendation | Reasoning |
|---|---|---|
EthCapabilitiesResult |
record class |
If the nested types become structs, each CapabilityResource is ~40 bytes × 6 = ~240 bytes inline. A 256-byte struct is too large — copying it is worse than a single class allocation. It's the top-level container; one heap allocation is fine. |
CapabilityHead |
readonly record struct |
Two string references = ~16 bytes. Stored inline in EthCapabilitiesResult, saves 1 heap allocation per call. Ideal struct size. |
CapabilityResource |
readonly record struct |
Used 6× per result — saves 6 heap allocations per call. Bool + string ref + Nullable<CapabilityDeleteStrategy> lands at ~32–40 bytes, acceptable when stored as fields in a class (not passed around by value). |
CapabilityDeleteStrategy |
readonly record struct |
String ref + long = ~16 bytes. Used as CapabilityDeleteStrategy? → becomes Nullable<CapabilityDeleteStrategy> inline in CapabilityResource — no heap allocation for the strategy object. |
Net effect on allocations per eth_capabilities call:
- Before: result + head + 6× resource + 0–1× deleteStrategy = 8–9 allocations
- After structs on all nested types: result only = 1 allocation
One caveat on CapabilityDeleteStrategy?: When CapabilityDeleteStrategy is a struct, DeleteStrategy becomes Nullable<CapabilityDeleteStrategy>. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] on a nullable value type works correctly in System.Text.Json — it omits the property when the nullable has no value.
Separate from the struct question — LukaszRozmej's inline comments also ask to keep Number as long and Hash as Hash256 (let the serializer format them, avoiding intermediate string allocations). That applies on top of these struct changes.
…g and Hash256 types This commit updates the `EthCapabilitiesResult` and `EthCapabilitiesProvider` classes to replace string representations of block numbers and hashes with their appropriate types: `long` for block numbers and `Hash256` for block hashes. This change enhances type safety and consistency across the codebase. Additionally, the related tests have been updated to reflect these changes, ensuring accurate validation of capabilities reporting.
Changed to |
…heck FullPruner now records the state-availability floor in the new state DB on a successful prune, so the post-prune snapshot legitimately contains one entry that's absent from the pre-prune snapshot. Verify the boundary key was written (closing a coverage gap for the FullPruner → IStateBoundary write), then exclude it before asserting the rest of the contents are a subset of the pre-prune values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The floor is meant to be monotonically non-decreasing: state sync writes the pivot, then full pruning writes the (later) copied block. The setter now rejects backward non-null writes so a stale caller can't regress the reported availability. Null reset remains allowed for explicit recovery (e.g. wiping a corrupt state DB). Also adds StateBoundaryStoreTests covering initial-null read, round-trip persistence, idempotent re-set, the monotonic guard, and the null-reset escape hatch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks in the epochs→blocks conversion (×32) so future refactors can't silently change the slot-per-epoch constant without test coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If the kv write threw after _value had already been overwritten, the in-memory snapshot would diverge from disk until the next forward write or process restart. Swap the order so a failed persistence leaves both caches at the previous value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude review |
|
@claude review |
|
Claude finished @LukaszRozmej's task in 5m 48s —— View job Code Review (Pass 9)
SummaryThis is pass 9. Eight commits landed since pass 8, addressing all outstanding @asdacap CHANGES_REQUESTED. Here is the complete status:
Architecture is clean. The key design choices:
No new findings. All prior Low observations are resolved. No Critical, High, or Medium findings remain. Verdict: ready to merge. |
EthCapabilitiesProvider only needs to read the floor; StateSyncRunner and FullPruner are the only legitimate writers. Splitting the contract makes the write surface explicit instead of leaking through every consumer of IWorldStateManager, which still implements both halves. Also promotes StateBoundaryStore.OldestStateBlockKey to internal so the full-pruning disk test can reference the key constant directly rather than recomputing the keccak inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests - Drop inline comments restating what IStateBoundary's XML doc already says (co-location, "no rolling window for flat", etc.). - Compress EthCapabilities docs by moving the Tx/Logs aliasing rationale into <remarks> and tightening to one sentence. - Drop the implementation-detail tail of the RetentionBlocks param doc. - Collapse the three single-action StateBoundaryStore tests (forward/backward/equal) into one [TestCase]-parameterized test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @LukaszRozmej's task in 4m 36s —— View job Code Review (Pass 9)
SummaryThis is pass 9. All @asdacap CHANGES_REQUESTED from May 14 have been addressed in the 11 commits since pass 8. @asdacap May 14 findings — resolved:
Architecture is clean:
All prior findings (passes 1–8, @asdacap × 2, @svlachakis × 2) remain resolved. No new Critical, High, or Medium findings. Verdict: ready to merge. |
| treeSync.FinalizeSync(finalPivot); | ||
|
|
||
| // Reported through eth_capabilities. | ||
| stateBoundary.OldestStateBlock = finalPivot.Number; |
|
|
||
| private readonly FlatTrieVerifier _trieVerifier = new(flatDbManager, persistence, logManager); | ||
|
|
||
| private readonly StateBoundaryStore _boundaryStore = new(flatDb.GetColumnDb(FlatDbColumns.Metadata)); |
There was a problem hiding this comment.
Wrong. There is no store for flat, it just check the IPersistence directly.
| public long? OldestStateBlock | ||
| { | ||
| get => _boundaryStore.OldestStateBlock; | ||
| set => _boundaryStore.OldestStateBlock = value; |
There was a problem hiding this comment.
No mutation with flat. Flat already have the the state information written in PersistenceManager.
| namespace Nethermind.State; | ||
|
|
||
| public interface IWorldStateManager | ||
| public interface IWorldStateManager : IStateBoundary, IStateBoundaryWriter |
There was a problem hiding this comment.
No IStateBoundaryWriter. That is clearly trie store specific.
| /// Write side of <see cref="IStateBoundary"/>. Held only by the components that legitimately | ||
| /// advance the floor. | ||
| /// </summary> | ||
| public interface IStateBoundaryWriter |
There was a problem hiding this comment.
Writer is triestore specific. Hide it. Flat already handle the state tracking. Only the reader is needed with flat.
- IStateBoundaryWriter is trie-specific: it's no longer on IWorldStateManager. Only the trie WorldStateManager implements it; FlatWorldStateManager exposes only the reader half. - Move the OldestStateBlock write from StateSyncRunner (wrong layer — sync runner orchestrates rounds and shouldn't know about the floor) to PatriciaTreeSyncStore.FinalizeSync, which is the trie-specific finalization hook that already has the pivot header. - FlatWorldStateManager no longer owns a StateBoundaryStore. Flat state tracking lives in PersistenceManager already, so OldestStateBlock just reads CurrentPersistedStateId.BlockNumber from there. No setter on flat. - DI: WorldStateModule keeps the IStateBoundary map (works for both backends since IWorldStateManager : IStateBoundary). IStateBoundaryWriter moves to PruningTrieStoreModule, mapped from the trie factory output so it stays unresolved when flat is the active backend. - FullPruner's worldStateManager argument is cast to IStateBoundaryWriter at the FullPrunerFactory boundary (safe — full pruning is trie-only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The store now serves only the trie backend — flat reads from PersistenceManager directly — so the dual-backend wording was misleading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…apabilities-support
…ions Master removed FluentAssertions (NethermindEth#11567 "Unify assertions"); the merged StateBoundaryStoreTests still used .Should() via Core.Test's transitive package reference and no longer builds. Convert to Assert.That to match the unified style.
|
@claude please check carefully this review whether it's valid or not 🔴 Critical — must fix before merge Stale rebase reverts two merged PRs. The diff against origin/master silently undoes:
Fix: rebase onto current origin/master and drop the reversions. 🟠 High
🟡 Medium
🟢 Low
|
|
Claude finished @svlachakis's task in 8m 44s —— View job Review Validation
Findings🔴 Critical — "Stale rebase reverts #11811 and #11794"#11811 (KBucketTree.cs): CONFIRMED VALID The diff against
Impact assessment: The rebase stale-merge explanation is correct: #11794 (TransactionProcessor.cs): INCORRECT — reviewer has the direction backwards The PR branch adds ReadOnlyBlockTree.cs blank line: CONFIRMED, BENIGN The PR removes one blank line (line 37 in master). Trivial, but real. 🟠 High #1 — FullPruner durability ordering: VALID
The suggested fix (write the marker into the cloning DB before 🟠 High #2 — StateBoundaryStore lock on every read: OVERSTATED
The lock is correct: 🟡 Medium #3 — Unsafe cast
|
| Claim | Verdict |
|---|---|
🔴 KBucketTree.cs _logger.Info / missing guard |
VALID — regression, but Medium (not Critical); LogDebugInfo only called inside if (_logger.IsDebug) |
🔴 TransactionProcessor.cs drops _blockCumulativeReceiptGas |
INCORRECT — PR adds it; master is the one without it |
| 🟠 High #1 FullPruner durability ordering | VALID High |
| 🟠 High #2 StateBoundaryStore lock on reads | Overstated — should be Low; lock is correct, path is not hot |
| 🟡 Medium #3 FullPrunerFactory unsafe cast | Valid concern, safe in practice — Low |
| 🟡 Medium #4 Silent backward write rejection | Valid — Low |
| 🟡 Medium #5 Redundant null check | Valid — Low |
| 🟢 Low #6 Test naming | Valid |
🟢 Low #7 Typo _readaOnlyCodeCb |
Valid |
The two actionable items before merge are: (1) fix the KBucketTree.cs regression (_logger.Info → _logger.Trace + if (!_logger.IsTrace) return; guard); (2) address the FullPruner durability gap by writing the floor marker into the cloning DB before Commit(). The TransactionProcessor.cs concern in the review is a false alarm.
- Restore NethermindEth#11811 KBucketTree.cs early-return guards and Trace level reverted by stale rebase - Make FullPruner OldestStateBlock advance atomic with DB swap: write the marker before pruning.Commit() so FullPruningDb mirrors it into the cloning DB - Drop unsafe (IStateBoundaryWriter) cast in FullPrunerFactory by injecting IStateBoundaryWriter from DI - Log backward OldestStateBlock writes at Warn instead of dropping them silently - Restructure BuildState with single early-return to remove the redundant retention null check - Restore ReadOnlyBlockTree.cs blank line removed in rebase - Rename Eth_capabilities_* tests to match the project's PascalCase prefix convention
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 3m 11s —— View job Code Review (Pass 10)
SummaryThis is pass 10. Two commits landed since pass 9:
All @svlachakis findings — resolved:
Architecture is sound. The DI wiring in Verdict: ready to merge. No Critical, High, or Medium findings remain unresolved. |
…rFactory Injecting IStateBoundaryWriter created a DI cycle: PruningTrieStateFactory → FullPrunerFactory → IStateBoundaryWriter (mapped from PruningTrieStateFactoryOutput, which is built by PruningTrieStateFactory). Revert to the runtime cast — it is safe in practice because Create only runs for trie storage, which is always WorldStateManager.
Closes #11260
Changes
eth_capabilitiesRPC method toIEthRpcModuleandEthRpcModuleper ethereum/execution-apis#755EthCapabilitiesResultresponse model withCapabilityHead,CapabilityResource, andCapabilityDeleteStrategytypesISyncConfigandIPruningConfigas optional constructor parameters intoEthRpcModuleandEthModuleFactoryto derive historical data availabilitynumber+hash) and six resource descriptors:state,tx,logs,receipts,blocks,stateproofsoldestBlockanddeleteStrategyare omitted (null) for disabled resources and archive nodes;deleteStrategyonly appears for in-memory pruned nodes (type: "window")stateproofsis disabled on pruned nodes sinceeth_getProofrequires full trie historyTypes of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
4 tests added in
EthRpcModuleTests.Capabilities.cs:eth_capabilities_returns_all_resources_and_head— verifies all 6 resource fields and head block info are presenteth_capabilities_archive_node_has_stateproofs_enabled_and_no_delete_strategy— archive node (PruningMode.None) serves state proofs, has no window delete strategyeth_capabilities_pruned_node_disables_stateproofs_and_reports_window— memory-pruned node disablesstateproofs, reportsdeleteStrategy.type = "window"with correctretentionBlockseth_capabilities_no_receipts_disables_tx_logs_receipts— whenDownloadReceiptsInFastSync = false,tx,logs, andreceiptsare disabled with nooldestBlockAll 5 tests pass (
total: 5 | failed: 0 | succeeded: 5).Documentation
Requires documentation update
Requires explanation in Release Notes
Add
eth_capabilitiesJSON-RPC method. Returns the node's current head block and historical data availability forstate,tx,logs,receipts,blocks, andstateproofsresources. Enables RPC routers and load balancers to make routing decisions without active probing. Implements ethereum/execution-apis#755.