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 }