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 }