diff --git a/eth/api_backend.go b/eth/api_backend.go index 5e3558d8eb..7d3b5d483e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -41,6 +41,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" ) @@ -279,6 +280,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 561ce2c2d2..3b72742e95 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 f23be85782..25f21c65da 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -91,6 +91,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..a08687f100 --- /dev/null +++ b/internal/ethapi/capabilities.go @@ -0,0 +1,218 @@ +// 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 { + 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,omitempty"` + DeleteStrategy *DeleteStrategy `json:"deleteStrategy,omitempty"` +} + +// DeleteStrategy describes how data of a resource is removed over time. +// +// 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"` +} + +// 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 capabilityOldestBlock(number uint64) *hexutil.Uint64 { + oldest := hexutil.Uint64(number) + return &oldest +} + +// 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. + // 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 { + if disabled { + return CapabilityResource{Disabled: true} + } + res := CapabilityResource{ + OldestBlock: capabilityOldestBlock(windowOldest(window, floor)), + } + if window != 0 { + 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 deleteStrategy is omitted and oldestBlock equals the history + // pruning cutoff. + blocks := CapabilityResource{ + OldestBlock: capabilityOldestBlock(cutoff), + } + 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{ + Number: hexutil.Uint64(headNum), + Hash: 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..e5085a0db2 --- /dev/null +++ b/internal/ethapi/capabilities_test.go @@ -0,0 +1,424 @@ +// 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") + + 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: hexUintPtr(0)}, + "receipts": {OldestBlock: hexUintPtr(0)}, + "tx": {OldestBlock: hexUintPtr(0)}, + "logs": {OldestBlock: hexUintPtr(0)}, + "state": {OldestBlock: hexUintPtr(0)}, + "stateproofs": {OldestBlock: hexUintPtr(0)}, + }, + }, + { + 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: hexUintPtr(postmerge)}, + "receipts": {OldestBlock: hexUintPtr(postmerge)}, + }, + }, + { + 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: hexUintPtr(5_000_000 - 2_350_000 + 1), + DeleteStrategy: windowStrategy(2_350_000), + }, + "logs": { + OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1), + DeleteStrategy: windowStrategy(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: hexUintPtr(0), + DeleteStrategy: windowStrategy(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: hexUintPtr(4_000_000), + DeleteStrategy: windowStrategy(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: hexUintPtr(5_000_000 - 90_000 + 1), + DeleteStrategy: windowStrategy(90_000), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - 100_000 + 1), + DeleteStrategy: windowStrategy(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, + }, + }, + }, + { + 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: hexUintPtr(5_000_000 - 90_000 + 1), + DeleteStrategy: windowStrategy(90_000), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - 50_000 + 1), + DeleteStrategy: windowStrategy(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: hexUintPtr(5_000_000 - 90_000 + 1), + DeleteStrategy: windowStrategy(90_000), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(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: hexUintPtr(0)}, + "stateproofs": {OldestBlock: hexUintPtr(0)}, + }, + }, + { + 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: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(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: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(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.Number) != tt.headNum { + t.Errorf("head.number = %d, want %d", uint64(caps.Head.Number), tt.headNum) + } + if caps.Head.Hash != headHash { + t.Errorf("head.hash = %x, want %x", caps.Head.Hash, 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) + } + 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 == 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) + } + } + } + } + }) + } +} + +// TestCapabilitiesJSONShape verifies that the marshalled JSON conforms to +// the schema defined in https://github.com/ethereum/execution-apis/pull/755: +// 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, + LogIndexDisabled: true, + 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.number must be a hex string ("0x..."), hash must be a 0x hash. + head := generic["head"].(map[string]any) + if number, ok := head["number"].(string); !ok || len(number) < 3 || number[:2] != "0x" { + t.Errorf("head.number not hex string: %v", head["number"]) + } + 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 have a fixed oldest block but no deletion strategy. + blocks := generic["blocks"].(map[string]any) + 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 := 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 + // 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"]) + } +} + +// 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} +} diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index ccb46a810d..f3fc16dcbb 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -411,4 +411,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 1d1b5fbcd1..6a5f3c9a8a 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -612,6 +612,11 @@ web3._extend({ name: 'config', call: 'eth_config', params: 0, + }), + new web3._extend.Method({ + name: 'capabilities', + call: 'eth_capabilities', + params: 0, }) ], properties: [