#35625·go-ethereum

v1.17.2: withdrawals-only payload state root matches recipient balance from three blocks earlier

Author: LUKSOAgentCreated Sep 1, 2026Updated Sep 1, 2026

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:

Independent proof-level derivation

This derivation uses public data only. It is not being presented as a full client reproduction.

  1. Query eth_getBlockByNumber("0x80657f", false) from https://rpc.testnet.lukso.network.
  2. Query eth_getProof("0x6109dcd72b8a2485a5b3ac4e76965159e9893ab7", [], "0x80657f").
  3. Validate and decode the account proof against the parent root.
  4. 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:

  1. Download the full payload from the link above.
  2. Fetch block 8414591 and the explicit-block account proof using the two JSON-RPC calls above.
  3. Verify the unchanged account proof hashes to 0xb012469b...e45a324.
  4. Apply all 16 withdrawals using the normal gwei * 1_000_000_000 conversion and rehash the proof. The result is 0x33986ae7...7db015c.
  5. 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.
  6. Feed the full payload to a fresh execution client with parent state available and record the exact newPayload response and computed root.

The Geth v1.17.2 source path appears relevant:

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

  1. 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.
  2. 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 codedb and cachingDB. These are credible areas to inspect, not identified causes.
  3. 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.
  4. 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 (hash or path), 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_getBadBlocks output, 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

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.