Skip to content

Commit 5e598c3

Browse files
LukaszRozmejclaude
andcommitted
fix(eth_capabilities): align with spec — integer retentionBlocks; share Resource helper
Reviewed against ethereum/execution-apis#755 schema and test fixtures: - DeleteStrategy.RetentionBlocks must be a JSON integer per spec ("retentionBlocks":90000), but Nethermind's default long converter writes hex. Apply [JsonConverter(typeof(LongRawJsonConverter))] to override. - Tighten the JSON-schema test: retentionBlocks is integer; deleteStrategy.type is enum ["window"] (the spec's only variant — others may be added via oneOf). - Extract a single Resource(enabled, oldestBlock, deleteStrategy?) helper used for all four resources. Enforces the spec invariant "disabled: true ⇒ no other fields" in one place. - Promote HistoryPruner.SlotsPerEpoch from private to public; reference it from EthCapabilitiesProvider so the constant has a single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8939c6a commit 5e598c3

4 files changed

Lines changed: 31 additions & 23 deletions

File tree

src/Nethermind/Nethermind.History/HistoryPruner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class HistoryPruner : IHistoryPruner
3030
{
3131
private const int MaxOptimisticSearchAttempts = 3;
3232
private const int LockWaitTimeoutMs = 100;
33-
private const int SlotsPerEpoch = 32;
33+
public const int SlotsPerEpoch = 32;
3434

3535
// only one pruning and one searching thread at a time
3636
private readonly object _pruneLock = new();

src/Nethermind/Nethermind.JsonRpc.Test/Modules/Eth/EthRpcModuleTests.Capabilities.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ public async Task eth_capabilities_memory_pruned_reports_window_and_disables_sta
196196
"additionalProperties": false,
197197
"required": ["type", "retentionBlocks"],
198198
"properties": {
199-
"type": { "type": "string" },
200-
"retentionBlocks": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" }
199+
"type": { "type": "string", "enum": ["window"] },
200+
"retentionBlocks": { "type": "integer", "minimum": 0 }
201201
}
202202
}
203203
}

src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthCapabilities.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Text.Json.Serialization;
55
using Nethermind.Core.Crypto;
6+
using Nethermind.Serialization.Json;
67

78
namespace Nethermind.JsonRpc.Modules.Eth;
89

@@ -16,7 +17,7 @@ namespace Nethermind.JsonRpc.Modules.Eth;
1617
/// <param name="Receipts">Receipt lookup availability (also exposed as <see cref="Tx"/> and <see cref="Logs"/>).</param>
1718
/// <param name="Blocks">Historical block and header availability.</param>
1819
/// <param name="Stateproofs">Proof / trie-node availability (eth_getProof depth window).</param>
19-
public record class EthCapabilities(
20+
public record EthCapabilities(
2021
[property: JsonPropertyOrder(0)] ChainHead Head,
2122
[property: JsonPropertyOrder(1)] ResourceAvailability State,
2223
[property: JsonPropertyOrder(4)] ResourceAvailability Receipts,
@@ -47,6 +48,8 @@ public readonly record struct ResourceAvailability(
4748
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] DeleteStrategy? DeleteStrategy = null);
4849

4950
/// <summary>Rolling-window deletion strategy for a resource.</summary>
50-
/// <param name="Type">Strategy type — always <c>"window"</c>.</param>
51-
/// <param name="RetentionBlocks">Number of recent blocks retained.</param>
52-
public readonly record struct DeleteStrategy(string Type, long RetentionBlocks);
51+
/// <param name="Type">Strategy type — currently always <c>"window"</c>; the spec uses <c>oneOf</c> to leave room for future strategies.</param>
52+
/// <param name="RetentionBlocks">Number of recent blocks retained. Per spec this is a JSON integer (not a hex quantity), so the default Ethereum hex converter is overridden.</param>
53+
public readonly record struct DeleteStrategy(
54+
string Type,
55+
[property: JsonConverter(typeof(LongRawJsonConverter))] long RetentionBlocks);

src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthCapabilitiesProvider.cs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ public class EthCapabilitiesProvider(
1818
IHistoryConfig? historyConfig = null,
1919
IHistoryPruner? historyPruner = null) : IEthCapabilitiesProvider
2020
{
21-
private const int SlotsPerEpoch = 32;
22-
2321
public EthCapabilities GetCapabilities()
2422
{
2523
BlockHeader? head = blockTree.Head?.Header;
@@ -44,28 +42,35 @@ public EthCapabilities GetCapabilities()
4442
// historyPruner.OldestBlockHeader.Number; Rolling mode produces a known retention window.
4543
long historyFloor = historyPruner?.OldestBlockHeader?.Number ?? 0;
4644
DeleteStrategy? historyWindow = historyConfig?.Pruning == PruningModes.Rolling
47-
? new DeleteStrategy("window", historyConfig.RetentionEpochs * SlotsPerEpoch)
45+
? new DeleteStrategy("window", historyConfig.RetentionEpochs * HistoryPruner.SlotsPerEpoch)
4846
: null;
4947

5048
return new EthCapabilities(
5149
Head: new ChainHead(head?.Number ?? 0, head?.Hash ?? Keccak.Zero),
5250
// State is always available (at minimum the genesis trie exists), even on a memory-pruned
5351
// node that hasn't yet synced past the first block — in which case OldestBlock is null
5452
// because the rolling window lower bound cannot yet be computed.
55-
State: new ResourceAvailability(
56-
Disabled: false,
57-
OldestBlock: stateOldest,
58-
DeleteStrategy: stateRetention > 0 ? new DeleteStrategy("window", stateRetention.Value) : null),
53+
State: Resource(
54+
enabled: true,
55+
oldestBlock: stateOldest,
56+
deleteStrategy: stateRetention > 0 ? new DeleteStrategy("window", stateRetention.Value) : null),
5957
// Receipts/Tx/Logs share storage and pruning policy in Nethermind — one descriptor covers
6058
// all three. EthCapabilities.Tx and .Logs are computed properties that alias .Receipts.
61-
Receipts: new ResourceAvailability(
62-
Disabled: !receiptsSynced,
63-
OldestBlock: receiptsSynced ? Math.Max(oldestReceipts, historyFloor) : null,
64-
DeleteStrategy: receiptsSynced ? historyWindow : null),
65-
Blocks: new ResourceAvailability(
66-
Disabled: !headersAvailable,
67-
OldestBlock: headersAvailable ? Math.Max(lowestBlock, historyFloor) : null,
68-
DeleteStrategy: headersAvailable ? historyWindow : null),
69-
Stateproofs: new ResourceAvailability(Disabled: !isArchive, OldestBlock: isArchive ? 0L : null));
59+
Receipts: Resource(receiptsSynced, Math.Max(oldestReceipts, historyFloor), historyWindow),
60+
Blocks: Resource(headersAvailable, Math.Max(lowestBlock, historyFloor), historyWindow),
61+
Stateproofs: Resource(isArchive, 0L));
7062
}
63+
64+
/// <summary>
65+
/// Builds a single resource descriptor. When <paramref name="enabled"/> is false, the spec
66+
/// requires <c>disabled: true</c> with no other fields present — the helper enforces that
67+
/// by zeroing <c>OldestBlock</c> and <c>DeleteStrategy</c> regardless of what was passed.
68+
/// </summary>
69+
private static ResourceAvailability Resource(
70+
bool enabled,
71+
long? oldestBlock,
72+
DeleteStrategy? deleteStrategy = null) =>
73+
new(Disabled: !enabled,
74+
OldestBlock: enabled ? oldestBlock : null,
75+
DeleteStrategy: enabled ? deleteStrategy : null);
7176
}

0 commit comments

Comments
 (0)