mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
507 lines
15 KiB
Go
507 lines
15 KiB
Go
// Copyright 2025 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package execdb
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/internal/era"
|
|
"github.com/ethereum/go-ethereum/internal/era/e2store"
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
"github.com/golang/snappy"
|
|
)
|
|
|
|
func TestEre(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
start uint64
|
|
preMerge int
|
|
postMerge int
|
|
accumulator bool // whether accumulator should exist
|
|
}{
|
|
{
|
|
name: "pre-merge",
|
|
start: 0,
|
|
preMerge: 128,
|
|
postMerge: 0,
|
|
accumulator: true,
|
|
},
|
|
{
|
|
name: "post-merge",
|
|
start: 0,
|
|
preMerge: 0,
|
|
postMerge: 64,
|
|
accumulator: false,
|
|
},
|
|
{
|
|
name: "transition",
|
|
start: 0,
|
|
preMerge: 32,
|
|
postMerge: 32,
|
|
accumulator: true,
|
|
},
|
|
{
|
|
name: "non-zero-start",
|
|
start: 8192,
|
|
preMerge: 64,
|
|
postMerge: 0,
|
|
accumulator: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "ere-test")
|
|
if err != nil {
|
|
t.Fatalf("error creating temp file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// Build test data.
|
|
type blockData struct {
|
|
header, body, receipts []byte
|
|
hash common.Hash
|
|
td *big.Int
|
|
difficulty *big.Int
|
|
}
|
|
var (
|
|
builder = NewBuilder(f)
|
|
blocks []blockData
|
|
totalBlocks = tt.preMerge + tt.postMerge
|
|
finalTD = big.NewInt(int64(tt.preMerge))
|
|
)
|
|
|
|
// Add pre-merge blocks.
|
|
for i := 0; i < tt.preMerge; i++ {
|
|
num := tt.start + uint64(i)
|
|
blk := blockData{
|
|
header: mustEncode(&types.Header{Number: big.NewInt(int64(num)), Difficulty: big.NewInt(1)}),
|
|
body: mustEncode(&types.Body{Transactions: []*types.Transaction{types.NewTransaction(0, common.Address{byte(i)}, nil, 0, nil, nil)}}),
|
|
receipts: mustEncode([]types.SlimReceipt{{CumulativeGasUsed: uint64(i)}}),
|
|
hash: common.Hash{byte(i)},
|
|
td: big.NewInt(int64(i + 1)),
|
|
difficulty: big.NewInt(1),
|
|
}
|
|
blocks = append(blocks, blk)
|
|
if err := builder.AddRLP(blk.header, blk.body, blk.receipts, num, blk.hash, blk.td, blk.difficulty); err != nil {
|
|
t.Fatalf("error adding pre-merge block %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Add post-merge blocks.
|
|
for i := 0; i < tt.postMerge; i++ {
|
|
idx := tt.preMerge + i
|
|
num := tt.start + uint64(idx)
|
|
blk := blockData{
|
|
header: mustEncode(&types.Header{Number: big.NewInt(int64(num)), Difficulty: big.NewInt(0)}),
|
|
body: mustEncode(&types.Body{}),
|
|
receipts: mustEncode([]types.SlimReceipt{}),
|
|
hash: common.Hash{byte(idx)},
|
|
difficulty: big.NewInt(0),
|
|
}
|
|
blocks = append(blocks, blk)
|
|
if err := builder.AddRLP(blk.header, blk.body, blk.receipts, num, blk.hash, nil, big.NewInt(0)); err != nil {
|
|
t.Fatalf("error adding post-merge block %d: %v", idx, err)
|
|
}
|
|
}
|
|
|
|
// Finalize and check return values.
|
|
epochID, err := builder.Finalize()
|
|
if err != nil {
|
|
t.Fatalf("error finalizing: %v", err)
|
|
}
|
|
|
|
// Verify epoch ID is always the last block hash.
|
|
expectedLastHash := blocks[len(blocks)-1].hash
|
|
if epochID != expectedLastHash {
|
|
t.Fatalf("wrong epoch ID: want %s, got %s", expectedLastHash.Hex(), epochID.Hex())
|
|
}
|
|
|
|
// Verify accumulator presence.
|
|
if tt.accumulator {
|
|
if builder.Accumulator() == nil {
|
|
t.Fatal("expected non-nil accumulator")
|
|
}
|
|
} else {
|
|
if builder.Accumulator() != nil {
|
|
t.Fatalf("expected nil accumulator, got %s", builder.Accumulator().Hex())
|
|
}
|
|
}
|
|
|
|
// Open and verify the era file.
|
|
e, err := Open(f.Name())
|
|
if err != nil {
|
|
t.Fatalf("failed to open era: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
// Verify metadata.
|
|
if e.Start() != tt.start {
|
|
t.Fatalf("wrong start block: want %d, got %d", tt.start, e.Start())
|
|
}
|
|
if e.Count() != uint64(totalBlocks) {
|
|
t.Fatalf("wrong block count: want %d, got %d", totalBlocks, e.Count())
|
|
}
|
|
// Verify the layout detected from on-disk type tags. Header,
|
|
// body, and receipts are always present; TD is only present
|
|
// when the epoch contains pre-merge blocks.
|
|
if !e.HasComponent(header) || !e.HasComponent(body) || !e.HasComponent(receipts) {
|
|
t.Fatalf("missing required component in layout %v", e.m.layout)
|
|
}
|
|
if got, want := e.HasComponent(td), tt.preMerge > 0; got != want {
|
|
t.Fatalf("td component presence mismatch: want %v, got %v", want, got)
|
|
}
|
|
if e.HasComponent(proof) {
|
|
t.Fatalf("proof component should not be present in layout %v", e.m.layout)
|
|
}
|
|
|
|
// Verify accumulator in file.
|
|
if tt.accumulator {
|
|
accRoot, err := e.Accumulator()
|
|
if err != nil {
|
|
t.Fatalf("error getting accumulator: %v", err)
|
|
}
|
|
if accRoot != *builder.Accumulator() {
|
|
t.Fatalf("accumulator mismatch: builder has %s, file contains %s",
|
|
builder.Accumulator().Hex(), accRoot.Hex())
|
|
}
|
|
} else {
|
|
if _, err := e.Accumulator(); err == nil {
|
|
t.Fatal("expected error when reading accumulator from post-merge epoch")
|
|
}
|
|
}
|
|
|
|
// Verify blocks via raw iterator.
|
|
it, err := NewRawIterator(e)
|
|
if err != nil {
|
|
t.Fatalf("failed to make iterator: %v", err)
|
|
}
|
|
for i := 0; i < totalBlocks; i++ {
|
|
if !it.Next() {
|
|
t.Fatalf("expected more entries at %d", i)
|
|
}
|
|
if it.Error() != nil {
|
|
t.Fatalf("unexpected error: %v", it.Error())
|
|
}
|
|
|
|
// Check header.
|
|
rawHeader, err := io.ReadAll(it.Header)
|
|
if err != nil {
|
|
t.Fatalf("error reading header: %v", err)
|
|
}
|
|
if !bytes.Equal(rawHeader, blocks[i].header) {
|
|
t.Fatalf("mismatched header at %d", i)
|
|
}
|
|
|
|
// Check body.
|
|
rawBody, err := io.ReadAll(it.Body)
|
|
if err != nil {
|
|
t.Fatalf("error reading body: %v", err)
|
|
}
|
|
if !bytes.Equal(rawBody, blocks[i].body) {
|
|
t.Fatalf("mismatched body at %d", i)
|
|
}
|
|
|
|
// Check receipts.
|
|
rawReceipts, err := io.ReadAll(it.Receipts)
|
|
if err != nil {
|
|
t.Fatalf("error reading receipts: %v", err)
|
|
}
|
|
if !bytes.Equal(rawReceipts, blocks[i].receipts) {
|
|
t.Fatalf("mismatched receipts at %d", i)
|
|
}
|
|
|
|
// Check TD (only for epochs that have TD stored).
|
|
if tt.preMerge > 0 && it.TotalDifficulty != nil {
|
|
rawTd, err := io.ReadAll(it.TotalDifficulty)
|
|
if err != nil {
|
|
t.Fatalf("error reading TD: %v", err)
|
|
}
|
|
slices.Reverse(rawTd)
|
|
td := new(big.Int).SetBytes(rawTd)
|
|
var expectedTD *big.Int
|
|
if i < tt.preMerge {
|
|
expectedTD = blocks[i].td
|
|
} else {
|
|
// Post-merge blocks in transition epoch use final TD.
|
|
expectedTD = finalTD
|
|
}
|
|
if td.Cmp(expectedTD) != 0 {
|
|
t.Fatalf("mismatched TD at %d: want %s, got %s", i, expectedTD, td)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify random access.
|
|
for _, blockNum := range []uint64{tt.start, tt.start + uint64(totalBlocks) - 1} {
|
|
blk, err := e.GetBlockByNumber(blockNum)
|
|
if err != nil {
|
|
t.Fatalf("error getting block %d: %v", blockNum, err)
|
|
}
|
|
if blk.Number().Uint64() != blockNum {
|
|
t.Fatalf("wrong block number: want %d, got %d", blockNum, blk.Number().Uint64())
|
|
}
|
|
}
|
|
|
|
// Verify out-of-range access fails.
|
|
if _, err := e.GetBlockByNumber(tt.start + uint64(totalBlocks)); err == nil {
|
|
t.Fatal("expected error for out-of-range block")
|
|
}
|
|
if tt.start > 0 {
|
|
if _, err := e.GetBlockByNumber(tt.start - 1); err == nil {
|
|
t.Fatal("expected error for block before start")
|
|
}
|
|
}
|
|
|
|
// Verify high-level iterator.
|
|
hlIt, err := e.Iterator()
|
|
if err != nil {
|
|
t.Fatalf("failed to create iterator: %v", err)
|
|
}
|
|
count := 0
|
|
for hlIt.Next() {
|
|
blk, err := hlIt.Block()
|
|
if err != nil {
|
|
t.Fatalf("error getting block: %v", err)
|
|
}
|
|
if blk.Number().Uint64() != tt.start+uint64(count) {
|
|
t.Fatalf("wrong block number: want %d, got %d", tt.start+uint64(count), blk.Number().Uint64())
|
|
}
|
|
count++
|
|
}
|
|
if hlIt.Error() != nil {
|
|
t.Fatalf("iterator error: %v", hlIt.Error())
|
|
}
|
|
if count != totalBlocks {
|
|
t.Fatalf("wrong iteration count: want %d, got %d", totalBlocks, count)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInitialTD tests the InitialTD calculation separately since it requires
|
|
// specific TD/difficulty values.
|
|
func TestInitialTD(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "ere-initial-td-test")
|
|
if err != nil {
|
|
t.Fatalf("error creating temp file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
builder := NewBuilder(f)
|
|
|
|
// First block: difficulty=5, TD=10, so initial TD = 10-5 = 5.
|
|
header := mustEncode(&types.Header{Number: big.NewInt(0), Difficulty: big.NewInt(5)})
|
|
body := mustEncode(&types.Body{})
|
|
receipts := mustEncode([]types.SlimReceipt{})
|
|
|
|
if err := builder.AddRLP(header, body, receipts, 0, common.Hash{0}, big.NewInt(10), big.NewInt(5)); err != nil {
|
|
t.Fatalf("error adding block: %v", err)
|
|
}
|
|
|
|
// Second block: difficulty=3, TD=13.
|
|
header2 := mustEncode(&types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(3)})
|
|
if err := builder.AddRLP(header2, body, receipts, 1, common.Hash{1}, big.NewInt(13), big.NewInt(3)); err != nil {
|
|
t.Fatalf("error adding block: %v", err)
|
|
}
|
|
|
|
if _, err := builder.Finalize(); err != nil {
|
|
t.Fatalf("error finalizing: %v", err)
|
|
}
|
|
|
|
e, err := Open(f.Name())
|
|
if err != nil {
|
|
t.Fatalf("failed to open era: %v", err)
|
|
}
|
|
defer e.Close()
|
|
|
|
initialTD, err := e.InitialTD()
|
|
if err != nil {
|
|
t.Fatalf("error getting initial TD: %v", err)
|
|
}
|
|
|
|
// Initial TD should be TD[0] - Difficulty[0] = 10 - 5 = 5.
|
|
if initialTD.Cmp(big.NewInt(5)) != 0 {
|
|
t.Fatalf("wrong initial TD: want 5, got %s", initialTD)
|
|
}
|
|
}
|
|
|
|
// TestDetectLayoutNoReceipts hand-builds an Ere file with the receipts slot
|
|
// replaced by a TotalDifficulty entry (the on-disk shape a "noreceipts"
|
|
// profile would take) and verifies the reader detects this from the e2store
|
|
// type tags rather than misreading TD as receipts. This is the core safety
|
|
// property of detectLayout — exercise it via From, where no filename is
|
|
// available.
|
|
func TestDetectLayoutNoReceipts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "synthetic.ere")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
w := e2store.NewWriter(f)
|
|
written := uint64(0)
|
|
writeEntry := func(typ uint16, data []byte) {
|
|
n, err := w.Write(typ, data)
|
|
if err != nil {
|
|
t.Fatalf("write type 0x%04x: %v", typ, err)
|
|
}
|
|
written += uint64(n)
|
|
}
|
|
|
|
var snappyBuf bytes.Buffer
|
|
writeSnappy := func(typ uint16, data []byte) {
|
|
snappyBuf.Reset()
|
|
sw := snappy.NewBufferedWriter(&snappyBuf)
|
|
if _, err := sw.Write(data); err != nil {
|
|
t.Fatalf("snappy write: %v", err)
|
|
}
|
|
if err := sw.Flush(); err != nil {
|
|
t.Fatalf("snappy flush: %v", err)
|
|
}
|
|
writeEntry(typ, snappyBuf.Bytes())
|
|
}
|
|
|
|
// Version
|
|
writeEntry(era.TypeVersion, nil)
|
|
|
|
// Block 0 components in order: header, body, td (no receipts).
|
|
headerBytes := mustEncode(&types.Header{Number: big.NewInt(0), Difficulty: big.NewInt(1)})
|
|
bodyBytes := mustEncode(&types.Body{})
|
|
tdLE := make([]byte, 32) // uint256(1) little-endian
|
|
tdLE[0] = 1
|
|
|
|
headerOff := written
|
|
writeSnappy(era.TypeCompressedHeader, headerBytes)
|
|
bodyOff := written
|
|
writeSnappy(era.TypeCompressedBody, bodyBytes)
|
|
tdOff := written
|
|
writeEntry(era.TypeTotalDifficulty, tdLE)
|
|
|
|
// Build the DynamicBlockIndex with 3 components per block, 1 block, and
|
|
// the third slot pointing at the TD entry rather than at receipts.
|
|
base := int64(written)
|
|
relative := func(absolute uint64) uint64 { return uint64(int64(absolute) - base) }
|
|
|
|
var indexBuf bytes.Buffer
|
|
writeU64 := func(v uint64) {
|
|
if err := binary.Write(&indexBuf, binary.LittleEndian, v); err != nil {
|
|
t.Fatalf("index write: %v", err)
|
|
}
|
|
}
|
|
writeU64(0) // starting block number
|
|
writeU64(relative(headerOff))
|
|
writeU64(relative(bodyOff))
|
|
writeU64(relative(tdOff))
|
|
writeU64(3) // component count
|
|
writeU64(1) // block count
|
|
writeEntry(era.TypeDynamicBlockIndex, indexBuf.Bytes())
|
|
|
|
if err := f.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
// Open via From — no filename is consulted, so the layout map is the
|
|
// only line of defence.
|
|
g, err := os.Open(path)
|
|
if err != nil {
|
|
t.Fatalf("reopen: %v", err)
|
|
}
|
|
t.Cleanup(func() { g.Close() })
|
|
e, err := From(g)
|
|
if err != nil {
|
|
t.Fatalf("From: %v", err)
|
|
}
|
|
defer e.Close()
|
|
ere := e.(*Era)
|
|
|
|
if ere.HasComponent(receipts) {
|
|
t.Errorf("receipts should not be reported as present in synthetic noreceipts file")
|
|
}
|
|
if !ere.HasComponent(td) {
|
|
t.Errorf("td should be reported as present")
|
|
}
|
|
if got, want := ere.m.layout[td], 2; got != want {
|
|
t.Errorf("td slot: want %d, got %d", want, got)
|
|
}
|
|
|
|
// Reading receipts must fail loudly, not silently decode TD bytes.
|
|
if _, err := ere.GetRawReceiptsByNumber(0); err == nil {
|
|
t.Error("expected error when reading receipts from a noreceipts file")
|
|
}
|
|
}
|
|
|
|
// TestOpenRejectsNoreceiptsProfile verifies that Open() refuses to decode an
|
|
// Ere file whose filename declares the unsupported "noreceipts" profile. This
|
|
// is the defence-in-depth filename check; structural safety is provided by
|
|
// detectLayout (covered separately by TestDetectLayoutNoReceipts).
|
|
func TestOpenRejectsNoreceiptsProfile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
|
|
// Build a valid Ere file with default-profile contents directly at the
|
|
// noreceipts path so Open() rejects it on the filename alone.
|
|
path := filepath.Join(dir, "mainnet-00000-deadbeef-noreceipts.ere")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("create file: %v", err)
|
|
}
|
|
builder := NewBuilder(f)
|
|
header := mustEncode(&types.Header{Number: big.NewInt(0), Difficulty: big.NewInt(1)})
|
|
body := mustEncode(&types.Body{})
|
|
receipts := mustEncode([]types.SlimReceipt{})
|
|
if err := builder.AddRLP(header, body, receipts, 0, common.Hash{0}, big.NewInt(1), big.NewInt(1)); err != nil {
|
|
t.Fatalf("AddRLP: %v", err)
|
|
}
|
|
if _, err := builder.Finalize(); err != nil {
|
|
t.Fatalf("Finalize: %v", err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
if _, err := Open(path); err == nil {
|
|
t.Fatal("expected Open to reject noreceipts profile")
|
|
}
|
|
}
|
|
|
|
func mustEncode(obj any) []byte {
|
|
b, err := rlp.EncodeToBytes(obj)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to encode obj: %v", err))
|
|
}
|
|
return b
|
|
}
|