github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/neotest/basic.go (about)

     1  package neotest
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math/big"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
    11  	"github.com/nspcc-dev/neo-go/pkg/core"
    12  	"github.com/nspcc-dev/neo-go/pkg/core/block"
    13  	"github.com/nspcc-dev/neo-go/pkg/core/fee"
    14  	"github.com/nspcc-dev/neo-go/pkg/core/native"
    15  	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
    16  	"github.com/nspcc-dev/neo-go/pkg/core/state"
    17  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    18  	"github.com/nspcc-dev/neo-go/pkg/io"
    19  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    20  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
    21  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
    22  	"github.com/nspcc-dev/neo-go/pkg/util"
    23  	"github.com/nspcc-dev/neo-go/pkg/vm"
    24  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    25  	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
    26  	"github.com/nspcc-dev/neo-go/pkg/wallet"
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  // Executor is a wrapper over chain state.
    31  type Executor struct {
    32  	Chain         *core.Blockchain
    33  	Validator     Signer
    34  	Committee     Signer
    35  	CommitteeHash util.Uint160
    36  	Contracts     map[string]*Contract
    37  }
    38  
    39  // NewExecutor creates a new executor instance from the provided blockchain and committee.
    40  func NewExecutor(t testing.TB, bc *core.Blockchain, validator, committee Signer) *Executor {
    41  	checkMultiSigner(t, validator)
    42  	checkMultiSigner(t, committee)
    43  
    44  	return &Executor{
    45  		Chain:         bc,
    46  		Validator:     validator,
    47  		Committee:     committee,
    48  		CommitteeHash: committee.ScriptHash(),
    49  		Contracts:     make(map[string]*Contract),
    50  	}
    51  }
    52  
    53  // TopBlock returns the block with the highest index.
    54  func (e *Executor) TopBlock(t testing.TB) *block.Block {
    55  	b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(e.Chain.BlockHeight()))
    56  	require.NoError(t, err)
    57  	return b
    58  }
    59  
    60  // NativeHash returns a native contract hash by the name.
    61  func (e *Executor) NativeHash(t testing.TB, name string) util.Uint160 {
    62  	h, err := e.Chain.GetNativeContractScriptHash(name)
    63  	require.NoError(t, err)
    64  	return h
    65  }
    66  
    67  // ContractHash returns a contract hash by the ID.
    68  func (e *Executor) ContractHash(t testing.TB, id int32) util.Uint160 {
    69  	h, err := e.Chain.GetContractScriptHash(id)
    70  	require.NoError(t, err)
    71  	return h
    72  }
    73  
    74  // NativeID returns a native contract ID by the name.
    75  func (e *Executor) NativeID(t testing.TB, name string) int32 {
    76  	h := e.NativeHash(t, name)
    77  	cs := e.Chain.GetContractState(h)
    78  	require.NotNil(t, cs)
    79  	return cs.ID
    80  }
    81  
    82  // NewUnsignedTx creates a new unsigned transaction which invokes the method of the contract with the hash.
    83  func (e *Executor) NewUnsignedTx(t testing.TB, hash util.Uint160, method string, args ...any) *transaction.Transaction {
    84  	script, err := smartcontract.CreateCallScript(hash, method, args...)
    85  	require.NoError(t, err)
    86  
    87  	tx := transaction.New(script, 0)
    88  	tx.Nonce = Nonce()
    89  	tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
    90  	return tx
    91  }
    92  
    93  // NewTx creates a new transaction which invokes the contract method.
    94  // The transaction is signed by the signers.
    95  func (e *Executor) NewTx(t testing.TB, signers []Signer,
    96  	hash util.Uint160, method string, args ...any) *transaction.Transaction {
    97  	tx := e.NewUnsignedTx(t, hash, method, args...)
    98  	return e.SignTx(t, tx, -1, signers...)
    99  }
   100  
   101  // SignTx signs a transaction using the provided signers.
   102  func (e *Executor) SignTx(t testing.TB, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction {
   103  	for _, acc := range signers {
   104  		tx.Signers = append(tx.Signers, transaction.Signer{
   105  			Account: acc.ScriptHash(),
   106  			Scopes:  transaction.Global,
   107  		})
   108  	}
   109  	AddNetworkFee(t, e.Chain, tx, signers...)
   110  	AddSystemFee(e.Chain, tx, sysFee)
   111  
   112  	for _, acc := range signers {
   113  		require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx))
   114  	}
   115  	return tx
   116  }
   117  
   118  // NewAccount returns a new signer holding 100.0 GAS (or given amount is specified).
   119  // This method advances the chain by one block with a transfer transaction.
   120  func (e *Executor) NewAccount(t testing.TB, expectedGASBalance ...int64) Signer {
   121  	acc, err := wallet.NewAccount()
   122  	require.NoError(t, err)
   123  
   124  	amount := int64(100_0000_0000)
   125  	if len(expectedGASBalance) != 0 {
   126  		amount = expectedGASBalance[0]
   127  	}
   128  	tx := e.NewTx(t, []Signer{e.Validator},
   129  		e.NativeHash(t, nativenames.Gas), "transfer",
   130  		e.Validator.ScriptHash(), acc.Contract.ScriptHash(), amount, nil)
   131  	e.AddNewBlock(t, tx)
   132  	e.CheckHalt(t, tx.Hash())
   133  	return NewSingleSigner(acc)
   134  }
   135  
   136  // DeployContract compiles and deploys a contract to the bc. It also checks that
   137  // the precalculated contract hash matches the actual one.
   138  // data is an optional argument to `_deploy`.
   139  // It returns the hash of the deploy transaction.
   140  func (e *Executor) DeployContract(t testing.TB, c *Contract, data any) util.Uint256 {
   141  	return e.DeployContractBy(t, e.Validator, c, data)
   142  }
   143  
   144  // DeployContractBy compiles and deploys a contract to the bc using the provided signer.
   145  // It also checks that the precalculated contract hash matches the actual one.
   146  // data is an optional argument to `_deploy`.
   147  // It returns the hash of the deploy transaction.
   148  func (e *Executor) DeployContractBy(t testing.TB, signer Signer, c *Contract, data any) util.Uint256 {
   149  	tx := NewDeployTxBy(t, e.Chain, signer, c, data)
   150  	e.AddNewBlock(t, tx)
   151  	e.CheckHalt(t, tx.Hash())
   152  
   153  	// Check that the precalculated hash matches the real one.
   154  	e.CheckTxNotificationEvent(t, tx.Hash(), -1, state.NotificationEvent{
   155  		ScriptHash: e.NativeHash(t, nativenames.Management),
   156  		Name:       "Deploy",
   157  		Item: stackitem.NewArray([]stackitem.Item{
   158  			stackitem.NewByteArray(c.Hash.BytesBE()),
   159  		}),
   160  	})
   161  
   162  	return tx.Hash()
   163  }
   164  
   165  // DeployContractCheckFAULT compiles and deploys a contract to the bc using the validator
   166  // account. It checks that the deploy transaction FAULTed with the specified error.
   167  func (e *Executor) DeployContractCheckFAULT(t testing.TB, c *Contract, data any, errMessage string) {
   168  	tx := e.NewDeployTx(t, e.Chain, c, data)
   169  	e.AddNewBlock(t, tx)
   170  	e.CheckFault(t, tx.Hash(), errMessage)
   171  }
   172  
   173  // InvokeScript adds a transaction with the specified script to the chain and
   174  // returns its hash. It does no faults check.
   175  func (e *Executor) InvokeScript(t testing.TB, script []byte, signers []Signer) util.Uint256 {
   176  	tx := e.PrepareInvocation(t, script, signers)
   177  	e.AddNewBlock(t, tx)
   178  	return tx.Hash()
   179  }
   180  
   181  // PrepareInvocation creates a transaction with the specified script and signs it
   182  // by the provided signer.
   183  func (e *Executor) PrepareInvocation(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) *transaction.Transaction {
   184  	tx := e.PrepareInvocationNoSign(t, script, validUntilBlock...)
   185  	e.SignTx(t, tx, -1, signers...)
   186  	return tx
   187  }
   188  
   189  func (e *Executor) PrepareInvocationNoSign(t testing.TB, script []byte, validUntilBlock ...uint32) *transaction.Transaction {
   190  	tx := transaction.New(script, 0)
   191  	tx.Nonce = Nonce()
   192  	tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
   193  	if len(validUntilBlock) != 0 {
   194  		tx.ValidUntilBlock = validUntilBlock[0]
   195  	}
   196  	return tx
   197  }
   198  
   199  // InvokeScriptCheckHALT adds a transaction with the specified script to the chain
   200  // and checks if it's HALTed with the specified items on stack.
   201  func (e *Executor) InvokeScriptCheckHALT(t testing.TB, script []byte, signers []Signer, stack ...stackitem.Item) {
   202  	hash := e.InvokeScript(t, script, signers)
   203  	e.CheckHalt(t, hash, stack...)
   204  }
   205  
   206  // InvokeScriptCheckFAULT adds a transaction with the specified script to the
   207  // chain and checks if it's FAULTed with the specified error.
   208  func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) util.Uint256 {
   209  	hash := e.InvokeScript(t, script, signers)
   210  	e.CheckFault(t, hash, errMessage)
   211  	return hash
   212  }
   213  
   214  // CheckHalt checks that the transaction is persisted with HALT state.
   215  func (e *Executor) CheckHalt(t testing.TB, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult {
   216  	aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
   217  	require.NoError(t, err)
   218  	require.Equal(t, vmstate.Halt, aer[0].VMState, aer[0].FaultException)
   219  	if len(stack) != 0 {
   220  		require.Equal(t, stack, aer[0].Stack)
   221  	}
   222  	return &aer[0]
   223  }
   224  
   225  // CheckFault checks that the transaction is persisted with FAULT state.
   226  // The raised exception is also checked to contain the s as a substring.
   227  func (e *Executor) CheckFault(t testing.TB, h util.Uint256, s string) {
   228  	aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
   229  	require.NoError(t, err)
   230  	require.Equal(t, vmstate.Fault, aer[0].VMState)
   231  	require.True(t, strings.Contains(aer[0].FaultException, s),
   232  		"expected: %s, got: %s", s, aer[0].FaultException)
   233  }
   234  
   235  // CheckTxNotificationEvent checks that the specified event was emitted at the specified position
   236  // during transaction script execution. Negative index corresponds to backwards enumeration.
   237  func (e *Executor) CheckTxNotificationEvent(t testing.TB, h util.Uint256, index int, expected state.NotificationEvent) {
   238  	aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
   239  	require.NoError(t, err)
   240  	l := len(aer[0].Events)
   241  	if index < 0 {
   242  		index = l + index
   243  	}
   244  	require.True(t, 0 <= index && index < l, fmt.Errorf("notification index is out of range: want %d, len is %d", index, l))
   245  	require.Equal(t, expected, aer[0].Events[index])
   246  }
   247  
   248  // CheckGASBalance ensures that the provided account owns the specified amount of GAS.
   249  func (e *Executor) CheckGASBalance(t testing.TB, acc util.Uint160, expected *big.Int) {
   250  	actual := e.Chain.GetUtilityTokenBalance(acc)
   251  	require.Equal(t, expected, actual, fmt.Errorf("invalid GAS balance: expected %s, got %s", expected.String(), actual.String()))
   252  }
   253  
   254  // EnsureGASBalance ensures that the provided account owns the amount of GAS that satisfies the provided condition.
   255  func (e *Executor) EnsureGASBalance(t testing.TB, acc util.Uint160, isOk func(balance *big.Int) bool) {
   256  	actual := e.Chain.GetUtilityTokenBalance(acc)
   257  	require.True(t, isOk(actual), fmt.Errorf("invalid GAS balance: got %s, condition is not satisfied", actual.String()))
   258  }
   259  
   260  // NewDeployTx returns a new deployment tx for the contract signed by the committee.
   261  func (e *Executor) NewDeployTx(t testing.TB, bc *core.Blockchain, c *Contract, data any) *transaction.Transaction {
   262  	return NewDeployTxBy(t, bc, e.Validator, c, data)
   263  }
   264  
   265  // NewDeployTxBy returns a new deployment tx for the contract signed by the specified signer.
   266  func NewDeployTxBy(t testing.TB, bc *core.Blockchain, signer Signer, c *Contract, data any) *transaction.Transaction {
   267  	rawManifest, err := json.Marshal(c.Manifest)
   268  	require.NoError(t, err)
   269  
   270  	neb, err := c.NEF.Bytes()
   271  	require.NoError(t, err)
   272  
   273  	script, err := smartcontract.CreateCallScript(bc.ManagementContractHash(), "deploy", neb, rawManifest, data)
   274  	require.NoError(t, err)
   275  
   276  	tx := transaction.New(script, 100*native.GASFactor)
   277  	tx.Nonce = Nonce()
   278  	tx.ValidUntilBlock = bc.BlockHeight() + 1
   279  	tx.Signers = []transaction.Signer{{
   280  		Account: signer.ScriptHash(),
   281  		Scopes:  transaction.Global,
   282  	}}
   283  	AddNetworkFee(t, bc, tx, signer)
   284  	require.NoError(t, signer.SignTx(netmode.UnitTestNet, tx))
   285  	return tx
   286  }
   287  
   288  // AddSystemFee adds system fee to the transaction. If negative value specified,
   289  // then system fee is defined by test invocation.
   290  func AddSystemFee(bc *core.Blockchain, tx *transaction.Transaction, sysFee int64) {
   291  	if sysFee >= 0 {
   292  		tx.SystemFee = sysFee
   293  		return
   294  	}
   295  	v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions
   296  	tx.SystemFee = v.GasConsumed()
   297  }
   298  
   299  // AddNetworkFee adds network fee to the transaction.
   300  func AddNetworkFee(t testing.TB, bc *core.Blockchain, tx *transaction.Transaction, signers ...Signer) {
   301  	baseFee := bc.GetBaseExecFee()
   302  	size := io.GetVarSize(tx)
   303  	for _, sgr := range signers {
   304  		csgr, ok := sgr.(SingleSigner)
   305  		if ok && csgr.Account().Contract.InvocationBuilder != nil {
   306  			sc, err := csgr.Account().Contract.InvocationBuilder(tx)
   307  			require.NoError(t, err)
   308  
   309  			txCopy := *tx
   310  			ic, err := bc.GetTestVM(trigger.Verification, &txCopy, nil)
   311  			require.NoError(t, err)
   312  
   313  			ic.UseSigners(tx.Signers)
   314  			ic.VM.GasLimit = bc.GetMaxVerificationGAS()
   315  
   316  			require.NoError(t, bc.InitVerificationContext(ic, csgr.ScriptHash(), &transaction.Witness{InvocationScript: sc, VerificationScript: csgr.Script()}))
   317  			require.NoError(t, ic.VM.Run())
   318  
   319  			tx.NetworkFee += ic.VM.GasConsumed()
   320  			size += io.GetVarSize(sc) + io.GetVarSize(csgr.Script())
   321  		} else {
   322  			netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script())
   323  			tx.NetworkFee += netFee
   324  			size += sizeDelta
   325  		}
   326  	}
   327  	tx.NetworkFee += int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(tx)
   328  }
   329  
   330  // NewUnsignedBlock creates a new unsigned block from txs.
   331  func (e *Executor) NewUnsignedBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
   332  	lastBlock := e.TopBlock(t)
   333  	b := &block.Block{
   334  		Header: block.Header{
   335  			NextConsensus: e.Validator.ScriptHash(),
   336  			Script: transaction.Witness{
   337  				VerificationScript: e.Validator.Script(),
   338  			},
   339  			Timestamp: lastBlock.Timestamp + 1,
   340  		},
   341  		Transactions: txs,
   342  	}
   343  	if e.Chain.GetConfig().StateRootInHeader {
   344  		b.StateRootEnabled = true
   345  		b.PrevStateRoot = e.Chain.GetStateModule().CurrentLocalStateRoot()
   346  	}
   347  	b.PrevHash = lastBlock.Hash()
   348  	b.Index = e.Chain.BlockHeight() + 1
   349  	b.RebuildMerkleRoot()
   350  	return b
   351  }
   352  
   353  // AddNewBlock creates a new block from the provided transactions and adds it on the bc.
   354  func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
   355  	b := e.NewUnsignedBlock(t, txs...)
   356  	e.SignBlock(b)
   357  	require.NoError(t, e.Chain.AddBlock(b))
   358  	return b
   359  }
   360  
   361  // GenerateNewBlocks adds the specified number of empty blocks to the chain.
   362  func (e *Executor) GenerateNewBlocks(t testing.TB, count int) []*block.Block {
   363  	blocks := make([]*block.Block, count)
   364  	for i := 0; i < count; i++ {
   365  		blocks[i] = e.AddNewBlock(t)
   366  	}
   367  	return blocks
   368  }
   369  
   370  // SignBlock add validators signature to b.
   371  func (e *Executor) SignBlock(b *block.Block) *block.Block {
   372  	invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b)
   373  	b.Script.InvocationScript = invoc
   374  	return b
   375  }
   376  
   377  // AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt.
   378  func (e *Executor) AddBlockCheckHalt(t testing.TB, txs ...*transaction.Transaction) *block.Block {
   379  	b := e.AddNewBlock(t, txs...)
   380  	for _, tx := range txs {
   381  		e.CheckHalt(t, tx.Hash())
   382  	}
   383  	return b
   384  }
   385  
   386  // TestInvoke creates a test VM with a dummy block and executes a transaction in it.
   387  func TestInvoke(bc *core.Blockchain, tx *transaction.Transaction) (*vm.VM, error) {
   388  	lastBlock, err := bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight()))
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  	b := &block.Block{
   393  		Header: block.Header{
   394  			Index:     bc.BlockHeight() + 1,
   395  			Timestamp: lastBlock.Timestamp + 1,
   396  		},
   397  	}
   398  
   399  	// `GetTestVM` as well as `Run` can use a transaction hash which will set a cached value.
   400  	// This is unwanted behavior, so we explicitly copy the transaction to perform execution.
   401  	ttx := *tx
   402  	ic, _ := bc.GetTestVM(trigger.Application, &ttx, b)
   403  
   404  	defer ic.Finalize()
   405  
   406  	ic.VM.LoadWithFlags(tx.Script, callflag.All)
   407  	err = ic.VM.Run()
   408  	return ic.VM, err
   409  }
   410  
   411  // GetTransaction returns a transaction and its height by the specified hash.
   412  func (e *Executor) GetTransaction(t testing.TB, h util.Uint256) (*transaction.Transaction, uint32) {
   413  	tx, height, err := e.Chain.GetTransaction(h)
   414  	require.NoError(t, err)
   415  	return tx, height
   416  }
   417  
   418  // GetBlockByIndex returns a block by the specified index.
   419  func (e *Executor) GetBlockByIndex(t testing.TB, idx uint32) *block.Block {
   420  	h := e.Chain.GetHeaderHash(idx)
   421  	require.NotEmpty(t, h)
   422  	b, err := e.Chain.GetBlock(h)
   423  	require.NoError(t, err)
   424  	return b
   425  }
   426  
   427  // GetTxExecResult returns application execution results for the specified transaction.
   428  func (e *Executor) GetTxExecResult(t testing.TB, h util.Uint256) *state.AppExecResult {
   429  	aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
   430  	require.NoError(t, err)
   431  	require.Equal(t, 1, len(aer))
   432  	return &aer[0]
   433  }