mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
411 lines
13 KiB
Go
411 lines
13 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 (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"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/klauspost/compress/snappy"
|
|
)
|
|
|
|
// Era object represents an era file that contains blocks and their components.
|
|
type Era struct {
|
|
f era.ReadAtSeekCloser
|
|
s *e2store.Reader
|
|
m metadata // metadata for the Era file
|
|
}
|
|
|
|
// Filename returns a recognizable filename for an Ere file.
|
|
// The filename uses the last block hash to uniquely identify the epoch's content.
|
|
//
|
|
// Files produced by this builder do not include Proof entries, so the
|
|
// "noproofs" profile postfix is appended per the Ere spec.
|
|
func Filename(network string, epoch int, lastBlockHash common.Hash) string {
|
|
return fmt.Sprintf("%s-%05d-%s-noproofs.ere", network, epoch, lastBlockHash.Hex()[2:10])
|
|
}
|
|
|
|
// Open accesses the era file at the given path. The basename is used to parse
|
|
// the profile postfix (per the Ere spec filename convention) as a defence-in-
|
|
// depth check; structural safety is enforced by detectLayout, which reads the
|
|
// e2store type tag at each index slot rather than trusting position.
|
|
func Open(path string) (*Era, error) {
|
|
if err := checkProfile(filepath.Base(path)); err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e, err := from(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
// Close closes the era file safely.
|
|
func (e *Era) Close() error {
|
|
if e.f == nil {
|
|
return nil
|
|
}
|
|
err := e.f.Close()
|
|
e.f = nil
|
|
return err
|
|
}
|
|
|
|
// From returns an Era backed by f. Component layout is derived from the
|
|
// e2store type tags stored in the file itself, so callers do not need to
|
|
// supply a filename or profile.
|
|
func From(f era.ReadAtSeekCloser) (era.Era, error) {
|
|
e, err := from(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func from(f era.ReadAtSeekCloser) (*Era, error) {
|
|
e := &Era{f: f, s: e2store.NewReader(f)}
|
|
if err := e.loadIndex(); err != nil {
|
|
return nil, err
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
// checkProfile inspects the profile postfix(es) in an Ere filename and rejects
|
|
// any combination this reader doesn't support. This is a best-effort, defence-
|
|
// in-depth check; the authoritative layout detection happens in detectLayout
|
|
// from the on-disk type tags.
|
|
//
|
|
// 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
|
|
}
|
|
|
|
// Start retrieves the starting block number.
|
|
func (e *Era) Start() uint64 {
|
|
return e.m.start
|
|
}
|
|
|
|
// Count retrieves the count of blocks present.
|
|
func (e *Era) Count() uint64 {
|
|
return e.m.count
|
|
}
|
|
|
|
// Iterator returns an iterator over the era file.
|
|
func (e *Era) Iterator() (era.Iterator, error) {
|
|
return NewIterator(e)
|
|
}
|
|
|
|
// GetBlockByNumber retrieves the block if present within the era file.
|
|
func (e *Era) GetBlockByNumber(blockNum uint64) (*types.Block, error) {
|
|
h, err := e.GetHeader(blockNum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := e.GetBody(blockNum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return types.NewBlockWithHeader(h).WithBody(*b), nil
|
|
}
|
|
|
|
// GetHeader retrieves the header from the era file through the cached offset table.
|
|
func (e *Era) GetHeader(num uint64) (*types.Header, error) {
|
|
off, err := e.headerOff(num)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, _, err := e.s.ReaderAt(era.TypeCompressedHeader, off)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r = snappy.NewReader(r)
|
|
var h types.Header
|
|
return &h, rlp.Decode(r, &h)
|
|
}
|
|
|
|
// GetBody retrieves the body from the era file through cached offset table.
|
|
func (e *Era) GetBody(num uint64) (*types.Body, error) {
|
|
off, err := e.bodyOff(num)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, _, err := e.s.ReaderAt(era.TypeCompressedBody, off)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r = snappy.NewReader(r)
|
|
var b types.Body
|
|
return &b, rlp.Decode(r, &b)
|
|
}
|
|
|
|
// GetTD retrieves the td from the era file through cached offset table.
|
|
func (e *Era) GetTD(blockNum uint64) (*big.Int, error) {
|
|
off, err := e.tdOff(blockNum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, _, err := e.s.ReaderAt(era.TypeTotalDifficulty, off)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf, _ := io.ReadAll(r)
|
|
slices.Reverse(buf)
|
|
td := new(big.Int).SetBytes(buf)
|
|
return td, nil
|
|
}
|
|
|
|
// GetRawBodyByNumber returns the RLP-encoded body for the given block number.
|
|
func (e *Era) GetRawBodyByNumber(blockNum uint64) ([]byte, error) {
|
|
off, err := e.bodyOff(blockNum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, _, err := e.s.ReaderAt(era.TypeCompressedBody, off)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r = snappy.NewReader(r)
|
|
return io.ReadAll(r)
|
|
}
|
|
|
|
// GetRawReceiptsByNumber returns the RLP-encoded receipts for the given block number.
|
|
func (e *Era) GetRawReceiptsByNumber(blockNum uint64) ([]byte, error) {
|
|
off, err := e.receiptOff(blockNum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, _, err := e.s.ReaderAt(era.TypeCompressedSlimReceipts, off)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r = snappy.NewReader(r)
|
|
return io.ReadAll(r)
|
|
}
|
|
|
|
// HasComponent reports whether the given component is recorded in the file's
|
|
// index, as detected from the on-disk e2store type tags.
|
|
func (e *Era) HasComponent(c componentType) bool {
|
|
_, ok := e.m.layout[c]
|
|
return ok
|
|
}
|
|
|
|
// InitialTD returns initial total difficulty before the difficulty of the
|
|
// first block of the Era is applied. Returns an error if TD is not available
|
|
// (e.g., post-merge epoch).
|
|
func (e *Era) InitialTD() (*big.Int, error) {
|
|
// Check if TD component exists.
|
|
if !e.HasComponent(td) {
|
|
return nil, fmt.Errorf("total difficulty not available in this epoch")
|
|
}
|
|
|
|
// Get first header to read its difficulty.
|
|
header, err := e.GetHeader(e.m.start)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read first header: %w", err)
|
|
}
|
|
|
|
// Get TD after first block using the index.
|
|
firstTD, err := e.GetTD(e.m.start)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read first TD: %w", err)
|
|
}
|
|
|
|
// Initial TD = TD[0] - Difficulty[0]
|
|
return new(big.Int).Sub(firstTD, header.Difficulty), nil
|
|
}
|
|
|
|
// Accumulator reads the accumulator entry if present. Only pre-merge and
|
|
// merge-transition Ere files contain one.
|
|
func (e *Era) Accumulator() (common.Hash, error) {
|
|
entry, err := e.s.Find(era.TypeAccumulator)
|
|
if err != nil {
|
|
return common.Hash{}, err
|
|
}
|
|
return common.BytesToHash(entry.Value), nil
|
|
}
|
|
|
|
// loadIndex loads in the index table trailer (start, count, component-count)
|
|
// and then derives the component→slot layout from the on-disk type tags.
|
|
func (e *Era) loadIndex() error {
|
|
var err error
|
|
e.m.length, err = e.f.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b := make([]byte, 16)
|
|
if _, err = e.f.ReadAt(b, e.m.length-16); err != nil {
|
|
return err
|
|
}
|
|
e.m.components = binary.LittleEndian.Uint64(b[0:8])
|
|
e.m.count = binary.LittleEndian.Uint64(b[8:16])
|
|
|
|
payloadlen := 8 + 8*e.m.count*e.m.components + 16 // 8 for start block, 8 per property per block, 16 for the number of properties and the number of blocks
|
|
tlvstart := e.m.length - int64(payloadlen) - 8
|
|
_, err = e.f.ReadAt(b[:8], tlvstart+8)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.m.start = binary.LittleEndian.Uint64(b[:8])
|
|
|
|
layout, err := e.detectLayout()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.m.layout = layout
|
|
return nil
|
|
}
|
|
|
|
// detectLayout reads the e2store type tag at each component slot of the first
|
|
// block and builds a componentType→slot map. This makes the reader robust
|
|
// against profile variations: receipts, td, and proof can appear in any
|
|
// supported subset, and the slot positions are looked up by tag.
|
|
func (e *Era) detectLayout() (map[componentType]int, error) {
|
|
if e.m.count == 0 {
|
|
return nil, errors.New("Ere file contains no blocks")
|
|
}
|
|
tagToComponent := map[uint16]componentType{
|
|
era.TypeCompressedHeader: header,
|
|
era.TypeCompressedBody: body,
|
|
era.TypeCompressedSlimReceipts: receipts,
|
|
era.TypeTotalDifficulty: td,
|
|
era.TypeProof: proof,
|
|
}
|
|
layout := make(map[componentType]int, e.m.components)
|
|
for slot := 0; slot < int(e.m.components); slot++ {
|
|
off, err := e.slotOffset(0, slot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read slot %d offset: %w", slot, err)
|
|
}
|
|
typ, _, err := e.s.ReadMetadataAt(off)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read slot %d type tag: %w", slot, err)
|
|
}
|
|
comp, ok := tagToComponent[typ]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown e2store type 0x%04x at index slot %d", typ, slot)
|
|
}
|
|
if existing, dup := layout[comp]; dup {
|
|
return nil, fmt.Errorf("duplicate component %d at slots %d and %d", comp, existing, slot)
|
|
}
|
|
layout[comp] = slot
|
|
}
|
|
if _, ok := layout[header]; !ok {
|
|
return nil, errors.New("Ere index has no header component")
|
|
}
|
|
if _, ok := layout[body]; !ok {
|
|
return nil, errors.New("Ere index has no body component")
|
|
}
|
|
return layout, nil
|
|
}
|
|
|
|
// slotOffset returns the absolute file offset of the entry at the given slot
|
|
// of the given block index (0 = first block in file). It does no validation
|
|
// against the layout map and is intended for use by detectLayout and
|
|
// indexOffset.
|
|
func (e *Era) slotOffset(blockIdx uint64, slot int) (int64, error) {
|
|
payloadlen := 8 + 8*e.m.count*e.m.components + 16
|
|
indstart := e.m.length - int64(payloadlen) - 8
|
|
|
|
rec := blockIdx*e.m.components + uint64(slot)
|
|
pos := indstart + 8 + 8 + int64(rec*8)
|
|
|
|
var buf [8]byte
|
|
if _, err := e.f.ReadAt(buf[:], pos); err != nil {
|
|
return 0, err
|
|
}
|
|
rel := binary.LittleEndian.Uint64(buf[:])
|
|
return int64(rel) + indstart, nil
|
|
}
|
|
|
|
// headerOff, bodyOff, receiptOff, and tdOff return the offsets of the respective components for a given block number.
|
|
func (e *Era) headerOff(num uint64) (int64, error) { return e.indexOffset(num, header) }
|
|
func (e *Era) bodyOff(num uint64) (int64, error) { return e.indexOffset(num, body) }
|
|
func (e *Era) receiptOff(num uint64) (int64, error) { return e.indexOffset(num, receipts) }
|
|
func (e *Era) tdOff(num uint64) (int64, error) { return e.indexOffset(num, td) }
|
|
|
|
// indexOffset calculates offset to a certain component for a block number
|
|
// within a file. The slot is resolved through the layout map detected at
|
|
// Open time, so files with optional components in any order are handled
|
|
// safely regardless of the on-disk position.
|
|
func (e *Era) indexOffset(n uint64, component componentType) (int64, error) {
|
|
if n < e.m.start || n >= e.m.start+e.m.count {
|
|
return 0, fmt.Errorf("block %d out of range [%d,%d)", n, e.m.start, e.m.start+e.m.count)
|
|
}
|
|
slot, ok := e.m.layout[component]
|
|
if !ok {
|
|
return 0, fmt.Errorf("component %d not present in this Ere file", component)
|
|
}
|
|
return e.slotOffset(n-e.m.start, slot)
|
|
}
|
|
|
|
// metadata contains the information about the era file that is written into the file.
|
|
type metadata struct {
|
|
start uint64 // start block number
|
|
count uint64 // number of blocks in the era
|
|
components uint64 // number of slots per block in the index
|
|
layout map[componentType]int // component → slot index, derived from on-disk type tags
|
|
length int64 // length of the file in bytes
|
|
}
|
|
|
|
// componentType identifies a kind of per-block entry (header, body, etc.).
|
|
type componentType int
|
|
|
|
// The Ere spec defines receipts, td, and proof as independently optional. The
|
|
// reader resolves a component to its actual slot via the metadata.layout map,
|
|
// which is built at Open time from the e2store type tag of each slot — so the
|
|
// position of a component within the index is never assumed.
|
|
const (
|
|
header componentType = iota
|
|
body
|
|
receipts
|
|
td
|
|
proof
|
|
)
|