mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
eth, internal/ethapi: add eth_capabilities RPC method
This commit is contained in:
parent
82fad31540
commit
f8fb64a285
7 changed files with 637 additions and 1 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
223
internal/ethapi/capabilities.go
Normal file
223
internal/ethapi/capabilities.go
Normal file
|
|
@ -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 <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 {
|
||||
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,
|
||||
}
|
||||
}
|
||||
389
internal/ethapi/capabilities_test.go
Normal file
389
internal/ethapi/capabilities_test.go
Normal file
|
|
@ -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 <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")
|
||||
|
||||
// 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) }
|
||||
|
|
@ -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{} }
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue