You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Summary
Two fixes addressing all 29 failures in the Hive `legacy-cancun`
[run](https://hive.ethpandaops.io/#/test/generic/1777223434-c411e5be836d6d6f8a466489c2874cf3)
against `erigon_default`, plus a regression test that catches the
EVM-side bug locally.
### (a) `cmd/utils/app` — TD-based fork choice for `erigon import`
The Hive consensus simulator writes each block to its own `00NN.rlp`
file and runs `erigon import /blocks/00NN.rlp` for each — the simulator
does not implement TD/fork-choice itself, it relies on the client's
native chain selection. `InsertChain` always called
`UpdateForkChoice(importedTip)`, so every imported block became head
even when its TD was lower than the current canonical head's. For
`lotsOfLeafs` block 6 (TD `0x9CCF4`) is the canonical winner, but Erigon
kept switching head through blocks 7-13 (TD `0x9CC34`) and ended on
`0x26ad10c0…` instead of `0xf7f9ea97…`.
This affects **23 tests** (all Berlin/Istanbul/London variants):
`lotsOfLeafs`, `ChainAtoChainB_difficultyB`, `ForkStressTest`,
`CallContractFromNotBestBlock`, `lotsOfBranchesOverrideAtTheMiddle`,
`sideChainWithMoreTransactions`, `uncleBlockAtBlock3afterBlock4`,
`blockChainFrontierWithLargerTDvsHomesteadBlockchain[2]`.
**Fix:** before calling `UpdateForkChoice`, compare the imported chain's
total difficulty against the current head's TD using a shared
`headerdownload.ShouldReorg` helper (extracted from the existing
`FeedHeaderPoW` tie-break: TD → height → lex hash). If the imported
chain doesn't beat the current head, write the headers/bodies/TDs
straight to the DB as a side chain (no execution, no head change) so
future side-chain extensions can still validate. Once a side-chain
extension surpasses the canonical TD, the regular `InsertBlocks` +
`UpdateForkChoice` path triggers the reorg and executes those blocks.
PoS imports (all `difficulty=0`, TD never grows) explicitly bypass this
branch — `ShouldReorg`'s "shorter wins on tie" rule would otherwise
prevent the head from advancing past genesis.
### (b) `execution/vm` — STATICCALL touch for EIP-161 state clearing
`RevertPrecompiledTouch_d3g0v0_{Berlin,Istanbul,London}` and
`RevertPrecompiledTouch_storage_d3g0v0_{Berlin,Istanbul,London}` (**6
tests**) all touch precompile 3 (RIPEMD-160) via STATICCALL inside a
frame that runs out of gas. Expected: ripemd state-cleared (deleted) at
end of tx via the `journal.dirty(ripemd)` consensus quirk. Erigon left
it.
The CALL path in `evm.call` calls `Exist(addr)` first, which loads
ripemd into `stateObjects`; the subsequent `AddBalance(addr, 0)` then
falls through to `TouchAccount` and the famous quirk fires, so
`FinalizeTx` deletes ripemd. But the STATICCALL path skipped `Exist` and
called `AddBalance(addr, 0)` directly — in serial mode that hits the
`versionMap == nil && addr == ripemd && amount.IsZero()` shortcut that
uses `balanceIncrease` instead of `GetOrNewStateObject`. ripemd never
enters `stateObjects`, so on revert `FinalizeTx`'s `if !exist { continue
}` branch skips it.
**Fix:** replace `AddBalance(addr, u256.Num0, …)` with
`TouchAccount(addr)` for STATICCALL — same end behavior as CALL's
post-`Exist` flow (loads the account, hits the dirty quirk on touch).
Identical for non-ripemd / non-serial paths.
`d0` (CALL), `d1` (DELEGATECALL), `d2` (CALLCODE) variants were
unaffected: CALL goes through `Exist`+`Transfer`; DELEGATECALL and
CALLCODE don't touch the callee at all (matching geth and the post-state
expectations).
### (c) `execution/tests` — local regression test for the d3 RIPEMD path
Add `TestLegacyCancunState`, walking
`legacy-tests/LegacyTests/Cancun/GeneralStateTests` in state-test
format. `TestLegacyBlockchain` (`block_test.go`) explicitly skips the
`LegacyTests/` subtree and `TestState` walks the EEST static_tests,
neither of which covers the d3 ripemd-touch case. Verified the new test
fails at baseline (Berlin/1, Istanbul/1, London/1 — the d3 indexes) and
passes with the fix in (b). Refactored the now-three state-test runners
(`TestStateCornerCases` / `TestState` / `TestLegacyCancunState`) to
share a `runStateTests(t, st, dir)` helper plus a `stateTestSetup(t)`
helper for parallel/log/Windows-skip boilerplate.
Six fixtures fail on Constantinople-only post-state-root mismatches
(sstoreGas / *_HighNonce* / Ecrecover_Overflow / ecrecoverShortBuff) —
pre-existing Erigon-side divergences from geth, unrelated to this PR.
Geth doesn't catch them locally because its runner walks
`LegacyTests/Constantinople/GeneralStateTests` (older snapshot, doesn't
include these). Tracked separately in #20894 and `SkipLoad`-ed for now
with a comment pointing at the issue.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: info@weblogix.biz <admin@10gbps.weblogix.it>
// See if the forking point affects the unwindPoint (the block number to which other stages will need to unwind before the new canonical chain is applied)
972
-
ifforkingPoint<hi.unwindPoint {
973
-
hi.SetUnwindPoint(forkingPoint)
974
-
hi.unwind=true
975
-
}
976
-
// This makes sure we end up choosing the chain with the max total difficulty
977
-
hi.localTd.Set(td)
955
+
hi.highest=blockHeight
956
+
hi.highestHash=hash
957
+
hi.highestTimestamp=header.Time
958
+
hi.canonicalCache.Add(blockHeight, hash)
959
+
// See if the forking point affects the unwindPoint (the block number to which other stages will need to unwind before the new canonical chain is applied)
960
+
ifforkingPoint<hi.unwindPoint {
961
+
hi.SetUnwindPoint(forkingPoint)
962
+
hi.unwind=true
978
963
}
964
+
// This makes sure we end up choosing the chain with the max total difficulty
0 commit comments