github.com/dim4egster/coreth@v0.10.2/plugin/evm/atomic_backend.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  	"fmt"
     9  	"time"
    10  
    11  	"github.com/dim4egster/qmallgo/chains/atomic"
    12  	"github.com/dim4egster/qmallgo/codec"
    13  	"github.com/dim4egster/qmallgo/database"
    14  	"github.com/dim4egster/qmallgo/database/prefixdb"
    15  	"github.com/dim4egster/qmallgo/database/versiondb"
    16  	"github.com/dim4egster/qmallgo/ids"
    17  	"github.com/dim4egster/qmallgo/utils/wrappers"
    18  	syncclient "github.com/dim4egster/coreth/sync/client"
    19  	"github.com/ethereum/go-ethereum/common"
    20  	"github.com/ethereum/go-ethereum/log"
    21  )
    22  
    23  var _ AtomicBackend = &atomicBackend{}
    24  
    25  // AtomicBackend abstracts the verification and processing
    26  // of atomic transactions
    27  type AtomicBackend interface {
    28  	// InsertTxs calculates the root of the atomic trie that would
    29  	// result from applying [txs] to the atomic trie, starting at the state
    30  	// corresponding to previously verified block [parentHash].
    31  	// If [blockHash] is provided, the modified atomic trie is pinned in memory
    32  	// and it's the caller's responsibility to call either Accept or Reject on
    33  	// the AtomicState which can be retreived from GetVerifiedAtomicState to commit the
    34  	// changes or abort them and free memory.
    35  	InsertTxs(blockHash common.Hash, blockHeight uint64, parentHash common.Hash, txs []*Tx) (common.Hash, error)
    36  
    37  	// Returns an AtomicState corresponding to a block hash that has been inserted
    38  	// but not Accepted or Rejected yet.
    39  	GetVerifiedAtomicState(blockHash common.Hash) (AtomicState, error)
    40  
    41  	// AtomicTrie returns the atomic trie managed by this backend.
    42  	AtomicTrie() AtomicTrie
    43  
    44  	// ApplyToSharedMemory applies the atomic operations that have been indexed into the trie
    45  	// but not yet applied to shared memory for heights less than or equal to [lastAcceptedBlock].
    46  	// This executes operations in the range [cursorHeight+1, lastAcceptedBlock].
    47  	// The cursor is initially set by  MarkApplyToSharedMemoryCursor to signal to the atomic trie
    48  	// the range of operations that were added to the trie without being executed on shared memory.
    49  	ApplyToSharedMemory(lastAcceptedBlock uint64) error
    50  
    51  	// MarkApplyToSharedMemoryCursor marks the atomic trie as containing atomic ops that
    52  	// have not been executed on shared memory starting at [previousLastAcceptedHeight+1].
    53  	// This is used when state sync syncs the atomic trie, such that the atomic operations
    54  	// from [previousLastAcceptedHeight+1] to the [lastAcceptedHeight] set by state sync
    55  	// will not have been executed on shared memory.
    56  	MarkApplyToSharedMemoryCursor(previousLastAcceptedHeight uint64) error
    57  
    58  	// Syncer creates and returns a new Syncer object that can be used to sync the
    59  	// state of the atomic trie from peers
    60  	Syncer(client syncclient.LeafClient, targetRoot common.Hash, targetHeight uint64) (Syncer, error)
    61  
    62  	// SetLastAccepted is used after state-sync to reset the last accepted block.
    63  	SetLastAccepted(lastAcceptedHash common.Hash)
    64  
    65  	// IsBonus returns true if the block for atomicState is a bonus block
    66  	IsBonus(blockHeight uint64, blockHash common.Hash) bool
    67  }
    68  
    69  // atomicBackend implements the AtomicBackend interface using
    70  // the AtomicTrie, AtomicTxRepository, and the VM's shared memory.
    71  type atomicBackend struct {
    72  	codec        codec.Manager
    73  	bonusBlocks  map[uint64]ids.ID   // Map of height to blockID for blocks to skip indexing
    74  	db           *versiondb.Database // Underlying database
    75  	metadataDB   database.Database   // Underlying database containing the atomic trie metadata
    76  	sharedMemory atomic.SharedMemory
    77  
    78  	repo       AtomicTxRepository
    79  	atomicTrie AtomicTrie
    80  
    81  	lastAcceptedHash common.Hash
    82  	verifiedRoots    map[common.Hash]AtomicState
    83  }
    84  
    85  // NewAtomicBackend creates an AtomicBackend from the specified dependencies
    86  func NewAtomicBackend(
    87  	db *versiondb.Database, sharedMemory atomic.SharedMemory,
    88  	bonusBlocks map[uint64]ids.ID, repo AtomicTxRepository,
    89  	lastAcceptedHeight uint64, lastAcceptedHash common.Hash, commitInterval uint64,
    90  ) (AtomicBackend, error) {
    91  	atomicTrieDB := prefixdb.New(atomicTrieDBPrefix, db)
    92  	metadataDB := prefixdb.New(atomicTrieMetaDBPrefix, db)
    93  	codec := repo.Codec()
    94  
    95  	atomicTrie, err := newAtomicTrie(atomicTrieDB, metadataDB, codec, lastAcceptedHeight, commitInterval)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	atomicBackend := &atomicBackend{
   100  		codec:            codec,
   101  		db:               db,
   102  		metadataDB:       metadataDB,
   103  		sharedMemory:     sharedMemory,
   104  		bonusBlocks:      bonusBlocks,
   105  		repo:             repo,
   106  		atomicTrie:       atomicTrie,
   107  		lastAcceptedHash: lastAcceptedHash,
   108  		verifiedRoots:    make(map[common.Hash]AtomicState),
   109  	}
   110  
   111  	// We call ApplyToSharedMemory here to ensure that if the node was shut down in the middle
   112  	// of applying atomic operations from state sync, we finish the operation to ensure we never
   113  	// return an atomic trie that is out of sync with shared memory.
   114  	// In normal operation, the cursor is not set, such that this call will be a no-op.
   115  	if err := atomicBackend.ApplyToSharedMemory(lastAcceptedHeight); err != nil {
   116  		return nil, err
   117  	}
   118  	return atomicBackend, atomicBackend.initialize(lastAcceptedHeight)
   119  }
   120  
   121  // initializes the atomic trie using the atomic repository height index.
   122  // Iterating from the last committed height to the last height indexed
   123  // in the atomic repository, making a single commit at the
   124  // most recent height divisible by the commitInterval.
   125  // Subsequent updates to this trie are made using the Index call as blocks are accepted.
   126  // Note: this method assumes no atomic txs are applied at genesis.
   127  func (a *atomicBackend) initialize(lastAcceptedHeight uint64) error {
   128  	start := time.Now()
   129  
   130  	// track the last committed height and last committed root
   131  	lastCommittedRoot, lastCommittedHeight := a.atomicTrie.LastCommitted()
   132  	log.Info("initializing atomic trie", "lastCommittedHeight", lastCommittedHeight)
   133  
   134  	// iterate by height, from [lastCommittedHeight+1] to [lastAcceptedBlockNumber]
   135  	height := lastCommittedHeight
   136  	iter := a.repo.IterateByHeight(lastCommittedHeight + 1)
   137  	defer iter.Release()
   138  
   139  	heightsIndexed := 0
   140  	lastUpdate := time.Now()
   141  
   142  	// open the atomic trie at the last committed root
   143  	tr, err := a.atomicTrie.OpenTrie(lastCommittedRoot)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	for iter.Next() {
   149  		// Get the height and transactions for this iteration (from the key and value, respectively)
   150  		// iterate over the transactions, indexing them if the height is < commit height
   151  		// otherwise, add the atomic operations from the transaction to the uncommittedOpsMap
   152  		height = binary.BigEndian.Uint64(iter.Key())
   153  		txs, err := ExtractAtomicTxs(iter.Value(), true, a.codec)
   154  		if err != nil {
   155  			return err
   156  		}
   157  
   158  		// combine atomic operations from all transactions at this block height
   159  		combinedOps, err := mergeAtomicOps(txs)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  		if _, found := a.bonusBlocks[height]; found {
   165  			// If [height] is a bonus block, do not index the atomic operations into the trie
   166  			continue
   167  		}
   168  		if err := a.atomicTrie.UpdateTrie(tr, height, combinedOps); err != nil {
   169  			return err
   170  		}
   171  		root, nodes, err := tr.Commit(false)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		if err := a.atomicTrie.InsertTrie(nodes, root); err != nil {
   176  			return err
   177  		}
   178  		isCommit, err := a.atomicTrie.AcceptTrie(height, root)
   179  		if err != nil {
   180  			return err
   181  		}
   182  		if isCommit {
   183  			if err := a.db.Commit(); err != nil {
   184  				return err
   185  			}
   186  		}
   187  
   188  		heightsIndexed++
   189  		if time.Since(lastUpdate) > progressLogFrequency {
   190  			log.Info("imported entries into atomic trie", "heightsIndexed", heightsIndexed)
   191  			lastUpdate = time.Now()
   192  		}
   193  	}
   194  	if err := iter.Error(); err != nil {
   195  		return err
   196  	}
   197  
   198  	// check if there are accepted blocks after the last block with accepted atomic txs.
   199  	if lastAcceptedHeight > height {
   200  		lastAcceptedRoot := a.atomicTrie.LastAcceptedRoot()
   201  		if err := a.atomicTrie.InsertTrie(nil, lastAcceptedRoot); err != nil {
   202  			return err
   203  		}
   204  		if _, err := a.atomicTrie.AcceptTrie(lastAcceptedHeight, lastAcceptedRoot); err != nil {
   205  			return err
   206  		}
   207  	}
   208  
   209  	lastCommittedRoot, lastCommittedHeight = a.atomicTrie.LastCommitted()
   210  	log.Info(
   211  		"finished initializing atomic trie",
   212  		"lastAcceptedHeight", lastAcceptedHeight,
   213  		"lastAcceptedAtomicRoot", a.atomicTrie.LastAcceptedRoot(),
   214  		"heightsIndexed", heightsIndexed,
   215  		"lastCommittedRoot", lastCommittedRoot,
   216  		"lastCommittedHeight", lastCommittedHeight,
   217  		"time", time.Since(start),
   218  	)
   219  	return nil
   220  }
   221  
   222  // ApplyToSharedMemory applies the atomic operations that have been indexed into the trie
   223  // but not yet applied to shared memory for heights less than or equal to [lastAcceptedBlock].
   224  // This executes operations in the range [cursorHeight+1, lastAcceptedBlock].
   225  // The cursor is initially set by  MarkApplyToSharedMemoryCursor to signal to the atomic trie
   226  // the range of operations that were added to the trie without being executed on shared memory.
   227  func (a *atomicBackend) ApplyToSharedMemory(lastAcceptedBlock uint64) error {
   228  	sharedMemoryCursor, err := a.metadataDB.Get(appliedSharedMemoryCursorKey)
   229  	if err == database.ErrNotFound {
   230  		return nil
   231  	} else if err != nil {
   232  		return err
   233  	}
   234  
   235  	lastCommittedRoot, _ := a.atomicTrie.LastCommitted()
   236  	log.Info("applying atomic operations to shared memory", "root", lastCommittedRoot, "lastAcceptedBlock", lastAcceptedBlock, "startHeight", binary.BigEndian.Uint64(sharedMemoryCursor[:wrappers.LongLen]))
   237  
   238  	it, err := a.atomicTrie.Iterator(lastCommittedRoot, sharedMemoryCursor)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	var (
   244  		lastUpdate                            = time.Now()
   245  		putRequests, removeRequests           = 0, 0
   246  		totalPutRequests, totalRemoveRequests = 0, 0
   247  	)
   248  
   249  	// value of sharedMemoryCursor is either a uint64 signifying the
   250  	// height iteration should begin at or is a uint64+blockchainID
   251  	// specifying the last atomic operation that was applied to shared memory.
   252  	// To avoid applying the same operation twice, we call [it.Next()] in the
   253  	// latter case.
   254  	if len(sharedMemoryCursor) > wrappers.LongLen {
   255  		it.Next()
   256  	}
   257  
   258  	batchOps := make(map[ids.ID]*atomic.Requests)
   259  	for it.Next() {
   260  		height := it.BlockNumber()
   261  		atomicOps := it.AtomicOps()
   262  
   263  		if height > lastAcceptedBlock {
   264  			log.Warn("Found height above last accepted block while applying operations to shared memory", "height", height, "lastAcceptedBlock", lastAcceptedBlock)
   265  			break
   266  		}
   267  
   268  		putRequests += len(atomicOps.PutRequests)
   269  		removeRequests += len(atomicOps.RemoveRequests)
   270  		totalPutRequests += len(atomicOps.PutRequests)
   271  		totalRemoveRequests += len(atomicOps.RemoveRequests)
   272  		if time.Since(lastUpdate) > progressLogFrequency {
   273  			log.Info("atomic trie iteration", "height", height, "puts", totalPutRequests, "removes", totalRemoveRequests)
   274  			lastUpdate = time.Now()
   275  		}
   276  		mergeAtomicOpsToMap(batchOps, it.BlockchainID(), atomicOps)
   277  
   278  		if putRequests+removeRequests > sharedMemoryApplyBatchSize {
   279  			// Update the cursor to the key of the atomic operation being executed on shared memory.
   280  			// If the node shuts down in the middle of this function call, ApplyToSharedMemory will
   281  			// resume operation starting at the key immediately following [it.Key()].
   282  			if err = a.metadataDB.Put(appliedSharedMemoryCursorKey, it.Key()); err != nil {
   283  				return err
   284  			}
   285  			batch, err := a.db.CommitBatch()
   286  			if err != nil {
   287  				return err
   288  			}
   289  			// calling [sharedMemory.Apply] updates the last applied pointer atomically with the shared memory operation.
   290  			if err = a.sharedMemory.Apply(batchOps, batch); err != nil {
   291  				return err
   292  			}
   293  			putRequests, removeRequests = 0, 0
   294  			batchOps = make(map[ids.ID]*atomic.Requests)
   295  		}
   296  	}
   297  	if err := it.Error(); err != nil {
   298  		return err
   299  	}
   300  
   301  	if err = a.metadataDB.Delete(appliedSharedMemoryCursorKey); err != nil {
   302  		return err
   303  	}
   304  	batch, err := a.db.CommitBatch()
   305  	if err != nil {
   306  		return err
   307  	}
   308  	if err = a.sharedMemory.Apply(batchOps, batch); err != nil {
   309  		return err
   310  	}
   311  	log.Info("finished applying atomic operations", "puts", totalPutRequests, "removes", totalRemoveRequests)
   312  	return nil
   313  }
   314  
   315  // MarkApplyToSharedMemoryCursor marks the atomic trie as containing atomic ops that
   316  // have not been executed on shared memory starting at [previousLastAcceptedHeight+1].
   317  // This is used when state sync syncs the atomic trie, such that the atomic operations
   318  // from [previousLastAcceptedHeight+1] to the [lastAcceptedHeight] set by state sync
   319  // will not have been executed on shared memory.
   320  func (a *atomicBackend) MarkApplyToSharedMemoryCursor(previousLastAcceptedHeight uint64) error {
   321  	// Set the cursor to [previousLastAcceptedHeight+1] so that we begin the iteration at the
   322  	// first item that has not been applied to shared memory.
   323  	return database.PutUInt64(a.metadataDB, appliedSharedMemoryCursorKey, previousLastAcceptedHeight+1)
   324  }
   325  
   326  // Syncer creates and returns a new Syncer object that can be used to sync the
   327  // state of the atomic trie from peers
   328  func (a *atomicBackend) Syncer(client syncclient.LeafClient, targetRoot common.Hash, targetHeight uint64) (Syncer, error) {
   329  	return newAtomicSyncer(client, a, targetRoot, targetHeight)
   330  }
   331  
   332  func (a *atomicBackend) GetVerifiedAtomicState(blockHash common.Hash) (AtomicState, error) {
   333  	if state, ok := a.verifiedRoots[blockHash]; ok {
   334  		return state, nil
   335  	}
   336  	return nil, fmt.Errorf("cannot access atomic state for block %s", blockHash)
   337  }
   338  
   339  // getAtomicRootAt returns the atomic trie root for a block that is either:
   340  // - the last accepted block
   341  // - a block that has been verified but not accepted or rejected yet.
   342  // If [blockHash] is neither of the above, an error is returned.
   343  func (a *atomicBackend) getAtomicRootAt(blockHash common.Hash) (common.Hash, error) {
   344  	// TODO: we can implement this in a few ways.
   345  	if blockHash == a.lastAcceptedHash {
   346  		return a.atomicTrie.LastAcceptedRoot(), nil
   347  	}
   348  	state, err := a.GetVerifiedAtomicState(blockHash)
   349  	if err != nil {
   350  		return common.Hash{}, err
   351  	}
   352  	return state.Root(), nil
   353  }
   354  
   355  // SetLastAccepted is used after state-sync to update the last accepted block hash.
   356  func (a *atomicBackend) SetLastAccepted(lastAcceptedHash common.Hash) {
   357  	a.lastAcceptedHash = lastAcceptedHash
   358  }
   359  
   360  // InsertTxs calculates the root of the atomic trie that would
   361  // result from applying [txs] to the atomic trie, starting at the state
   362  // corresponding to previously verified block [parentHash].
   363  // If [blockHash] is provided, the modified atomic trie is pinned in memory
   364  // and it's the caller's responsibility to call either Accept or Reject on
   365  // the AtomicState which can be retreived from GetVerifiedAtomicState to commit the
   366  // changes or abort them and free memory.
   367  func (a *atomicBackend) InsertTxs(blockHash common.Hash, blockHeight uint64, parentHash common.Hash, txs []*Tx) (common.Hash, error) {
   368  	// access the atomic trie at the parent block
   369  	parentRoot, err := a.getAtomicRootAt(parentHash)
   370  	if err != nil {
   371  		return common.Hash{}, err
   372  	}
   373  	tr, err := a.atomicTrie.OpenTrie(parentRoot)
   374  	if err != nil {
   375  		return common.Hash{}, err
   376  	}
   377  
   378  	// update the atomic trie
   379  	atomicOps, err := mergeAtomicOps(txs)
   380  	if err != nil {
   381  		return common.Hash{}, err
   382  	}
   383  	if err := a.atomicTrie.UpdateTrie(tr, blockHeight, atomicOps); err != nil {
   384  		return common.Hash{}, err
   385  	}
   386  
   387  	// If block hash is not provided, we do not pin the atomic state in memory and can return early
   388  	if blockHash == (common.Hash{}) {
   389  		return tr.Hash(), nil
   390  	}
   391  
   392  	// get the new root and pin the atomic trie changes in memory.
   393  	root, nodes, err := tr.Commit(false)
   394  	if err != nil {
   395  		return common.Hash{}, err
   396  	}
   397  	if err := a.atomicTrie.InsertTrie(nodes, root); err != nil {
   398  		return common.Hash{}, err
   399  	}
   400  	// track this block so further blocks can be inserted on top
   401  	// of this block
   402  	a.verifiedRoots[blockHash] = &atomicState{
   403  		backend:     a,
   404  		blockHash:   blockHash,
   405  		blockHeight: blockHeight,
   406  		txs:         txs,
   407  		atomicOps:   atomicOps,
   408  		atomicRoot:  root,
   409  	}
   410  	return root, nil
   411  }
   412  
   413  // IsBonus returns true if the block for atomicState is a bonus block
   414  func (a *atomicBackend) IsBonus(blockHeight uint64, blockHash common.Hash) bool {
   415  	if bonusID, found := a.bonusBlocks[blockHeight]; found {
   416  		return bonusID == ids.ID(blockHash)
   417  	}
   418  	return false
   419  }
   420  
   421  func (a *atomicBackend) AtomicTrie() AtomicTrie {
   422  	return a.atomicTrie
   423  }