github.com/dim4egster/coreth@v0.10.2/plugin/evm/atomic_tx_repository_test.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 "sort" 10 "testing" 11 12 "github.com/dim4egster/qmallgo/chains/atomic" 13 "github.com/dim4egster/qmallgo/database" 14 "github.com/dim4egster/qmallgo/database/prefixdb" 15 "github.com/dim4egster/qmallgo/database/versiondb" 16 "github.com/ethereum/go-ethereum/common" 17 18 "github.com/dim4egster/qmallgo/codec" 19 "github.com/dim4egster/qmallgo/utils/wrappers" 20 21 "github.com/stretchr/testify/assert" 22 23 "github.com/dim4egster/qmallgo/database/memdb" 24 "github.com/dim4egster/qmallgo/ids" 25 ) 26 27 // addTxs writes [txsPerHeight] txs for heights ranging in [fromHeight, toHeight) directly to [acceptedAtomicTxDB], 28 // storing the resulting transactions in [txMap] if non-nil and the resulting atomic operations in [operationsMap] 29 // if non-nil. 30 func addTxs(t testing.TB, codec codec.Manager, acceptedAtomicTxDB database.Database, fromHeight uint64, toHeight uint64, txsPerHeight int, txMap map[uint64][]*Tx, operationsMap map[uint64]map[ids.ID]*atomic.Requests) { 31 for height := fromHeight; height < toHeight; height++ { 32 txs := make([]*Tx, 0, txsPerHeight) 33 for i := 0; i < txsPerHeight; i++ { 34 tx := newTestTx() 35 txs = append(txs, tx) 36 txBytes, err := codec.Marshal(codecVersion, tx) 37 assert.NoError(t, err) 38 39 // Write atomic transactions to the [acceptedAtomicTxDB] 40 // in the format handled prior to the migration to the atomic 41 // tx repository. 42 packer := wrappers.Packer{Bytes: make([]byte, 1), MaxSize: 1024 * 1024} 43 packer.PackLong(height) 44 packer.PackBytes(txBytes) 45 txID := tx.ID() 46 err = acceptedAtomicTxDB.Put(txID[:], packer.Bytes) 47 assert.NoError(t, err) 48 } 49 // save this to the map (if non-nil) for verifying expected results in verifyTxs 50 if txMap != nil { 51 txMap[height] = txs 52 } 53 if operationsMap != nil { 54 atomicRequests, err := mergeAtomicOps(txs) 55 if err != nil { 56 t.Fatal(err) 57 } 58 operationsMap[height] = atomicRequests 59 } 60 } 61 } 62 63 // constTxsPerHeight returns a function for passing to [writeTxs], which will return a constant number 64 // as the number of atomic txs per height to create. 65 func constTxsPerHeight(txCount int) func(uint64) int { 66 return func(uint64) int { return txCount } 67 } 68 69 // writeTxs writes [txsPerHeight] txs for heights ranging in [fromHeight, toHeight) through the Write call on [repo], 70 // storing the resulting transactions in [txMap] if non-nil and the resulting atomic operations in [operationsMap] 71 // if non-nil. 72 func writeTxs(t testing.TB, repo AtomicTxRepository, fromHeight uint64, toHeight uint64, 73 txsPerHeight func(height uint64) int, txMap map[uint64][]*Tx, operationsMap map[uint64]map[ids.ID]*atomic.Requests, 74 ) { 75 for height := fromHeight; height < toHeight; height++ { 76 txs := newTestTxs(txsPerHeight(height)) 77 if err := repo.Write(height, txs); err != nil { 78 t.Fatal(err) 79 } 80 // save this to the map (if non-nil) for verifying expected results in verifyTxs 81 if txMap != nil { 82 txMap[height] = txs 83 } 84 if operationsMap != nil { 85 atomicRequests, err := mergeAtomicOps(txs) 86 if err != nil { 87 t.Fatal(err) 88 } 89 if len(atomicRequests) == 0 { 90 continue 91 } 92 operationsMap[height] = atomicRequests 93 } 94 } 95 } 96 97 // verifyTxs asserts [repo] can find all txs in [txMap] by height and txID 98 func verifyTxs(t testing.TB, repo AtomicTxRepository, txMap map[uint64][]*Tx) { 99 // We should be able to fetch indexed txs by height: 100 getComparator := func(txs []*Tx) func(int, int) bool { 101 return func(i, j int) bool { 102 return txs[i].ID().Hex() < txs[j].ID().Hex() 103 } 104 } 105 for height, expectedTxs := range txMap { 106 txs, err := repo.GetByHeight(height) 107 assert.NoErrorf(t, err, "unexpected error on GetByHeight at height=%d", height) 108 assert.Lenf(t, txs, len(expectedTxs), "wrong len of txs at height=%d", height) 109 // txs should be stored in order of txID 110 sort.Slice(expectedTxs, getComparator(expectedTxs)) 111 112 txIDs := ids.Set{} 113 for i := 0; i < len(txs); i++ { 114 assert.Equalf(t, expectedTxs[i].ID().Hex(), txs[i].ID().Hex(), "wrong txID at height=%d idx=%d", height, i) 115 txIDs.Add(txs[i].ID()) 116 } 117 assert.Equalf(t, len(txs), txIDs.Len(), "incorrect number of unique transactions in slice at height %d, expected %d, found %d", height, len(txs), txIDs.Len()) 118 } 119 } 120 121 // verifyOperations creates an iterator over the atomicTrie at [rootHash] and verifies that the all of the operations in the trie in the interval [from, to] are identical to 122 // the atomic operations contained in [operationsMap] on the same interval. 123 func verifyOperations(t testing.TB, atomicTrie AtomicTrie, codec codec.Manager, rootHash common.Hash, from, to uint64, operationsMap map[uint64]map[ids.ID]*atomic.Requests) { 124 t.Helper() 125 126 // Start the iterator at [from] 127 fromBytes := make([]byte, wrappers.LongLen) 128 binary.BigEndian.PutUint64(fromBytes, from) 129 iter, err := atomicTrie.Iterator(rootHash, fromBytes) 130 if err != nil { 131 t.Fatal(err) 132 } 133 134 // Generate map of the marshalled atomic operations on the interval [from, to] 135 // based on [operationsMap]. 136 marshalledOperationsMap := make(map[uint64]map[ids.ID][]byte) 137 for height, blockRequests := range operationsMap { 138 if height < from || height > to { 139 continue 140 } 141 for blockchainID, atomicRequests := range blockRequests { 142 b, err := codec.Marshal(0, atomicRequests) 143 if err != nil { 144 t.Fatal(err) 145 } 146 if requestsMap, exists := marshalledOperationsMap[height]; exists { 147 requestsMap[blockchainID] = b 148 } else { 149 requestsMap = make(map[ids.ID][]byte) 150 requestsMap[blockchainID] = b 151 marshalledOperationsMap[height] = requestsMap 152 } 153 } 154 } 155 156 // Generate map of marshalled atomic operations on the interval [from, to] 157 // based on the contents of the trie. 158 iteratorMarshalledOperationsMap := make(map[uint64]map[ids.ID][]byte) 159 for iter.Next() { 160 height := iter.BlockNumber() 161 if height < from { 162 t.Fatalf("Iterator starting at (%d) found value at block height (%d)", from, height) 163 } 164 if height > to { 165 continue 166 } 167 168 blockchainID := iter.BlockchainID() 169 b, err := codec.Marshal(0, iter.AtomicOps()) 170 if err != nil { 171 t.Fatal(err) 172 } 173 if requestsMap, exists := iteratorMarshalledOperationsMap[height]; exists { 174 requestsMap[blockchainID] = b 175 } else { 176 requestsMap = make(map[ids.ID][]byte) 177 requestsMap[blockchainID] = b 178 iteratorMarshalledOperationsMap[height] = requestsMap 179 } 180 } 181 if err := iter.Error(); err != nil { 182 t.Fatal(err) 183 } 184 185 assert.Equal(t, marshalledOperationsMap, iteratorMarshalledOperationsMap) 186 } 187 188 func TestAtomicRepositoryReadWriteSingleTx(t *testing.T) { 189 db := versiondb.New(memdb.New()) 190 codec := testTxCodec() 191 repo, err := NewAtomicTxRepository(db, codec, 0, nil, nil, nil) 192 if err != nil { 193 t.Fatal(err) 194 } 195 txMap := make(map[uint64][]*Tx) 196 197 writeTxs(t, repo, 1, 100, constTxsPerHeight(1), txMap, nil) 198 verifyTxs(t, repo, txMap) 199 } 200 201 func TestAtomicRepositoryReadWriteMultipleTxs(t *testing.T) { 202 db := versiondb.New(memdb.New()) 203 codec := testTxCodec() 204 repo, err := NewAtomicTxRepository(db, codec, 0, nil, nil, nil) 205 if err != nil { 206 t.Fatal(err) 207 } 208 txMap := make(map[uint64][]*Tx) 209 210 writeTxs(t, repo, 1, 100, constTxsPerHeight(10), txMap, nil) 211 verifyTxs(t, repo, txMap) 212 } 213 214 func TestAtomicRepositoryPreAP5Migration(t *testing.T) { 215 db := versiondb.New(memdb.New()) 216 codec := testTxCodec() 217 218 acceptedAtomicTxDB := prefixdb.New(atomicTxIDDBPrefix, db) 219 txMap := make(map[uint64][]*Tx) 220 addTxs(t, codec, acceptedAtomicTxDB, 1, 100, 1, txMap, nil) 221 if err := db.Commit(); err != nil { 222 t.Fatal(err) 223 } 224 225 // Ensure the atomic repository can correctly migrate the transactions 226 // from the old accepted atomic tx DB to add the height index. 227 repo, err := NewAtomicTxRepository(db, codec, 100, nil, nil, nil) 228 if err != nil { 229 t.Fatal(err) 230 } 231 assert.NoError(t, err) 232 verifyTxs(t, repo, txMap) 233 234 writeTxs(t, repo, 100, 150, constTxsPerHeight(1), txMap, nil) 235 writeTxs(t, repo, 150, 200, constTxsPerHeight(10), txMap, nil) 236 verifyTxs(t, repo, txMap) 237 } 238 239 func TestAtomicRepositoryPostAP5Migration(t *testing.T) { 240 db := versiondb.New(memdb.New()) 241 codec := testTxCodec() 242 243 acceptedAtomicTxDB := prefixdb.New(atomicTxIDDBPrefix, db) 244 txMap := make(map[uint64][]*Tx) 245 addTxs(t, codec, acceptedAtomicTxDB, 1, 100, 1, txMap, nil) 246 addTxs(t, codec, acceptedAtomicTxDB, 100, 200, 10, txMap, nil) 247 if err := db.Commit(); err != nil { 248 t.Fatal(err) 249 } 250 251 // Ensure the atomic repository can correctly migrate the transactions 252 // from the old accepted atomic tx DB to add the height index. 253 repo, err := NewAtomicTxRepository(db, codec, 200, nil, nil, nil) 254 if err != nil { 255 t.Fatal(err) 256 } 257 assert.NoError(t, err) 258 verifyTxs(t, repo, txMap) 259 260 writeTxs(t, repo, 200, 300, constTxsPerHeight(10), txMap, nil) 261 verifyTxs(t, repo, txMap) 262 } 263 264 func benchAtomicRepositoryIndex10_000(b *testing.B, maxHeight uint64, txsPerHeight int) { 265 db := versiondb.New(memdb.New()) 266 codec := testTxCodec() 267 268 acceptedAtomicTxDB := prefixdb.New(atomicTxIDDBPrefix, db) 269 txMap := make(map[uint64][]*Tx) 270 271 addTxs(b, codec, acceptedAtomicTxDB, 0, maxHeight, txsPerHeight, txMap, nil) 272 if err := db.Commit(); err != nil { 273 b.Fatal(err) 274 } 275 repo, err := NewAtomicTxRepository(db, codec, maxHeight, nil, nil, nil) 276 if err != nil { 277 b.Fatal(err) 278 } 279 assert.NoError(b, err) 280 verifyTxs(b, repo, txMap) 281 } 282 283 func BenchmarkAtomicRepositoryIndex_10kBlocks_1Tx(b *testing.B) { 284 for n := 0; n < b.N; n++ { 285 benchAtomicRepositoryIndex10_000(b, 10_000, 1) 286 } 287 } 288 289 func BenchmarkAtomicRepositoryIndex_10kBlocks_10Tx(b *testing.B) { 290 for n := 0; n < b.N; n++ { 291 benchAtomicRepositoryIndex10_000(b, 10_000, 10) 292 } 293 } 294 295 func TestRepairAtomicRepositoryForBonusBlockTxs(t *testing.T) { 296 db := versiondb.New(memdb.New()) 297 atomicTxRepository, err := NewAtomicTxRepository(db, testTxCodec(), 0, nil, nil, nil) 298 if err != nil { 299 t.Fatal(err) 300 } 301 302 // check completion flag is set 303 done, err := atomicTxRepository.isBonusBlocksRepaired() 304 assert.NoError(t, err) 305 assert.True(t, done) 306 307 // delete the key so we can simulate an unrepaired repository 308 atomicTxRepository.atomicRepoMetadataDB.Delete(bonusBlocksRepairedKey) 309 310 tx := newTestTx() 311 // write the same tx to 3 heights. 312 canonical, bonus1, bonus2 := uint64(10), uint64(20), uint64(30) 313 atomicTxRepository.Write(canonical, []*Tx{tx}) 314 atomicTxRepository.Write(bonus1, []*Tx{tx}) 315 atomicTxRepository.Write(bonus2, []*Tx{tx}) 316 db.Commit() 317 318 _, foundHeight, err := atomicTxRepository.GetByTxID(tx.ID()) 319 assert.NoError(t, err) 320 assert.Equal(t, bonus2, foundHeight) 321 322 allHeights := []uint64{canonical, bonus1, bonus2} 323 if err := atomicTxRepository.RepairForBonusBlocks( 324 allHeights, 325 func(height uint64) (*Tx, error) { 326 if height == 10 || height == 20 || height == 30 { 327 return tx, nil 328 } 329 return nil, fmt.Errorf("unexpected height %d", height) 330 }, 331 ); err != nil { 332 t.Fatal(err) 333 } 334 335 // check canonical height is indexed against txID 336 _, foundHeight, err = atomicTxRepository.GetByTxID(tx.ID()) 337 assert.NoError(t, err) 338 assert.Equal(t, canonical, foundHeight) 339 340 // check tx can be found with any of the heights 341 for _, height := range allHeights { 342 txs, err := atomicTxRepository.GetByHeight(height) 343 if err != nil { 344 t.Fatal(err) 345 } 346 assert.Len(t, txs, 1) 347 assert.Equal(t, tx.ID(), txs[0].ID()) 348 } 349 350 // check completion flag is set 351 done, err = atomicTxRepository.isBonusBlocksRepaired() 352 assert.NoError(t, err) 353 assert.True(t, done) 354 }