From dddbaa4bf3dbabd84bc8135ea6acd65ec3a4ca7d Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Sat, 18 Jul 2026 00:54:07 +0800 Subject: [PATCH] cmd/utils: export chain with block-level accessList (#35369) --- cmd/utils/cmd.go | 67 +++++++++++++++++++++++++++++++++++----- cmd/utils/export_test.go | 36 +++++++++++++++++++++ 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go index 3cb97ff947..25013d39cc 100644 --- a/cmd/utils/cmd.go +++ b/cmd/utils/cmd.go @@ -40,6 +40,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethdb" @@ -57,6 +58,38 @@ const ( importBatchSize = 2500 ) +// exportBlock is the on-disk representation used by geth export and import. +// +// It intentionally differs from types.Block's network encoding: a block access +// list is stored as an optional trailing field so that an exported chain retains +// EIP-7928 data. Older export files, which end at Withdrawals, remain valid. +type exportBlock struct { + Header *types.Header + Txs []*types.Transaction + Uncles []*types.Header + Withdrawals []*types.Withdrawal `rlp:"optional"` + AccessList *bal.BlockAccessList `rlp:"optional"` +} + +func makeExportBlock(block *types.Block) exportBlock { + return exportBlock{ + Header: block.Header(), + Txs: block.Transactions(), + Uncles: block.Uncles(), + Withdrawals: block.Withdrawals(), + AccessList: block.AccessList(), + } +} + +func (b exportBlock) block() *types.Block { + block := types.NewBlockWithHeader(b.Header).WithBody(types.Body{ + Transactions: b.Txs, + Uncles: b.Uncles, + Withdrawals: b.Withdrawals, + }) + return block.WithAccessListUnsafe(b.AccessList) +} + type EraFileFormat int // ErrImportInterrupted is returned when the user interrupts the import process. @@ -202,18 +235,19 @@ func ImportChain(chain *core.BlockChain, fn string) error { } i := 0 for ; i < importBatchSize; i++ { - var b types.Block - if err := stream.Decode(&b); err == io.EOF { + var record exportBlock + if err := stream.Decode(&record); err == io.EOF { break } else if err != nil { return fmt.Errorf("at block %d: %v", n, err) } + b := record.block() // don't import first block if b.NumberU64() == 0 { i-- continue } - blocks[i] = &b + blocks[i] = b n++ } if i == 0 { @@ -411,8 +445,7 @@ func ExportChain(blockchain *core.BlockChain, fn string) error { writer = gzip.NewWriter(writer) defer writer.(*gzip.Writer).Close() } - // Iterate over the blocks and export them - if err := blockchain.Export(writer); err != nil { + if err := exportChain(blockchain, writer, 0, blockchain.CurrentBlock().Number.Uint64()); err != nil { return err } log.Info("Exported blockchain", "file", fn) @@ -436,14 +469,34 @@ func ExportAppendChain(blockchain *core.BlockChain, fn string, first uint64, las writer = gzip.NewWriter(writer) defer writer.(*gzip.Writer).Close() } - // Iterate over the blocks and export them - if err := blockchain.ExportN(writer, first, last); err != nil { + if err := exportChain(blockchain, writer, first, last); err != nil { return err } log.Info("Exported blockchain to", "file", fn) return nil } +func exportChain(chain *core.BlockChain, writer io.Writer, first, last uint64) error { + if first > last { + return fmt.Errorf("export failed: first (%d) is greater than last (%d)", first, last) + } + var parentHash common.Hash + for number := first; number <= last; number++ { + block := chain.GetBlockByNumber(number) + if block == nil { + return fmt.Errorf("export failed on #%d: not found", number) + } + if number > first && block.ParentHash() != parentHash { + return errors.New("export failed: chain reorg during export") + } + parentHash = block.Hash() + if err := rlp.Encode(writer, makeExportBlock(block)); err != nil { + return err + } + } + return nil +} + // ExportHistory exports blockchain history into the specified directory, // following the Era format. func ExportHistory(bc *core.BlockChain, dir string, first, last uint64, newBuilder func(io.Writer) era.Builder, filename func(network string, epoch int, lastBlockHash common.Hash) string) error { diff --git a/cmd/utils/export_test.go b/cmd/utils/export_test.go index d1a004d9b7..3cc427b809 100644 --- a/cmd/utils/export_test.go +++ b/cmd/utils/export_test.go @@ -18,12 +18,17 @@ package utils import ( "fmt" + "math/big" "os" + "reflect" "strings" "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/rlp" ) @@ -38,6 +43,37 @@ func TestExportGzip(t *testing.T) { testExport(t, f) } +func TestExportBlockAccessList(t *testing.T) { + accessList := &bal.BlockAccessList{{Address: common.HexToAddress("0x1234")}} + block := types.NewBlockWithHeader(&types.Header{Number: big.NewInt(1)}).WithAccessListUnsafe(accessList) + encoded, err := rlp.EncodeToBytes(makeExportBlock(block)) + if err != nil { + t.Fatal(err) + } + var decoded exportBlock + if err := rlp.DecodeBytes(encoded, &decoded); err != nil { + t.Fatal(err) + } + if got := decoded.block().AccessList(); !reflect.DeepEqual(got, accessList) { + t.Fatalf("access list = %#v, want %#v", got, accessList) + } +} + +func TestImportLegacyBlock(t *testing.T) { + block := types.NewBlockWithHeader(&types.Header{Number: big.NewInt(1)}) + encoded, err := rlp.EncodeToBytes(block) + if err != nil { + t.Fatal(err) + } + var decoded exportBlock + if err := rlp.DecodeBytes(encoded, &decoded); err != nil { + t.Fatal(err) + } + if got := decoded.block().AccessList(); got != nil { + t.Fatalf("access list = %#v, want nil", got) + } +} + type testIterator struct { index int }