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  }