github.com/ethereum/go-ethereum@v1.16.1/core/rawdb/eradb/eradb.go (about)

     1  // Copyright 2025 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // Package eradb implements a history backend using era1 files.
    18  package eradb
    19  
    20  import (
    21  	"bytes"
    22  	"errors"
    23  	"fmt"
    24  	"io/fs"
    25  	"path/filepath"
    26  	"sync"
    27  
    28  	"github.com/ethereum/go-ethereum/common/lru"
    29  	"github.com/ethereum/go-ethereum/internal/era"
    30  	"github.com/ethereum/go-ethereum/log"
    31  	"github.com/ethereum/go-ethereum/rlp"
    32  )
    33  
    34  const openFileLimit = 64
    35  
    36  var errClosed = errors.New("era store is closed")
    37  
    38  // Store manages read access to a directory of era1 files.
    39  // The getter methods are thread-safe.
    40  type Store struct {
    41  	datadir string
    42  
    43  	// The mutex protects all remaining fields.
    44  	mu      sync.Mutex
    45  	cond    *sync.Cond
    46  	lru     lru.BasicLRU[uint64, *fileCacheEntry]
    47  	opening map[uint64]*fileCacheEntry
    48  	closing bool
    49  }
    50  
    51  type fileCacheEntry struct {
    52  	refcount int           // reference count. This is protected by Store.mu!
    53  	opened   chan struct{} // signals opening of file has completed
    54  	file     *era.Era      // the file
    55  	err      error         // error from opening the file
    56  }
    57  
    58  type fileCacheStatus byte
    59  
    60  const (
    61  	storeClosing fileCacheStatus = iota
    62  	fileIsNew
    63  	fileIsOpening
    64  	fileIsCached
    65  )
    66  
    67  // New opens the store directory.
    68  func New(datadir string) (*Store, error) {
    69  	db := &Store{
    70  		datadir: datadir,
    71  		lru:     lru.NewBasicLRU[uint64, *fileCacheEntry](openFileLimit),
    72  		opening: make(map[uint64]*fileCacheEntry),
    73  	}
    74  	db.cond = sync.NewCond(&db.mu)
    75  	log.Info("Opened Era store", "datadir", datadir)
    76  	return db, nil
    77  }
    78  
    79  // Close closes all open era1 files in the cache.
    80  func (db *Store) Close() {
    81  	db.mu.Lock()
    82  	defer db.mu.Unlock()
    83  
    84  	// Prevent new cache additions.
    85  	db.closing = true
    86  
    87  	// Deref all active files. Since inactive files have a refcount of one, they will be
    88  	// closed right here and now after decrementing. Files which are currently being used
    89  	// have a refcount > 1 and will hit zero when their access finishes.
    90  	for _, epoch := range db.lru.Keys() {
    91  		entry, _ := db.lru.Peek(epoch)
    92  		if entry.derefAndClose(epoch) {
    93  			db.lru.Remove(epoch)
    94  		}
    95  	}
    96  
    97  	// Wait for all store access to finish.
    98  	for db.lru.Len() > 0 || len(db.opening) > 0 {
    99  		db.cond.Wait()
   100  	}
   101  }
   102  
   103  // GetRawBody returns the raw body for a given block number.
   104  func (db *Store) GetRawBody(number uint64) ([]byte, error) {
   105  	epoch := number / uint64(era.MaxEra1Size)
   106  	entry := db.getEraByEpoch(epoch)
   107  	if entry.err != nil {
   108  		if errors.Is(entry.err, fs.ErrNotExist) {
   109  			return nil, nil
   110  		}
   111  		return nil, entry.err
   112  	}
   113  	defer db.doneWithFile(epoch, entry)
   114  
   115  	return entry.file.GetRawBodyByNumber(number)
   116  }
   117  
   118  // GetRawReceipts returns the raw receipts for a given block number.
   119  func (db *Store) GetRawReceipts(number uint64) ([]byte, error) {
   120  	epoch := number / uint64(era.MaxEra1Size)
   121  	entry := db.getEraByEpoch(epoch)
   122  	if entry.err != nil {
   123  		if errors.Is(entry.err, fs.ErrNotExist) {
   124  			return nil, nil
   125  		}
   126  		return nil, entry.err
   127  	}
   128  	defer db.doneWithFile(epoch, entry)
   129  
   130  	data, err := entry.file.GetRawReceiptsByNumber(number)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	return convertReceipts(data)
   135  }
   136  
   137  // convertReceipts transforms an encoded block receipts list from the format
   138  // used by era1 into the 'storage' format used by the go-ethereum ancients database.
   139  func convertReceipts(input []byte) ([]byte, error) {
   140  	var (
   141  		out bytes.Buffer
   142  		enc = rlp.NewEncoderBuffer(&out)
   143  	)
   144  	blockListIter, err := rlp.NewListIterator(input)
   145  	if err != nil {
   146  		return nil, fmt.Errorf("invalid block receipts list: %v", err)
   147  	}
   148  	outerList := enc.List()
   149  	for i := 0; blockListIter.Next(); i++ {
   150  		kind, content, _, err := rlp.Split(blockListIter.Value())
   151  		if err != nil {
   152  			return nil, fmt.Errorf("receipt %d invalid: %v", i, err)
   153  		}
   154  		var receiptData []byte
   155  		switch kind {
   156  		case rlp.Byte:
   157  			return nil, fmt.Errorf("receipt %d is single byte", i)
   158  		case rlp.String:
   159  			// Typed receipt - skip type.
   160  			receiptData = content[1:]
   161  		case rlp.List:
   162  			// Legacy receipt
   163  			receiptData = blockListIter.Value()
   164  		}
   165  		// Convert data list.
   166  		// Input is  [status, gas-used, bloom, logs]
   167  		// Output is [status, gas-used, logs], i.e. we need to skip the bloom.
   168  		dataIter, err := rlp.NewListIterator(receiptData)
   169  		if err != nil {
   170  			return nil, fmt.Errorf("receipt %d has invalid data: %v", i, err)
   171  		}
   172  		innerList := enc.List()
   173  		for field := 0; dataIter.Next(); field++ {
   174  			if field == 2 {
   175  				continue // skip bloom
   176  			}
   177  			enc.Write(dataIter.Value())
   178  		}
   179  		enc.ListEnd(innerList)
   180  		if dataIter.Err() != nil {
   181  			return nil, fmt.Errorf("receipt %d iterator error: %v", i, dataIter.Err())
   182  		}
   183  	}
   184  	enc.ListEnd(outerList)
   185  	if blockListIter.Err() != nil {
   186  		return nil, fmt.Errorf("block receipt list iterator error: %v", blockListIter.Err())
   187  	}
   188  	enc.Flush()
   189  	return out.Bytes(), nil
   190  }
   191  
   192  // getEraByEpoch opens an era file or gets it from the cache.
   193  // The caller can freely access the returned entry's .file and .err
   194  // db.doneWithFile must be called when it is done reading the file.
   195  func (db *Store) getEraByEpoch(epoch uint64) *fileCacheEntry {
   196  	stat, entry := db.getCacheEntry(epoch)
   197  
   198  	switch stat {
   199  	case storeClosing:
   200  		return &fileCacheEntry{err: errClosed}
   201  
   202  	case fileIsNew:
   203  		// Open the file and put it into the cache.
   204  		e, err := db.openEraFile(epoch)
   205  		if err != nil {
   206  			db.fileFailedToOpen(epoch, entry, err)
   207  		} else {
   208  			db.fileOpened(epoch, entry, e)
   209  		}
   210  		close(entry.opened)
   211  
   212  	case fileIsOpening:
   213  		// Wait for open to finish.
   214  		<-entry.opened
   215  
   216  	case fileIsCached:
   217  		// Nothing to do.
   218  
   219  	default:
   220  		panic(fmt.Sprintf("invalid file state %d", stat))
   221  	}
   222  	return entry
   223  }
   224  
   225  // getCacheEntry gets an open era file from the cache.
   226  func (db *Store) getCacheEntry(epoch uint64) (stat fileCacheStatus, entry *fileCacheEntry) {
   227  	db.mu.Lock()
   228  	defer db.mu.Unlock()
   229  
   230  	if db.closing {
   231  		return storeClosing, nil
   232  	}
   233  	if entry = db.opening[epoch]; entry != nil {
   234  		stat = fileIsOpening
   235  	} else if entry, _ = db.lru.Get(epoch); entry != nil {
   236  		stat = fileIsCached
   237  	} else {
   238  		// It's a new file, create an entry in the opening table. Note the entry is
   239  		// created with an initial refcount of one. We increment the count once more
   240  		// before returning, but the count will return to one when the file has been
   241  		// accessed. When the store is closed or the file gets evicted from the cache,
   242  		// refcount will be decreased by one, thus allowing it to hit zero.
   243  		entry = &fileCacheEntry{refcount: 1, opened: make(chan struct{})}
   244  		db.opening[epoch] = entry
   245  		stat = fileIsNew
   246  	}
   247  	entry.refcount++
   248  	return stat, entry
   249  }
   250  
   251  // fileOpened is called after an era file has been successfully opened.
   252  func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *era.Era) {
   253  	db.mu.Lock()
   254  	defer db.mu.Unlock()
   255  
   256  	delete(db.opening, epoch)
   257  	db.cond.Signal() // db.opening was modified
   258  
   259  	// The database may have been closed while opening the file. When that happens, we
   260  	// need to close the file here, since it isn't tracked by the LRU yet.
   261  	if db.closing {
   262  		entry.err = errClosed
   263  		file.Close()
   264  		return
   265  	}
   266  
   267  	// Add it to the LRU. This may evict an existing item, which we have to close.
   268  	entry.file = file
   269  	evictedEpoch, evictedEntry, _ := db.lru.Add3(epoch, entry)
   270  	if evictedEntry != nil {
   271  		evictedEntry.derefAndClose(evictedEpoch)
   272  	}
   273  }
   274  
   275  // fileFailedToOpen is called when an era file could not be opened.
   276  func (db *Store) fileFailedToOpen(epoch uint64, entry *fileCacheEntry, err error) {
   277  	db.mu.Lock()
   278  	defer db.mu.Unlock()
   279  
   280  	delete(db.opening, epoch)
   281  	db.cond.Signal() // db.opening was modified
   282  	entry.err = err
   283  }
   284  
   285  func (db *Store) openEraFile(epoch uint64) (*era.Era, error) {
   286  	// File name scheme is <network>-<epoch>-<root>.
   287  	glob := fmt.Sprintf("*-%05d-*.era1", epoch)
   288  	matches, err := filepath.Glob(filepath.Join(db.datadir, glob))
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	if len(matches) > 1 {
   293  		return nil, fmt.Errorf("multiple era1 files found for epoch %d", epoch)
   294  	}
   295  	if len(matches) == 0 {
   296  		return nil, fs.ErrNotExist
   297  	}
   298  	filename := matches[0]
   299  
   300  	e, err := era.Open(filename)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	// Sanity-check start block.
   305  	if e.Start()%uint64(era.MaxEra1Size) != 0 {
   306  		return nil, fmt.Errorf("pre-merge era1 file has invalid boundary. %d %% %d != 0", e.Start(), era.MaxEra1Size)
   307  	}
   308  	log.Debug("Opened era1 file", "epoch", epoch)
   309  	return e, nil
   310  }
   311  
   312  // doneWithFile signals that the caller has finished using a file.
   313  // This decrements the refcount and ensures the file is closed by the last user.
   314  func (db *Store) doneWithFile(epoch uint64, entry *fileCacheEntry) {
   315  	db.mu.Lock()
   316  	defer db.mu.Unlock()
   317  
   318  	if entry.err != nil {
   319  		return
   320  	}
   321  	if entry.derefAndClose(epoch) {
   322  		// Delete closed entry from LRU if it is still present.
   323  		if e, _ := db.lru.Peek(epoch); e == entry {
   324  			db.lru.Remove(epoch)
   325  			db.cond.Signal() // db.lru was modified
   326  		}
   327  	}
   328  }
   329  
   330  // derefAndClose decrements the reference counter and closes the file
   331  // when it hits zero.
   332  func (entry *fileCacheEntry) derefAndClose(epoch uint64) (closed bool) {
   333  	entry.refcount--
   334  	if entry.refcount > 0 {
   335  		return false
   336  	}
   337  
   338  	closeErr := entry.file.Close()
   339  	if closeErr == nil {
   340  		log.Debug("Closed era1 file", "epoch", epoch)
   341  	} else {
   342  		log.Warn("Error closing era1 file", "epoch", epoch, "err", closeErr)
   343  	}
   344  	return true
   345  }