Skip to content

Commit 6772428

Browse files
feat(evm): EIP-8250 keyed-nonce state helper (#12458)
* feat(evm): EIP-8250 keyed-nonce state helper Add the standalone state/crypto primitive for EIP-8250 (Keyed Nonces): - Eip8250Constants: MAX_NONCE_KEYS, MAX_NONCE_SEQ, KEYED_NONCE_FIRST_USE_GAS, and the provisional NONCE_MANAGER address/code (spec address TBD, mirrors the only existing implementation pending ratification). - KeyedNonceManager: NONCE_MANAGER storage-slot derivation keccak256(left_pad_32(sender) || bytes32(key)), per-key current-nonce-seq reads with a defensive u256->u64 clamp, first-use detection, and nonce-set consumption (account-nonce increment for [0], else nonce_seq+1 per key). - KeyedNonceManagerTests covering slot derivation, key-0 aliasing, absent slots, the truncation clamp, consumption, and first-use transitions. Independent of the frame-transaction processor; integration to follow. * refactor(core): expose NONCE_MANAGER bytecode as immutable ReadOnlySpan Return the NONCE_MANAGER runtime code as a ReadOnlySpan<byte> instead of a public static readonly byte[], so the shared bytecode cannot be mutated. * Trim code comments to essential spec/guard notes * Trim code comments to essential spec/guard notes * Address review: drop unused spec param, guard IsFirstUse/ConsumeNonceSet for key 0, avoid ToBigEndian allocation * Address review: drop unused spec param, guard IsFirstUse/ConsumeNonceSet for key 0, avoid ToBigEndian allocation
1 parent 845700c commit 6772428

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
6+
namespace Nethermind.Core;
7+
8+
/// <summary><see href="https://eips.ethereum.org/EIPS/eip-8250">EIP-8250</see> (Keyed Nonces) parameters.</summary>
9+
public static class Eip8250Constants
10+
{
11+
public const int MaxNonceKeys = 16;
12+
public const ulong MaxNonceSeq = ulong.MaxValue;
13+
public const long KeyedNonceFirstUseGas = 20_000;
14+
15+
// Provisional: spec address is TBD; mirrors the only existing implementation.
16+
public static readonly Address NonceManagerAddress = new("0x0000000000000000000000000000000000008250");
17+
18+
// Spec-pinned revert(0, 0): a storage namespace only, never callable.
19+
public static ReadOnlySpan<byte> NonceManagerCode => [0x60, 0x00, 0x60, 0x00, 0xfd];
20+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
using Nethermind.Core;
6+
using Nethermind.Core.Extensions;
7+
using Nethermind.Core.Specs;
8+
using Nethermind.Core.Test;
9+
using Nethermind.Core.Test.Builders;
10+
using Nethermind.Evm.State;
11+
using Nethermind.Evm.TransactionProcessing;
12+
using Nethermind.Int256;
13+
using Nethermind.Specs.Forks;
14+
using NUnit.Framework;
15+
16+
namespace Nethermind.Evm.Test;
17+
18+
public class KeyedNonceManagerTests
19+
{
20+
private static readonly IReleaseSpec Spec = Prague.Instance;
21+
private IWorldState _state = null!;
22+
private IDisposable _scope = null!;
23+
24+
[SetUp]
25+
public void Setup()
26+
{
27+
_state = TestWorldStateFactory.CreateForTest();
28+
_scope = _state.BeginScope(IWorldState.PreGenesis);
29+
_state.CreateAccount(TestItem.AddressA, 1.Ether);
30+
_state.CreateAccount(Eip8250Constants.NonceManagerAddress, UInt256.Zero, 1);
31+
_state.Commit(Spec);
32+
_state.CommitTree(0);
33+
}
34+
35+
[TearDown]
36+
public void TearDown() => _scope.Dispose();
37+
38+
[Test]
39+
public void StorageSlot_is_deterministic_and_distinct_per_sender_and_key()
40+
{
41+
StorageCell slotA1 = KeyedNonceManager.StorageSlot(TestItem.AddressA, (UInt256)1);
42+
StorageCell slotA1Again = KeyedNonceManager.StorageSlot(TestItem.AddressA, (UInt256)1);
43+
StorageCell slotA2 = KeyedNonceManager.StorageSlot(TestItem.AddressA, (UInt256)2);
44+
StorageCell slotB1 = KeyedNonceManager.StorageSlot(TestItem.AddressB, (UInt256)1);
45+
46+
Assert.That(slotA1.Address, Is.EqualTo(Eip8250Constants.NonceManagerAddress));
47+
Assert.That(slotA1Again.Index, Is.EqualTo(slotA1.Index), "same inputs must yield the same slot");
48+
Assert.That(slotA2.Index, Is.Not.EqualTo(slotA1.Index), "distinct keys must yield distinct slots");
49+
Assert.That(slotB1.Index, Is.Not.EqualTo(slotA1.Index), "distinct senders must yield distinct slots");
50+
}
51+
52+
[Test]
53+
public void CurrentNonceSeq_for_key_zero_returns_account_nonce()
54+
{
55+
_state.SetNonce(TestItem.AddressA, 7);
56+
57+
Assert.That(KeyedNonceManager.CurrentNonceSeq(_state, TestItem.AddressA, UInt256.Zero), Is.EqualTo(7UL));
58+
}
59+
60+
[Test]
61+
public void CurrentNonceSeq_for_absent_keyed_slot_is_zero() =>
62+
Assert.That(KeyedNonceManager.CurrentNonceSeq(_state, TestItem.AddressA, (UInt256)5), Is.EqualTo(0UL));
63+
64+
[TestCaseSource(nameof(AboveUlongMaxValues))]
65+
public void CurrentNonceSeq_clamps_slot_value_above_ulong_max(UInt256 storedValue)
66+
{
67+
StorageCell slot = KeyedNonceManager.StorageSlot(TestItem.AddressA, (UInt256)5);
68+
_state.Set(slot, storedValue.ToBigEndian().WithoutLeadingZeros().ToArray());
69+
70+
Assert.That(KeyedNonceManager.CurrentNonceSeq(_state, TestItem.AddressA, (UInt256)5), Is.EqualTo(ulong.MaxValue));
71+
}
72+
73+
private static UInt256[] AboveUlongMaxValues() =>
74+
[
75+
(UInt256)ulong.MaxValue + UInt256.One,
76+
UInt256.MaxValue
77+
];
78+
79+
[Test]
80+
public void ConsumeNonceSet_with_zero_key_increments_account_nonce()
81+
{
82+
_state.SetNonce(TestItem.AddressA, 3);
83+
84+
KeyedNonceManager.ConsumeNonceSet(_state, TestItem.AddressA, [UInt256.Zero], nonceSeq: 99);
85+
86+
Assert.That(_state.GetNonce(TestItem.AddressA), Is.EqualTo(4UL), "the account nonce must be incremented, not set to nonceSeq + 1");
87+
}
88+
89+
[Test]
90+
public void ConsumeNonceSet_with_keyed_values_writes_next_seq()
91+
{
92+
_state.SetNonce(TestItem.AddressA, 3);
93+
94+
KeyedNonceManager.ConsumeNonceSet(_state, TestItem.AddressA, [(UInt256)5, (UInt256)9], nonceSeq: 42);
95+
96+
Assert.That(KeyedNonceManager.CurrentNonceSeq(_state, TestItem.AddressA, (UInt256)5), Is.EqualTo(43UL));
97+
Assert.That(KeyedNonceManager.CurrentNonceSeq(_state, TestItem.AddressA, (UInt256)9), Is.EqualTo(43UL));
98+
Assert.That(_state.GetNonce(TestItem.AddressA), Is.EqualTo(3UL), "keyed consumption must not touch the account nonce");
99+
}
100+
101+
[Test]
102+
public void IsFirstUse_transitions_true_to_false_after_consume()
103+
{
104+
Assert.That(KeyedNonceManager.IsFirstUse(_state, TestItem.AddressA, (UInt256)7), Is.True);
105+
106+
KeyedNonceManager.ConsumeNonceSet(_state, TestItem.AddressA, [(UInt256)7], nonceSeq: 0);
107+
108+
Assert.That(KeyedNonceManager.IsFirstUse(_state, TestItem.AddressA, (UInt256)7), Is.False);
109+
Assert.That(KeyedNonceManager.CurrentNonceSeq(_state, TestItem.AddressA, (UInt256)7), Is.EqualTo(1UL));
110+
}
111+
112+
[Test]
113+
public void IsFirstUse_for_key_zero_is_false() =>
114+
Assert.That(KeyedNonceManager.IsFirstUse(_state, TestItem.AddressA, UInt256.Zero), Is.False);
115+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
using System.Diagnostics;
6+
using Nethermind.Core;
7+
using Nethermind.Core.Crypto;
8+
using Nethermind.Core.Extensions;
9+
using Nethermind.Evm.State;
10+
using Nethermind.Int256;
11+
12+
namespace Nethermind.Evm.TransactionProcessing;
13+
14+
/// <summary>State helper for <see href="https://eips.ethereum.org/EIPS/eip-8250">EIP-8250</see> keyed nonces: NONCE_MANAGER slot derivation and per-key nonce reads/consumption.</summary>
15+
public static class KeyedNonceManager
16+
{
17+
private const int SlotPreimageLength = 2 * 32;
18+
19+
public static StorageCell StorageSlot(Address sender, in UInt256 nonceKey)
20+
{
21+
Span<byte> preimage = stackalloc byte[SlotPreimageLength];
22+
preimage.Clear();
23+
sender.Bytes.CopyTo(preimage.Slice(32 - Address.Size, Address.Size));
24+
nonceKey.ToBigEndian(preimage.Slice(32));
25+
UInt256 index = new(ValueKeccak.Compute(preimage).Bytes, isBigEndian: true);
26+
return new StorageCell(Eip8250Constants.NonceManagerAddress, index);
27+
}
28+
29+
public static ulong CurrentNonceSeq(IWorldState state, Address sender, in UInt256 nonceKey)
30+
{
31+
if (nonceKey.IsZero)
32+
{
33+
return state.GetNonce(sender);
34+
}
35+
36+
UInt256 stored = new(state.Get(StorageSlot(sender, nonceKey)), isBigEndian: true);
37+
// Clamp so a crafted high-bit slot cannot false-match a valid nonce_seq < MAX_NONCE_SEQ.
38+
return stored > Eip8250Constants.MaxNonceSeq ? ulong.MaxValue : (ulong)stored;
39+
}
40+
41+
public static bool IsFirstUse(IWorldState state, Address sender, in UInt256 nonceKey) =>
42+
!nonceKey.IsZero && CurrentNonceSeq(state, sender, nonceKey) == 0;
43+
44+
public static void ConsumeNonceSet(IWorldState state, Address sender, ReadOnlySpan<UInt256> nonceKeys, ulong nonceSeq)
45+
{
46+
if (nonceKeys.Length == 1 && nonceKeys[0].IsZero)
47+
{
48+
state.IncrementNonce(sender);
49+
return;
50+
}
51+
52+
Span<byte> buffer = stackalloc byte[32];
53+
((UInt256)nonceSeq + UInt256.One).ToBigEndian(buffer);
54+
byte[] nextSeq = buffer.WithoutLeadingZeros().ToArray();
55+
foreach (UInt256 nonceKey in nonceKeys)
56+
{
57+
// EIP-8250 rejects key 0 in a non-[0] set; the decode-time validity check owns that, this guards the primitive.
58+
Debug.Assert(!nonceKey.IsZero, "key 0 must not appear in a non-[0] nonce_keys set");
59+
state.Set(StorageSlot(sender, nonceKey), nextSeq);
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)