github.com/ethereum/go-ethereum@v1.14.4-0.20240516095835-473ee8fc07a3/core/txpool/blobpool/limbo.go (about)

     1  // Copyright 2023 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 blobpool
    18  
    19  import (
    20  	"errors"
    21  
    22  	"github.com/ethereum/go-ethereum/common"
    23  	"github.com/ethereum/go-ethereum/core/types"
    24  	"github.com/ethereum/go-ethereum/log"
    25  	"github.com/ethereum/go-ethereum/rlp"
    26  	"github.com/holiman/billy"
    27  )
    28  
    29  // limboBlob is a wrapper around an opaque blobset that also contains the tx hash
    30  // to which it belongs as well as the block number in which it was included for
    31  // finality eviction.
    32  type limboBlob struct {
    33  	TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs
    34  	Block  uint64      // Block in which the blob transaction was included
    35  	Tx     *types.Transaction
    36  }
    37  
    38  // limbo is a light, indexed database to temporarily store recently included
    39  // blobs until they are finalized. The purpose is to support small reorgs, which
    40  // would require pulling back up old blobs (which aren't part of the chain).
    41  //
    42  // TODO(karalabe): Currently updating the inclusion block of a blob needs a full db rewrite. Can we do without?
    43  type limbo struct {
    44  	store billy.Database // Persistent data store for limboed blobs
    45  
    46  	index  map[common.Hash]uint64            // Mappings from tx hashes to datastore ids
    47  	groups map[uint64]map[uint64]common.Hash // Set of txs included in past blocks
    48  }
    49  
    50  // newLimbo opens and indexes a set of limboed blob transactions.
    51  func newLimbo(datadir string) (*limbo, error) {
    52  	l := &limbo{
    53  		index:  make(map[common.Hash]uint64),
    54  		groups: make(map[uint64]map[uint64]common.Hash),
    55  	}
    56  	// Index all limboed blobs on disk and delete anything unprocessable
    57  	var fails []uint64
    58  	index := func(id uint64, size uint32, data []byte) {
    59  		if l.parseBlob(id, data) != nil {
    60  			fails = append(fails, id)
    61  		}
    62  	}
    63  	store, err := billy.Open(billy.Options{Path: datadir, Repair: true}, newSlotter(), index)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	l.store = store
    68  
    69  	if len(fails) > 0 {
    70  		log.Warn("Dropping invalidated limboed blobs", "ids", fails)
    71  		for _, id := range fails {
    72  			if err := l.store.Delete(id); err != nil {
    73  				l.Close()
    74  				return nil, err
    75  			}
    76  		}
    77  	}
    78  	return l, nil
    79  }
    80  
    81  // Close closes down the underlying persistent store.
    82  func (l *limbo) Close() error {
    83  	return l.store.Close()
    84  }
    85  
    86  // parseBlob is a callback method on limbo creation that gets called for each
    87  // limboed blob on disk to create the in-memory metadata index.
    88  func (l *limbo) parseBlob(id uint64, data []byte) error {
    89  	item := new(limboBlob)
    90  	if err := rlp.DecodeBytes(data, item); err != nil {
    91  		// This path is impossible unless the disk data representation changes
    92  		// across restarts. For that ever improbable case, recover gracefully
    93  		// by ignoring this data entry.
    94  		log.Error("Failed to decode blob limbo entry", "id", id, "err", err)
    95  		return err
    96  	}
    97  	if _, ok := l.index[item.TxHash]; ok {
    98  		// This path is impossible, unless due to a programming error a blob gets
    99  		// inserted into the limbo which was already part of if. Recover gracefully
   100  		// by ignoring this data entry.
   101  		log.Error("Dropping duplicate blob limbo entry", "owner", item.TxHash, "id", id)
   102  		return errors.New("duplicate blob")
   103  	}
   104  	l.index[item.TxHash] = id
   105  
   106  	if _, ok := l.groups[item.Block]; !ok {
   107  		l.groups[item.Block] = make(map[uint64]common.Hash)
   108  	}
   109  	l.groups[item.Block][id] = item.TxHash
   110  
   111  	return nil
   112  }
   113  
   114  // finalize evicts all blobs belonging to a recently finalized block or older.
   115  func (l *limbo) finalize(final *types.Header) {
   116  	// Just in case there's no final block yet (network not yet merged, weird
   117  	// restart, sethead, etc), fail gracefully.
   118  	if final == nil {
   119  		log.Error("Nil finalized block cannot evict old blobs")
   120  		return
   121  	}
   122  	for block, ids := range l.groups {
   123  		if block > final.Number.Uint64() {
   124  			continue
   125  		}
   126  		for id, owner := range ids {
   127  			if err := l.store.Delete(id); err != nil {
   128  				log.Error("Failed to drop finalized blob", "block", block, "id", id, "err", err)
   129  			}
   130  			delete(l.index, owner)
   131  		}
   132  		delete(l.groups, block)
   133  	}
   134  }
   135  
   136  // push stores a new blob transaction into the limbo, waiting until finality for
   137  // it to be automatically evicted.
   138  func (l *limbo) push(tx *types.Transaction, block uint64) error {
   139  	// If the blobs are already tracked by the limbo, consider it a programming
   140  	// error. There's not much to do against it, but be loud.
   141  	if _, ok := l.index[tx.Hash()]; ok {
   142  		log.Error("Limbo cannot push already tracked blobs", "tx", tx)
   143  		return errors.New("already tracked blob transaction")
   144  	}
   145  	if err := l.setAndIndex(tx, block); err != nil {
   146  		log.Error("Failed to set and index limboed blobs", "tx", tx, "err", err)
   147  		return err
   148  	}
   149  	return nil
   150  }
   151  
   152  // pull retrieves a previously pushed set of blobs back from the limbo, removing
   153  // it at the same time. This method should be used when a previously included blob
   154  // transaction gets reorged out.
   155  func (l *limbo) pull(tx common.Hash) (*types.Transaction, error) {
   156  	// If the blobs are not tracked by the limbo, there's not much to do. This
   157  	// can happen for example if a blob transaction is mined without pushing it
   158  	// into the network first.
   159  	id, ok := l.index[tx]
   160  	if !ok {
   161  		log.Trace("Limbo cannot pull non-tracked blobs", "tx", tx)
   162  		return nil, errors.New("unseen blob transaction")
   163  	}
   164  	item, err := l.getAndDrop(id)
   165  	if err != nil {
   166  		log.Error("Failed to get and drop limboed blobs", "tx", tx, "id", id, "err", err)
   167  		return nil, err
   168  	}
   169  	return item.Tx, nil
   170  }
   171  
   172  // update changes the block number under which a blob transaction is tracked. This
   173  // method should be used when a reorg changes a transaction's inclusion block.
   174  //
   175  // The method may log errors for various unexpected scenarios but will not return
   176  // any of it since there's no clear error case. Some errors may be due to coding
   177  // issues, others caused by signers mining MEV stuff or swapping transactions. In
   178  // all cases, the pool needs to continue operating.
   179  func (l *limbo) update(txhash common.Hash, block uint64) {
   180  	// If the blobs are not tracked by the limbo, there's not much to do. This
   181  	// can happen for example if a blob transaction is mined without pushing it
   182  	// into the network first.
   183  	id, ok := l.index[txhash]
   184  	if !ok {
   185  		log.Trace("Limbo cannot update non-tracked blobs", "tx", txhash)
   186  		return
   187  	}
   188  	// If there was no change in the blob's inclusion block, don't mess around
   189  	// with heavy database operations.
   190  	if _, ok := l.groups[block][id]; ok {
   191  		log.Trace("Blob transaction unchanged in limbo", "tx", txhash, "block", block)
   192  		return
   193  	}
   194  	// Retrieve the old blobs from the data store and write them back with a new
   195  	// block number. IF anything fails, there's not much to do, go on.
   196  	item, err := l.getAndDrop(id)
   197  	if err != nil {
   198  		log.Error("Failed to get and drop limboed blobs", "tx", txhash, "id", id, "err", err)
   199  		return
   200  	}
   201  	if err := l.setAndIndex(item.Tx, block); err != nil {
   202  		log.Error("Failed to set and index limboed blobs", "tx", txhash, "err", err)
   203  		return
   204  	}
   205  	log.Trace("Blob transaction updated in limbo", "tx", txhash, "old-block", item.Block, "new-block", block)
   206  }
   207  
   208  // getAndDrop retrieves a blob item from the limbo store and deletes it both from
   209  // the store and indices.
   210  func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) {
   211  	data, err := l.store.Get(id)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	item := new(limboBlob)
   216  	if err = rlp.DecodeBytes(data, item); err != nil {
   217  		return nil, err
   218  	}
   219  	delete(l.index, item.TxHash)
   220  	delete(l.groups[item.Block], id)
   221  	if len(l.groups[item.Block]) == 0 {
   222  		delete(l.groups, item.Block)
   223  	}
   224  	if err := l.store.Delete(id); err != nil {
   225  		return nil, err
   226  	}
   227  	return item, nil
   228  }
   229  
   230  // setAndIndex assembles a limbo blob database entry and stores it, also updating
   231  // the in-memory indices.
   232  func (l *limbo) setAndIndex(tx *types.Transaction, block uint64) error {
   233  	txhash := tx.Hash()
   234  	item := &limboBlob{
   235  		TxHash: txhash,
   236  		Block:  block,
   237  		Tx:     tx,
   238  	}
   239  	data, err := rlp.EncodeToBytes(item)
   240  	if err != nil {
   241  		panic(err) // cannot happen runtime, dev error
   242  	}
   243  	id, err := l.store.Put(data)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	l.index[txhash] = id
   248  	if _, ok := l.groups[block]; !ok {
   249  		l.groups[block] = make(map[uint64]common.Hash)
   250  	}
   251  	l.groups[block][id] = txhash
   252  	return nil
   253  }