internal/ethapi: align capabilities response with spec

This commit is contained in:
locoholy 2026-05-18 21:10:41 +05:00
parent f8fb64a285
commit e4ac40b5b5
2 changed files with 149 additions and 119 deletions

View file

@ -76,41 +76,36 @@ type Capabilities struct {
// CapabilityHead is the current canonical head as reported by the node. // CapabilityHead is the current canonical head as reported by the node.
type CapabilityHead struct { type CapabilityHead struct {
BlockNumber hexutil.Uint64 `json:"blockNumber"` Number hexutil.Uint64 `json:"number"`
BlockHash common.Hash `json:"blockHash"` Hash common.Hash `json:"hash"`
} }
// CapabilityResource describes the availability of a single data resource. // CapabilityResource describes the availability of a single data resource.
type CapabilityResource struct { type CapabilityResource struct {
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
OldestBlock hexutil.Uint64 `json:"oldestBlock"` OldestBlock *hexutil.Uint64 `json:"oldestBlock,omitempty"`
DeleteStrategy DeleteStrategy `json:"deleteStrategy"` DeleteStrategy *DeleteStrategy `json:"deleteStrategy,omitempty"`
} }
// DeleteStrategy describes how data of a resource is removed over time. // DeleteStrategy describes how data of a resource is removed over time.
// //
// Two strategies are defined by the spec: // The spec currently defines one strategy: "window", meaning data is retained
// // for a sliding window of the most recent RetentionBlocks blocks. Resources
// - "none": data is never deleted; the resource is permanently // without sliding deletion omit deleteStrategy.
// 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 DeleteStrategy struct {
Type string `json:"type"` Type string `json:"type"`
RetentionBlocks *uint64 `json:"retentionBlocks,omitempty"` 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 // strategyWindow returns a DeleteStrategy with type "window" and the given
// retention block count. // retention block count.
func strategyWindow(retention uint64) DeleteStrategy { func strategyWindow(retention uint64) *DeleteStrategy {
return DeleteStrategy{Type: "window", RetentionBlocks: &retention} 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 // 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. // 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 { 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 { if window != 0 {
ds = strategyWindow(window) res.DeleteStrategy = strategyWindow(window)
}
return CapabilityResource{
Disabled: disabled,
OldestBlock: hexutil.Uint64(windowOldest(window, floor)),
DeleteStrategy: ds,
} }
return res
} }
// Bodies and receipts share the same retention model in // Bodies and receipts share the same retention model in
// geth: they are either kept in full ("all") or pruned to a fixed // geth: they are either kept in full ("all") or pruned to a fixed
// boundary ("postmerge"). In neither case is there a sliding // boundary ("postmerge"). In neither case is there a sliding deletion
// deletion window, so the strategy is always "none" and the oldest // window, so deleteStrategy is omitted and oldestBlock equals the history
// block equals the history pruning cutoff. // pruning cutoff.
blocks := CapabilityResource{ blocks := CapabilityResource{
Disabled: false, OldestBlock: capabilityOldestBlock(cutoff),
OldestBlock: hexutil.Uint64(cutoff),
DeleteStrategy: strategyNone(),
} }
receipts := blocks receipts := blocks
@ -210,8 +205,8 @@ func buildCapabilities(headNum uint64, headHash common.Hash, cutoff uint64, ret
return &Capabilities{ return &Capabilities{
Head: CapabilityHead{ Head: CapabilityHead{
BlockNumber: hexutil.Uint64(headNum), Number: hexutil.Uint64(headNum),
BlockHash: headHash, Hash: headHash,
}, },
State: state, State: state,
Tx: tx, Tx: tx,

View file

@ -33,10 +33,6 @@ func TestBuildCapabilities(t *testing.T) {
) )
headHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") headHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
// retentionWindow is a small helper for asserting on
// CapabilityResource fields.
retentionWindow := func(n uint64) *uint64 { return &n }
tests := []struct { tests := []struct {
name string name string
headNum uint64 headNum uint64
@ -53,12 +49,12 @@ func TestBuildCapabilities(t *testing.T) {
StateScheme: rawdb.PathScheme, StateScheme: rawdb.PathScheme,
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"blocks": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "blocks": {OldestBlock: hexUintPtr(0)},
"receipts": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "receipts": {OldestBlock: hexUintPtr(0)},
"tx": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "tx": {OldestBlock: hexUintPtr(0)},
"logs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "logs": {OldestBlock: hexUintPtr(0)},
"state": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "state": {OldestBlock: hexUintPtr(0)},
"stateproofs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "stateproofs": {OldestBlock: hexUintPtr(0)},
}, },
}, },
{ {
@ -71,8 +67,8 @@ func TestBuildCapabilities(t *testing.T) {
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
// blocks/receipts honor the absolute cutoff with no // blocks/receipts honor the absolute cutoff with no
// sliding window. // sliding window.
"blocks": {OldestBlock: hexUint(postmerge), DeleteStrategy: DeleteStrategy{Type: "none"}}, "blocks": {OldestBlock: hexUintPtr(postmerge)},
"receipts": {OldestBlock: hexUint(postmerge), DeleteStrategy: DeleteStrategy{Type: "none"}}, "receipts": {OldestBlock: hexUintPtr(postmerge)},
}, },
}, },
{ {
@ -86,12 +82,12 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"tx": { "tx": {
OldestBlock: hexUint(5_000_000 - 2_350_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)}, DeleteStrategy: windowStrategy(2_350_000),
}, },
"logs": { "logs": {
OldestBlock: hexUint(5_000_000 - 2_350_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)}, DeleteStrategy: windowStrategy(2_350_000),
}, },
}, },
}, },
@ -105,8 +101,8 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"tx": { "tx": {
OldestBlock: 0, OldestBlock: hexUintPtr(0),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)}, DeleteStrategy: windowStrategy(2_350_000),
}, },
}, },
}, },
@ -120,8 +116,8 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"tx": { "tx": {
OldestBlock: hexUint(4_000_000), OldestBlock: hexUintPtr(4_000_000),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)}, DeleteStrategy: windowStrategy(2_350_000),
}, },
}, },
}, },
@ -137,12 +133,12 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"state": { "state": {
OldestBlock: hexUint(5_000_000 - 90_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)}, DeleteStrategy: windowStrategy(90_000),
}, },
"stateproofs": { "stateproofs": {
OldestBlock: hexUint(5_000_000 - 100_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 100_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(100_000)}, DeleteStrategy: windowStrategy(100_000),
}, },
}, },
}, },
@ -157,9 +153,7 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"logs": { "logs": {
Disabled: true, Disabled: true,
OldestBlock: hexUint(5_000_000 - 2_350_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(2_350_000)},
}, },
}, },
}, },
@ -175,12 +169,12 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"state": { "state": {
OldestBlock: hexUint(5_000_000 - 90_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)}, DeleteStrategy: windowStrategy(90_000),
}, },
"stateproofs": { "stateproofs": {
OldestBlock: hexUint(5_000_000 - 50_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 50_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(50_000)}, DeleteStrategy: windowStrategy(50_000),
}, },
}, },
}, },
@ -196,12 +190,12 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"state": { "state": {
OldestBlock: hexUint(5_000_000 - 90_000 + 1), OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(90_000)}, DeleteStrategy: windowStrategy(90_000),
}, },
"stateproofs": { "stateproofs": {
OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1), OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)}, DeleteStrategy: windowStrategy(corestate.TriesInMemory),
}, },
}, },
}, },
@ -215,8 +209,8 @@ func TestBuildCapabilities(t *testing.T) {
StateHistory: 90_000, StateHistory: 90_000,
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"state": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "state": {OldestBlock: hexUintPtr(0)},
"stateproofs": {OldestBlock: 0, DeleteStrategy: DeleteStrategy{Type: "none"}}, "stateproofs": {OldestBlock: hexUintPtr(0)},
}, },
}, },
{ {
@ -229,12 +223,12 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"state": { "state": {
OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1), OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)}, DeleteStrategy: windowStrategy(corestate.TriesInMemory),
}, },
"stateproofs": { "stateproofs": {
OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1), OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)}, DeleteStrategy: windowStrategy(corestate.TriesInMemory),
}, },
}, },
}, },
@ -248,12 +242,12 @@ func TestBuildCapabilities(t *testing.T) {
}, },
expected: map[string]CapabilityResource{ expected: map[string]CapabilityResource{
"state": { "state": {
OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1), OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)}, DeleteStrategy: windowStrategy(corestate.TriesInMemory),
}, },
"stateproofs": { "stateproofs": {
OldestBlock: hexUint(5_000_000 - corestate.TriesInMemory + 1), OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1),
DeleteStrategy: DeleteStrategy{Type: "window", RetentionBlocks: retentionWindow(corestate.TriesInMemory)}, DeleteStrategy: windowStrategy(corestate.TriesInMemory),
}, },
}, },
}, },
@ -264,11 +258,11 @@ func TestBuildCapabilities(t *testing.T) {
caps := buildCapabilities(tt.headNum, headHash, tt.cutoff, tt.ret) caps := buildCapabilities(tt.headNum, headHash, tt.cutoff, tt.ret)
// Head is always present. // Head is always present.
if uint64(caps.Head.BlockNumber) != tt.headNum { if uint64(caps.Head.Number) != tt.headNum {
t.Errorf("head.blockNumber = %d, want %d", uint64(caps.Head.BlockNumber), tt.headNum) t.Errorf("head.number = %d, want %d", uint64(caps.Head.Number), tt.headNum)
} }
if caps.Head.BlockHash != headHash { if caps.Head.Hash != headHash {
t.Errorf("head.blockHash = %x, want %x", caps.Head.BlockHash, headHash) t.Errorf("head.hash = %x, want %x", caps.Head.Hash, headHash)
} }
actual := map[string]CapabilityResource{ actual := map[string]CapabilityResource{
@ -284,23 +278,38 @@ func TestBuildCapabilities(t *testing.T) {
if got.Disabled != want.Disabled { if got.Disabled != want.Disabled {
t.Errorf("%s.disabled = %v, want %v", name, got.Disabled, want.Disabled) t.Errorf("%s.disabled = %v, want %v", name, got.Disabled, want.Disabled)
} }
if got.OldestBlock != want.OldestBlock { switch {
t.Errorf("%s.oldestBlock = %d, want %d", name, uint64(got.OldestBlock), uint64(want.OldestBlock)) case want.OldestBlock == nil && got.OldestBlock != nil:
} t.Errorf("%s.oldestBlock = %d, want absent", name, uint64(*got.OldestBlock))
if got.DeleteStrategy.Type != want.DeleteStrategy.Type { case want.OldestBlock != nil && got.OldestBlock == nil:
t.Errorf("%s.deleteStrategy.type = %q, want %q", name, got.DeleteStrategy.Type, want.DeleteStrategy.Type) 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 { switch {
case want.DeleteStrategy.RetentionBlocks == nil && got.DeleteStrategy.RetentionBlocks != nil: case want.DeleteStrategy == nil && got.DeleteStrategy != nil:
t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want absent", t.Errorf("%s.deleteStrategy = %#v, want absent", name, got.DeleteStrategy)
name, *got.DeleteStrategy.RetentionBlocks) case want.DeleteStrategy != nil && got.DeleteStrategy == nil:
case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks == nil: t.Errorf("%s.deleteStrategy absent, want %#v", name, want.DeleteStrategy)
t.Errorf("%s.deleteStrategy.retentionBlocks absent, want %d", case want.DeleteStrategy != nil && got.DeleteStrategy != nil:
name, *want.DeleteStrategy.RetentionBlocks) if got.DeleteStrategy.Type != want.DeleteStrategy.Type {
case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks != nil: t.Errorf("%s.deleteStrategy.type = %q, want %q", name, got.DeleteStrategy.Type, want.DeleteStrategy.Type)
if *got.DeleteStrategy.RetentionBlocks != *want.DeleteStrategy.RetentionBlocks { }
t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want %d", switch {
name, *got.DeleteStrategy.RetentionBlocks, *want.DeleteStrategy.RetentionBlocks) 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 // TestCapabilitiesJSONShape verifies that the marshalled JSON conforms to
// the schema defined in https://github.com/ethereum/execution-apis/pull/755: // the schema defined in https://github.com/ethereum/execution-apis/pull/755:
// "none" strategies must omit retentionBlocks, oldestBlock must be a hex // head fields are named number/hash, resources without a sliding window omit
// quantity, retentionBlocks must be a decimal integer. // deleteStrategy, disabled resources omit range fields, and retentionBlocks is
// a decimal integer.
func TestCapabilitiesJSONShape(t *testing.T) { func TestCapabilitiesJSONShape(t *testing.T) {
caps := buildCapabilities( caps := buildCapabilities(
5_000_000, 5_000_000,
common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"), common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"),
0, 0,
HistoryRetention{ HistoryRetention{
StateScheme: rawdb.PathScheme, StateScheme: rawdb.PathScheme,
TxIndexHistory: 2_350_000, TxIndexHistory: 2_350_000,
LogIndexHistory: 2_350_000, LogIndexHistory: 2_350_000,
StateHistory: 90_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) head := generic["head"].(map[string]any)
if bn, ok := head["blockNumber"].(string); !ok || len(bn) < 3 || bn[:2] != "0x" { if number, ok := head["number"].(string); !ok || len(number) < 3 || number[:2] != "0x" {
t.Errorf("head.blockNumber not hex string: %v", head["blockNumber"]) t.Errorf("head.number not hex string: %v", head["number"])
} }
if bh, ok := head["blockHash"].(string); !ok || len(bh) != 66 { if hash, ok := head["hash"].(string); !ok || len(hash) != 66 {
t.Errorf("head.blockHash not 32-byte hex string: %v", head["blockHash"]) 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) blocks := generic["blocks"].(map[string]any)
bds := blocks["deleteStrategy"].(map[string]any) if ob, ok := blocks["oldestBlock"].(string); !ok || len(ob) < 3 || ob[:2] != "0x" {
if bds["type"] != "none" { t.Errorf("blocks.oldestBlock not hex string: %v", blocks["oldestBlock"])
t.Errorf("blocks.deleteStrategy.type = %v, want none", bds["type"])
} }
if _, present := bds["retentionBlocks"]; present { if _, present := blocks["deleteStrategy"]; present {
t.Errorf("blocks.deleteStrategy must not include retentionBlocks for type=none") 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 // 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. // hexUintPtr and windowStrategy keep the test tables compact.
func hexUint(n uint64) hexutil.Uint64 { return hexutil.Uint64(n) } func hexUintPtr(n uint64) *hexutil.Uint64 {
v := hexutil.Uint64(n)
return &v
}
func windowStrategy(n uint64) *DeleteStrategy {
return &DeleteStrategy{Type: "window", RetentionBlocks: &n}
}