v1.17.2: withdrawals-only payload state root matches recipient balance from three blocks earlier
System information
Geth version: v1.17.2 (payload extraData decodes to geth/v1.17.2 go1.26.1 linux)
CL client & version: not yet available from the proposer
OS & Version: Linux
Commit hash: release tag v1.17.2 (be4dc0c4be2fe316dbdd0a73e48421f64978232f)
Expected behaviour
A payload builder starting from the declared parent state should apply the 16 withdrawals and put the resulting post-state root in the block header. A fresh execution/import of that payload should derive the same root.
Actual behaviour
On LUKSO testnet, a finalized withdrawals-only payload built by Geth v1.17.2 declares:
0x0c9916983660769626ac87965447cf873047afdd9d62f8a907d18fa9a1ce8d9a
as its state root. Applying the payload's withdrawals to a public Merkle proof from the declared parent derives:
0x33986ae79dc11959ca90b93b9e46ef0a3764d7386b2aea10b2b7390a27db015c
That second root exactly matches the InvalidStateRoot result reported by a Nethermind live-sync node. More specifically, the header root can be reproduced exactly by using the withdrawal recipient's balance from three execution blocks earlier, while keeping the rest of the parent trie proof and then applying the current withdrawals.
In the reported Nethermind error, expected is the payload/header root 0x0c9916...e8d9a, while got is the independently executed root 0x33986a...015c.
This report is about an upstream Geth code path used on LUKSO. LUKSO documents that its blockchain runs an unmodified version of the Ethereum protocol, and its execution layer is designed as an Ethereum-equivalent, 1:1 EVM execution environment. LUKSO is nevertheless a separate network with different chain IDs, genesis data, fork activation settings, and operational configuration. This evidence does not establish that Ethereum mainnet is affected. If an empty or withdrawals-only payload plus the same stale-state condition is the trigger class, the upstream bug could theoretically affect Ethereum under a comparable condition.
Primary public case
| Field | Value |
|---|---|
| Network | LUKSO testnet, chain ID 4201 |
| Consensus slot | 8740857, finalized/canonical |
| Execution block | 8414592 |
| Execution hash | 0xe1b258f8dcbc7705d3afe60cda960a35886efc5ac56ed5f5cecbd7d0c860e5db |
| Parent hash | 0xcb3e73ce00236266d625f26070ce8916440c95a069fdb85eb81143f53e84d66b |
| Time | 2026-08-29 16:31:24 UTC |
| Proposer | 3532 |
| Transactions | 0 |
| Withdrawals | 16 |
| Withdrawal recipient | 0x6109dcd72b8a2485A5b3Ac4E76965159e9893aB7 for all 16 |
| Total withdrawn | 22,438,550 gwei |
| Header state root | 0x0c9916983660769626ac87965447cf873047afdd9d62f8a907d18fa9a1ce8d9a |
| Recomputed post-state root | 0x33986ae79dc11959ca90b93b9e46ef0a3764d7386b2aea10b2b7390a27db015c |
extraData |
0xd883011102846765746888676f312e32362e31856c696e7578 |
Public raw artifacts:
- Full signed consensus block JSON, including the execution payload
- Consensus block body JSON
- Execution explorer record
- Official LUKSO testnet parameters and network-config repository link
Independent proof-level derivation
This derivation uses public data only. It is not being presented as a full client reproduction.
- Query
eth_getBlockByNumber("0x80657f", false)fromhttps://rpc.testnet.lukso.network. - Query
eth_getProof("0x6109dcd72b8a2485a5b3ac4e76965159e9893ab7", [], "0x80657f"). - Validate and decode the account proof against the parent root.
- Sum the 16 payload withdrawals, convert gwei to wei, update the account RLP leaf, and rehash the proof path.
The public parent data is:
- Block:
8414591(0x80657f) - Parent state root:
0xb012469ba44ac5964510af5309f9429ee6f16b68fa2f0be9a43b08a61e45a324 - Account nonce:
0x50 - Account balance:
0x5603ad286d033e1c1bc10 - Empty storage root:
0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421 - Empty code hash:
0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
Re-encoding the unchanged account leaf round-trips exactly to the parent root. Adding 22,438,550,000,000,000 wei produces account balance 0x5603ad2d687f1ebc81810 and root:
0x33986ae79dc11959ca90b93b9e46ef0a3764d7386b2aea10b2b7390a27db015c
The surprising exact match is:
- Balance after block 8414588:
0x5603ad1a0f72d4a58ec10 - Add the current payload's 22,438,550 gwei
- Resulting balance:
0x5603ad1f0aeeb545f4810 - Rehash that leaf in the parent-8414591 proof
- Resulting root:
0x0c9916983660769626ac87965447cf873047afdd9d62f8a907d18fa9a1ce8d9a, exactly the payload header root
Blocks 8414589, 8414590, and 8414591 were also zero-transaction blocks with 16 withdrawals to the same address. Their withdrawal totals were 24,117,205, 18,842,466, and 21,736,721 gwei. The skipped balance delta is therefore exactly 64,696,392 gwei. Candidate starting balances from the parent and 512 earlier blocks were checked; only parent minus 3 reproduced the bad header root.
This strongly indicates a stale account read or a hybrid state view during payload construction, but it does not by itself identify which Geth subsystem supplied that value.
Steps to reproduce the behaviour
A complete end-to-end reproduction still needs the proposer database/configuration or an isolated way to trigger the stale view. The deterministic diagnostic reproduction available now is:
- Download the full payload from the link above.
- Fetch block 8414591 and the explicit-block account proof using the two JSON-RPC calls above.
- Verify the unchanged account proof hashes to
0xb012469b...e45a324. - Apply all 16 withdrawals using the normal
gwei * 1_000_000_000conversion and rehash the proof. The result is0x33986ae7...7db015c. - Replace only the starting balance with the public balance at block 8414588, apply the same current withdrawals, and rehash the parent proof. The result is the declared header root
0x0c991698...1ce8d9a. - Feed the full payload to a fresh execution client with parent state available and record the exact
newPayloadresponse and computed root.
The Geth v1.17.2 source path appears relevant:
- The miner opens
StateAt(parent.Root)and starts the"miner"prefetcher. - Beacon finalization adds each withdrawal balance and calls
IntermediateRoot. IntermediateRootcan replace its trie with the prefetched trie.- For an unfamiliar payload, Engine API executes it through
InsertBlockWithoutSetHead, while an already-known payload returnsVALIDwithout re-execution.
Observed scope
Four earlier LUKSO testnet payloads have the same public trigger shape:
| Execution block | Hash | Consensus slot | Publicly verified shape |
|---|---|---|---|
| 8364162 | 0x43690a9b6188fa5f3c1bc8006639f32071ebd71f2d23e636878b3c24ce2169bd |
8688329 | 0 tx, 16 withdrawals, Geth v1.17.2 |
| 8364606 | 0x589139f52ebaef3c0ee2ae6eb2251505cf7f486689e87ee09fd80a06ecf95943 |
8688940 | 0 tx, 16 withdrawals, Geth v1.17.2 |
| 8364719 | 0x98d2888e127e46e2330892d53549b41150865dfc8bb2ba51e16b424a8f952447 |
8689105 | 0 tx, 16 withdrawals, Geth v1.17.2 |
| 8365292 | 0x6304da019f2979601fa82adeeee0f17d0dc47dc229d873d0b544cd3d68460449 |
8689853 | 0 tx, 16 withdrawals, Geth v1.17.2; Dora now shows Missing |
The reporter states that geth replay tooling, Erigon, and Nethermind independently derived the same alternative root for each of those four blocks. Those replay logs, exact client versions, and root pairs are not public yet, so that statement should be treated as reporter-supplied corroboration rather than a reproduction attached to this issue.
There are also two reporter-supplied LUKSO mainnet incidents:
| Consensus slot | Time | Proposer | Reporter-supplied execution hash | Publicly verified |
|---|---|---|---|---|
| 8565560 | 2026-08-25 08:12:00 UTC | 64233 | 0x2670f604...ae13c83f |
Finalized slot now shown as Missing after reorg |
| 8587561 | 2026-08-28 09:32:12 UTC | 306536 | 0x12851d85...1e3624c1 |
Finalized slot now shown as Missing after reorg |
The reporter says an Erigon archive node rejected both with wrong trie root, and that the payloads were withdrawals-only or near-empty and identified as Geth v1.17.2. Public explorers no longer expose the orphaned execution payloads, so the full hashes, payload fields, declared/computed roots, client identification, and Erigon logs remain unverified here.
Impact
On the observed LUKSO testnet case, the consensus-finalized payload cannot be imported by a fresh client that derives the parent state normally. This can halt syncing at the affected height and prevent execution-client diversity. The mainnet reports indicate missed/orphaned proposals rather than a canonical invalid block, but require raw artifacts before they can be compared one-to-one with the primary case.
Again, this issue does not claim Ethereum mainnet impact. It asks whether the shared upstream payload-building/state machinery can emit a root from a stale account view under this trigger class.
Hypotheses, not conclusions
- Stale flat-state/snapshot/account reader during block building. The exact parent-minus-3 balance correspondence is the strongest evidence. The hybrid-root calculation keeps every other node on the account proof path from the declared parent state.
- Miner trie-prefetch or state-reader interaction introduced/exposed in v1.17.2. PR #33945 enabled the trie prefetcher in the block builder. PR #33816 refactored
codedbandcachingDB. These are credible areas to inspect, not identified causes. - A state database error was recorded but the payload was still returned. Later PR #35427, merged after v1.17.5, added a
state.Error()check so a node does not seal a block after a state read failure. It is unknown whether the observed stale value involved any recorded DB error. - Production versus import lifecycle. A producer and its live Geth may share the same stale snapshot/cache lifetime, while a fresh sync/replay reconstructs from trie state. Engine API also has distinct unfamiliar/already-known payload paths. Exact Engine request/response logs are needed to tell which path accepted the producer's block.
Simple withdrawal amount conversion or ordering looks less likely for the primary case: applying all 16 withdrawals with Geth's normal gwei-to-wei formula produces the independent replay root exactly.
Diagnostic guidance requested
Could maintainers advise:
- Whether the exact ancestor-balance match points to a known flat-state, snapshot, trie-prefetch, or payload-builder cache failure mode.
- Whether v1.17.2 could return/seal a payload after a state read/prefetch error that current master would reject via #35427.
- Which debug log categories, metrics, RPC methods, or database checks best distinguish a stale snapshot read from a trie-prefetch issue.
- Whether there is a supported way to A/B the v1.17.2 builder with snapshot/flat-state and miner prefetching disabled without changing execution semantics.
- Whether the producing node should normally re-execute its own payload through
newPayload, or can reach the already-known fast path in this lifecycle. - Whether this report should be moved to a security channel if maintainers consider the condition sensitive.
Critical artifacts still being collected:
- Canonical execution header RLP and full Engine API payload/request transcript. The full primary payload JSON is linked above.
- Parent header/state root and account/trie proofs from the producer itself.
- Exact chain configuration and fork activation settings used by the proposer, not only the public repository defaults.
- Full
geth version, command line/config, state scheme (hashorpath), sync mode, pruning/archive mode, cache settings, snapshot generation/status, and database history. - Proposer Geth and CL logs around
forkchoiceUpdated, payload ID creation,getPayload,newPayload, and any snapshot/trie/prefetch/database errors. debug_getBadBlocksoutput, if present.- Raw Nethermind, Erigon, and geth-replay logs with exact versions and declared/computed roots.
- Full orphaned mainnet execution hashes, headers/payloads, parent state references, and rejection logs.
Related upstream work searched
- Geth v1.17.2 release
- #33945: miner: enable trie prefetcher in block builder
- #33816: core, miner, tests: introduce codedb and simplify cachingDB
- #35427: miner: don't seal block if a db error occurred
- #29880: Unexpected trie missing in prefetcher, a prior empty-block/prefetch issue but not the same failure
- #27254: flaw in snapshot difflayer lookup, a historical snapshot lookup bug but not evidence of a regression here
Searches for withdrawals-only state root, stale state root miner, InvalidStateRoot, and wrong trie root found no direct duplicate in this repository as of 2026-09-01.
Source: ethereum/go-ethereum