From c2299d4cb13c5e21df55586774d44ef20c020837 Mon Sep 17 00:00:00 2001 From: Yenya030 Date: Fri, 20 Mar 2026 17:59:25 -0500 Subject: [PATCH] cmd/utils: validate imported history before insertion --- cmd/utils/cmd.go | 45 +++++++++++ cmd/utils/history_test.go | 160 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go index e490f613b3..70958be7c8 100644 --- a/cmd/utils/cmd.go +++ b/cmd/utils/cmd.go @@ -49,6 +49,7 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" "github.com/urfave/cli/v2" ) @@ -249,6 +250,47 @@ func readList(filename string) ([]string, error) { return strings.Split(string(b), "\n"), nil } +func validateImportedHistoryBlock(block *types.Block, receipts types.Receipts) error { + header := block.Header() + + if hash := types.CalcUncleHash(block.Uncles()); hash != header.UncleHash { + return fmt.Errorf("uncle root hash mismatch (header value %x, calculated %x)", header.UncleHash, hash) + } + if hash := types.DeriveSha(block.Transactions(), trie.NewStackTrie(nil)); hash != header.TxHash { + return fmt.Errorf("transaction root hash mismatch (header value %x, calculated %x)", header.TxHash, hash) + } + if header.WithdrawalsHash != nil { + if block.Withdrawals() == nil { + return errors.New("missing withdrawals in block body") + } + if hash := types.DeriveSha(block.Withdrawals(), trie.NewStackTrie(nil)); hash != *header.WithdrawalsHash { + return fmt.Errorf("withdrawals root hash mismatch (header value %x, calculated %x)", *header.WithdrawalsHash, hash) + } + } else if block.Withdrawals() != nil { + return errors.New("withdrawals present in block body") + } + + var blobs int + for i, tx := range block.Transactions() { + blobs += len(tx.BlobHashes()) + if tx.BlobTxSidecar() != nil { + return fmt.Errorf("unexpected blob sidecar in transaction at index %d", i) + } + } + if header.BlobGasUsed != nil { + if want := *header.BlobGasUsed / params.BlobTxBlobGasPerBlob; uint64(blobs) != want { + return fmt.Errorf("blob gas used mismatch (header %v, calculated %v)", *header.BlobGasUsed, blobs*params.BlobTxBlobGasPerBlob) + } + } else if blobs > 0 { + return errors.New("data blobs present in block body") + } + + if hash := types.DeriveSha(receipts, trie.NewStackTrie(nil)); hash != header.ReceiptHash { + return fmt.Errorf("receipt root hash mismatch (header value %x, calculated %x)", header.ReceiptHash, hash) + } + return nil +} + // ImportHistory imports Era1 files containing historical block information, // starting from genesis. The assumption is held that the provided chain // segment in Era1 file should all be canonical and verified. @@ -346,6 +388,9 @@ func ImportHistory(chain *core.BlockChain, dir string, network string, from func if err != nil { return fmt.Errorf("error reading receipts %d: %w", it.Number(), err) } + if err := validateImportedHistoryBlock(block, receipts); err != nil { + return fmt.Errorf("error validating block %d: %w", it.Number(), err) + } blocks = append(blocks, block) receiptsList = append(receiptsList, receipts) if len(blocks) == importBatchSize { diff --git a/cmd/utils/history_test.go b/cmd/utils/history_test.go index 6631946129..f97b061564 100644 --- a/cmd/utils/history_test.go +++ b/cmd/utils/history_test.go @@ -27,6 +27,8 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" @@ -191,3 +193,161 @@ func TestHistoryImportAndExport(t *testing.T) { }) } } + +func TestImportHistoryRejectsForgedTxRootArchive(t *testing.T) { + genesis, honest, honestReceipts, _ := makeHistorySingleTxFixture(t) + header := honest.Header() + header.TxHash = common.HexToHash("0x1234") + if header.TxHash == honest.TxHash() { + t.Fatal("fixture is not malformed: tx root unexpectedly matches original block") + } + poisoned := honest.WithSeal(header) + + dir := t.TempDir() + if err := writeEra1(dir, poisoned, honestReceipts); err != nil { + t.Fatalf("failed to build forged-tx-root era1 archive: %v", err) + } + imported := newHistoryImportChain(t, genesis, ethash.NewFaker()) + + if err := ImportHistory(imported, dir, "mainnet", onedb.From); err == nil { + t.Fatal("ImportHistory unexpectedly accepted forged tx root") + } else if !strings.Contains(err.Error(), "transaction root hash mismatch") { + t.Fatalf("unexpected ImportHistory error: %v", err) + } +} + +func TestImportHistoryRejectsForgedWithdrawalsArchive(t *testing.T) { + genesis, honest, honestReceipts := makeMergedHistoryFixture(t) + header := honest.Header() + if header.WithdrawalsHash == nil { + t.Fatal("fixture does not have a withdrawals root") + } + forgedWithdrawalsHash := common.HexToHash("0x5678") + if forgedWithdrawalsHash == *header.WithdrawalsHash { + t.Fatal("fixture is not malformed: withdrawals root unexpectedly matches original block") + } + header.WithdrawalsHash = &forgedWithdrawalsHash + poisoned := honest.WithSeal(header) + + dir := t.TempDir() + if err := writeEra1(dir, poisoned, honestReceipts); err != nil { + t.Fatalf("failed to build forged-withdrawals era1 archive: %v", err) + } + imported := newHistoryImportChain(t, genesis, beacon.New(ethash.NewFaker())) + + if err := ImportHistory(imported, dir, "mainnet", onedb.From); err == nil { + t.Fatal("ImportHistory unexpectedly accepted forged withdrawals root") + } else if !strings.Contains(err.Error(), "withdrawals root hash mismatch") { + t.Fatalf("unexpected ImportHistory error: %v", err) + } +} + +func TestImportHistoryRejectsForgedReceiptArchive(t *testing.T) { + genesis, honest, honestReceipts, _ := makeHistorySingleTxFixture(t) + forgedReceipt := new(types.Receipt) + *forgedReceipt = *honestReceipts[0] + forgedReceipt.Status = types.ReceiptStatusFailed + if got := types.DeriveSha(types.Receipts{forgedReceipt}, trie.NewStackTrie(nil)); got == honest.ReceiptHash() { + t.Fatalf("fixture is not malformed: receipt root unexpectedly matches header %s", got) + } + + dir := t.TempDir() + if err := writeEra1(dir, honest, types.Receipts{forgedReceipt}); err != nil { + t.Fatalf("failed to build forged-receipt era1 archive: %v", err) + } + imported := newHistoryImportChain(t, genesis, ethash.NewFaker()) + + if err := ImportHistory(imported, dir, "mainnet", onedb.From); err == nil { + t.Fatal("ImportHistory unexpectedly accepted forged receipt root") + } else if !strings.Contains(err.Error(), "receipt root hash mismatch") { + t.Fatalf("unexpected ImportHistory error: %v", err) + } +} + +func writeEra1(dir string, block *types.Block, receipts types.Receipts) error { + buf := new(bytes.Buffer) + builder := onedb.NewBuilder(buf) + + td := new(big.Int) + if diff := block.Difficulty(); diff != nil { + td.Set(diff) + } + root, err := func() (common.Hash, error) { + if err := builder.Add(block, receipts, td); err != nil { + return common.Hash{}, err + } + return builder.Finalize() + }() + if err != nil { + return err + } + + filename := filepath.Join(dir, onedb.Filename("mainnet", 0, root)) + if err := os.WriteFile(filename, buf.Bytes(), 0o644); err != nil { + return err + } + sum := sha256.Sum256(buf.Bytes()) + return os.WriteFile(filepath.Join(dir, "checksums.txt"), []byte(common.BytesToHash(sum[:]).Hex()), 0o644) +} + +func makeHistorySingleTxFixture(t *testing.T) (*core.Genesis, *types.Block, types.Receipts, *types.Transaction) { + t.Helper() + + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("generate key: %v", err) + } + address := crypto.PubkeyToAddress(key.PublicKey) + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{address: {Balance: big.NewInt(1000000000000000000)}}, + } + signer := types.LatestSigner(genesis.Config) + _, blocks, receipts := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), 1, func(i int, g *core.BlockGen) { + tx, err := types.SignNewTx(key, signer, &types.LegacyTx{ + Nonce: uint64(i), + To: &common.Address{0xaa}, + Value: big.NewInt(1), + Gas: 50_000, + GasPrice: big.NewInt(1_000_000_000), + }) + if err != nil { + t.Fatalf("sign tx: %v", err) + } + g.AddTx(tx) + }) + if len(blocks) != 1 || len(receipts) != 1 || len(receipts[0]) != 1 || len(blocks[0].Transactions()) != 1 { + t.Fatalf("unexpected fixture lengths: blocks=%d receiptBlocks=%d receipts=%d txs=%d", len(blocks), len(receipts), len(receipts[0]), len(blocks[0].Transactions())) + } + return genesis, blocks[0], receipts[0], blocks[0].Transactions()[0] +} + +func makeMergedHistoryFixture(t *testing.T) (*core.Genesis, *types.Block, types.Receipts) { + t.Helper() + + config := *params.MergedTestChainConfig + genesis := &core.Genesis{Config: &config} + _, blocks, receipts := core.GenerateChainWithGenesis(genesis, beacon.New(ethash.NewFaker()), 1, nil) + if len(blocks) != 1 || len(receipts) != 1 { + t.Fatalf("unexpected merged fixture lengths: blocks=%d receiptBlocks=%d", len(blocks), len(receipts)) + } + return genesis, blocks[0], receipts[0] +} + +func newHistoryImportChain(t *testing.T, genesis *core.Genesis, engine consensus.Engine) *core.BlockChain { + t.Helper() + + db, err := rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{}) + if err != nil { + t.Fatalf("failed to open import db: %v", err) + } + t.Cleanup(func() { db.Close() }) + + genesis.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults)) + imported, err := core.NewBlockChain(db, genesis, engine, nil) + if err != nil { + t.Fatalf("unable to initialize imported chain: %v", err) + } + t.Cleanup(imported.Stop) + return imported +}