This commit is contained in:
locoholy 2026-05-21 21:55:09 -07:00 committed by GitHub
commit cc12b0a3b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 667 additions and 1 deletions

View file

@ -41,6 +41,7 @@ import (
"github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
@ -279,6 +280,19 @@ func (b *EthAPIBackend) HistoryPruningCutoff() uint64 {
return bn 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) { func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
return b.eth.blockchain.GetReceiptsByHash(hash), nil return b.eth.blockchain.GetReceiptsByHash(hash), nil
} }

View file

@ -708,6 +708,9 @@ func (b testBackend) HistoryPruningCutoff() uint64 {
bn, _ := b.chain.HistoryPruningCutoff() bn, _ := b.chain.HistoryPruningCutoff()
return bn return bn
} }
func (b testBackend) HistoryRetention() HistoryRetention {
return HistoryRetention{StateScheme: b.chain.TrieDB().Scheme()}
}
func TestEstimateGas(t *testing.T) { func TestEstimateGas(t *testing.T) {
t.Parallel() t.Parallel()

View file

@ -91,6 +91,7 @@ type Backend interface {
ChainConfig() *params.ChainConfig ChainConfig() *params.ChainConfig
Engine() consensus.Engine Engine() consensus.Engine
HistoryPruningCutoff() uint64 HistoryPruningCutoff() uint64
HistoryRetention() HistoryRetention
// This is copied from filters.Backend // This is copied from filters.Backend
// eth/filters needs to be initialized from this backend type, so methods needed by // eth/filters needs to be initialized from this backend type, so methods needed by

View file

@ -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 <http://www.gnu.org/licenses/>.
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,
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
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}
}

View file

@ -411,4 +411,5 @@ func (b *backendMock) Engine() consensus.Engine { return nil }
func (b *backendMock) CurrentView() *filtermaps.ChainView { return nil } func (b *backendMock) CurrentView() *filtermaps.ChainView { return nil }
func (b *backendMock) NewMatcherBackend() filtermaps.MatcherBackend { 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{} }

View file

@ -612,6 +612,11 @@ web3._extend({
name: 'config', name: 'config',
call: 'eth_config', call: 'eth_config',
params: 0, params: 0,
}),
new web3._extend.Method({
name: 'capabilities',
call: 'eth_capabilities',
params: 0,
}) })
], ],
properties: [ properties: [