github.com/dim4egster/coreth@v0.10.2/plugin/evm/atomic_trie.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/ids"
    15  	"github.com/dim4egster/qmallgo/utils/units"
    16  	"github.com/dim4egster/qmallgo/utils/wrappers"
    17  
    18  	"github.com/dim4egster/coreth/core"
    19  	"github.com/dim4egster/coreth/core/types"
    20  	"github.com/dim4egster/coreth/ethdb"
    21  	"github.com/dim4egster/coreth/trie"
    22  	"github.com/ethereum/go-ethereum/common"
    23  	"github.com/ethereum/go-ethereum/log"
    24  )
    25  
    26  const (
    27  	progressLogFrequency       = 30 * time.Second
    28  	atomicKeyLength            = wrappers.LongLen + common.HashLength
    29  	sharedMemoryApplyBatchSize = 10_000 // specifies the number of atomic operations to batch progress updates
    30  
    31  	atomicTrieTipBufferSize = 1 // No need to support a buffer of previously accepted tries for the atomic trie
    32  	atomicTrieMemoryCap     = 64 * units.MiB
    33  )
    34  
    35  var (
    36  	_                            AtomicTrie = &atomicTrie{}
    37  	lastCommittedKey                        = []byte("atomicTrieLastCommittedBlock")
    38  	appliedSharedMemoryCursorKey            = []byte("atomicTrieLastAppliedToSharedMemory")
    39  )
    40  
    41  // AtomicTrie maintains an index of atomic operations by blockchainIDs for every block
    42  // height containing atomic transactions. The backing data structure for this index is
    43  // a Trie. The keys of the trie are block heights and the values (leaf nodes)
    44  // are the atomic operations applied to shared memory while processing the block accepted
    45  // at the corresponding height.
    46  type AtomicTrie interface {
    47  	// OpenTrie returns a modifiable instance of the atomic trie backed by trieDB
    48  	// opened at hash.
    49  	OpenTrie(hash common.Hash) (*trie.Trie, error)
    50  
    51  	// UpdateTrie updates [tr] to inlude atomicOps for height.
    52  	UpdateTrie(tr *trie.Trie, height uint64, atomicOps map[ids.ID]*atomic.Requests) error
    53  
    54  	// Iterator returns an AtomicTrieIterator to iterate the trie at the given
    55  	// root hash starting at [cursor].
    56  	Iterator(hash common.Hash, cursor []byte) (AtomicTrieIterator, error)
    57  
    58  	// LastCommitted returns the last committed hash and corresponding block height
    59  	LastCommitted() (common.Hash, uint64)
    60  
    61  	// TrieDB returns the underlying trie database
    62  	TrieDB() *trie.Database
    63  
    64  	// Root returns hash if it exists at specified height
    65  	// if trie was not committed at provided height, it returns
    66  	// common.Hash{} instead
    67  	Root(height uint64) (common.Hash, error)
    68  
    69  	// LastAcceptedRoot returns the most recent accepted root of the atomic trie,
    70  	// or the root it was initialized to if no new tries were accepted yet.
    71  	LastAcceptedRoot() common.Hash
    72  
    73  	// InsertTrie updates the trieDB with the provided node set and adds a reference
    74  	// to root in the trieDB. Once InsertTrie is called, it is expected either
    75  	// AcceptTrie or RejectTrie be called for the same root.
    76  	InsertTrie(nodes *trie.NodeSet, root common.Hash) error
    77  
    78  	// AcceptTrie marks root as the last accepted atomic trie root, and
    79  	// commits the trie to persistent storage if height is divisible by
    80  	// the commit interval. Returns true if the trie was committed.
    81  	AcceptTrie(height uint64, root common.Hash) (bool, error)
    82  
    83  	// RejectTrie dereferences root from the trieDB, freeing memory.
    84  	RejectTrie(root common.Hash) error
    85  }
    86  
    87  // AtomicTrieIterator is a stateful iterator that iterates the leafs of an AtomicTrie
    88  type AtomicTrieIterator interface {
    89  	// Next advances the iterator to the next node in the atomic trie and
    90  	// returns true if there are more leaves to iterate
    91  	Next() bool
    92  
    93  	// Key returns the current database key that the iterator is iterating
    94  	// returned []byte can be freely modified
    95  	Key() []byte
    96  
    97  	// BlockNumber returns the current block number
    98  	BlockNumber() uint64
    99  
   100  	// BlockchainID returns the current blockchain ID at the current block number
   101  	BlockchainID() ids.ID
   102  
   103  	// AtomicOps returns a map of blockchainIDs to the set of atomic requests
   104  	// for that blockchainID at the current block number
   105  	AtomicOps() *atomic.Requests
   106  
   107  	// Error returns error, if any encountered during this iteration
   108  	Error() error
   109  }
   110  
   111  // atomicTrie implements the AtomicTrie interface
   112  type atomicTrie struct {
   113  	commitInterval      uint64            // commit interval, same as commitHeightInterval by default
   114  	metadataDB          database.Database // Underlying database containing the atomic trie metadata
   115  	trieDB              *trie.Database    // Trie database
   116  	lastCommittedRoot   common.Hash       // trie root of the most recent commit
   117  	lastCommittedHeight uint64            // index height of the most recent commit
   118  	lastAcceptedRoot    common.Hash       // most recent trie root passed to accept trie or the root of the atomic trie on intialization.
   119  	codec               codec.Manager
   120  	memoryCap           common.StorageSize
   121  	tipBuffer           *core.BoundedBuffer
   122  }
   123  
   124  // newAtomicTrie returns a new instance of a atomicTrie with a configurable commitHeightInterval, used in testing.
   125  // Initializes the trie before returning it.
   126  func newAtomicTrie(
   127  	atomicTrieDB database.Database, metadataDB database.Database,
   128  	codec codec.Manager, lastAcceptedHeight uint64, commitHeightInterval uint64,
   129  ) (*atomicTrie, error) {
   130  	root, height, err := lastCommittedRootIfExists(metadataDB)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	// initialize to EmptyRootHash if there is no committed root.
   135  	if root == (common.Hash{}) {
   136  		root = types.EmptyRootHash
   137  	}
   138  	// If the last committed height is above the last accepted height, then we fall back to
   139  	// the last commit below the last accepted height.
   140  	if height > lastAcceptedHeight {
   141  		height = nearestCommitHeight(lastAcceptedHeight, commitHeightInterval)
   142  		root, err = getRoot(metadataDB, height)
   143  		if err != nil {
   144  			return nil, err
   145  		}
   146  	}
   147  
   148  	trieDB := trie.NewDatabaseWithConfig(
   149  		Database{atomicTrieDB},
   150  		&trie.Config{
   151  			Cache: 64, // Allocate 64MB of memory for clean cache
   152  		},
   153  	)
   154  
   155  	return &atomicTrie{
   156  		commitInterval:      commitHeightInterval,
   157  		metadataDB:          metadataDB,
   158  		trieDB:              trieDB,
   159  		codec:               codec,
   160  		lastCommittedRoot:   root,
   161  		lastCommittedHeight: height,
   162  		tipBuffer:           core.NewBoundedBuffer(atomicTrieTipBufferSize, trieDB.Dereference),
   163  		memoryCap:           atomicTrieMemoryCap,
   164  		// Initialize lastAcceptedRoot to the last committed root.
   165  		// If there were further blocks processed (ahead of the commit interval),
   166  		// AtomicBackend will call InsertTrie/AcceptTrie on atomic ops
   167  		// for those blocks.
   168  		lastAcceptedRoot: root,
   169  	}, nil
   170  }
   171  
   172  // lastCommittedRootIfExists returns the last committed trie root and height if it exists
   173  // else returns empty common.Hash{} and 0
   174  // returns error only if there are issues with the underlying data store
   175  // or if values present in the database are not as expected
   176  func lastCommittedRootIfExists(db database.Database) (common.Hash, uint64, error) {
   177  	// read the last committed entry if it exists and set the root hash
   178  	lastCommittedHeightBytes, err := db.Get(lastCommittedKey)
   179  	switch {
   180  	case err == database.ErrNotFound:
   181  		return common.Hash{}, 0, nil
   182  	case err != nil:
   183  		return common.Hash{}, 0, err
   184  	case len(lastCommittedHeightBytes) != wrappers.LongLen:
   185  		return common.Hash{}, 0, fmt.Errorf("expected value of lastCommittedKey to be %d but was %d", wrappers.LongLen, len(lastCommittedHeightBytes))
   186  	}
   187  	height := binary.BigEndian.Uint64(lastCommittedHeightBytes)
   188  	hash, err := db.Get(lastCommittedHeightBytes)
   189  	if err != nil {
   190  		return common.Hash{}, 0, fmt.Errorf("committed hash does not exist for committed height: %d: %w", height, err)
   191  	}
   192  	return common.BytesToHash(hash), height, nil
   193  }
   194  
   195  // nearestCommitheight returns the nearest multiple of commitInterval less than or equal to blockNumber
   196  func nearestCommitHeight(blockNumber uint64, commitInterval uint64) uint64 {
   197  	return blockNumber - (blockNumber % commitInterval)
   198  }
   199  
   200  func (a *atomicTrie) OpenTrie(root common.Hash) (*trie.Trie, error) {
   201  	return trie.New(common.Hash{}, root, a.trieDB)
   202  }
   203  
   204  // commit calls commit on the underlying trieDB and updates metadata pointers.
   205  func (a *atomicTrie) commit(height uint64, root common.Hash) error {
   206  	if err := a.trieDB.Commit(root, false, nil); err != nil {
   207  		return err
   208  	}
   209  	log.Info("committed atomic trie", "root", root.String(), "height", height)
   210  	return a.updateLastCommitted(root, height)
   211  }
   212  
   213  func (a *atomicTrie) UpdateTrie(trie *trie.Trie, height uint64, atomicOps map[ids.ID]*atomic.Requests) error {
   214  	for blockchainID, requests := range atomicOps {
   215  		valueBytes, err := a.codec.Marshal(codecVersion, requests)
   216  		if err != nil {
   217  			// highly unlikely but possible if atomic.Element
   218  			// has a change that is unsupported by the codec
   219  			return err
   220  		}
   221  
   222  		// key is [height]+[blockchainID]
   223  		keyPacker := wrappers.Packer{Bytes: make([]byte, atomicKeyLength)}
   224  		keyPacker.PackLong(height)
   225  		keyPacker.PackFixedBytes(blockchainID[:])
   226  		if err := trie.TryUpdate(keyPacker.Bytes, valueBytes); err != nil {
   227  			return err
   228  		}
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  // LastCommitted returns the last committed trie hash and last committed height
   235  func (a *atomicTrie) LastCommitted() (common.Hash, uint64) {
   236  	return a.lastCommittedRoot, a.lastCommittedHeight
   237  }
   238  
   239  // updateLastCommitted adds [height] -> [root] to the index and marks it as the last committed
   240  // root/height pair.
   241  func (a *atomicTrie) updateLastCommitted(root common.Hash, height uint64) error {
   242  	heightBytes := make([]byte, wrappers.LongLen)
   243  	binary.BigEndian.PutUint64(heightBytes, height)
   244  
   245  	// now save the trie hash against the height it was committed at
   246  	if err := a.metadataDB.Put(heightBytes, root[:]); err != nil {
   247  		return err
   248  	}
   249  
   250  	// update lastCommittedKey with the current height
   251  	if err := a.metadataDB.Put(lastCommittedKey, heightBytes); err != nil {
   252  		return err
   253  	}
   254  
   255  	a.lastCommittedRoot = root
   256  	a.lastCommittedHeight = height
   257  	return nil
   258  }
   259  
   260  // Iterator returns a types.AtomicTrieIterator that iterates the trie from the given
   261  // atomic trie root, starting at the specified [cursor].
   262  func (a *atomicTrie) Iterator(root common.Hash, cursor []byte) (AtomicTrieIterator, error) {
   263  	t, err := trie.New(common.Hash{}, root, a.trieDB)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	iter := trie.NewIterator(t.NodeIterator(cursor))
   269  	return NewAtomicTrieIterator(iter, a.codec), iter.Err
   270  }
   271  
   272  func (a *atomicTrie) TrieDB() *trie.Database {
   273  	return a.trieDB
   274  }
   275  
   276  // Root returns hash if it exists at specified height
   277  // if trie was not committed at provided height, it returns
   278  // common.Hash{} instead
   279  func (a *atomicTrie) Root(height uint64) (common.Hash, error) {
   280  	return getRoot(a.metadataDB, height)
   281  }
   282  
   283  // getRoot is a helper function to return the committed atomic trie root hash at [height]
   284  // from [metadataDB].
   285  func getRoot(metadataDB database.Database, height uint64) (common.Hash, error) {
   286  	if height == 0 {
   287  		// if root is queried at height == 0, return the empty root hash
   288  		// this may occur if peers ask for the most recent state summary
   289  		// and number of accepted blocks is less than the commit interval.
   290  		return types.EmptyRootHash, nil
   291  	}
   292  
   293  	heightBytes := make([]byte, wrappers.LongLen)
   294  	binary.BigEndian.PutUint64(heightBytes, height)
   295  
   296  	hash, err := metadataDB.Get(heightBytes)
   297  	switch {
   298  	case err == database.ErrNotFound:
   299  		return common.Hash{}, nil
   300  	case err != nil:
   301  		return common.Hash{}, err
   302  	}
   303  	return common.BytesToHash(hash), nil
   304  }
   305  
   306  func (a *atomicTrie) LastAcceptedRoot() common.Hash {
   307  	return a.lastAcceptedRoot
   308  }
   309  
   310  func (a *atomicTrie) InsertTrie(nodes *trie.NodeSet, root common.Hash) error {
   311  	if nodes != nil {
   312  		if err := a.trieDB.Update(trie.NewWithNodeSet(nodes)); err != nil {
   313  			return err
   314  		}
   315  	}
   316  	a.trieDB.Reference(root, common.Hash{})
   317  
   318  	// The use of [Cap] in [insertTrie] prevents exceeding the configured memory
   319  	// limit (and OOM) in case there is a large backlog of processing (unaccepted) blocks.
   320  	if nodeSize, _ := a.trieDB.Size(); nodeSize <= a.memoryCap {
   321  		return nil
   322  	}
   323  	if err := a.trieDB.Cap(a.memoryCap - ethdb.IdealBatchSize); err != nil {
   324  		return fmt.Errorf("failed to cap atomic trie for root %s: %w", root, err)
   325  	}
   326  
   327  	return nil
   328  }
   329  
   330  // AcceptTrie commits the triedb at [root] if needed and returns true if a commit
   331  // was performed.
   332  func (a *atomicTrie) AcceptTrie(height uint64, root common.Hash) (bool, error) {
   333  	// Check whether we have crossed over a commitHeight.
   334  	// If so, make a commit with the last accepted root.
   335  	hasCommitted := false
   336  	commitHeight := nearestCommitHeight(height, a.commitInterval)
   337  	for commitHeight > a.lastCommittedHeight && height > commitHeight {
   338  		nextCommitHeight := a.lastCommittedHeight + a.commitInterval
   339  		if err := a.commit(nextCommitHeight, a.lastAcceptedRoot); err != nil {
   340  			return false, err
   341  		}
   342  		hasCommitted = true
   343  	}
   344  
   345  	// Attempt to dereference roots at least [tipBufferSize] old
   346  	//
   347  	// Note: It is safe to dereference roots that have been committed to disk
   348  	// (they are no-ops).
   349  	a.tipBuffer.Insert(root)
   350  
   351  	// Commit this root if we have reached the [commitInterval].
   352  	if commitHeight == height {
   353  		if err := a.commit(height, root); err != nil {
   354  			return false, err
   355  		}
   356  		hasCommitted = true
   357  	}
   358  
   359  	a.lastAcceptedRoot = root
   360  	return hasCommitted, nil
   361  }
   362  
   363  func (a *atomicTrie) RejectTrie(root common.Hash) error {
   364  	a.trieDB.Dereference(root)
   365  	return nil
   366  }