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 }