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} +}