mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-03 13:38:39 +00:00
reject noreceipts, fixes & test
This commit is contained in:
parent
77d19df307
commit
8f4f38002f
2 changed files with 109 additions and 5 deletions
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -165,6 +166,15 @@ func TestEre(t *testing.T) {
|
||||||
if e.Count() != uint64(totalBlocks) {
|
if e.Count() != uint64(totalBlocks) {
|
||||||
t.Fatalf("wrong block count: want %d, got %d", totalBlocks, e.Count())
|
t.Fatalf("wrong block count: want %d, got %d", totalBlocks, e.Count())
|
||||||
}
|
}
|
||||||
|
// Verify component count: 4 when TD is stored (pre-merge or
|
||||||
|
// transition), 3 otherwise (pure post-merge).
|
||||||
|
wantComponents := uint64(3)
|
||||||
|
if tt.preMerge > 0 {
|
||||||
|
wantComponents = 4
|
||||||
|
}
|
||||||
|
if e.m.components != wantComponents {
|
||||||
|
t.Fatalf("wrong component count: want %d, got %d", wantComponents, e.m.components)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify accumulator in file.
|
// Verify accumulator in file.
|
||||||
if tt.accumulator {
|
if tt.accumulator {
|
||||||
|
|
@ -339,6 +349,43 @@ func TestInitialTD(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestOpenRejectsNoreceiptsProfile verifies that Open() refuses to decode an
|
||||||
|
// Ere file whose filename declares the unsupported "noreceipts" profile. The
|
||||||
|
// positional reader can't safely interpret such a file because TD would be
|
||||||
|
// shifted into the receipts slot.
|
||||||
|
func TestOpenRejectsNoreceiptsProfile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// Build a valid Ere file with default-profile contents, then rename it
|
||||||
|
// to claim a noreceipts profile in its filename.
|
||||||
|
src, err := os.CreateTemp(dir, "ere-src-*.ere")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
builder := NewBuilder(src)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
renamed := filepath.Join(dir, "mainnet-00000-deadbeef-noreceipts.ere")
|
||||||
|
if err := os.Rename(src.Name(), renamed); err != nil {
|
||||||
|
t.Fatalf("rename: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := Open(renamed); err == nil {
|
||||||
|
t.Fatal("expected Open to reject noreceipts profile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustEncode(obj any) []byte {
|
func mustEncode(obj any) []byte {
|
||||||
b, err := rlp.EncodeToBytes(obj)
|
b, err := rlp.EncodeToBytes(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
|
@ -48,8 +50,14 @@ func Filename(network string, epoch int, lastBlockHash common.Hash) string {
|
||||||
return fmt.Sprintf("%s-%05d-%s-noproofs.ere", network, epoch, lastBlockHash.Hex()[2:10])
|
return fmt.Sprintf("%s-%05d-%s-noproofs.ere", network, epoch, lastBlockHash.Hex()[2:10])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open accesses the era file.
|
// Open accesses the era file. The path is used to parse the profile postfix
|
||||||
|
// (per the Ere spec filename convention); files written with the "noreceipts"
|
||||||
|
// profile are rejected because the positional index reader assumes receipts
|
||||||
|
// are present.
|
||||||
func Open(path string) (*Era, error) {
|
func Open(path string) (*Era, error) {
|
||||||
|
if err := checkProfile(filepath.Base(path)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -59,6 +67,10 @@ func Open(path string) (*Era, error) {
|
||||||
f.Close()
|
f.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := e.checkComponents(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,16 +84,56 @@ func (e *Era) Close() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// From returns an Era backed by f.
|
// From returns an Era backed by f. Since no filename is available, the profile
|
||||||
|
// cannot be inspected; the component count is still validated against the
|
||||||
|
// supported layouts (header, body, receipts, [td]).
|
||||||
func From(f era.ReadAtSeekCloser) (era.Era, error) {
|
func From(f era.ReadAtSeekCloser) (era.Era, error) {
|
||||||
e := &Era{f: f, s: e2store.NewReader(f)}
|
e := &Era{f: f, s: e2store.NewReader(f)}
|
||||||
if err := e.loadIndex(); err != nil {
|
if err := e.loadIndex(); err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := e.checkComponents(); err != nil {
|
||||||
|
f.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkProfile inspects the profile postfix(es) in an Ere filename and rejects
|
||||||
|
// any combination this reader can't safely decode. The reader maps components
|
||||||
|
// by fixed positions (header, body, receipts, td?, proof?), so a file written
|
||||||
|
// with the "noreceipts" profile would silently shift TD into the receipts slot.
|
||||||
|
//
|
||||||
|
// The Ere format itself does not require a particular filename, so this check
|
||||||
|
// is permissive about non-conforming names: validation only kicks in when a
|
||||||
|
// profile postfix is actually present.
|
||||||
|
func checkProfile(name string) error {
|
||||||
|
name = strings.TrimSuffix(name, ".ere")
|
||||||
|
parts := strings.Split(name, "-")
|
||||||
|
if len(parts) <= 3 {
|
||||||
|
return nil // no profile postfix to validate
|
||||||
|
}
|
||||||
|
for _, p := range parts[3:] {
|
||||||
|
if p == "noreceipts" {
|
||||||
|
return fmt.Errorf("Ere file %q uses the noreceipts profile, which is not supported", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkComponents verifies the file's component count matches what this reader
|
||||||
|
// supports. The reader assumes the fixed positional layout
|
||||||
|
// (header, body, receipts, td?, proof?), and the builder in this package only
|
||||||
|
// produces files with 3 (post-merge) or 4 (pre-merge / transition) components.
|
||||||
|
// Files with 2 (noreceipts) or 5 (proofs present) components are rejected.
|
||||||
|
func (e *Era) checkComponents() error {
|
||||||
|
if e.m.components < 3 || e.m.components > 4 {
|
||||||
|
return fmt.Errorf("unsupported Ere component count %d (reader expects header, body, receipts, and optional total difficulty)", e.m.components)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Start retrieves the starting block number.
|
// Start retrieves the starting block number.
|
||||||
func (e *Era) Start() uint64 {
|
func (e *Era) Start() uint64 {
|
||||||
return e.m.start
|
return e.m.start
|
||||||
|
|
@ -213,8 +265,8 @@ func (e *Era) InitialTD() (*big.Int, error) {
|
||||||
return new(big.Int).Sub(firstTD, header.Difficulty), nil
|
return new(big.Int).Sub(firstTD, header.Difficulty), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulator reads the accumulator entry in the Ere file if it exists.
|
// Accumulator reads the accumulator entry if present. Only pre-merge and
|
||||||
// Only pre-merge and merge-transition Ere files contain an accumulator entry.
|
// merge-transition Ere files contain one.
|
||||||
func (e *Era) Accumulator() (common.Hash, error) {
|
func (e *Era) Accumulator() (common.Hash, error) {
|
||||||
entry, err := e.s.Find(era.TypeAccumulator)
|
entry, err := e.s.Find(era.TypeAccumulator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -289,7 +341,12 @@ type metadata struct {
|
||||||
// componentType represents the integer form of a specific type that can be present in the era file.
|
// componentType represents the integer form of a specific type that can be present in the era file.
|
||||||
type componentType int
|
type componentType int
|
||||||
|
|
||||||
// header, body, receipts, td, and proof are the different types of components that can be present in the era file.
|
// header, body, receipts, td, and proof are the different types of components
|
||||||
|
// that can be present in the era file. The Ere spec defines receipts, td, and
|
||||||
|
// proof as independently optional, but this reader maps components to their
|
||||||
|
// position in the index using this fixed enum. That positional mapping is only
|
||||||
|
// safe as long as receipts are present (no "noreceipts" profile) — Open() and
|
||||||
|
// From() enforce this via checkProfile and checkComponents.
|
||||||
const (
|
const (
|
||||||
header componentType = iota
|
header componentType = iota
|
||||||
body
|
body
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue