github.com/dim4egster/coreth@v0.10.2/plugin/evm/atomic_tx_repository.go (about)

     1  // (c) 2020-2021, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package evm
     5  
     6  import (
     7  	"encoding/binary"
     8  	"errors"
     9  	"fmt"
    10  	"sort"
    11  	"time"
    12  
    13  	"github.com/ethereum/go-ethereum/common"
    14  	"github.com/ethereum/go-ethereum/log"
    15  
    16  	"github.com/dim4egster/qmallgo/codec"
    17  	"github.com/dim4egster/qmallgo/database"
    18  	"github.com/dim4egster/qmallgo/database/prefixdb"
    19  	"github.com/dim4egster/qmallgo/database/versiondb"
    20  	"github.com/dim4egster/qmallgo/ids"
    21  	"github.com/dim4egster/qmallgo/utils/units"
    22  	"github.com/dim4egster/qmallgo/utils/wrappers"
    23  )
    24  
    25  const (
    26  	repoCommitSizeCap = 10 * units.MiB
    27  )
    28  
    29  var (
    30  	atomicTxIDDBPrefix         = []byte("atomicTxDB")
    31  	atomicHeightTxDBPrefix     = []byte("atomicHeightTxDB")
    32  	atomicRepoMetadataDBPrefix = []byte("atomicRepoMetadataDB")
    33  	maxIndexedHeightKey        = []byte("maxIndexedAtomicTxHeight")
    34  	bonusBlocksRepairedKey     = []byte("bonusBlocksRepaired")
    35  )
    36  
    37  // AtomicTxRepository defines an entity that manages storage and indexing of
    38  // atomic transactions
    39  type AtomicTxRepository interface {
    40  	GetIndexHeight() (uint64, error)
    41  	GetByTxID(txID ids.ID) (*Tx, uint64, error)
    42  	GetByHeight(height uint64) ([]*Tx, error)
    43  	Write(height uint64, txs []*Tx) error
    44  	WriteBonus(height uint64, txs []*Tx) error
    45  
    46  	IterateByHeight(start uint64) database.Iterator
    47  	Codec() codec.Manager
    48  }
    49  
    50  // atomicTxRepository is a prefixdb implementation of the AtomicTxRepository interface
    51  type atomicTxRepository struct {
    52  	// [acceptedAtomicTxDB] maintains an index of [txID] => [height]+[atomic tx] for all accepted atomic txs.
    53  	acceptedAtomicTxDB database.Database
    54  
    55  	// [acceptedAtomicTxByHeightDB] maintains an index of [height] => [atomic txs] for all accepted block heights.
    56  	acceptedAtomicTxByHeightDB database.Database
    57  
    58  	// [atomicRepoMetadataDB] maintains a single key-value pair which tracks the height up to which the atomic repository
    59  	// has indexed.
    60  	atomicRepoMetadataDB database.Database
    61  
    62  	// [db] is used to commit to the underlying versiondb.
    63  	db *versiondb.Database
    64  
    65  	// Use this codec for serializing
    66  	codec codec.Manager
    67  }
    68  
    69  func NewAtomicTxRepository(
    70  	db *versiondb.Database, codec codec.Manager, lastAcceptedHeight uint64,
    71  	bonusBlocks map[uint64]ids.ID, canonicalBlocks []uint64,
    72  	getAtomicTxFromBlockByHeight func(height uint64) (*Tx, error),
    73  ) (*atomicTxRepository, error) {
    74  	repo := &atomicTxRepository{
    75  		acceptedAtomicTxDB:         prefixdb.New(atomicTxIDDBPrefix, db),
    76  		acceptedAtomicTxByHeightDB: prefixdb.New(atomicHeightTxDBPrefix, db),
    77  		atomicRepoMetadataDB:       prefixdb.New(atomicRepoMetadataDBPrefix, db),
    78  		codec:                      codec,
    79  		db:                         db,
    80  	}
    81  	if err := repo.initializeHeightIndex(lastAcceptedHeight); err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	// TODO: remove post banff as all network participants will have applied the repair script.
    86  	repairHeights := getAtomicRepositoryRepairHeights(bonusBlocks, canonicalBlocks)
    87  	if err := repo.RepairForBonusBlocks(repairHeights, getAtomicTxFromBlockByHeight); err != nil {
    88  		return nil, fmt.Errorf("failed to repair atomic repository: %w", err)
    89  	}
    90  
    91  	return repo, nil
    92  }
    93  
    94  // initializeHeightIndex initializes the atomic repository and takes care of any required migration from the previous database
    95  // format which did not have a height -> txs index.
    96  func (a *atomicTxRepository) initializeHeightIndex(lastAcceptedHeight uint64) error {
    97  	startTime := time.Now()
    98  	lastLogTime := startTime
    99  
   100  	// [lastTxID] will be initialized to the last transaction that we indexed
   101  	// if we are part way through a migration.
   102  	var lastTxID ids.ID
   103  	indexHeightBytes, err := a.atomicRepoMetadataDB.Get(maxIndexedHeightKey)
   104  	switch err {
   105  	case nil:
   106  		break
   107  	case database.ErrNotFound:
   108  		break
   109  	default: // unexpected value in the database
   110  		return fmt.Errorf("found invalid value at max indexed height: %v", indexHeightBytes)
   111  	}
   112  
   113  	switch len(indexHeightBytes) {
   114  	case 0:
   115  		log.Info("Initializing atomic transaction repository from scratch")
   116  	case common.HashLength: // partially initialized
   117  		lastTxID, err = ids.ToID(indexHeightBytes)
   118  		if err != nil {
   119  			return err
   120  		}
   121  		log.Info("Initializing atomic transaction repository from txID", "lastTxID", lastTxID)
   122  	case wrappers.LongLen: // already initialized
   123  		return nil
   124  	default: // unexpected value in the database
   125  		return fmt.Errorf("found invalid value at max indexed height: %v", indexHeightBytes)
   126  	}
   127  
   128  	// Iterate from [lastTxID] to complete the re-index -> generating an index
   129  	// from height to a slice of transactions accepted at that height
   130  	iter := a.acceptedAtomicTxDB.NewIteratorWithStart(lastTxID[:])
   131  	defer iter.Release()
   132  
   133  	indexedTxs := 0
   134  
   135  	// Keep track of the size of the currently pending writes
   136  	pendingBytesApproximation := 0
   137  	for iter.Next() {
   138  		// iter.Value() consists of [height packed as uint64] + [tx serialized as packed []byte]
   139  		iterValue := iter.Value()
   140  		if len(iterValue) < wrappers.LongLen {
   141  			return fmt.Errorf("atomic tx DB iterator value had invalid length (%d) < (%d)", len(iterValue), wrappers.LongLen)
   142  		}
   143  		heightBytes := iterValue[:wrappers.LongLen]
   144  
   145  		// Get the tx iter is pointing to, len(txs) == 1 is expected here.
   146  		txBytes := iterValue[wrappers.LongLen+wrappers.IntLen:]
   147  		tx, err := ExtractAtomicTx(txBytes, a.codec)
   148  		if err != nil {
   149  			return err
   150  		}
   151  
   152  		// Check if there are already transactions at [height], to ensure that we
   153  		// add [txs] to the already indexed transactions at [height] instead of
   154  		// overwriting them.
   155  		if err := a.appendTxToHeightIndex(heightBytes, tx); err != nil {
   156  			return err
   157  		}
   158  		lastTxID = tx.ID()
   159  		pendingBytesApproximation += len(txBytes)
   160  
   161  		// call commitFn to write to underlying DB if we have reached
   162  		// [commitSizeCap]
   163  		if pendingBytesApproximation > repoCommitSizeCap {
   164  			if err := a.atomicRepoMetadataDB.Put(maxIndexedHeightKey, lastTxID[:]); err != nil {
   165  				return err
   166  			}
   167  			if err := a.db.Commit(); err != nil {
   168  				return err
   169  			}
   170  			log.Info("Committing work initializing the atomic repository", "lastTxID", lastTxID, "pendingBytesApprox", pendingBytesApproximation)
   171  			pendingBytesApproximation = 0
   172  		}
   173  		indexedTxs++
   174  		// Periodically log progress
   175  		if time.Since(lastLogTime) > 15*time.Second {
   176  			lastLogTime = time.Now()
   177  			log.Info("Atomic repository initialization", "indexedTxs", indexedTxs)
   178  		}
   179  	}
   180  	if err := iter.Error(); err != nil {
   181  		return fmt.Errorf("atomic tx DB iterator errored while initializing atomic trie: %w", err)
   182  	}
   183  
   184  	// Updated the value stored [maxIndexedHeightKey] to be the lastAcceptedHeight
   185  	indexedHeight := make([]byte, wrappers.LongLen)
   186  	binary.BigEndian.PutUint64(indexedHeight, lastAcceptedHeight)
   187  	if err := a.atomicRepoMetadataDB.Put(maxIndexedHeightKey, indexedHeight); err != nil {
   188  		return err
   189  	}
   190  
   191  	log.Info("Completed atomic transaction repository migration", "lastAcceptedHeight", lastAcceptedHeight, "duration", time.Since(startTime))
   192  	return a.db.Commit()
   193  }
   194  
   195  // GetIndexHeight returns the last height that was indexed by the atomic repository
   196  func (a *atomicTxRepository) GetIndexHeight() (uint64, error) {
   197  	indexHeightBytes, err := a.atomicRepoMetadataDB.Get(maxIndexedHeightKey)
   198  	if err != nil {
   199  		return 0, err
   200  	}
   201  
   202  	if len(indexHeightBytes) != wrappers.LongLen {
   203  		return 0, fmt.Errorf("unexpected length for indexHeightBytes %d", len(indexHeightBytes))
   204  	}
   205  	indexHeight := binary.BigEndian.Uint64(indexHeightBytes)
   206  	return indexHeight, nil
   207  }
   208  
   209  // GetByTxID queries [acceptedAtomicTxDB] for the [txID], parses a [*Tx] object
   210  // if an entry is found, and returns it with the block height the atomic tx it
   211  // represents was accepted on, along with an optional error.
   212  func (a *atomicTxRepository) GetByTxID(txID ids.ID) (*Tx, uint64, error) {
   213  	indexedTxBytes, err := a.acceptedAtomicTxDB.Get(txID[:])
   214  	if err != nil {
   215  		return nil, 0, err
   216  	}
   217  
   218  	if len(indexedTxBytes) < wrappers.LongLen {
   219  		return nil, 0, fmt.Errorf("acceptedAtomicTxDB entry too short: %d", len(indexedTxBytes))
   220  	}
   221  
   222  	// value is stored as [height]+[tx bytes], decompose with a packer.
   223  	packer := wrappers.Packer{Bytes: indexedTxBytes}
   224  	height := packer.UnpackLong()
   225  	txBytes := packer.UnpackBytes()
   226  	tx, err := ExtractAtomicTx(txBytes, a.codec)
   227  	if err != nil {
   228  		return nil, 0, err
   229  	}
   230  
   231  	return tx, height, nil
   232  }
   233  
   234  // GetByHeight returns all atomic txs processed on block at [height].
   235  // Returns [database.ErrNotFound] if there are no atomic transactions indexed at [height].
   236  // Note: if [height] is below the last accepted height, then this means that there were
   237  // no atomic transactions in the block accepted at [height].
   238  // If [height] is greater than the last accepted height, then this will always return
   239  // [database.ErrNotFound]
   240  func (a *atomicTxRepository) GetByHeight(height uint64) ([]*Tx, error) {
   241  	heightBytes := make([]byte, wrappers.LongLen)
   242  	binary.BigEndian.PutUint64(heightBytes, height)
   243  
   244  	return a.getByHeightBytes(heightBytes)
   245  }
   246  
   247  func (a *atomicTxRepository) getByHeightBytes(heightBytes []byte) ([]*Tx, error) {
   248  	txsBytes, err := a.acceptedAtomicTxByHeightDB.Get(heightBytes)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	return ExtractAtomicTxsBatch(txsBytes, a.codec)
   253  }
   254  
   255  // Write updates indexes maintained on atomic txs, so they can be queried
   256  // by txID or height. This method must be called only once per height,
   257  // and [txs] must include all atomic txs for the block accepted at the
   258  // corresponding height.
   259  func (a *atomicTxRepository) Write(height uint64, txs []*Tx) error {
   260  	return a.write(height, txs, false)
   261  }
   262  
   263  // WriteBonus is similar to Write, except the [txID] => [height] is not
   264  // overwritten if already exists.
   265  func (a *atomicTxRepository) WriteBonus(height uint64, txs []*Tx) error {
   266  	return a.write(height, txs, true)
   267  }
   268  
   269  func (a *atomicTxRepository) write(height uint64, txs []*Tx, bonus bool) error {
   270  	if len(txs) > 1 {
   271  		// txs should be stored in order of txID to ensure consistency
   272  		// with txs initialized from the txID index.
   273  		copyTxs := make([]*Tx, len(txs))
   274  		copy(copyTxs, txs)
   275  		sort.Slice(copyTxs, func(i, j int) bool { return copyTxs[i].ID().Hex() < copyTxs[j].ID().Hex() })
   276  		txs = copyTxs
   277  	}
   278  	heightBytes := make([]byte, wrappers.LongLen)
   279  	binary.BigEndian.PutUint64(heightBytes, height)
   280  	// Skip adding an entry to the height index if [txs] is empty.
   281  	if len(txs) > 0 {
   282  		for _, tx := range txs {
   283  			if bonus {
   284  				switch _, _, err := a.GetByTxID(tx.ID()); err {
   285  				case nil:
   286  					// avoid overwriting existing value if [bonus] is true
   287  					continue
   288  				case database.ErrNotFound:
   289  					// no existing value to overwrite, proceed as normal
   290  				default:
   291  					// unexpected error
   292  					return err
   293  				}
   294  			}
   295  			if err := a.indexTxByID(heightBytes, tx); err != nil {
   296  				return err
   297  			}
   298  		}
   299  		if err := a.indexTxsAtHeight(heightBytes, txs); err != nil {
   300  			return err
   301  		}
   302  	}
   303  
   304  	// Update the index height regardless of if any atomic transactions
   305  	// were present at [height].
   306  	return a.atomicRepoMetadataDB.Put(maxIndexedHeightKey, heightBytes)
   307  }
   308  
   309  // indexTxByID writes [tx] into the [acceptedAtomicTxDB] stored as
   310  // [height] + [tx bytes]
   311  func (a *atomicTxRepository) indexTxByID(heightBytes []byte, tx *Tx) error {
   312  	txBytes, err := a.codec.Marshal(codecVersion, tx)
   313  	if err != nil {
   314  		return err
   315  	}
   316  
   317  	// map txID => [height]+[tx bytes]
   318  	heightTxPacker := wrappers.Packer{Bytes: make([]byte, wrappers.LongLen+wrappers.IntLen+len(txBytes))}
   319  	heightTxPacker.PackFixedBytes(heightBytes)
   320  	heightTxPacker.PackBytes(txBytes)
   321  	txID := tx.ID()
   322  
   323  	if err := a.acceptedAtomicTxDB.Put(txID[:], heightTxPacker.Bytes); err != nil {
   324  		return err
   325  	}
   326  
   327  	return nil
   328  }
   329  
   330  // indexTxsAtHeight adds [height] -> [txs] to the [acceptedAtomicTxByHeightDB]
   331  func (a *atomicTxRepository) indexTxsAtHeight(heightBytes []byte, txs []*Tx) error {
   332  	txsBytes, err := a.codec.Marshal(codecVersion, txs)
   333  	if err != nil {
   334  		return err
   335  	}
   336  	if err := a.acceptedAtomicTxByHeightDB.Put(heightBytes, txsBytes); err != nil {
   337  		return err
   338  	}
   339  	return nil
   340  }
   341  
   342  // appendTxToHeightIndex retrieves the transactions stored at [heightBytes] and appends
   343  // [tx] to the slice of transactions stored there.
   344  // This function is used while initializing the atomic repository to re-index the atomic transactions
   345  // by txID into the height -> txs index.
   346  func (a *atomicTxRepository) appendTxToHeightIndex(heightBytes []byte, tx *Tx) error {
   347  	txs, err := a.getByHeightBytes(heightBytes)
   348  	if err != nil && err != database.ErrNotFound {
   349  		return err
   350  	}
   351  
   352  	// Iterate over the existing transactions to ensure we do not add a
   353  	// duplicate to the index.
   354  	for _, existingTx := range txs {
   355  		if existingTx.ID() == tx.ID() {
   356  			return nil
   357  		}
   358  	}
   359  
   360  	txs = append(txs, tx)
   361  	return a.indexTxsAtHeight(heightBytes, txs)
   362  }
   363  
   364  // IterateByHeight returns an iterator beginning at [height].
   365  // Note [height] must be greater than 0 since we assume there are no
   366  // atomic txs in genesis.
   367  func (a *atomicTxRepository) IterateByHeight(height uint64) database.Iterator {
   368  	heightBytes := make([]byte, wrappers.LongLen)
   369  	binary.BigEndian.PutUint64(heightBytes, height)
   370  	return a.acceptedAtomicTxByHeightDB.NewIteratorWithStart(heightBytes)
   371  }
   372  
   373  func (a *atomicTxRepository) Codec() codec.Manager {
   374  	return a.codec
   375  }
   376  
   377  func (a *atomicTxRepository) isBonusBlocksRepaired() (bool, error) {
   378  	return a.atomicRepoMetadataDB.Has(bonusBlocksRepairedKey)
   379  }
   380  
   381  func (a *atomicTxRepository) markBonusBlocksRepaired(repairedEntries uint64) error {
   382  	val := make([]byte, wrappers.LongLen)
   383  	binary.BigEndian.PutUint64(val, repairedEntries)
   384  	return a.atomicRepoMetadataDB.Put(bonusBlocksRepairedKey, val)
   385  }
   386  
   387  // RepairForBonusBlocks ensures that atomic txs that were processed on more than one block
   388  // (canonical block + a number of bonus blocks) are indexed to the first height they were
   389  // processed on (canonical block). [sortedHeights] should include all canonical block and
   390  // bonus block heights in ascending order, and will only be passed as non-empty on mainnet.
   391  func (a *atomicTxRepository) RepairForBonusBlocks(
   392  	sortedHeights []uint64, getAtomicTxFromBlockByHeight func(height uint64) (*Tx, error),
   393  ) error {
   394  	done, err := a.isBonusBlocksRepaired()
   395  	if err != nil {
   396  		return err
   397  	}
   398  	if done {
   399  		return nil
   400  	}
   401  	repairedEntries := uint64(0)
   402  	seenTxs := make(map[ids.ID][]uint64)
   403  	for _, height := range sortedHeights {
   404  		// get atomic tx from block
   405  		tx, err := getAtomicTxFromBlockByHeight(height)
   406  		if err != nil {
   407  			return err
   408  		}
   409  		if tx == nil {
   410  			continue
   411  		}
   412  
   413  		// get the tx by txID and update it, the first time we encounter
   414  		// a given [txID], overwrite the previous [txID] => [height]
   415  		// mapping. This provides a canonical mapping across nodes.
   416  		heights, seen := seenTxs[tx.ID()]
   417  		_, foundHeight, err := a.GetByTxID(tx.ID())
   418  		if err != nil && !errors.Is(err, database.ErrNotFound) {
   419  			return err
   420  		}
   421  		if !seen {
   422  			if err := a.Write(height, []*Tx{tx}); err != nil {
   423  				return err
   424  			}
   425  		} else {
   426  			if err := a.WriteBonus(height, []*Tx{tx}); err != nil {
   427  				return err
   428  			}
   429  		}
   430  		if foundHeight != height && !seen {
   431  			repairedEntries++
   432  		}
   433  		seenTxs[tx.ID()] = append(heights, height)
   434  	}
   435  	if err := a.markBonusBlocksRepaired(repairedEntries); err != nil {
   436  		return err
   437  	}
   438  	log.Info("atomic tx repository RepairForBonusBlocks complete", "repairedEntries", repairedEntries)
   439  	return a.db.Commit()
   440  }
   441  
   442  // getAtomicRepositoryRepairHeights returns a slice containing heights from bonus blocks and
   443  // canonical blocks sorted by height.
   444  func getAtomicRepositoryRepairHeights(bonusBlocks map[uint64]ids.ID, canonicalBlocks []uint64) []uint64 {
   445  	repairHeights := make([]uint64, 0, len(bonusBlocks)+len(canonicalBlocks))
   446  	for height := range bonusBlocks {
   447  		repairHeights = append(repairHeights, height)
   448  	}
   449  	for _, height := range canonicalBlocks {
   450  		// avoid appending duplicates
   451  		if _, exists := bonusBlocks[height]; !exists {
   452  			repairHeights = append(repairHeights, height)
   453  		}
   454  	}
   455  	sort.Slice(repairHeights, func(i, j int) bool { return repairHeights[i] < repairHeights[j] })
   456  	return repairHeights
   457  }