Skip to content

Commit fd74bee

Browse files
yperbasisclaudeinfo@weblogix.biz
authored
cmd/utils/app, execution/vm: fix hive legacy-cancun blockchain failures (#20892)
## 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>
1 parent 6bafc50 commit fd74bee

6 files changed

Lines changed: 284 additions & 86 deletions

File tree

cmd/utils/app/import_cmd.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"errors"
2323
"fmt"
2424
"io"
25+
"math/big"
2526
"os"
2627
"os/signal"
2728
"strings"
@@ -33,12 +34,14 @@ import (
3334

3435
"github.com/erigontech/erigon/cmd/erigon/node"
3536
"github.com/erigontech/erigon/cmd/utils"
37+
"github.com/erigontech/erigon/common"
3638
"github.com/erigontech/erigon/common/log/v3"
3739
"github.com/erigontech/erigon/db/kv"
3840
"github.com/erigontech/erigon/db/rawdb"
3941
"github.com/erigontech/erigon/db/services"
4042
"github.com/erigontech/erigon/execution/execmodule"
4143
"github.com/erigontech/erigon/execution/execmodule/chainreader"
44+
"github.com/erigontech/erigon/execution/protocol/rules/ethash"
4245
"github.com/erigontech/erigon/execution/rlp"
4346
"github.com/erigontech/erigon/execution/tests/blockgen"
4447
"github.com/erigontech/erigon/execution/types"
@@ -252,6 +255,92 @@ type stateChangesClient interface {
252255
}
253256

254257
func InsertChain(ethereum *eth.Ethereum, chain *blockgen.ChainPack, setHead bool) error {
258+
if len(chain.Blocks) == 0 {
259+
return nil
260+
}
261+
for _, block := range chain.Blocks {
262+
if err := block.HashCheck(true); err != nil {
263+
return err
264+
}
265+
}
266+
267+
ctx := context.Background()
268+
269+
// Compare the imported chain's total difficulty against the current canonical
270+
// head's via the same PoW fork-choice rule as the legacy header sync
271+
// (ethash.ShouldReorg). If the imported chain doesn't win, persist
272+
// the blocks as a side chain (without changing head or executing them), so
273+
// future imports extending this branch can still be validated. Once a
274+
// side-chain extension surpasses the canonical TD, the regular
275+
// InsertBlocks + UpdateForkChoice path triggers the reorg and executes
276+
// those blocks. This matches what ethereum/tests BlockchainTests with
277+
// multi-chain layouts (lotsOfLeafs, ChainAtoChainB, ForkStressTest, etc.)
278+
// expect when Hive imports one block per file.
279+
firstBlock := chain.Blocks[0]
280+
tipBlock := chain.TopBlock
281+
var parentTd, currentHeadTd *big.Int
282+
var currentHeadHash common.Hash
283+
var currentHeadNumber uint64
284+
if err := ethereum.ChainDB().View(ctx, func(tx kv.Tx) error {
285+
if firstBlock.NumberU64() > 0 {
286+
td, readErr := rawdb.ReadTd(tx, firstBlock.ParentHash(), firstBlock.NumberU64()-1)
287+
if readErr != nil {
288+
return fmt.Errorf("read parent TD: %w", readErr)
289+
}
290+
parentTd = td
291+
} else {
292+
parentTd = new(big.Int)
293+
}
294+
if hash := rawdb.ReadHeadBlockHash(tx); hash != (common.Hash{}) {
295+
if num := rawdb.ReadHeaderNumber(tx, hash); num != nil {
296+
td, readErr := rawdb.ReadTd(tx, hash, *num)
297+
if readErr != nil {
298+
return fmt.Errorf("read head TD: %w", readErr)
299+
}
300+
currentHeadTd = td
301+
currentHeadHash = hash
302+
currentHeadNumber = *num
303+
}
304+
}
305+
return nil
306+
}); err != nil {
307+
return err
308+
}
309+
// Apply the side-chain path only for PoW imports where both TDs are known.
310+
// If parent TD is missing (orphan import) or there is no current head yet,
311+
// fall through to the regular insert path so any error surfaces normally.
312+
// PoS blocks have difficulty=0 so TD never grows; ShouldReorg's PoW
313+
// tie-break (shorter chain wins on equal TD) would prevent the head from
314+
// ever advancing, so PoS imports skip the side-chain branch and rely on
315+
// UpdateForkChoice as before.
316+
isPoW := tipBlock.Header().Difficulty.Sign() > 0
317+
if setHead && isPoW && parentTd != nil && currentHeadTd != nil {
318+
importedTipTd := new(big.Int).Set(parentTd)
319+
for _, b := range chain.Blocks {
320+
importedTipTd.Add(importedTipTd, b.Header().Difficulty.ToBig())
321+
}
322+
if !ethash.ShouldReorg(currentHeadTd, currentHeadNumber, currentHeadHash, importedTipTd, tipBlock.NumberU64(), tipBlock.Hash()) {
323+
// Side chain — write headers/bodies/TDs directly without executing
324+
// or changing head.
325+
return ethereum.ChainDB().Update(ctx, func(tx kv.RwTx) error {
326+
td := new(big.Int).Set(parentTd)
327+
for _, b := range chain.Blocks {
328+
td.Add(td, b.Header().Difficulty.ToBig())
329+
if err := rawdb.WriteHeader(tx, b.Header()); err != nil {
330+
return fmt.Errorf("write side-chain header: %w", err)
331+
}
332+
if err := rawdb.WriteTd(tx, b.Hash(), b.NumberU64(), td); err != nil {
333+
return fmt.Errorf("write side-chain TD: %w", err)
334+
}
335+
if _, err := rawdb.WriteRawBodyIfNotExists(tx, b.Hash(), b.NumberU64(), b.RawBody()); err != nil {
336+
return fmt.Errorf("write side-chain body: %w", err)
337+
}
338+
}
339+
return nil
340+
})
341+
}
342+
}
343+
255344
streamCtx, cancel := context.WithCancel(ethereum.SentryCtx())
256345
defer cancel()
257346
stream, err := ethereum.StateDiffClient().StateChanges(streamCtx, &remoteproto.StateChangeRequest{WithStorage: false, WithTransactions: false}, grpc.WaitForReady(true))
@@ -260,17 +349,12 @@ func InsertChain(ethereum *eth.Ethereum, chain *blockgen.ChainPack, setHead bool
260349
}
261350

262351
insertedBlocks := map[uint64]struct{}{}
263-
264352
for _, block := range chain.Blocks {
265-
if err := block.HashCheck(true); err != nil {
266-
return err
267-
}
268353
insertedBlocks[block.NumberU64()] = struct{}{}
269354
}
270355

271356
chainRW := chainreader.NewChainReaderEth1(ethereum.ChainConfig(), ethereum.ExecutionModule(), time.Hour)
272357

273-
ctx := context.Background()
274358
if err := chainRW.InsertBlocksAndWait(ctx, chain.Blocks); err != nil {
275359
return err
276360
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2024 The Erigon Authors
2+
// This file is part of Erigon.
3+
//
4+
// Erigon is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Erigon is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethash
18+
19+
import (
20+
"bytes"
21+
"math/big"
22+
23+
"github.com/erigontech/erigon/common"
24+
)
25+
26+
// ShouldReorg reports whether a candidate header should replace the current
27+
// canonical head per PoW total-difficulty fork choice.
28+
//
29+
// The new chain wins if its total difficulty is strictly higher. On equal
30+
// total difficulty, the shorter chain wins (smaller block height); on equal
31+
// total difficulty AND equal height, the lexicographically larger block hash
32+
// wins. This matches geth's tie-break — see
33+
// https://github.com/maticnetwork/bor/blob/master/core/forkchoice.go#L81.
34+
func ShouldReorg(localTd *big.Int, localHeight uint64, localHash common.Hash, newTd *big.Int, newHeight uint64, newHash common.Hash) bool {
35+
if cmp := newTd.Cmp(localTd); cmp != 0 {
36+
return cmp > 0
37+
}
38+
if newHeight != localHeight {
39+
return newHeight < localHeight
40+
}
41+
return bytes.Compare(localHash.Bytes(), newHash.Bytes()) < 0
42+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2024 The Erigon Authors
2+
// This file is part of Erigon.
3+
//
4+
// Erigon is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Erigon is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package ethash
18+
19+
import (
20+
"math/big"
21+
"testing"
22+
23+
"github.com/erigontech/erigon/common"
24+
)
25+
26+
func TestShouldReorg(t *testing.T) {
27+
t.Parallel()
28+
29+
// Two distinct hashes ordered so hashLow < hashHigh (lex byte compare).
30+
hashLow := common.Hash{0x01}
31+
hashHigh := common.Hash{0x02}
32+
33+
tests := []struct {
34+
name string
35+
localTd int64
36+
localHeight uint64
37+
localHash common.Hash
38+
newTd int64
39+
newHeight uint64
40+
newHash common.Hash
41+
want bool
42+
}{
43+
{"new TD strictly higher", 100, 5, hashLow, 101, 5, hashLow, true},
44+
{"new TD strictly lower", 100, 5, hashLow, 99, 5, hashLow, false},
45+
{"equal TD, new height shallower", 100, 10, hashLow, 100, 5, hashHigh, true},
46+
{"equal TD, new height deeper", 100, 5, hashLow, 100, 10, hashHigh, false},
47+
{"equal TD, equal height, new hash larger", 100, 5, hashLow, 100, 5, hashHigh, true},
48+
{"equal TD, equal height, new hash smaller", 100, 5, hashHigh, 100, 5, hashLow, false},
49+
{"equal TD, equal height, equal hash", 100, 5, hashLow, 100, 5, hashLow, false},
50+
}
51+
for _, tc := range tests {
52+
got := ShouldReorg(big.NewInt(tc.localTd), tc.localHeight, tc.localHash,
53+
big.NewInt(tc.newTd), tc.newHeight, tc.newHash)
54+
if got != tc.want {
55+
t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
56+
}
57+
}
58+
}

execution/stagedsync/headerdownload/header_algos.go

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/erigontech/erigon/db/services"
4141
"github.com/erigontech/erigon/execution/metrics"
4242
"github.com/erigontech/erigon/execution/protocol/rules"
43+
"github.com/erigontech/erigon/execution/protocol/rules/ethash"
4344
"github.com/erigontech/erigon/execution/rlp"
4445
"github.com/erigontech/erigon/execution/stagedsync/dataflow"
4546
"github.com/erigontech/erigon/execution/stagedsync/stages"
@@ -944,38 +945,24 @@ func (hi *HeaderInserter) FeedHeaderPoW(db kv.StatelessRwTx, headerReader servic
944945
td = new(big.Int).Add(parentTd, header.Difficulty.ToBig())
945946

946947
// Now we can decide whether this header will create a change in the canonical head
947-
if td.Cmp(hi.localTd) >= 0 {
948-
reorg := true
949-
950-
// TODO: Add bor check here if required
951-
// Borrowed from https://github.com/maticnetwork/bor/blob/master/core/forkchoice.go#L81
952-
if td.Cmp(hi.localTd) == 0 {
953-
if blockHeight > hi.highest {
954-
reorg = false
955-
} else if blockHeight == hi.highest {
956-
// Compare hashes of block in case of tie breaker. Lexicographically larger hash wins.
957-
reorg = bytes.Compare(hi.highestHash.Bytes(), hash.Bytes()) < 0
958-
}
948+
// TODO: Add bor check here if required
949+
if ethash.ShouldReorg(hi.localTd, hi.highest, hi.highestHash, td, blockHeight, hash) {
950+
hi.newCanonical = true
951+
forkingPoint, err := hi.ForkingPoint(db, header, parent)
952+
if err != nil {
953+
return nil, err
959954
}
960-
961-
if reorg {
962-
hi.newCanonical = true
963-
forkingPoint, err := hi.ForkingPoint(db, header, parent)
964-
if err != nil {
965-
return nil, err
966-
}
967-
hi.highest = blockHeight
968-
hi.highestHash = hash
969-
hi.highestTimestamp = header.Time
970-
hi.canonicalCache.Add(blockHeight, hash)
971-
// 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-
if forkingPoint < 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+
if forkingPoint < hi.unwindPoint {
961+
hi.SetUnwindPoint(forkingPoint)
962+
hi.unwind = true
978963
}
964+
// This makes sure we end up choosing the chain with the max total difficulty
965+
hi.localTd.Set(td)
979966
}
980967
if err = rawdb.WriteTd(db, hash, blockHeight, td); err != nil {
981968
return nil, fmt.Errorf("[%s] failed to WriteTd: %w", hi.logPrefix, err)

0 commit comments

Comments
 (0)