go-ethereum/core/rawdb/eradb/eradb.go
shazam8253 c9b7ae422c
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run
internal/era: New EraE implementation (#32157)
Here is a draft for the New EraE implementation. The code follows along
with the spec listed at https://hackmd.io/pIZlxnitSciV5wUgW6W20w.

---------

Co-authored-by: shantichanal <158101918+shantichanal@users.noreply.github.com>
Co-authored-by: lightclient <lightclient@protonmail.com>
Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
2026-02-09 08:30:19 -07:00

347 lines
9.7 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 eradb implements a history backend using era1 files.
package eradb
import (
"bytes"
"errors"
"fmt"
"io/fs"
"path/filepath"
"sync"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/internal/era"
"github.com/ethereum/go-ethereum/internal/era/onedb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
)
const openFileLimit = 64
var errClosed = errors.New("era store is closed")
// Store manages read access to a directory of era1 files.
// The getter methods are thread-safe.
type Store struct {
datadir string
// The mutex protects all remaining fields.
mu sync.Mutex
cond *sync.Cond
lru lru.BasicLRU[uint64, *fileCacheEntry]
opening map[uint64]*fileCacheEntry
closing bool
}
type fileCacheEntry struct {
refcount int // reference count. This is protected by Store.mu!
opened chan struct{} // signals opening of file has completed
file *onedb.Era // the file
err error // error from opening the file
}
type fileCacheStatus byte
const (
storeClosing fileCacheStatus = iota
fileIsNew
fileIsOpening
fileIsCached
)
// New opens the store directory.
func New(datadir string) (*Store, error) {
db := &Store{
datadir: datadir,
lru: lru.NewBasicLRU[uint64, *fileCacheEntry](openFileLimit),
opening: make(map[uint64]*fileCacheEntry),
}
db.cond = sync.NewCond(&db.mu)
log.Info("Opened Era store", "datadir", datadir)
return db, nil
}
// Close closes all open era1 files in the cache.
func (db *Store) Close() {
db.mu.Lock()
defer db.mu.Unlock()
// Prevent new cache additions.
db.closing = true
// Deref all active files. Since inactive files have a refcount of one, they will be
// closed right here and now after decrementing. Files which are currently being used
// have a refcount > 1 and will hit zero when their access finishes.
for _, epoch := range db.lru.Keys() {
entry, _ := db.lru.Peek(epoch)
if entry.derefAndClose(epoch) {
db.lru.Remove(epoch)
}
}
// Wait for all store access to finish.
for db.lru.Len() > 0 || len(db.opening) > 0 {
db.cond.Wait()
}
}
// GetRawBody returns the raw body for a given block number.
func (db *Store) GetRawBody(number uint64) ([]byte, error) {
epoch := number / uint64(era.MaxSize)
entry := db.getEraByEpoch(epoch)
if entry.err != nil {
if errors.Is(entry.err, fs.ErrNotExist) {
return nil, nil
}
return nil, entry.err
}
defer db.doneWithFile(epoch, entry)
return entry.file.GetRawBodyByNumber(number)
}
// GetRawReceipts returns the raw receipts for a given block number.
func (db *Store) GetRawReceipts(number uint64) ([]byte, error) {
epoch := number / uint64(era.MaxSize)
entry := db.getEraByEpoch(epoch)
if entry.err != nil {
if errors.Is(entry.err, fs.ErrNotExist) {
return nil, nil
}
return nil, entry.err
}
defer db.doneWithFile(epoch, entry)
data, err := entry.file.GetRawReceiptsByNumber(number)
if err != nil {
return nil, err
}
return convertReceipts(data)
}
// convertReceipts transforms an encoded block receipts list from the format
// used by era1 into the 'storage' format used by the go-ethereum ancients database.
func convertReceipts(input []byte) ([]byte, error) {
var (
out bytes.Buffer
enc = rlp.NewEncoderBuffer(&out)
)
blockListIter, err := rlp.NewListIterator(input)
if err != nil {
return nil, fmt.Errorf("invalid block receipts list: %v", err)
}
outerList := enc.List()
for i := 0; blockListIter.Next(); i++ {
kind, content, _, err := rlp.Split(blockListIter.Value())
if err != nil {
return nil, fmt.Errorf("receipt %d invalid: %v", i, err)
}
var receiptData []byte
switch kind {
case rlp.Byte:
return nil, fmt.Errorf("receipt %d is single byte", i)
case rlp.String:
// Typed receipt - skip type.
receiptData = content[1:]
case rlp.List:
// Legacy receipt
receiptData = blockListIter.Value()
}
// Convert data list.
// Input is [status, gas-used, bloom, logs]
// Output is [status, gas-used, logs], i.e. we need to skip the bloom.
dataIter, err := rlp.NewListIterator(receiptData)
if err != nil {
return nil, fmt.Errorf("receipt %d has invalid data: %v", i, err)
}
innerList := enc.List()
for field := 0; dataIter.Next(); field++ {
if field == 2 {
continue // skip bloom
}
enc.Write(dataIter.Value())
}
enc.ListEnd(innerList)
if dataIter.Err() != nil {
return nil, fmt.Errorf("receipt %d iterator error: %v", i, dataIter.Err())
}
}
enc.ListEnd(outerList)
if blockListIter.Err() != nil {
return nil, fmt.Errorf("block receipt list iterator error: %v", blockListIter.Err())
}
enc.Flush()
return out.Bytes(), nil
}
// getEraByEpoch opens an era file or gets it from the cache.
// The caller can freely access the returned entry's .file and .err
// db.doneWithFile must be called when it is done reading the file.
func (db *Store) getEraByEpoch(epoch uint64) *fileCacheEntry {
stat, entry := db.getCacheEntry(epoch)
switch stat {
case storeClosing:
return &fileCacheEntry{err: errClosed}
case fileIsNew:
// Open the file and put it into the cache.
e, err := db.openEraFile(epoch)
if err != nil {
db.fileFailedToOpen(epoch, entry, err)
} else {
db.fileOpened(epoch, entry, e)
}
close(entry.opened)
case fileIsOpening:
// Wait for open to finish.
<-entry.opened
case fileIsCached:
// Nothing to do.
default:
panic(fmt.Sprintf("invalid file state %d", stat))
}
return entry
}
// getCacheEntry gets an open era file from the cache.
func (db *Store) getCacheEntry(epoch uint64) (stat fileCacheStatus, entry *fileCacheEntry) {
db.mu.Lock()
defer db.mu.Unlock()
if db.closing {
return storeClosing, nil
}
if entry = db.opening[epoch]; entry != nil {
stat = fileIsOpening
} else if entry, _ = db.lru.Get(epoch); entry != nil {
stat = fileIsCached
} else {
// It's a new file, create an entry in the opening table. Note the entry is
// created with an initial refcount of one. We increment the count once more
// before returning, but the count will return to one when the file has been
// accessed. When the store is closed or the file gets evicted from the cache,
// refcount will be decreased by one, thus allowing it to hit zero.
entry = &fileCacheEntry{refcount: 1, opened: make(chan struct{})}
db.opening[epoch] = entry
stat = fileIsNew
}
entry.refcount++
return stat, entry
}
// fileOpened is called after an era file has been successfully opened.
func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *onedb.Era) {
db.mu.Lock()
defer db.mu.Unlock()
delete(db.opening, epoch)
db.cond.Signal() // db.opening was modified
// The database may have been closed while opening the file. When that happens, we
// need to close the file here, since it isn't tracked by the LRU yet.
if db.closing {
entry.err = errClosed
file.Close()
return
}
// Add it to the LRU. This may evict an existing item, which we have to close.
entry.file = file
evictedEpoch, evictedEntry, _ := db.lru.Add3(epoch, entry)
if evictedEntry != nil {
evictedEntry.derefAndClose(evictedEpoch)
}
}
// fileFailedToOpen is called when an era file could not be opened.
func (db *Store) fileFailedToOpen(epoch uint64, entry *fileCacheEntry, err error) {
db.mu.Lock()
defer db.mu.Unlock()
delete(db.opening, epoch)
db.cond.Signal() // db.opening was modified
entry.err = err
}
func (db *Store) openEraFile(epoch uint64) (*onedb.Era, error) {
// File name scheme is <network>-<epoch>-<root>.
glob := fmt.Sprintf("*-%05d-*.era1", epoch)
matches, err := filepath.Glob(filepath.Join(db.datadir, glob))
if err != nil {
return nil, err
}
if len(matches) > 1 {
return nil, fmt.Errorf("multiple era1 files found for epoch %d", epoch)
}
if len(matches) == 0 {
return nil, fs.ErrNotExist
}
filename := matches[0]
e, err := onedb.Open(filename)
if err != nil {
return nil, err
}
// Sanity-check start block.
if e.Start()%uint64(era.MaxSize) != 0 {
e.Close()
return nil, fmt.Errorf("pre-merge era1 file has invalid boundary. %d %% %d != 0", e.Start(), era.MaxSize)
}
log.Debug("Opened era1 file", "epoch", epoch)
return e.(*onedb.Era), nil
}
// doneWithFile signals that the caller has finished using a file.
// This decrements the refcount and ensures the file is closed by the last user.
func (db *Store) doneWithFile(epoch uint64, entry *fileCacheEntry) {
db.mu.Lock()
defer db.mu.Unlock()
if entry.err != nil {
return
}
if entry.derefAndClose(epoch) {
// Delete closed entry from LRU if it is still present.
if e, _ := db.lru.Peek(epoch); e == entry {
db.lru.Remove(epoch)
db.cond.Signal() // db.lru was modified
}
}
}
// derefAndClose decrements the reference counter and closes the file
// when it hits zero.
func (entry *fileCacheEntry) derefAndClose(epoch uint64) (closed bool) {
entry.refcount--
if entry.refcount > 0 {
return false
}
closeErr := entry.file.Close()
if closeErr == nil {
log.Debug("Closed era1 file", "epoch", epoch)
} else {
log.Warn("Error closing era1 file", "epoch", epoch, "err", closeErr)
}
return true
}