From f8fb64a285a2dc8a981d81b937c337e94c5da061 Mon Sep 17 00:00:00 2001
From: locoholy <68823405+locoholy@users.noreply.github.com>
Date: Wed, 8 Apr 2026 02:04:38 +0500
Subject: [PATCH 1/2] eth, internal/ethapi: add eth_capabilities RPC method
---
eth/api_backend.go | 14 +
internal/ethapi/api_test.go | 3 +
internal/ethapi/backend.go | 1 +
internal/ethapi/capabilities.go | 223 +++++++++++++
internal/ethapi/capabilities_test.go | 389 +++++++++++++++++++++++
internal/ethapi/transaction_args_test.go | 3 +-
internal/web3ext/web3ext.go | 5 +
7 files changed, 637 insertions(+), 1 deletion(-)
create mode 100644 internal/ethapi/capabilities.go
create mode 100644 internal/ethapi/capabilities_test.go
diff --git a/eth/api_backend.go b/eth/api_backend.go
index 3f826b7861..5806f433e3 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -40,6 +40,7 @@ import (
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
)
@@ -278,6 +279,19 @@ func (b *EthAPIBackend) HistoryPruningCutoff() uint64 {
return bn
}
+func (b *EthAPIBackend) HistoryRetention() ethapi.HistoryRetention {
+ cfg := b.eth.config
+ return ethapi.HistoryRetention{
+ TxIndexHistory: cfg.TransactionHistory,
+ LogIndexHistory: cfg.LogHistory,
+ LogIndexDisabled: cfg.LogNoHistory,
+ StateHistory: cfg.StateHistory,
+ TrienodeHistory: cfg.TrienodeHistory,
+ StateArchive: cfg.NoPruning,
+ StateScheme: b.eth.blockchain.TrieDB().Scheme(),
+ }
+}
+
func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
return b.eth.blockchain.GetReceiptsByHash(hash), nil
}
diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go
index 2f0c07694d..bf33009780 100644
--- a/internal/ethapi/api_test.go
+++ b/internal/ethapi/api_test.go
@@ -708,6 +708,9 @@ func (b testBackend) HistoryPruningCutoff() uint64 {
bn, _ := b.chain.HistoryPruningCutoff()
return bn
}
+func (b testBackend) HistoryRetention() HistoryRetention {
+ return HistoryRetention{StateScheme: b.chain.TrieDB().Scheme()}
+}
func TestEstimateGas(t *testing.T) {
t.Parallel()
diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go
index af3d592b82..b0a8790572 100644
--- a/internal/ethapi/backend.go
+++ b/internal/ethapi/backend.go
@@ -90,6 +90,7 @@ type Backend interface {
ChainConfig() *params.ChainConfig
Engine() consensus.Engine
HistoryPruningCutoff() uint64
+ HistoryRetention() HistoryRetention
// This is copied from filters.Backend
// eth/filters needs to be initialized from this backend type, so methods needed by
diff --git a/internal/ethapi/capabilities.go b/internal/ethapi/capabilities.go
new file mode 100644
index 0000000000..95eba8ffad
--- /dev/null
+++ b/internal/ethapi/capabilities.go
@@ -0,0 +1,223 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package ethapi
+
+import (
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ corestate "github.com/ethereum/go-ethereum/core/state"
+)
+
+// HistoryRetention reports a node's configured history retention windows.
+// It is consumed by the eth_capabilities RPC method to derive the response
+// described in https://github.com/ethereum/execution-apis/pull/755.
+type HistoryRetention struct {
+ // TxIndexHistory is the number of recent blocks for which the
+ // transaction lookup index is maintained. Zero means the index covers
+ // the entire available chain.
+ TxIndexHistory uint64
+
+ // LogIndexHistory is the number of recent blocks for which the log
+ // search index is maintained. Zero means the index covers the entire
+ // available chain.
+ LogIndexHistory uint64
+
+ // LogIndexDisabled reports whether the log search index has been
+ // turned off entirely.
+ LogIndexDisabled bool
+
+ // StateHistory is the number of recent blocks for which historical
+ // state is retained in path-based archive mode. Zero means the entire
+ // available state history is kept.
+ StateHistory uint64
+
+ // TrienodeHistory is the number of recent blocks for which trie node
+ // history is retained in path-based archive mode. Zero means the entire
+ // available trienode history is kept; negative means no trienode history
+ // is stored.
+ TrienodeHistory int64
+
+ // StateArchive reports whether state pruning is disabled
+ // (--gcmode=archive).
+ StateArchive bool
+
+ // StateScheme is the state storage scheme in use, either "hash" or
+ // "path".
+ StateScheme string
+}
+
+// Capabilities reports which historical data the node can serve. It is
+// returned by the eth_capabilities RPC method as defined in
+// https://github.com/ethereum/execution-apis/pull/755.
+type Capabilities struct {
+ Head CapabilityHead `json:"head"`
+ State CapabilityResource `json:"state"`
+ Tx CapabilityResource `json:"tx"`
+ Logs CapabilityResource `json:"logs"`
+ Receipts CapabilityResource `json:"receipts"`
+ Blocks CapabilityResource `json:"blocks"`
+ StateProofs CapabilityResource `json:"stateproofs"`
+}
+
+// CapabilityHead is the current canonical head as reported by the node.
+type CapabilityHead struct {
+ BlockNumber hexutil.Uint64 `json:"blockNumber"`
+ BlockHash common.Hash `json:"blockHash"`
+}
+
+// CapabilityResource describes the availability of a single data resource.
+type CapabilityResource struct {
+ Disabled bool `json:"disabled"`
+ OldestBlock hexutil.Uint64 `json:"oldestBlock"`
+ DeleteStrategy DeleteStrategy `json:"deleteStrategy"`
+}
+
+// DeleteStrategy describes how data of a resource is removed over time.
+//
+// Two strategies are defined by the spec:
+//
+// - "none": data is never deleted; the resource is permanently
+// retained from oldestBlock onwards.
+// - "window": data is retained for a sliding window of the most recent
+// RetentionBlocks blocks.
+//
+// RetentionBlocks is omitted from the JSON output for the "none" strategy.
+type DeleteStrategy struct {
+ Type string `json:"type"`
+ RetentionBlocks *uint64 `json:"retentionBlocks,omitempty"`
+}
+
+// strategyNone returns a DeleteStrategy with type "none".
+func strategyNone() DeleteStrategy {
+ return DeleteStrategy{Type: "none"}
+}
+
+// strategyWindow returns a DeleteStrategy with type "window" and the given
+// retention block count.
+func strategyWindow(retention uint64) DeleteStrategy {
+ return DeleteStrategy{Type: "window", RetentionBlocks: &retention}
+}
+
+// Capabilities implements the eth_capabilities RPC method as defined in
+// https://github.com/ethereum/execution-apis/pull/755. It returns a
+// description of the historical data this node can serve, allowing RPC
+// routers to determine which queries can be answered without hitting
+// "history pruned" errors.
+func (api *BlockChainAPI) Capabilities() *Capabilities {
+ head := api.b.CurrentHeader()
+ return buildCapabilities(
+ head.Number.Uint64(),
+ head.Hash(),
+ api.b.HistoryPruningCutoff(),
+ api.b.HistoryRetention(),
+ )
+}
+
+// buildCapabilities computes the eth_capabilities response from the head
+// block, the absolute history pruning cutoff, and the configured retention
+// windows. It is split out from the RPC method so the mapping rules can be
+// unit tested without a backend.
+func buildCapabilities(headNum uint64, headHash common.Hash, cutoff uint64, ret HistoryRetention) *Capabilities {
+ // windowOldest returns the oldest block reachable through a sliding
+ // window of `window` blocks, never going below the supplied floor. A
+ // window of zero means "no sliding deletion" and reports the floor
+ // itself.
+ windowOldest := func(window uint64, floor uint64) uint64 {
+ if window == 0 || headNum+1 <= window {
+ return floor
+ }
+ oldest := headNum + 1 - window
+ if oldest < floor {
+ return floor
+ }
+ return oldest
+ }
+
+ // resource builds a CapabilityResource for a window-style resource.
+ // A window of zero is reported as deleteStrategy "none".
+ resource := func(disabled bool, window uint64, floor uint64) CapabilityResource {
+ ds := strategyNone()
+ if window != 0 {
+ ds = strategyWindow(window)
+ }
+ return CapabilityResource{
+ Disabled: disabled,
+ OldestBlock: hexutil.Uint64(windowOldest(window, floor)),
+ DeleteStrategy: ds,
+ }
+ }
+
+ // Bodies and receipts share the same retention model in
+ // geth: they are either kept in full ("all") or pruned to a fixed
+ // boundary ("postmerge"). In neither case is there a sliding
+ // deletion window, so the strategy is always "none" and the oldest
+ // block equals the history pruning cutoff.
+ blocks := CapabilityResource{
+ Disabled: false,
+ OldestBlock: hexutil.Uint64(cutoff),
+ DeleteStrategy: strategyNone(),
+ }
+ receipts := blocks
+
+ tx := resource(false, ret.TxIndexHistory, cutoff)
+ logs := resource(ret.LogIndexDisabled, ret.LogIndexHistory, cutoff)
+
+ // State availability is determined primarily by gcmode:
+ //
+ // - full mode: only the in-memory state window is reachable,
+ // regardless of the storage scheme.
+ // - archive+hash: full state history is reachable.
+ // - archive+path: honors the configured StateHistory window.
+ var state CapabilityResource
+ switch {
+ case !ret.StateArchive:
+ state = resource(false, corestate.TriesInMemory, 0)
+ case ret.StateScheme == rawdb.HashScheme:
+ state = resource(false, 0, 0)
+ default:
+ state = resource(false, ret.StateHistory, 0)
+ }
+
+ // eth_getProof availability tracks state availability in hash mode and
+ // in path-based full mode. Path-based archive nodes store trie node
+ // history separately from state history.
+ stateproofs := state
+ if ret.StateArchive && ret.StateScheme == rawdb.PathScheme {
+ switch {
+ case ret.TrienodeHistory < 0:
+ stateproofs = resource(false, corestate.TriesInMemory, 0)
+ case ret.TrienodeHistory == 0:
+ stateproofs = resource(false, 0, 0)
+ default:
+ stateproofs = resource(false, uint64(ret.TrienodeHistory), 0)
+ }
+ }
+
+ return &Capabilities{
+ Head: CapabilityHead{
+ BlockNumber: hexutil.Uint64(headNum),
+ BlockHash: headHash,
+ },
+ State: state,
+ Tx: tx,
+ Logs: logs,
+ Receipts: receipts,
+ Blocks: blocks,
+ StateProofs: stateproofs,
+ }
+}
diff --git a/internal/ethapi/capabilities_test.go b/internal/ethapi/capabilities_test.go
new file mode 100644
index 0000000000..0724433a68
--- /dev/null
+++ b/internal/ethapi/capabilities_test.go
@@ -0,0 +1,389 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package ethapi
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ corestate "github.com/ethereum/go-ethereum/core/state"
+)
+
+func TestBuildCapabilities(t *testing.T) {
+ const (
+ archiveHead uint64 = 3_000_000
+ postmerge uint64 = 15_537_393
+ )
+ headHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
+
+ // retentionWindow is a small helper for asserting on
+ // CapabilityResource fields.
+ retentionWindow := func(n uint64) *uint64 { return &n }
+
+ tests := []struct {
+ name string
+ headNum uint64
+ cutoff uint64
+ ret HistoryRetention
+ expected map[string]CapabilityResource // by JSON field name
+ }{
+ {
+ name: "archive node, path scheme, all defaults",
+ headNum: archiveHead,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateArchive: true,
+ StateScheme: rawdb.PathScheme,
+ },
+ expected: map[string]CapabilityResource{
+ "blocks": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "receipts": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "tx": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "logs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "state": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "stateproofs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ },
+ },
+ {
+ name: "post-merge pruned chain",
+ headNum: archiveHead,
+ cutoff: postmerge,
+ ret: HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ },
+ expected: map[string]CapabilityResource{
+ // blocks/receipts honor the absolute cutoff with no
+ // sliding window.
+ "blocks": {OldestBlock: hexUint(postmerge), DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "receipts": {OldestBlock: hexUint(postmerge), DeleteStrategy: DeleteStrategy{Type: "none"}},
+ },
+ },
+ {
+ name: "default tx and log indices, head above window",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ TxIndexHistory: 2_350_000,
+ LogIndexHistory: 2_350_000,
+ },
+ expected: map[string]CapabilityResource{
+ "tx": {
+ OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ },
+ "logs": {
+ OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ },
+ },
+ },
+ {
+ name: "head below tx window: clamp to cutoff, no underflow",
+ headNum: 100,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ TxIndexHistory: 2_350_000,
+ },
+ expected: map[string]CapabilityResource{
+ "tx": {
+ OldestBlock: 0,
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ },
+ },
+ },
+ {
+ name: "tx window oldest clamped to history pruning cutoff",
+ headNum: 5_000_000,
+ cutoff: 4_000_000,
+ ret: HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ TxIndexHistory: 2_350_000, // would otherwise reach back to 2.65M
+ },
+ expected: map[string]CapabilityResource{
+ "tx": {
+ OldestBlock: hexUint(4_000_000),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ },
+ },
+ },
+ {
+ name: "state windows are not clamped to history pruning cutoff",
+ headNum: 5_000_000,
+ cutoff: 4_950_000,
+ ret: HistoryRetention{
+ StateArchive: true,
+ StateScheme: rawdb.PathScheme,
+ StateHistory: 90_000,
+ TrienodeHistory: 100_000,
+ },
+ expected: map[string]CapabilityResource{
+ "state": {
+ OldestBlock: hexUint(5_000_000 - 90_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)},
+ },
+ "stateproofs": {
+ OldestBlock: hexUint(5_000_000 - 100_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(100_000)},
+ },
+ },
+ },
+ {
+ name: "log index disabled",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ LogIndexHistory: 2_350_000,
+ LogIndexDisabled: true,
+ },
+ expected: map[string]CapabilityResource{
+ "logs": {
+ Disabled: true,
+ OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ },
+ },
+ },
+ {
+ name: "path archive with separate state and trienode history windows",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateArchive: true,
+ StateScheme: rawdb.PathScheme,
+ StateHistory: 90_000,
+ TrienodeHistory: 50_000,
+ },
+ expected: map[string]CapabilityResource{
+ "state": {
+ OldestBlock: hexUint(5_000_000 - 90_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)},
+ },
+ "stateproofs": {
+ OldestBlock: hexUint(5_000_000 - 50_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(50_000)},
+ },
+ },
+ },
+ {
+ name: "path archive with trienode history disabled retains in-memory proofs",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateArchive: true,
+ StateScheme: rawdb.PathScheme,
+ StateHistory: 90_000,
+ TrienodeHistory: -1,
+ },
+ expected: map[string]CapabilityResource{
+ "state": {
+ OldestBlock: hexUint(5_000_000 - 90_000 + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)},
+ },
+ "stateproofs": {
+ OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ },
+ },
+ },
+ {
+ name: "hash scheme archive ignores StateHistory",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateArchive: true,
+ StateScheme: rawdb.HashScheme,
+ StateHistory: 90_000,
+ },
+ expected: map[string]CapabilityResource{
+ "state": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "stateproofs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ },
+ },
+ {
+ name: "full mode hash scheme retains in-memory state window",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateScheme: rawdb.HashScheme,
+ StateHistory: 90_000, // ignored under hash scheme
+ },
+ expected: map[string]CapabilityResource{
+ "state": {
+ OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ },
+ "stateproofs": {
+ OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ },
+ },
+ },
+ {
+ name: "full mode path scheme ignores StateHistory",
+ headNum: 5_000_000,
+ cutoff: 0,
+ ret: HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ StateHistory: 90_000,
+ },
+ expected: map[string]CapabilityResource{
+ "state": {
+ OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ },
+ "stateproofs": {
+ OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ caps := buildCapabilities(tt.headNum, headHash, tt.cutoff, tt.ret)
+
+ // Head is always present.
+ if uint64(caps.Head.BlockNumber) != tt.headNum {
+ t.Errorf("head.blockNumber = %d, want %d", uint64(caps.Head.BlockNumber), tt.headNum)
+ }
+ if caps.Head.BlockHash != headHash {
+ t.Errorf("head.blockHash = %x, want %x", caps.Head.BlockHash, headHash)
+ }
+
+ actual := map[string]CapabilityResource{
+ "state": caps.State,
+ "tx": caps.Tx,
+ "logs": caps.Logs,
+ "receipts": caps.Receipts,
+ "blocks": caps.Blocks,
+ "stateproofs": caps.StateProofs,
+ }
+ for name, want := range tt.expected {
+ got := actual[name]
+ if got.Disabled != want.Disabled {
+ t.Errorf("%s.disabled = %v, want %v", name, got.Disabled, want.Disabled)
+ }
+ if got.OldestBlock != want.OldestBlock {
+ t.Errorf("%s.oldestBlock = %d, want %d", name, uint64(got.OldestBlock), uint64(want.OldestBlock))
+ }
+ if got.DeleteStrategy.Type != want.DeleteStrategy.Type {
+ t.Errorf("%s.deleteStrategy.type = %q, want %q", name, got.DeleteStrategy.Type, want.DeleteStrategy.Type)
+ }
+ switch {
+ case want.DeleteStrategy.RetentionBlocks == nil && got.DeleteStrategy.RetentionBlocks != nil:
+ t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want absent",
+ name, *got.DeleteStrategy.RetentionBlocks)
+ case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks == nil:
+ t.Errorf("%s.deleteStrategy.retentionBlocks absent, want %d",
+ name, *want.DeleteStrategy.RetentionBlocks)
+ case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks != nil:
+ if *got.DeleteStrategy.RetentionBlocks != *want.DeleteStrategy.RetentionBlocks {
+ t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want %d",
+ name, *got.DeleteStrategy.RetentionBlocks, *want.DeleteStrategy.RetentionBlocks)
+ }
+ }
+ }
+ })
+ }
+}
+
+// TestCapabilitiesJSONShape verifies that the marshalled JSON conforms to
+// the schema defined in https://github.com/ethereum/execution-apis/pull/755:
+// "none" strategies must omit retentionBlocks, oldestBlock must be a hex
+// quantity, retentionBlocks must be a decimal integer.
+func TestCapabilitiesJSONShape(t *testing.T) {
+ caps := buildCapabilities(
+ 5_000_000,
+ common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"),
+ 0,
+ HistoryRetention{
+ StateScheme: rawdb.PathScheme,
+ TxIndexHistory: 2_350_000,
+ LogIndexHistory: 2_350_000,
+ StateHistory: 90_000,
+ },
+ )
+
+ raw, err := json.Marshal(caps)
+ if err != nil {
+ t.Fatalf("marshal: %v", err)
+ }
+
+ // Round-trip through a generic map so we can assert on key presence.
+ var generic map[string]any
+ if err := json.Unmarshal(raw, &generic); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+
+ // Top-level keys must match the spec.
+ required := []string{"head", "state", "tx", "logs", "receipts", "blocks", "stateproofs"}
+ for _, k := range required {
+ if _, ok := generic[k]; !ok {
+ t.Errorf("missing top-level key %q", k)
+ }
+ }
+
+ // head.blockNumber must be a hex string ("0x..."), blockHash must be a 0x hash.
+ head := generic["head"].(map[string]any)
+ if bn, ok := head["blockNumber"].(string); !ok || len(bn) < 3 || bn[:2] != "0x" {
+ t.Errorf("head.blockNumber not hex string: %v", head["blockNumber"])
+ }
+ if bh, ok := head["blockHash"].(string); !ok || len(bh) != 66 {
+ t.Errorf("head.blockHash not 32-byte hex string: %v", head["blockHash"])
+ }
+
+ // blocks.deleteStrategy is "none" → must NOT contain retentionBlocks.
+ blocks := generic["blocks"].(map[string]any)
+ bds := blocks["deleteStrategy"].(map[string]any)
+ if bds["type"] != "none" {
+ t.Errorf("blocks.deleteStrategy.type = %v, want none", bds["type"])
+ }
+ if _, present := bds["retentionBlocks"]; present {
+ t.Errorf("blocks.deleteStrategy must not include retentionBlocks for type=none")
+ }
+
+ // tx.deleteStrategy is "window" → must contain retentionBlocks as a
+ // decimal number, not a hex string.
+ tx := generic["tx"].(map[string]any)
+ tds := tx["deleteStrategy"].(map[string]any)
+ if tds["type"] != "window" {
+ t.Errorf("tx.deleteStrategy.type = %v, want window", tds["type"])
+ }
+ rb, ok := tds["retentionBlocks"].(float64)
+ if !ok {
+ t.Fatalf("tx.deleteStrategy.retentionBlocks not a JSON number: %T %v",
+ tds["retentionBlocks"], tds["retentionBlocks"])
+ }
+ if uint64(rb) != 2_350_000 {
+ t.Errorf("tx.deleteStrategy.retentionBlocks = %v, want 2350000", rb)
+ }
+
+ // tx.oldestBlock must be a hex string.
+ if ob, ok := tx["oldestBlock"].(string); !ok || len(ob) < 3 || ob[:2] != "0x" {
+ t.Errorf("tx.oldestBlock not hex string: %v", tx["oldestBlock"])
+ }
+}
+
+// hexUint is a small helper to keep the test tables compact.
+func hexUint(n uint64) hexutil.Uint64 { return hexutil.Uint64(n) }
diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go
index 30791f32b5..bae02c0f77 100644
--- a/internal/ethapi/transaction_args_test.go
+++ b/internal/ethapi/transaction_args_test.go
@@ -410,4 +410,5 @@ func (b *backendMock) Engine() consensus.Engine { return nil }
func (b *backendMock) CurrentView() *filtermaps.ChainView { return nil }
func (b *backendMock) NewMatcherBackend() filtermaps.MatcherBackend { return nil }
-func (b *backendMock) HistoryPruningCutoff() uint64 { return 0 }
+func (b *backendMock) HistoryPruningCutoff() uint64 { return 0 }
+func (b *backendMock) HistoryRetention() HistoryRetention { return HistoryRetention{} }
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index 9ba8776360..979c84d80b 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -611,6 +611,11 @@ web3._extend({
name: 'config',
call: 'eth_config',
params: 0,
+ }),
+ new web3._extend.Method({
+ name: 'capabilities',
+ call: 'eth_capabilities',
+ params: 0,
})
],
properties: [
From e4ac40b5b5395ecc23cf5f307634d4a8094d8c18 Mon Sep 17 00:00:00 2001
From: locoholy <68823405+locoholy@users.noreply.github.com>
Date: Mon, 18 May 2026 21:10:41 +0500
Subject: [PATCH 2/2] internal/ethapi: align capabilities response with spec
---
internal/ethapi/capabilities.go | 67 +++++----
internal/ethapi/capabilities_test.go | 201 ++++++++++++++++-----------
2 files changed, 149 insertions(+), 119 deletions(-)
diff --git a/internal/ethapi/capabilities.go b/internal/ethapi/capabilities.go
index 95eba8ffad..a08687f100 100644
--- a/internal/ethapi/capabilities.go
+++ b/internal/ethapi/capabilities.go
@@ -76,41 +76,36 @@ type Capabilities struct {
// CapabilityHead is the current canonical head as reported by the node.
type CapabilityHead struct {
- BlockNumber hexutil.Uint64 `json:"blockNumber"`
- BlockHash common.Hash `json:"blockHash"`
+ Number hexutil.Uint64 `json:"number"`
+ Hash common.Hash `json:"hash"`
}
// CapabilityResource describes the availability of a single data resource.
type CapabilityResource struct {
- Disabled bool `json:"disabled"`
- OldestBlock hexutil.Uint64 `json:"oldestBlock"`
- DeleteStrategy DeleteStrategy `json:"deleteStrategy"`
+ Disabled bool `json:"disabled"`
+ OldestBlock *hexutil.Uint64 `json:"oldestBlock,omitempty"`
+ DeleteStrategy *DeleteStrategy `json:"deleteStrategy,omitempty"`
}
// DeleteStrategy describes how data of a resource is removed over time.
//
-// Two strategies are defined by the spec:
-//
-// - "none": data is never deleted; the resource is permanently
-// retained from oldestBlock onwards.
-// - "window": data is retained for a sliding window of the most recent
-// RetentionBlocks blocks.
-//
-// RetentionBlocks is omitted from the JSON output for the "none" strategy.
+// The spec currently defines one strategy: "window", meaning data is retained
+// for a sliding window of the most recent RetentionBlocks blocks. Resources
+// without sliding deletion omit deleteStrategy.
type DeleteStrategy struct {
Type string `json:"type"`
RetentionBlocks *uint64 `json:"retentionBlocks,omitempty"`
}
-// strategyNone returns a DeleteStrategy with type "none".
-func strategyNone() DeleteStrategy {
- return DeleteStrategy{Type: "none"}
-}
-
// strategyWindow returns a DeleteStrategy with type "window" and the given
// retention block count.
-func strategyWindow(retention uint64) DeleteStrategy {
- return DeleteStrategy{Type: "window", RetentionBlocks: &retention}
+func strategyWindow(retention uint64) *DeleteStrategy {
+ return &DeleteStrategy{Type: "window", RetentionBlocks: &retention}
+}
+
+func capabilityOldestBlock(number uint64) *hexutil.Uint64 {
+ oldest := hexutil.Uint64(number)
+ return &oldest
}
// Capabilities implements the eth_capabilities RPC method as defined in
@@ -149,28 +144,28 @@ func buildCapabilities(headNum uint64, headHash common.Hash, cutoff uint64, ret
}
// resource builds a CapabilityResource for a window-style resource.
- // A window of zero is reported as deleteStrategy "none".
+ // Disabled resources intentionally omit oldestBlock and deleteStrategy,
+ // because those fields would otherwise look like usable history ranges.
resource := func(disabled bool, window uint64, floor uint64) CapabilityResource {
- ds := strategyNone()
+ if disabled {
+ return CapabilityResource{Disabled: true}
+ }
+ res := CapabilityResource{
+ OldestBlock: capabilityOldestBlock(windowOldest(window, floor)),
+ }
if window != 0 {
- ds = strategyWindow(window)
- }
- return CapabilityResource{
- Disabled: disabled,
- OldestBlock: hexutil.Uint64(windowOldest(window, floor)),
- DeleteStrategy: ds,
+ res.DeleteStrategy = strategyWindow(window)
}
+ return res
}
// Bodies and receipts share the same retention model in
// geth: they are either kept in full ("all") or pruned to a fixed
- // boundary ("postmerge"). In neither case is there a sliding
- // deletion window, so the strategy is always "none" and the oldest
- // block equals the history pruning cutoff.
+ // boundary ("postmerge"). In neither case is there a sliding deletion
+ // window, so deleteStrategy is omitted and oldestBlock equals the history
+ // pruning cutoff.
blocks := CapabilityResource{
- Disabled: false,
- OldestBlock: hexutil.Uint64(cutoff),
- DeleteStrategy: strategyNone(),
+ OldestBlock: capabilityOldestBlock(cutoff),
}
receipts := blocks
@@ -210,8 +205,8 @@ func buildCapabilities(headNum uint64, headHash common.Hash, cutoff uint64, ret
return &Capabilities{
Head: CapabilityHead{
- BlockNumber: hexutil.Uint64(headNum),
- BlockHash: headHash,
+ Number: hexutil.Uint64(headNum),
+ Hash: headHash,
},
State: state,
Tx: tx,
diff --git a/internal/ethapi/capabilities_test.go b/internal/ethapi/capabilities_test.go
index 0724433a68..e5085a0db2 100644
--- a/internal/ethapi/capabilities_test.go
+++ b/internal/ethapi/capabilities_test.go
@@ -33,10 +33,6 @@ func TestBuildCapabilities(t *testing.T) {
)
headHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
- // retentionWindow is a small helper for asserting on
- // CapabilityResource fields.
- retentionWindow := func(n uint64) *uint64 { return &n }
-
tests := []struct {
name string
headNum uint64
@@ -53,12 +49,12 @@ func TestBuildCapabilities(t *testing.T) {
StateScheme: rawdb.PathScheme,
},
expected: map[string]CapabilityResource{
- "blocks": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
- "receipts": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
- "tx": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
- "logs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
- "state": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
- "stateproofs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "blocks": {OldestBlock: hexUintPtr(0)},
+ "receipts": {OldestBlock: hexUintPtr(0)},
+ "tx": {OldestBlock: hexUintPtr(0)},
+ "logs": {OldestBlock: hexUintPtr(0)},
+ "state": {OldestBlock: hexUintPtr(0)},
+ "stateproofs": {OldestBlock: hexUintPtr(0)},
},
},
{
@@ -71,8 +67,8 @@ func TestBuildCapabilities(t *testing.T) {
expected: map[string]CapabilityResource{
// blocks/receipts honor the absolute cutoff with no
// sliding window.
- "blocks": {OldestBlock: hexUint(postmerge), DeleteStrategy: DeleteStrategy{Type: "none"}},
- "receipts": {OldestBlock: hexUint(postmerge), DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "blocks": {OldestBlock: hexUintPtr(postmerge)},
+ "receipts": {OldestBlock: hexUintPtr(postmerge)},
},
},
{
@@ -86,12 +82,12 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"tx": {
- OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1),
+ DeleteStrategy: windowStrategy(2_350_000),
},
"logs": {
- OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1),
+ DeleteStrategy: windowStrategy(2_350_000),
},
},
},
@@ -105,8 +101,8 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"tx": {
- OldestBlock: 0,
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ OldestBlock: hexUintPtr(0),
+ DeleteStrategy: windowStrategy(2_350_000),
},
},
},
@@ -120,8 +116,8 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"tx": {
- OldestBlock: hexUint(4_000_000),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ OldestBlock: hexUintPtr(4_000_000),
+ DeleteStrategy: windowStrategy(2_350_000),
},
},
},
@@ -137,12 +133,12 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"state": {
- OldestBlock: hexUint(5_000_000 - 90_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1),
+ DeleteStrategy: windowStrategy(90_000),
},
"stateproofs": {
- OldestBlock: hexUint(5_000_000 - 100_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(100_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 100_000 + 1),
+ DeleteStrategy: windowStrategy(100_000),
},
},
},
@@ -157,9 +153,7 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"logs": {
- Disabled: true,
- OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
+ Disabled: true,
},
},
},
@@ -175,12 +169,12 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"state": {
- OldestBlock: hexUint(5_000_000 - 90_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1),
+ DeleteStrategy: windowStrategy(90_000),
},
"stateproofs": {
- OldestBlock: hexUint(5_000_000 - 50_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(50_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 50_000 + 1),
+ DeleteStrategy: windowStrategy(50_000),
},
},
},
@@ -196,12 +190,12 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"state": {
- OldestBlock: hexUint(5_000_000 - 90_000 + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)},
+ OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1),
+ DeleteStrategy: windowStrategy(90_000),
},
"stateproofs": {
- OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: windowStrategy(corestate.TriesInMemory),
},
},
},
@@ -215,8 +209,8 @@ func TestBuildCapabilities(t *testing.T) {
StateHistory: 90_000,
},
expected: map[string]CapabilityResource{
- "state": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
- "stateproofs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}},
+ "state": {OldestBlock: hexUintPtr(0)},
+ "stateproofs": {OldestBlock: hexUintPtr(0)},
},
},
{
@@ -229,12 +223,12 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"state": {
- OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: windowStrategy(corestate.TriesInMemory),
},
"stateproofs": {
- OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: windowStrategy(corestate.TriesInMemory),
},
},
},
@@ -248,12 +242,12 @@ func TestBuildCapabilities(t *testing.T) {
},
expected: map[string]CapabilityResource{
"state": {
- OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: windowStrategy(corestate.TriesInMemory),
},
"stateproofs": {
- OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1),
- DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)},
+ OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
+ DeleteStrategy: windowStrategy(corestate.TriesInMemory),
},
},
},
@@ -264,11 +258,11 @@ func TestBuildCapabilities(t *testing.T) {
caps := buildCapabilities(tt.headNum, headHash, tt.cutoff, tt.ret)
// Head is always present.
- if uint64(caps.Head.BlockNumber) != tt.headNum {
- t.Errorf("head.blockNumber = %d, want %d", uint64(caps.Head.BlockNumber), tt.headNum)
+ if uint64(caps.Head.Number) != tt.headNum {
+ t.Errorf("head.number = %d, want %d", uint64(caps.Head.Number), tt.headNum)
}
- if caps.Head.BlockHash != headHash {
- t.Errorf("head.blockHash = %x, want %x", caps.Head.BlockHash, headHash)
+ if caps.Head.Hash != headHash {
+ t.Errorf("head.hash = %x, want %x", caps.Head.Hash, headHash)
}
actual := map[string]CapabilityResource{
@@ -284,23 +278,38 @@ func TestBuildCapabilities(t *testing.T) {
if got.Disabled != want.Disabled {
t.Errorf("%s.disabled = %v, want %v", name, got.Disabled, want.Disabled)
}
- if got.OldestBlock != want.OldestBlock {
- t.Errorf("%s.oldestBlock = %d, want %d", name, uint64(got.OldestBlock), uint64(want.OldestBlock))
- }
- if got.DeleteStrategy.Type != want.DeleteStrategy.Type {
- t.Errorf("%s.deleteStrategy.type = %q, want %q", name, got.DeleteStrategy.Type, want.DeleteStrategy.Type)
+ switch {
+ case want.OldestBlock == nil && got.OldestBlock != nil:
+ t.Errorf("%s.oldestBlock = %d, want absent", name, uint64(*got.OldestBlock))
+ case want.OldestBlock != nil && got.OldestBlock == nil:
+ t.Errorf("%s.oldestBlock absent, want %d", name, uint64(*want.OldestBlock))
+ case want.OldestBlock != nil && got.OldestBlock != nil:
+ if *got.OldestBlock != *want.OldestBlock {
+ t.Errorf("%s.oldestBlock = %d, want %d",
+ name, uint64(*got.OldestBlock), uint64(*want.OldestBlock))
+ }
}
switch {
- case want.DeleteStrategy.RetentionBlocks == nil && got.DeleteStrategy.RetentionBlocks != nil:
- t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want absent",
- name, *got.DeleteStrategy.RetentionBlocks)
- case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks == nil:
- t.Errorf("%s.deleteStrategy.retentionBlocks absent, want %d",
- name, *want.DeleteStrategy.RetentionBlocks)
- case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks != nil:
- if *got.DeleteStrategy.RetentionBlocks != *want.DeleteStrategy.RetentionBlocks {
- t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want %d",
- name, *got.DeleteStrategy.RetentionBlocks, *want.DeleteStrategy.RetentionBlocks)
+ case want.DeleteStrategy == nil && got.DeleteStrategy != nil:
+ t.Errorf("%s.deleteStrategy = %#v, want absent", name, got.DeleteStrategy)
+ case want.DeleteStrategy != nil && got.DeleteStrategy == nil:
+ t.Errorf("%s.deleteStrategy absent, want %#v", name, want.DeleteStrategy)
+ case want.DeleteStrategy != nil && got.DeleteStrategy != nil:
+ if got.DeleteStrategy.Type != want.DeleteStrategy.Type {
+ t.Errorf("%s.deleteStrategy.type = %q, want %q", name, got.DeleteStrategy.Type, want.DeleteStrategy.Type)
+ }
+ switch {
+ case want.DeleteStrategy.RetentionBlocks == nil && got.DeleteStrategy.RetentionBlocks != nil:
+ t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want absent",
+ name, *got.DeleteStrategy.RetentionBlocks)
+ case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks == nil:
+ t.Errorf("%s.deleteStrategy.retentionBlocks absent, want %d",
+ name, *want.DeleteStrategy.RetentionBlocks)
+ case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks != nil:
+ if *got.DeleteStrategy.RetentionBlocks != *want.DeleteStrategy.RetentionBlocks {
+ t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want %d",
+ name, *got.DeleteStrategy.RetentionBlocks, *want.DeleteStrategy.RetentionBlocks)
+ }
}
}
}
@@ -310,18 +319,20 @@ func TestBuildCapabilities(t *testing.T) {
// TestCapabilitiesJSONShape verifies that the marshalled JSON conforms to
// the schema defined in https://github.com/ethereum/execution-apis/pull/755:
-// "none" strategies must omit retentionBlocks, oldestBlock must be a hex
-// quantity, retentionBlocks must be a decimal integer.
+// head fields are named number/hash, resources without a sliding window omit
+// deleteStrategy, disabled resources omit range fields, and retentionBlocks is
+// a decimal integer.
func TestCapabilitiesJSONShape(t *testing.T) {
caps := buildCapabilities(
5_000_000,
common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"),
0,
HistoryRetention{
- StateScheme: rawdb.PathScheme,
- TxIndexHistory: 2_350_000,
- LogIndexHistory: 2_350_000,
- StateHistory: 90_000,
+ StateScheme: rawdb.PathScheme,
+ TxIndexHistory: 2_350_000,
+ LogIndexHistory: 2_350_000,
+ LogIndexDisabled: true,
+ StateHistory: 90_000,
},
)
@@ -344,23 +355,40 @@ func TestCapabilitiesJSONShape(t *testing.T) {
}
}
- // head.blockNumber must be a hex string ("0x..."), blockHash must be a 0x hash.
+ // head.number must be a hex string ("0x..."), hash must be a 0x hash.
head := generic["head"].(map[string]any)
- if bn, ok := head["blockNumber"].(string); !ok || len(bn) < 3 || bn[:2] != "0x" {
- t.Errorf("head.blockNumber not hex string: %v", head["blockNumber"])
+ if number, ok := head["number"].(string); !ok || len(number) < 3 || number[:2] != "0x" {
+ t.Errorf("head.number not hex string: %v", head["number"])
}
- if bh, ok := head["blockHash"].(string); !ok || len(bh) != 66 {
- t.Errorf("head.blockHash not 32-byte hex string: %v", head["blockHash"])
+ if hash, ok := head["hash"].(string); !ok || len(hash) != 66 {
+ t.Errorf("head.hash not 32-byte hex string: %v", head["hash"])
+ }
+ if _, present := head["blockNumber"]; present {
+ t.Errorf("head must not include blockNumber")
+ }
+ if _, present := head["blockHash"]; present {
+ t.Errorf("head must not include blockHash")
}
- // blocks.deleteStrategy is "none" → must NOT contain retentionBlocks.
+ // blocks have a fixed oldest block but no deletion strategy.
blocks := generic["blocks"].(map[string]any)
- bds := blocks["deleteStrategy"].(map[string]any)
- if bds["type"] != "none" {
- t.Errorf("blocks.deleteStrategy.type = %v, want none", bds["type"])
+ if ob, ok := blocks["oldestBlock"].(string); !ok || len(ob) < 3 || ob[:2] != "0x" {
+ t.Errorf("blocks.oldestBlock not hex string: %v", blocks["oldestBlock"])
}
- if _, present := bds["retentionBlocks"]; present {
- t.Errorf("blocks.deleteStrategy must not include retentionBlocks for type=none")
+ if _, present := blocks["deleteStrategy"]; present {
+ t.Errorf("blocks must not include deleteStrategy without sliding deletion")
+ }
+
+ // Disabled resources must not advertise an availability range.
+ logs := generic["logs"].(map[string]any)
+ if logs["disabled"] != true {
+ t.Errorf("logs.disabled = %v, want true", logs["disabled"])
+ }
+ if _, present := logs["oldestBlock"]; present {
+ t.Errorf("disabled logs must not include oldestBlock")
+ }
+ if _, present := logs["deleteStrategy"]; present {
+ t.Errorf("disabled logs must not include deleteStrategy")
}
// tx.deleteStrategy is "window" → must contain retentionBlocks as a
@@ -385,5 +413,12 @@ func TestCapabilitiesJSONShape(t *testing.T) {
}
}
-// hexUint is a small helper to keep the test tables compact.
-func hexUint(n uint64) hexutil.Uint64 { return hexutil.Uint64(n) }
+// hexUintPtr and windowStrategy keep the test tables compact.
+func hexUintPtr(n uint64) *hexutil.Uint64 {
+ v := hexutil.Uint64(n)
+ return &v
+}
+
+func windowStrategy(n uint64) *DeleteStrategy {
+ return &DeleteStrategy{Type: "window", RetentionBlocks: &n}
+}