mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 13:21:37 +00:00
core/rawdb/eradb: integrate ere backend
This commit is contained in:
parent
17aab1ac9a
commit
bf0094bf43
4 changed files with 195 additions and 46 deletions
|
|
@ -14,7 +14,7 @@
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
// 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/>.
|
// 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 implements a history backend using era1 or ere files.
|
||||||
package eradb
|
package eradb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/lru"
|
"github.com/ethereum/go-ethereum/common/lru"
|
||||||
"github.com/ethereum/go-ethereum/internal/era"
|
"github.com/ethereum/go-ethereum/internal/era"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/era/execdb"
|
||||||
"github.com/ethereum/go-ethereum/internal/era/onedb"
|
"github.com/ethereum/go-ethereum/internal/era/onedb"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
|
|
@ -36,7 +37,7 @@ const openFileLimit = 64
|
||||||
|
|
||||||
var errClosed = errors.New("era store is closed")
|
var errClosed = errors.New("era store is closed")
|
||||||
|
|
||||||
// Store manages read access to a directory of era1 files.
|
// Store manages read access to a directory of era1 or ere files.
|
||||||
// The getter methods are thread-safe.
|
// The getter methods are thread-safe.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
datadir string
|
datadir string
|
||||||
|
|
@ -52,7 +53,8 @@ type Store struct {
|
||||||
type fileCacheEntry struct {
|
type fileCacheEntry struct {
|
||||||
refcount int // reference count. This is protected by Store.mu!
|
refcount int // reference count. This is protected by Store.mu!
|
||||||
opened chan struct{} // signals opening of file has completed
|
opened chan struct{} // signals opening of file has completed
|
||||||
file *onedb.Era // the file
|
file era.Era // the file
|
||||||
|
slim bool // true if receipts are stored in the ere slim encoding
|
||||||
err error // error from opening the file
|
err error // error from opening the file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,7 +79,7 @@ func New(datadir string) (*Store, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes all open era1 files in the cache.
|
// Close closes all open era files in the cache.
|
||||||
func (db *Store) Close() {
|
func (db *Store) Close() {
|
||||||
db.mu.Lock()
|
db.mu.Lock()
|
||||||
defer db.mu.Unlock()
|
defer db.mu.Unlock()
|
||||||
|
|
@ -132,12 +134,14 @@ func (db *Store) GetRawReceipts(number uint64) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return convertReceipts(data)
|
return convertReceipts(data, entry.slim)
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertReceipts transforms an encoded block receipts list from the format
|
// convertReceipts transforms an encoded block receipts list into the 'storage'
|
||||||
// used by era1 into the 'storage' format used by the go-ethereum ancients database.
|
// format used by the go-ethereum ancients database, i.e. a list of
|
||||||
func convertReceipts(input []byte) ([]byte, error) {
|
// [status, gas-used, logs]. The input uses the era1 network encoding, or the
|
||||||
|
// ere slim encoding when slim is true.
|
||||||
|
func convertReceipts(input []byte, slim bool) ([]byte, error) {
|
||||||
var (
|
var (
|
||||||
out bytes.Buffer
|
out bytes.Buffer
|
||||||
enc = rlp.NewEncoderBuffer(&out)
|
enc = rlp.NewEncoderBuffer(&out)
|
||||||
|
|
@ -148,32 +152,41 @@ func convertReceipts(input []byte) ([]byte, error) {
|
||||||
}
|
}
|
||||||
outerList := enc.List()
|
outerList := enc.List()
|
||||||
for i := 0; blockListIter.Next(); i++ {
|
for i := 0; blockListIter.Next(); i++ {
|
||||||
kind, content, _, err := rlp.Split(blockListIter.Value())
|
var (
|
||||||
if err != nil {
|
receiptData []byte
|
||||||
return nil, fmt.Errorf("receipt %d invalid: %v", i, err)
|
skip int
|
||||||
}
|
)
|
||||||
var receiptData []byte
|
if slim {
|
||||||
switch kind {
|
// Slim receipt is [tx-type, status, gas-used, logs]: skip the tx-type.
|
||||||
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()
|
receiptData = blockListIter.Value()
|
||||||
|
skip = 0
|
||||||
|
} else {
|
||||||
|
// Era1 receipt is [status, gas-used, bloom, logs], prefixed by the
|
||||||
|
// tx type if non-legacy: skip the bloom.
|
||||||
|
kind, content, _, err := rlp.Split(blockListIter.Value())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("receipt %d invalid: %v", i, err)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
skip = 2
|
||||||
}
|
}
|
||||||
// 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)
|
dataIter, err := rlp.NewListIterator(receiptData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("receipt %d has invalid data: %v", i, err)
|
return nil, fmt.Errorf("receipt %d has invalid data: %v", i, err)
|
||||||
}
|
}
|
||||||
innerList := enc.List()
|
innerList := enc.List()
|
||||||
for field := 0; dataIter.Next(); field++ {
|
for field := 0; dataIter.Next(); field++ {
|
||||||
if field == 2 {
|
if field == skip {
|
||||||
continue // skip bloom
|
continue
|
||||||
}
|
}
|
||||||
enc.Write(dataIter.Value())
|
enc.Write(dataIter.Value())
|
||||||
}
|
}
|
||||||
|
|
@ -202,11 +215,11 @@ func (db *Store) getEraByEpoch(epoch uint64) *fileCacheEntry {
|
||||||
|
|
||||||
case fileIsNew:
|
case fileIsNew:
|
||||||
// Open the file and put it into the cache.
|
// Open the file and put it into the cache.
|
||||||
e, err := db.openEraFile(epoch)
|
e, slim, err := db.openEraFile(epoch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.fileFailedToOpen(epoch, entry, err)
|
db.fileFailedToOpen(epoch, entry, err)
|
||||||
} else {
|
} else {
|
||||||
db.fileOpened(epoch, entry, e)
|
db.fileOpened(epoch, entry, e, slim)
|
||||||
}
|
}
|
||||||
close(entry.opened)
|
close(entry.opened)
|
||||||
|
|
||||||
|
|
@ -250,7 +263,7 @@ func (db *Store) getCacheEntry(epoch uint64) (stat fileCacheStatus, entry *fileC
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileOpened is called after an era file has been successfully opened.
|
// fileOpened is called after an era file has been successfully opened.
|
||||||
func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *onedb.Era) {
|
func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file era.Era, slim bool) {
|
||||||
db.mu.Lock()
|
db.mu.Lock()
|
||||||
defer db.mu.Unlock()
|
defer db.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -267,6 +280,7 @@ func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *onedb.Era
|
||||||
|
|
||||||
// Add it to the LRU. This may evict an existing item, which we have to close.
|
// Add it to the LRU. This may evict an existing item, which we have to close.
|
||||||
entry.file = file
|
entry.file = file
|
||||||
|
entry.slim = slim
|
||||||
evictedEpoch, evictedEntry, _ := db.lru.Add3(epoch, entry)
|
evictedEpoch, evictedEntry, _ := db.lru.Add3(epoch, entry)
|
||||||
if evictedEntry != nil {
|
if evictedEntry != nil {
|
||||||
evictedEntry.derefAndClose(evictedEpoch)
|
evictedEntry.derefAndClose(evictedEpoch)
|
||||||
|
|
@ -283,32 +297,61 @@ func (db *Store) fileFailedToOpen(epoch uint64, entry *fileCacheEntry, err error
|
||||||
entry.err = err
|
entry.err = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Store) openEraFile(epoch uint64) (*onedb.Era, error) {
|
// openEraFile opens the era file of the given epoch. The second return value
|
||||||
// File name scheme is <network>-<epoch>-<root>.
|
// signals whether the receipts in the file use the ere slim encoding.
|
||||||
glob := fmt.Sprintf("*-%05d-*.era1", epoch)
|
func (db *Store) openEraFile(epoch uint64) (era.Era, bool, error) {
|
||||||
matches, err := filepath.Glob(filepath.Join(db.datadir, glob))
|
// File name scheme is <network>-<epoch>-<root> for era1 files and
|
||||||
if err != nil {
|
// <network>-<epoch>-<root>(-<profile>)* for ere files.
|
||||||
return nil, err
|
var matches []string
|
||||||
|
for _, glob := range []string{
|
||||||
|
fmt.Sprintf("*-%05d-*.era1", epoch),
|
||||||
|
fmt.Sprintf("*-%05d-*.ere", epoch),
|
||||||
|
} {
|
||||||
|
m, err := filepath.Glob(filepath.Join(db.datadir, glob))
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
matches = append(matches, m...)
|
||||||
}
|
}
|
||||||
if len(matches) > 1 {
|
if len(matches) > 1 {
|
||||||
return nil, fmt.Errorf("multiple era1 files found for epoch %d", epoch)
|
return nil, false, fmt.Errorf("multiple era files found for epoch %d: %v", epoch, matches)
|
||||||
}
|
}
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
return nil, fs.ErrNotExist
|
return nil, false, fs.ErrNotExist
|
||||||
}
|
}
|
||||||
filename := matches[0]
|
filename := matches[0]
|
||||||
|
|
||||||
e, err := onedb.Open(filename)
|
var (
|
||||||
if err != nil {
|
e era.Era
|
||||||
return nil, err
|
err error
|
||||||
|
slim = filepath.Ext(filename) == ".ere"
|
||||||
|
)
|
||||||
|
if slim {
|
||||||
|
var f *execdb.Era
|
||||||
|
f, err = execdb.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
// Files written with the "noreceipts" profile cannot serve as a
|
||||||
|
// history backend, since the receipts cannot be retrieved.
|
||||||
|
if !f.HasReceipts() {
|
||||||
|
f.Close()
|
||||||
|
return nil, false, fmt.Errorf("ere file %s contains no receipts", filepath.Base(filename))
|
||||||
|
}
|
||||||
|
e = f
|
||||||
|
} else {
|
||||||
|
e, err = onedb.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Sanity-check start block.
|
// Sanity-check start block.
|
||||||
if e.Start()%uint64(era.MaxSize) != 0 {
|
if e.Start() != epoch*uint64(era.MaxSize) {
|
||||||
e.Close()
|
e.Close()
|
||||||
return nil, fmt.Errorf("pre-merge era1 file has invalid boundary. %d %% %d != 0", e.Start(), era.MaxSize)
|
return nil, false, fmt.Errorf("era file %s has wrong start block %d for epoch %d", filepath.Base(filename), e.Start(), epoch)
|
||||||
}
|
}
|
||||||
log.Debug("Opened era1 file", "epoch", epoch)
|
log.Debug("Opened era file", "epoch", epoch, "name", filepath.Base(filename))
|
||||||
return e.(*onedb.Era), nil
|
return e, slim, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// doneWithFile signals that the caller has finished using a file.
|
// doneWithFile signals that the caller has finished using a file.
|
||||||
|
|
@ -339,9 +382,9 @@ func (entry *fileCacheEntry) derefAndClose(epoch uint64) (closed bool) {
|
||||||
|
|
||||||
closeErr := entry.file.Close()
|
closeErr := entry.file.Close()
|
||||||
if closeErr == nil {
|
if closeErr == nil {
|
||||||
log.Debug("Closed era1 file", "epoch", epoch)
|
log.Debug("Closed era file", "epoch", epoch)
|
||||||
} else {
|
} else {
|
||||||
log.Warn("Error closing era1 file", "epoch", epoch, "err", closeErr)
|
log.Warn("Error closing era file", "epoch", epoch, "err", closeErr)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,15 @@
|
||||||
package eradb
|
package eradb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/era/execdb"
|
||||||
|
"github.com/ethereum/go-ethereum/internal/era/onedb"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
@ -48,6 +53,101 @@ func TestEraDatabase(t *testing.T) {
|
||||||
assert.Equal(t, 3, len(receipts), "receipts length mismatch")
|
assert.Equal(t, 3, len(receipts), "receipts length mismatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestEreDatabase checks that the store can serve bodies and receipts from a
|
||||||
|
// directory of ere files, and that the receipts returned are byte-identical to
|
||||||
|
// the ones derived from the equivalent era1 files.
|
||||||
|
func TestEreDatabase(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
convertEra1ToEre(t, "testdata/sepolia-00000-643a00f7.era1", dir, "sepolia", 0)
|
||||||
|
convertEra1ToEre(t, "testdata/sepolia-00021-b8814b14.era1", dir, "sepolia", 21)
|
||||||
|
|
||||||
|
db, err := New(dir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
r, err := db.GetRawBody(175881)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var body *types.Body
|
||||||
|
err = rlp.DecodeBytes(r, &body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, body, "block body not found")
|
||||||
|
assert.Equal(t, 3, len(body.Transactions))
|
||||||
|
|
||||||
|
r, err = db.GetRawReceipts(175881)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var receipts []*types.ReceiptForStorage
|
||||||
|
err = rlp.DecodeBytes(r, &receipts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, receipts, "receipts not found")
|
||||||
|
assert.Equal(t, 3, len(receipts), "receipts length mismatch")
|
||||||
|
|
||||||
|
// Cross-check against the era1 store: both backends must return the same
|
||||||
|
// storage encoding.
|
||||||
|
eraDB, err := New("testdata")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer eraDB.Close()
|
||||||
|
for _, num := range []uint64{0, 1024, 172032, 175881, 180223} {
|
||||||
|
want, err := eraDB.GetRawReceipts(num)
|
||||||
|
require.NoError(t, err)
|
||||||
|
got, err := db.GetRawReceipts(num)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got, "receipts mismatch at block %d", num)
|
||||||
|
|
||||||
|
wantBody, err := eraDB.GetRawBody(num)
|
||||||
|
require.NoError(t, err)
|
||||||
|
gotBody, err := db.GetRawBody(num)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, wantBody, gotBody, "body mismatch at block %d", num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEreDatabaseNoReceipts checks that ere files written with the
|
||||||
|
// "noreceipts" profile are rejected by the store. The testdata fixture is a
|
||||||
|
// minimal single-block ere file whose index has no receipts component.
|
||||||
|
func TestEreDatabaseNoReceipts(t *testing.T) {
|
||||||
|
db, err := New("testdata/noreceipts")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
_, err = db.GetRawBody(0)
|
||||||
|
require.ErrorContains(t, err, "no receipts")
|
||||||
|
_, err = db.GetRawReceipts(0)
|
||||||
|
require.ErrorContains(t, err, "no receipts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertEra1ToEre reads an era1 file and writes its contents as an ere file
|
||||||
|
// into dir, using the canonical ere file name.
|
||||||
|
func convertEra1ToEre(t *testing.T, era1Path, dir, network string, epoch int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
e, err := onedb.Open(era1Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer e.Close()
|
||||||
|
|
||||||
|
f, err := os.CreateTemp(dir, "ere-convert-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
builder := execdb.NewBuilder(f)
|
||||||
|
td, err := e.InitialTD()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
it, err := e.Iterator()
|
||||||
|
require.NoError(t, err)
|
||||||
|
for it.Next() {
|
||||||
|
block, receipts, err := it.BlockAndReceipts()
|
||||||
|
require.NoError(t, err)
|
||||||
|
td.Add(td, block.Difficulty())
|
||||||
|
require.NoError(t, builder.Add(block, receipts, new(big.Int).Set(td)))
|
||||||
|
}
|
||||||
|
require.NoError(t, it.Error())
|
||||||
|
|
||||||
|
lastHash, err := builder.Finalize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, f.Close())
|
||||||
|
require.NoError(t, os.Rename(f.Name(), filepath.Join(dir, execdb.Filename(network, epoch, lastHash))))
|
||||||
|
}
|
||||||
|
|
||||||
func TestEraDatabaseConcurrentOpen(t *testing.T) {
|
func TestEraDatabaseConcurrentOpen(t *testing.T) {
|
||||||
db, err := New("testdata")
|
db, err := New("testdata")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
||||||
BIN
core/rawdb/eradb/testdata/noreceipts/sepolia-00000-deadbeef-noreceipts.ere
vendored
Normal file
BIN
core/rawdb/eradb/testdata/noreceipts/sepolia-00000-deadbeef-noreceipts.ere
vendored
Normal file
Binary file not shown.
|
|
@ -204,6 +204,12 @@ func (e *Era) HasComponent(c componentType) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasReceipts reports whether the file contains a receipts component. Files
|
||||||
|
// written with the "noreceipts" profile omit it.
|
||||||
|
func (e *Era) HasReceipts() bool {
|
||||||
|
return e.HasComponent(receipts)
|
||||||
|
}
|
||||||
|
|
||||||
// InitialTD returns initial total difficulty before the difficulty of the
|
// 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
|
// first block of the Era is applied. Returns an error if TD is not available
|
||||||
// (e.g., post-merge epoch).
|
// (e.g., post-merge epoch).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue