Skip to content

Commit abf6ff6

Browse files
authored
feat(tests): BLOCKHASH recency window in state-test runner (#2943)
* feat: nethermind state test added * fix: i was lINTING again
1 parent efd53ff commit abf6ff6

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
State-test regression for the BLOCKHASH (0x40) opcode recency window.
3+
4+
In a *state test*, nethermind returns a non-zero hash for
5+
``BLOCKHASH(0)`` even when block 0 lies far outside the recency window (256
6+
blocks pre-Prague, 8191 via EIP-2935 from Prague). eels (the reference),
7+
go-ethereum, besu, erigon, evmone and reth all correctly return 0.
8+
9+
Root cause (nethermind, state-test only):
10+
``src/Nethermind/Ethereum.Test.Base/TestBlockhashProvider.cs`` implements::
11+
12+
number != 0 ? Keccak.Zero : Keccak.Compute(number.ToString())
13+
14+
It performs no recency-window check, and the opcode handler
15+
``InstructionBlockHash`` (in ``EvmInstructions.Environment.cs``) delegates
16+
that check to the provider -- it only rejects ``number >= current``. So
17+
``BLOCKHASH(0)`` returns ``keccak256("0")`` regardless of how ancient block
18+
0 is. Nethermind's *production* ``BlockhashProvider`` does enforce the
19+
window, so this is a state-test tooling bug, not a mainnet consensus bug --
20+
but it makes nethermind diverge under differential state-test fuzzing.
21+
"""
22+
23+
import pytest
24+
from execution_testing import (
25+
Account,
26+
Alloc,
27+
Environment,
28+
Hash,
29+
Op,
30+
StateTestFiller,
31+
Storage,
32+
Transaction,
33+
)
34+
35+
# keccak256(b"0") -- the state-test convention hash for block 0 used by both
36+
# go-ethereum (vmTestBlockHash) and nethermind (TestBlockhashProvider). The
37+
# fuzzer sets env.previousHash to exactly this value so the in-window
38+
# block-0 hash agrees across clients.
39+
KECCAK_OF_BLOCK_0 = Hash(
40+
0x044852B2A670ADE5407E78FB2863C51DE9FCB96542A07186FE3AEDA6BB8A116D
41+
)
42+
43+
44+
@pytest.mark.valid_from("Frontier")
45+
@pytest.mark.state_test_only
46+
@pytest.mark.parametrize(
47+
"current_number",
48+
[
49+
pytest.param(0x101, id="just_past_256_window"),
50+
pytest.param(0x2001, id="just_past_8191_window"),
51+
pytest.param(0x100000, id="far_out_of_window"),
52+
pytest.param(0xA63CCE, id="fuzzer_discovered_number"),
53+
],
54+
)
55+
def test_blockhash_zero_out_of_window(
56+
state_test: StateTestFiller,
57+
pre: Alloc,
58+
current_number: int,
59+
) -> None:
60+
"""
61+
BLOCKHASH(0) must be 0 when block 0 is outside the recency window.
62+
63+
At ``current_number`` well past the window, block 0 is ancient, so every
64+
spec-conformant client returns 0. Nethermind's state-test runner instead
65+
returns ``keccak256("0")`` because ``TestBlockhashProvider`` skips the
66+
window check, which is the bug this test guards against.
67+
68+
Marked ``state_test_only``: the bug lives solely in the state-test code
69+
path (the production blockhash provider enforces the window), and a state
70+
test with such a high ``env.number`` cannot be converted into a real
71+
blockchain-test fixture (no genesis chain up to that height).
72+
"""
73+
storage = Storage()
74+
contract = pre.deploy_contract(
75+
code=(
76+
# slot 0: the raw hash -- must be zero
77+
Op.SSTORE(
78+
storage.store_next(0, "blockhash_0_value"),
79+
Op.BLOCKHASH(0),
80+
)
81+
# slot 1: ISZERO of the hash -- must be 1 (written, so the
82+
# divergence lands in a non-default slot too)
83+
+ Op.SSTORE(
84+
storage.store_next(1, "blockhash_0_is_zero"),
85+
Op.ISZERO(Op.BLOCKHASH(0)),
86+
)
87+
)
88+
)
89+
sender = pre.fund_eoa()
90+
91+
tx = Transaction(
92+
sender=sender,
93+
to=contract,
94+
gas_limit=200_000,
95+
protected=False, # legacy tx so it fills on pre-EIP-155 forks too
96+
)
97+
98+
state_test(
99+
env=Environment(number=current_number),
100+
pre=pre,
101+
post={contract: Account(storage=storage)},
102+
tx=tx,
103+
)
104+
105+
106+
@pytest.mark.valid_from("Frontier")
107+
@pytest.mark.valid_until("Cancun")
108+
@pytest.mark.state_test_only
109+
def test_blockhash_zero_in_window_control(
110+
state_test: StateTestFiller,
111+
pre: Alloc,
112+
) -> None:
113+
"""
114+
In-window control: BLOCKHASH(0) returns the block-0 hash at block 1.
115+
116+
This is the positive counterpart to the out-of-window test. With block 0
117+
inside the recency window (current block == 1) and ``previousHash`` set
118+
to the state-test convention value ``keccak256("0")``, all clients --
119+
including nethermind -- agree on the result, pinning the window boundary.
120+
121+
Restricted to <= Cancun because EIP-2935 (Prague+) serves BLOCKHASH from
122+
the history storage contract, which is not pre-populated in a bare state
123+
test.
124+
Marked ``state_test_only``: the asserted non-zero hash would not match the
125+
real genesis hash of a derived blockchain-test fixture.
126+
"""
127+
storage = Storage()
128+
contract = pre.deploy_contract(
129+
code=Op.SSTORE(
130+
storage.store_next(KECCAK_OF_BLOCK_0, "blockhash_0_value"),
131+
Op.BLOCKHASH(0),
132+
)
133+
)
134+
sender = pre.fund_eoa()
135+
136+
tx = Transaction(
137+
sender=sender,
138+
to=contract,
139+
gas_limit=200_000,
140+
protected=False, # legacy tx so it fills on pre-EIP-155 forks too
141+
)
142+
143+
state_test(
144+
env=Environment(
145+
number=1,
146+
block_hashes={0: KECCAK_OF_BLOCK_0},
147+
),
148+
pre=pre,
149+
post={contract: Account(storage=storage)},
150+
tx=tx,
151+
)

0 commit comments

Comments
 (0)