github.com/MetalBlockchain/metalgo@v1.11.9/vms/avm/index_test.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package avm
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/prometheus/client_golang/prometheus"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/MetalBlockchain/metalgo/database"
    13  	"github.com/MetalBlockchain/metalgo/database/memdb"
    14  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    15  	"github.com/MetalBlockchain/metalgo/database/versiondb"
    16  	"github.com/MetalBlockchain/metalgo/ids"
    17  	"github.com/MetalBlockchain/metalgo/utils"
    18  	"github.com/MetalBlockchain/metalgo/utils/constants"
    19  	"github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1"
    20  	"github.com/MetalBlockchain/metalgo/utils/logging"
    21  	"github.com/MetalBlockchain/metalgo/vms/avm/txs"
    22  	"github.com/MetalBlockchain/metalgo/vms/components/avax"
    23  	"github.com/MetalBlockchain/metalgo/vms/components/index"
    24  	"github.com/MetalBlockchain/metalgo/vms/secp256k1fx"
    25  )
    26  
    27  func TestIndexTransaction_Ordered(t *testing.T) {
    28  	require := require.New(t)
    29  
    30  	env := setup(t, &envConfig{fork: durango})
    31  	defer env.vm.ctx.Lock.Unlock()
    32  
    33  	key := keys[0]
    34  	addr := key.PublicKey().Address()
    35  	txAssetID := avax.Asset{ID: env.genesisTx.ID()}
    36  
    37  	var txs []*txs.Tx
    38  	for i := 0; i < 5; i++ {
    39  		// make utxo
    40  		utxoID := avax.UTXOID{
    41  			TxID: ids.GenerateTestID(),
    42  		}
    43  		utxo := buildUTXO(utxoID, txAssetID, addr)
    44  		env.vm.state.AddUTXO(utxo)
    45  
    46  		// make transaction
    47  		tx := buildTX(env.vm.ctx.XChainID, utxoID, txAssetID, addr)
    48  		require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{key}}))
    49  
    50  		env.vm.ctx.Lock.Unlock()
    51  
    52  		issueAndAccept(require, env.vm, env.issuer, tx)
    53  
    54  		env.vm.ctx.Lock.Lock()
    55  
    56  		txs = append(txs, tx)
    57  	}
    58  
    59  	// for each tx check its indexed at right index
    60  	for i, tx := range txs {
    61  		assertIndexedTX(t, env.vm.db, uint64(i), addr, txAssetID.ID, tx.ID())
    62  	}
    63  	assertLatestIdx(t, env.vm.db, addr, txAssetID.ID, 5)
    64  }
    65  
    66  func TestIndexTransaction_MultipleTransactions(t *testing.T) {
    67  	require := require.New(t)
    68  
    69  	env := setup(t, &envConfig{fork: durango})
    70  	defer env.vm.ctx.Lock.Unlock()
    71  
    72  	addressTxMap := map[ids.ShortID]*txs.Tx{}
    73  	txAssetID := avax.Asset{ID: env.genesisTx.ID()}
    74  
    75  	for _, key := range keys {
    76  		addr := key.PublicKey().Address()
    77  
    78  		// make utxo
    79  		utxoID := avax.UTXOID{
    80  			TxID: ids.GenerateTestID(),
    81  		}
    82  		utxo := buildUTXO(utxoID, txAssetID, addr)
    83  		env.vm.state.AddUTXO(utxo)
    84  
    85  		// make transaction
    86  		tx := buildTX(env.vm.ctx.XChainID, utxoID, txAssetID, addr)
    87  		require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{key}}))
    88  
    89  		env.vm.ctx.Lock.Unlock()
    90  
    91  		// issue transaction
    92  		issueAndAccept(require, env.vm, env.issuer, tx)
    93  
    94  		env.vm.ctx.Lock.Lock()
    95  
    96  		addressTxMap[addr] = tx
    97  	}
    98  
    99  	// ensure length is same as keys length
   100  	require.Len(addressTxMap, len(keys))
   101  
   102  	// for each *UniqueTx check its indexed at right index for the right address
   103  	for addr, tx := range addressTxMap {
   104  		assertIndexedTX(t, env.vm.db, 0, addr, txAssetID.ID, tx.ID())
   105  		assertLatestIdx(t, env.vm.db, addr, txAssetID.ID, 1)
   106  	}
   107  }
   108  
   109  func TestIndexTransaction_MultipleAddresses(t *testing.T) {
   110  	require := require.New(t)
   111  
   112  	env := setup(t, &envConfig{fork: durango})
   113  	defer env.vm.ctx.Lock.Unlock()
   114  
   115  	addrs := make([]ids.ShortID, len(keys))
   116  	for i, key := range keys {
   117  		addrs[i] = key.PublicKey().Address()
   118  	}
   119  	utils.Sort(addrs)
   120  
   121  	txAssetID := avax.Asset{ID: env.genesisTx.ID()}
   122  
   123  	key := keys[0]
   124  	addr := key.PublicKey().Address()
   125  
   126  	// make utxo
   127  	utxoID := avax.UTXOID{
   128  		TxID: ids.GenerateTestID(),
   129  	}
   130  	utxo := buildUTXO(utxoID, txAssetID, addr)
   131  	env.vm.state.AddUTXO(utxo)
   132  
   133  	// make transaction
   134  	tx := buildTX(env.vm.ctx.XChainID, utxoID, txAssetID, addrs...)
   135  	require.NoError(tx.SignSECP256K1Fx(env.vm.parser.Codec(), [][]*secp256k1.PrivateKey{{key}}))
   136  
   137  	env.vm.ctx.Lock.Unlock()
   138  
   139  	issueAndAccept(require, env.vm, env.issuer, tx)
   140  
   141  	env.vm.ctx.Lock.Lock()
   142  
   143  	assertIndexedTX(t, env.vm.db, 0, addr, txAssetID.ID, tx.ID())
   144  	assertLatestIdx(t, env.vm.db, addr, txAssetID.ID, 1)
   145  }
   146  
   147  func TestIndexer_Read(t *testing.T) {
   148  	require := require.New(t)
   149  
   150  	env := setup(t, &envConfig{fork: durango})
   151  	defer env.vm.ctx.Lock.Unlock()
   152  
   153  	// generate test address and asset IDs
   154  	assetID := ids.GenerateTestID()
   155  	addr := ids.GenerateTestShortID()
   156  
   157  	// setup some fake txs under the above generated address and asset IDs
   158  	testTxs := initTestTxIndex(t, env.vm.db, addr, assetID, 25)
   159  	require.Len(testTxs, 25)
   160  
   161  	// read the pages, 5 items at a time
   162  	var (
   163  		cursor   uint64
   164  		pageSize uint64 = 5
   165  	)
   166  	for cursor < 25 {
   167  		txIDs, err := env.vm.addressTxsIndexer.Read(addr[:], assetID, cursor, pageSize)
   168  		require.NoError(err)
   169  		require.Len(txIDs, 5)
   170  		require.Equal(txIDs, testTxs[cursor:cursor+pageSize])
   171  		cursor += pageSize
   172  	}
   173  }
   174  
   175  func TestIndexingNewInitWithIndexingEnabled(t *testing.T) {
   176  	require := require.New(t)
   177  
   178  	db := memdb.New()
   179  
   180  	// start with indexing enabled
   181  	_, err := index.NewIndexer(db, logging.NoWarn{}, "", prometheus.NewRegistry(), true)
   182  	require.NoError(err)
   183  
   184  	// now disable indexing with allow-incomplete set to false
   185  	_, err = index.NewNoIndexer(db, false)
   186  	require.ErrorIs(err, index.ErrCausesIncompleteIndex)
   187  
   188  	// now disable indexing with allow-incomplete set to true
   189  	_, err = index.NewNoIndexer(db, true)
   190  	require.NoError(err)
   191  }
   192  
   193  func TestIndexingNewInitWithIndexingDisabled(t *testing.T) {
   194  	require := require.New(t)
   195  
   196  	db := memdb.New()
   197  
   198  	// disable indexing with allow-incomplete set to false
   199  	_, err := index.NewNoIndexer(db, false)
   200  	require.NoError(err)
   201  
   202  	// It's not OK to have an incomplete index when allowIncompleteIndices is false
   203  	_, err = index.NewIndexer(db, logging.NoWarn{}, "", prometheus.NewRegistry(), false)
   204  	require.ErrorIs(err, index.ErrIndexingRequiredFromGenesis)
   205  
   206  	// It's OK to have an incomplete index when allowIncompleteIndices is true
   207  	_, err = index.NewIndexer(db, logging.NoWarn{}, "", prometheus.NewRegistry(), true)
   208  	require.NoError(err)
   209  
   210  	// It's OK to have an incomplete index when indexing currently disabled
   211  	_, err = index.NewNoIndexer(db, false)
   212  	require.NoError(err)
   213  
   214  	// It's OK to have an incomplete index when allowIncompleteIndices is true
   215  	_, err = index.NewNoIndexer(db, true)
   216  	require.NoError(err)
   217  }
   218  
   219  func TestIndexingAllowIncomplete(t *testing.T) {
   220  	require := require.New(t)
   221  
   222  	baseDB := memdb.New()
   223  	db := versiondb.New(baseDB)
   224  	// disabled indexer will persist idxEnabled as false
   225  	_, err := index.NewNoIndexer(db, false)
   226  	require.NoError(err)
   227  
   228  	// we initialize with indexing enabled now and allow incomplete indexing as false
   229  	_, err = index.NewIndexer(db, logging.NoWarn{}, "", prometheus.NewRegistry(), false)
   230  	// we should get error because:
   231  	// - indexing was disabled previously
   232  	// - node now is asked to enable indexing with allow incomplete set to false
   233  	require.ErrorIs(err, index.ErrIndexingRequiredFromGenesis)
   234  }
   235  
   236  func buildUTXO(utxoID avax.UTXOID, txAssetID avax.Asset, addr ids.ShortID) *avax.UTXO {
   237  	return &avax.UTXO{
   238  		UTXOID: utxoID,
   239  		Asset:  txAssetID,
   240  		Out: &secp256k1fx.TransferOutput{
   241  			Amt: startBalance,
   242  			OutputOwners: secp256k1fx.OutputOwners{
   243  				Threshold: 1,
   244  				Addrs:     []ids.ShortID{addr},
   245  			},
   246  		},
   247  	}
   248  }
   249  
   250  func buildTX(chainID ids.ID, utxoID avax.UTXOID, txAssetID avax.Asset, address ...ids.ShortID) *txs.Tx {
   251  	return &txs.Tx{Unsigned: &txs.BaseTx{
   252  		BaseTx: avax.BaseTx{
   253  			NetworkID:    constants.UnitTestID,
   254  			BlockchainID: chainID,
   255  			Ins: []*avax.TransferableInput{{
   256  				UTXOID: utxoID,
   257  				Asset:  txAssetID,
   258  				In: &secp256k1fx.TransferInput{
   259  					Amt:   startBalance,
   260  					Input: secp256k1fx.Input{SigIndices: []uint32{0}},
   261  				},
   262  			}},
   263  			Outs: []*avax.TransferableOutput{{
   264  				Asset: txAssetID,
   265  				Out: &secp256k1fx.TransferOutput{
   266  					Amt: startBalance - testTxFee,
   267  					OutputOwners: secp256k1fx.OutputOwners{
   268  						Threshold: 1,
   269  						Addrs:     address,
   270  					},
   271  				},
   272  			}},
   273  		},
   274  	}}
   275  }
   276  
   277  func assertLatestIdx(t *testing.T, db database.Database, sourceAddress ids.ShortID, assetID ids.ID, expectedIdx uint64) {
   278  	require := require.New(t)
   279  
   280  	addressDB := prefixdb.New(sourceAddress[:], db)
   281  	assetDB := prefixdb.New(assetID[:], addressDB)
   282  
   283  	expectedIdxBytes := database.PackUInt64(expectedIdx)
   284  	idxBytes, err := assetDB.Get([]byte("idx"))
   285  	require.NoError(err)
   286  	require.Equal(expectedIdxBytes, idxBytes)
   287  }
   288  
   289  func assertIndexedTX(t *testing.T, db database.Database, index uint64, sourceAddress ids.ShortID, assetID ids.ID, transactionID ids.ID) {
   290  	require := require.New(t)
   291  
   292  	addressDB := prefixdb.New(sourceAddress[:], db)
   293  	assetDB := prefixdb.New(assetID[:], addressDB)
   294  
   295  	idxBytes := database.PackUInt64(index)
   296  	txID, err := database.GetID(assetDB, idxBytes)
   297  	require.NoError(err)
   298  	require.Equal(transactionID, txID)
   299  }
   300  
   301  // Sets up test tx IDs in DB in the following structure for the indexer to pick
   302  // them up:
   303  //
   304  //	[address] prefix DB
   305  //	  [assetID] prefix DB
   306  //	    - "idx": 2
   307  //	    - 0: txID1
   308  //	    - 1: txID1
   309  func initTestTxIndex(t *testing.T, db *versiondb.Database, address ids.ShortID, assetID ids.ID, txCount int) []ids.ID {
   310  	require := require.New(t)
   311  
   312  	testTxs := make([]ids.ID, txCount)
   313  	for i := 0; i < txCount; i++ {
   314  		testTxs[i] = ids.GenerateTestID()
   315  	}
   316  
   317  	addressPrefixDB := prefixdb.New(address[:], db)
   318  	assetPrefixDB := prefixdb.New(assetID[:], addressPrefixDB)
   319  
   320  	for i, txID := range testTxs {
   321  		idxBytes := database.PackUInt64(uint64(i))
   322  		txID := txID
   323  		require.NoError(assetPrefixDB.Put(idxBytes, txID[:]))
   324  	}
   325  	_, err := db.CommitBatch()
   326  	require.NoError(err)
   327  
   328  	idxBytes := database.PackUInt64(uint64(len(testTxs)))
   329  	require.NoError(assetPrefixDB.Put([]byte("idx"), idxBytes))
   330  	require.NoError(db.Commit())
   331  	return testTxs
   332  }