github.com/MetalBlockchain/metalgo@v1.11.9/vms/avm/environment_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  	"context"
     8  	"encoding/json"
     9  	"math/rand"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/MetalBlockchain/metalgo/api/keystore"
    16  	"github.com/MetalBlockchain/metalgo/chains/atomic"
    17  	"github.com/MetalBlockchain/metalgo/database/memdb"
    18  	"github.com/MetalBlockchain/metalgo/database/prefixdb"
    19  	"github.com/MetalBlockchain/metalgo/ids"
    20  	"github.com/MetalBlockchain/metalgo/snow"
    21  	"github.com/MetalBlockchain/metalgo/snow/engine/common"
    22  	"github.com/MetalBlockchain/metalgo/snow/snowtest"
    23  	"github.com/MetalBlockchain/metalgo/utils/constants"
    24  	"github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1"
    25  	"github.com/MetalBlockchain/metalgo/utils/formatting"
    26  	"github.com/MetalBlockchain/metalgo/utils/formatting/address"
    27  	"github.com/MetalBlockchain/metalgo/utils/logging"
    28  	"github.com/MetalBlockchain/metalgo/utils/sampler"
    29  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    30  	"github.com/MetalBlockchain/metalgo/vms/avm/block/executor"
    31  	"github.com/MetalBlockchain/metalgo/vms/avm/config"
    32  	"github.com/MetalBlockchain/metalgo/vms/avm/fxs"
    33  	"github.com/MetalBlockchain/metalgo/vms/avm/txs"
    34  	"github.com/MetalBlockchain/metalgo/vms/avm/txs/txstest"
    35  	"github.com/MetalBlockchain/metalgo/vms/components/avax"
    36  	"github.com/MetalBlockchain/metalgo/vms/nftfx"
    37  	"github.com/MetalBlockchain/metalgo/vms/secp256k1fx"
    38  
    39  	avajson "github.com/MetalBlockchain/metalgo/utils/json"
    40  	keystoreutils "github.com/MetalBlockchain/metalgo/vms/components/keystore"
    41  )
    42  
    43  type fork uint8
    44  
    45  const (
    46  	durango fork = iota
    47  	eUpgrade
    48  
    49  	latest = durango
    50  
    51  	testTxFee    uint64 = 1000
    52  	startBalance uint64 = 50000
    53  
    54  	username       = "bobby"
    55  	password       = "StrnasfqewiurPasswdn56d" //#nosec G101
    56  	feeAssetName   = "TEST"
    57  	otherAssetName = "OTHER"
    58  )
    59  
    60  var (
    61  	testChangeAddr = ids.GenerateTestShortID()
    62  	testCases      = []struct {
    63  		name      string
    64  		avaxAsset bool
    65  	}{
    66  		{
    67  			name:      "genesis asset is AVAX",
    68  			avaxAsset: true,
    69  		},
    70  		{
    71  			name:      "genesis asset is TEST",
    72  			avaxAsset: false,
    73  		},
    74  	}
    75  
    76  	assetID = ids.ID{1, 2, 3}
    77  
    78  	keys  = secp256k1.TestKeys()[:3] // TODO: Remove [:3]
    79  	addrs []ids.ShortID              // addrs[i] corresponds to keys[i]
    80  )
    81  
    82  func init() {
    83  	addrs = make([]ids.ShortID, len(keys))
    84  	for i, key := range keys {
    85  		addrs[i] = key.Address()
    86  	}
    87  }
    88  
    89  type user struct {
    90  	username    string
    91  	password    string
    92  	initialKeys []*secp256k1.PrivateKey
    93  }
    94  
    95  type envConfig struct {
    96  	fork             fork
    97  	isCustomFeeAsset bool
    98  	keystoreUsers    []*user
    99  	vmStaticConfig   *config.Config
   100  	vmDynamicConfig  *Config
   101  	additionalFxs    []*common.Fx
   102  	notLinearized    bool
   103  	notBootstrapped  bool
   104  }
   105  
   106  type environment struct {
   107  	genesisBytes []byte
   108  	genesisTx    *txs.Tx
   109  	sharedMemory *atomic.Memory
   110  	issuer       chan common.Message
   111  	vm           *VM
   112  	txBuilder    *txstest.Builder
   113  }
   114  
   115  // setup the testing environment
   116  func setup(tb testing.TB, c *envConfig) *environment {
   117  	require := require.New(tb)
   118  
   119  	var (
   120  		genesisArgs *BuildGenesisArgs
   121  		assetName   = "AVAX"
   122  	)
   123  	if c.isCustomFeeAsset {
   124  		genesisArgs = makeCustomAssetGenesis(tb)
   125  		assetName = feeAssetName
   126  	} else {
   127  		genesisArgs = makeDefaultGenesis(tb)
   128  	}
   129  
   130  	genesisBytes := buildGenesisTestWithArgs(tb, genesisArgs)
   131  
   132  	ctx := snowtest.Context(tb, snowtest.XChainID)
   133  
   134  	baseDB := memdb.New()
   135  	m := atomic.NewMemory(prefixdb.New([]byte{0}, baseDB))
   136  	ctx.SharedMemory = m.NewSharedMemory(ctx.ChainID)
   137  
   138  	// NB: this lock is intentionally left locked when this function returns.
   139  	// The caller of this function is responsible for unlocking.
   140  	ctx.Lock.Lock()
   141  
   142  	userKeystore := keystore.New(logging.NoLog{}, memdb.New())
   143  	ctx.Keystore = userKeystore.NewBlockchainKeyStore(ctx.ChainID)
   144  
   145  	for _, user := range c.keystoreUsers {
   146  		require.NoError(userKeystore.CreateUser(user.username, user.password))
   147  
   148  		// Import the initially funded private keys
   149  		keystoreUser, err := keystoreutils.NewUserFromKeystore(ctx.Keystore, user.username, user.password)
   150  		require.NoError(err)
   151  
   152  		require.NoError(keystoreUser.PutKeys(user.initialKeys...))
   153  		require.NoError(keystoreUser.Close())
   154  	}
   155  
   156  	vmStaticConfig := staticConfig(tb, c.fork)
   157  	if c.vmStaticConfig != nil {
   158  		vmStaticConfig = *c.vmStaticConfig
   159  	}
   160  
   161  	vm := &VM{
   162  		Config: vmStaticConfig,
   163  	}
   164  
   165  	vmDynamicConfig := DefaultConfig
   166  	vmDynamicConfig.IndexTransactions = true
   167  	if c.vmDynamicConfig != nil {
   168  		vmDynamicConfig = *c.vmDynamicConfig
   169  	}
   170  	configBytes, err := json.Marshal(vmDynamicConfig)
   171  	require.NoError(err)
   172  
   173  	require.NoError(vm.Initialize(
   174  		context.Background(),
   175  		ctx,
   176  		prefixdb.New([]byte{1}, baseDB),
   177  		genesisBytes,
   178  		nil,
   179  		configBytes,
   180  		nil,
   181  		append(
   182  			[]*common.Fx{
   183  				{
   184  					ID: secp256k1fx.ID,
   185  					Fx: &secp256k1fx.Fx{},
   186  				},
   187  				{
   188  					ID: nftfx.ID,
   189  					Fx: &nftfx.Fx{},
   190  				},
   191  			},
   192  			c.additionalFxs...,
   193  		),
   194  		&common.SenderTest{},
   195  	))
   196  
   197  	stopVertexID := ids.GenerateTestID()
   198  	issuer := make(chan common.Message, 1)
   199  
   200  	env := &environment{
   201  		genesisBytes: genesisBytes,
   202  		genesisTx:    getCreateTxFromGenesisTest(tb, genesisBytes, assetName),
   203  		sharedMemory: m,
   204  		issuer:       issuer,
   205  		vm:           vm,
   206  		txBuilder:    txstest.New(vm.parser.Codec(), vm.ctx, &vm.Config, vm.feeAssetID, vm.state),
   207  	}
   208  
   209  	require.NoError(vm.SetState(context.Background(), snow.Bootstrapping))
   210  	if c.notLinearized {
   211  		return env
   212  	}
   213  
   214  	require.NoError(vm.Linearize(context.Background(), stopVertexID, issuer))
   215  	if c.notBootstrapped {
   216  		return env
   217  	}
   218  
   219  	require.NoError(vm.SetState(context.Background(), snow.NormalOp))
   220  
   221  	tb.Cleanup(func() {
   222  		env.vm.ctx.Lock.Lock()
   223  		defer env.vm.ctx.Lock.Unlock()
   224  
   225  		require.NoError(env.vm.Shutdown(context.Background()))
   226  	})
   227  
   228  	return env
   229  }
   230  
   231  func staticConfig(tb testing.TB, f fork) config.Config {
   232  	c := config.Config{
   233  		TxFee:            testTxFee,
   234  		CreateAssetTxFee: testTxFee,
   235  		EUpgradeTime:     mockable.MaxTime,
   236  	}
   237  
   238  	switch f {
   239  	case eUpgrade:
   240  		c.EUpgradeTime = time.Time{}
   241  	case durango:
   242  	default:
   243  		require.FailNow(tb, "unhandled fork", f)
   244  	}
   245  
   246  	return c
   247  }
   248  
   249  // Returns:
   250  //
   251  //  1. tx in genesis that creates asset
   252  //  2. the index of the output
   253  func getCreateTxFromGenesisTest(tb testing.TB, genesisBytes []byte, assetName string) *txs.Tx {
   254  	require := require.New(tb)
   255  
   256  	parser, err := txs.NewParser(
   257  		[]fxs.Fx{
   258  			&secp256k1fx.Fx{},
   259  		},
   260  	)
   261  	require.NoError(err)
   262  
   263  	cm := parser.GenesisCodec()
   264  	genesis := Genesis{}
   265  	_, err = cm.Unmarshal(genesisBytes, &genesis)
   266  	require.NoError(err)
   267  	require.NotEmpty(genesis.Txs)
   268  
   269  	var assetTx *GenesisAsset
   270  	for _, tx := range genesis.Txs {
   271  		if tx.Name == assetName {
   272  			assetTx = tx
   273  			break
   274  		}
   275  	}
   276  	require.NotNil(assetTx)
   277  
   278  	tx := &txs.Tx{
   279  		Unsigned: &assetTx.CreateAssetTx,
   280  	}
   281  	require.NoError(tx.Initialize(parser.GenesisCodec()))
   282  	return tx
   283  }
   284  
   285  // buildGenesisTest is the common Genesis builder for most tests
   286  func buildGenesisTest(tb testing.TB) []byte {
   287  	defaultArgs := makeDefaultGenesis(tb)
   288  	return buildGenesisTestWithArgs(tb, defaultArgs)
   289  }
   290  
   291  // buildGenesisTestWithArgs allows building the genesis while injecting different starting points (args)
   292  func buildGenesisTestWithArgs(tb testing.TB, args *BuildGenesisArgs) []byte {
   293  	require := require.New(tb)
   294  
   295  	ss := CreateStaticService()
   296  
   297  	reply := BuildGenesisReply{}
   298  	require.NoError(ss.BuildGenesis(nil, args, &reply))
   299  
   300  	b, err := formatting.Decode(reply.Encoding, reply.Bytes)
   301  	require.NoError(err)
   302  	return b
   303  }
   304  
   305  func newTx(tb testing.TB, genesisBytes []byte, chainID ids.ID, parser txs.Parser, assetName string) *txs.Tx {
   306  	require := require.New(tb)
   307  
   308  	createTx := getCreateTxFromGenesisTest(tb, genesisBytes, assetName)
   309  	tx := &txs.Tx{Unsigned: &txs.BaseTx{
   310  		BaseTx: avax.BaseTx{
   311  			NetworkID:    constants.UnitTestID,
   312  			BlockchainID: chainID,
   313  			Ins: []*avax.TransferableInput{{
   314  				UTXOID: avax.UTXOID{
   315  					TxID:        createTx.ID(),
   316  					OutputIndex: 2,
   317  				},
   318  				Asset: avax.Asset{ID: createTx.ID()},
   319  				In: &secp256k1fx.TransferInput{
   320  					Amt: startBalance,
   321  					Input: secp256k1fx.Input{
   322  						SigIndices: []uint32{
   323  							0,
   324  						},
   325  					},
   326  				},
   327  			}},
   328  		},
   329  	}}
   330  	require.NoError(tx.SignSECP256K1Fx(parser.Codec(), [][]*secp256k1.PrivateKey{{keys[0]}}))
   331  	return tx
   332  }
   333  
   334  // Sample from a set of addresses and return them raw and formatted as strings.
   335  // The size of the sample is between 1 and len(addrs)
   336  // If len(addrs) == 0, returns nil
   337  func sampleAddrs(tb testing.TB, addressFormatter avax.AddressManager, addrs []ids.ShortID) ([]ids.ShortID, []string) {
   338  	require := require.New(tb)
   339  
   340  	sampledAddrs := []ids.ShortID{}
   341  	sampledAddrsStr := []string{}
   342  
   343  	sampler := sampler.NewUniform()
   344  	sampler.Initialize(uint64(len(addrs)))
   345  
   346  	numAddrs := 1 + rand.Intn(len(addrs)) // #nosec G404
   347  	indices, ok := sampler.Sample(numAddrs)
   348  	require.True(ok)
   349  	for _, index := range indices {
   350  		addr := addrs[index]
   351  		addrStr, err := addressFormatter.FormatLocalAddress(addr)
   352  		require.NoError(err)
   353  
   354  		sampledAddrs = append(sampledAddrs, addr)
   355  		sampledAddrsStr = append(sampledAddrsStr, addrStr)
   356  	}
   357  	return sampledAddrs, sampledAddrsStr
   358  }
   359  
   360  func makeDefaultGenesis(tb testing.TB) *BuildGenesisArgs {
   361  	require := require.New(tb)
   362  
   363  	addr0Str, err := address.FormatBech32(constants.UnitTestHRP, addrs[0].Bytes())
   364  	require.NoError(err)
   365  
   366  	addr1Str, err := address.FormatBech32(constants.UnitTestHRP, addrs[1].Bytes())
   367  	require.NoError(err)
   368  
   369  	addr2Str, err := address.FormatBech32(constants.UnitTestHRP, addrs[2].Bytes())
   370  	require.NoError(err)
   371  
   372  	return &BuildGenesisArgs{
   373  		Encoding: formatting.Hex,
   374  		GenesisData: map[string]AssetDefinition{
   375  			"asset1": {
   376  				Name:   "AVAX",
   377  				Symbol: "SYMB",
   378  				InitialState: map[string][]interface{}{
   379  					"fixedCap": {
   380  						Holder{
   381  							Amount:  avajson.Uint64(startBalance),
   382  							Address: addr0Str,
   383  						},
   384  						Holder{
   385  							Amount:  avajson.Uint64(startBalance),
   386  							Address: addr1Str,
   387  						},
   388  						Holder{
   389  							Amount:  avajson.Uint64(startBalance),
   390  							Address: addr2Str,
   391  						},
   392  					},
   393  				},
   394  			},
   395  			"asset2": {
   396  				Name:   "myVarCapAsset",
   397  				Symbol: "MVCA",
   398  				InitialState: map[string][]interface{}{
   399  					"variableCap": {
   400  						Owners{
   401  							Threshold: 1,
   402  							Minters: []string{
   403  								addr0Str,
   404  								addr1Str,
   405  							},
   406  						},
   407  						Owners{
   408  							Threshold: 2,
   409  							Minters: []string{
   410  								addr0Str,
   411  								addr1Str,
   412  								addr2Str,
   413  							},
   414  						},
   415  					},
   416  				},
   417  			},
   418  			"asset3": {
   419  				Name: "myOtherVarCapAsset",
   420  				InitialState: map[string][]interface{}{
   421  					"variableCap": {
   422  						Owners{
   423  							Threshold: 1,
   424  							Minters: []string{
   425  								addr0Str,
   426  							},
   427  						},
   428  					},
   429  				},
   430  			},
   431  			"asset4": {
   432  				Name: "myFixedCapAsset",
   433  				InitialState: map[string][]interface{}{
   434  					"fixedCap": {
   435  						Holder{
   436  							Amount:  avajson.Uint64(startBalance),
   437  							Address: addr0Str,
   438  						},
   439  						Holder{
   440  							Amount:  avajson.Uint64(startBalance),
   441  							Address: addr1Str,
   442  						},
   443  					},
   444  				},
   445  			},
   446  		},
   447  	}
   448  }
   449  
   450  func makeCustomAssetGenesis(tb testing.TB) *BuildGenesisArgs {
   451  	require := require.New(tb)
   452  
   453  	addr0Str, err := address.FormatBech32(constants.UnitTestHRP, addrs[0].Bytes())
   454  	require.NoError(err)
   455  
   456  	addr1Str, err := address.FormatBech32(constants.UnitTestHRP, addrs[1].Bytes())
   457  	require.NoError(err)
   458  
   459  	addr2Str, err := address.FormatBech32(constants.UnitTestHRP, addrs[2].Bytes())
   460  	require.NoError(err)
   461  
   462  	return &BuildGenesisArgs{
   463  		Encoding: formatting.Hex,
   464  		GenesisData: map[string]AssetDefinition{
   465  			"asset1": {
   466  				Name:   feeAssetName,
   467  				Symbol: "TST",
   468  				InitialState: map[string][]interface{}{
   469  					"fixedCap": {
   470  						Holder{
   471  							Amount:  avajson.Uint64(startBalance),
   472  							Address: addr0Str,
   473  						},
   474  						Holder{
   475  							Amount:  avajson.Uint64(startBalance),
   476  							Address: addr1Str,
   477  						},
   478  						Holder{
   479  							Amount:  avajson.Uint64(startBalance),
   480  							Address: addr2Str,
   481  						},
   482  					},
   483  				},
   484  			},
   485  			"asset2": {
   486  				Name:   otherAssetName,
   487  				Symbol: "OTH",
   488  				InitialState: map[string][]interface{}{
   489  					"fixedCap": {
   490  						Holder{
   491  							Amount:  avajson.Uint64(startBalance),
   492  							Address: addr0Str,
   493  						},
   494  						Holder{
   495  							Amount:  avajson.Uint64(startBalance),
   496  							Address: addr1Str,
   497  						},
   498  						Holder{
   499  							Amount:  avajson.Uint64(startBalance),
   500  							Address: addr2Str,
   501  						},
   502  					},
   503  				},
   504  			},
   505  		},
   506  	}
   507  }
   508  
   509  // issueAndAccept expects the context lock not to be held
   510  func issueAndAccept(
   511  	require *require.Assertions,
   512  	vm *VM,
   513  	issuer <-chan common.Message,
   514  	tx *txs.Tx,
   515  ) {
   516  	txID, err := vm.issueTxFromRPC(tx)
   517  	require.NoError(err)
   518  	require.Equal(tx.ID(), txID)
   519  
   520  	buildAndAccept(require, vm, issuer, txID)
   521  }
   522  
   523  // buildAndAccept expects the context lock not to be held
   524  func buildAndAccept(
   525  	require *require.Assertions,
   526  	vm *VM,
   527  	issuer <-chan common.Message,
   528  	txID ids.ID,
   529  ) {
   530  	require.Equal(common.PendingTxs, <-issuer)
   531  
   532  	vm.ctx.Lock.Lock()
   533  	defer vm.ctx.Lock.Unlock()
   534  
   535  	blkIntf, err := vm.BuildBlock(context.Background())
   536  	require.NoError(err)
   537  	require.IsType(&executor.Block{}, blkIntf)
   538  
   539  	blk := blkIntf.(*executor.Block)
   540  	txs := blk.Txs()
   541  	require.Len(txs, 1)
   542  
   543  	issuedTx := txs[0]
   544  	require.Equal(txID, issuedTx.ID())
   545  	require.NoError(blk.Verify(context.Background()))
   546  	require.NoError(vm.SetPreference(context.Background(), blk.ID()))
   547  	require.NoError(blk.Accept(context.Background()))
   548  }