neo-go/pkg/neotest/basic.go
Anna Shaleva 6dbae7edc4 rpcclient: fix WS-client unsubscription process
Do not block subscribers until the unsubscription request to RPC server
is completed. Otherwise, another notification may be received from the
RPC server which will block the unsubscription process.

At the same time, fix event-based waiter. We must not block the receiver
channel during unsubscription because there's a chance that subsequent
event will be sent by the server. We need to read this event in order not
to block the WSClient's readloop.
2022-11-16 23:44:30 +03:00

409 lines
15 KiB
Go

package neotest
import (
"encoding/json"
"fmt"
"math/big"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"github.com/nspcc-dev/neo-go/pkg/core/fee"
"github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
// Executor is a wrapper over chain state.
type Executor struct {
Chain *core.Blockchain
Validator Signer
Committee Signer
CommitteeHash util.Uint160
Contracts map[string]*Contract
}
// NewExecutor creates a new executor instance from the provided blockchain and committee.
func NewExecutor(t testing.TB, bc *core.Blockchain, validator, committee Signer) *Executor {
checkMultiSigner(t, validator)
checkMultiSigner(t, committee)
return &Executor{
Chain: bc,
Validator: validator,
Committee: committee,
CommitteeHash: committee.ScriptHash(),
Contracts: make(map[string]*Contract),
}
}
// TopBlock returns the block with the highest index.
func (e *Executor) TopBlock(t testing.TB) *block.Block {
b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(int(e.Chain.BlockHeight())))
require.NoError(t, err)
return b
}
// NativeHash returns a native contract hash by the name.
func (e *Executor) NativeHash(t testing.TB, name string) util.Uint160 {
h, err := e.Chain.GetNativeContractScriptHash(name)
require.NoError(t, err)
return h
}
// ContractHash returns a contract hash by the ID.
func (e *Executor) ContractHash(t testing.TB, id int32) util.Uint160 {
h, err := e.Chain.GetContractScriptHash(id)
require.NoError(t, err)
return h
}
// NativeID returns a native contract ID by the name.
func (e *Executor) NativeID(t testing.TB, name string) int32 {
h := e.NativeHash(t, name)
cs := e.Chain.GetContractState(h)
require.NotNil(t, cs)
return cs.ID
}
// NewUnsignedTx creates a new unsigned transaction which invokes the method of the contract with the hash.
func (e *Executor) NewUnsignedTx(t testing.TB, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
script, err := smartcontract.CreateCallScript(hash, method, args...)
require.NoError(t, err)
tx := transaction.New(script, 0)
tx.Nonce = Nonce()
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
return tx
}
// NewTx creates a new transaction which invokes the contract method.
// The transaction is signed by the signers.
func (e *Executor) NewTx(t testing.TB, signers []Signer,
hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
tx := e.NewUnsignedTx(t, hash, method, args...)
return e.SignTx(t, tx, -1, signers...)
}
// SignTx signs a transaction using the provided signers.
func (e *Executor) SignTx(t testing.TB, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction {
for _, acc := range signers {
tx.Signers = append(tx.Signers, transaction.Signer{
Account: acc.ScriptHash(),
Scopes: transaction.Global,
})
}
AddNetworkFee(e.Chain, tx, signers...)
AddSystemFee(e.Chain, tx, sysFee)
for _, acc := range signers {
require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx))
}
return tx
}
// NewAccount returns a new signer holding 100.0 GAS (or given amount is specified).
// This method advances the chain by one block with a transfer transaction.
func (e *Executor) NewAccount(t testing.TB, expectedGASBalance ...int64) Signer {
acc, err := wallet.NewAccount()
require.NoError(t, err)
amount := int64(100_0000_0000)
if len(expectedGASBalance) != 0 {
amount = expectedGASBalance[0]
}
tx := e.NewTx(t, []Signer{e.Validator},
e.NativeHash(t, nativenames.Gas), "transfer",
e.Validator.ScriptHash(), acc.Contract.ScriptHash(), amount, nil)
e.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash())
return NewSingleSigner(acc)
}
// DeployContract compiles and deploys a contract to the bc. It also checks that
// the precalculated contract hash matches the actual one.
// data is an optional argument to `_deploy`.
// It returns the hash of the deploy transaction.
func (e *Executor) DeployContract(t testing.TB, c *Contract, data interface{}) util.Uint256 {
return e.DeployContractBy(t, e.Validator, c, data)
}
// DeployContractBy compiles and deploys a contract to the bc using the provided signer.
// It also checks that the precalculated contract hash matches the actual one.
// data is an optional argument to `_deploy`.
// It returns the hash of the deploy transaction.
func (e *Executor) DeployContractBy(t testing.TB, signer Signer, c *Contract, data interface{}) util.Uint256 {
tx := NewDeployTxBy(t, e.Chain, signer, c, data)
e.AddNewBlock(t, tx)
e.CheckHalt(t, tx.Hash())
// Check that the precalculated hash matches the real one.
e.CheckTxNotificationEvent(t, tx.Hash(), -1, state.NotificationEvent{
ScriptHash: e.NativeHash(t, nativenames.Management),
Name: "Deploy",
Item: stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(c.Hash.BytesBE()),
}),
})
return tx.Hash()
}
// DeployContractCheckFAULT compiles and deploys a contract to the bc using the validator
// account. It checks that the deploy transaction FAULTed with the specified error.
func (e *Executor) DeployContractCheckFAULT(t testing.TB, c *Contract, data interface{}, errMessage string) {
tx := e.NewDeployTx(t, e.Chain, c, data)
e.AddNewBlock(t, tx)
e.CheckFault(t, tx.Hash(), errMessage)
}
// InvokeScript adds a transaction with the specified script to the chain and
// returns its hash. It does no faults check.
func (e *Executor) InvokeScript(t testing.TB, script []byte, signers []Signer) util.Uint256 {
tx := e.PrepareInvocation(t, script, signers)
e.AddNewBlock(t, tx)
return tx.Hash()
}
// PrepareInvocation creates a transaction with the specified script and signs it
// by the provided signer.
func (e *Executor) PrepareInvocation(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) *transaction.Transaction {
tx := transaction.New(script, 0)
tx.Nonce = Nonce()
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
if len(validUntilBlock) != 0 {
tx.ValidUntilBlock = validUntilBlock[0]
}
e.SignTx(t, tx, -1, signers...)
return tx
}
// InvokeScriptCheckHALT adds a transaction with the specified script to the chain
// and checks if it's HALTed with the specified items on stack.
func (e *Executor) InvokeScriptCheckHALT(t testing.TB, script []byte, signers []Signer, stack ...stackitem.Item) {
hash := e.InvokeScript(t, script, signers)
e.CheckHalt(t, hash, stack...)
}
// InvokeScriptCheckFAULT adds a transaction with the specified script to the
// chain and checks if it's FAULTed with the specified error.
func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) util.Uint256 {
hash := e.InvokeScript(t, script, signers)
e.CheckFault(t, hash, errMessage)
return hash
}
// CheckHalt checks that the transaction is persisted with HALT state.
func (e *Executor) CheckHalt(t testing.TB, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult {
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
require.Equal(t, vmstate.Halt, aer[0].VMState, aer[0].FaultException)
if len(stack) != 0 {
require.Equal(t, stack, aer[0].Stack)
}
return &aer[0]
}
// CheckFault checks that the transaction is persisted with FAULT state.
// The raised exception is also checked to contain the s as a substring.
func (e *Executor) CheckFault(t testing.TB, h util.Uint256, s string) {
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
require.Equal(t, vmstate.Fault, aer[0].VMState)
require.True(t, strings.Contains(aer[0].FaultException, s),
"expected: %s, got: %s", s, aer[0].FaultException)
}
// CheckTxNotificationEvent checks that the specified event was emitted at the specified position
// during transaction script execution. Negative index corresponds to backwards enumeration.
func (e *Executor) CheckTxNotificationEvent(t testing.TB, h util.Uint256, index int, expected state.NotificationEvent) {
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
l := len(aer[0].Events)
if index < 0 {
index = l + index
}
require.True(t, 0 <= index && index < l, fmt.Errorf("notification index is out of range: want %d, len is %d", index, l))
require.Equal(t, expected, aer[0].Events[index])
}
// CheckGASBalance ensures that the provided account owns the specified amount of GAS.
func (e *Executor) CheckGASBalance(t testing.TB, acc util.Uint160, expected *big.Int) {
actual := e.Chain.GetUtilityTokenBalance(acc)
require.Equal(t, expected, actual, fmt.Errorf("invalid GAS balance: expected %s, got %s", expected.String(), actual.String()))
}
// EnsureGASBalance ensures that the provided account owns the amount of GAS that satisfies the provided condition.
func (e *Executor) EnsureGASBalance(t testing.TB, acc util.Uint160, isOk func(balance *big.Int) bool) {
actual := e.Chain.GetUtilityTokenBalance(acc)
require.True(t, isOk(actual), fmt.Errorf("invalid GAS balance: got %s, condition is not satisfied", actual.String()))
}
// NewDeployTx returns a new deployment tx for the contract signed by the committee.
func (e *Executor) NewDeployTx(t testing.TB, bc *core.Blockchain, c *Contract, data interface{}) *transaction.Transaction {
return NewDeployTxBy(t, bc, e.Validator, c, data)
}
// NewDeployTxBy returns a new deployment tx for the contract signed by the specified signer.
func NewDeployTxBy(t testing.TB, bc *core.Blockchain, signer Signer, c *Contract, data interface{}) *transaction.Transaction {
rawManifest, err := json.Marshal(c.Manifest)
require.NoError(t, err)
neb, err := c.NEF.Bytes()
require.NoError(t, err)
script, err := smartcontract.CreateCallScript(bc.ManagementContractHash(), "deploy", neb, rawManifest, data)
require.NoError(t, err)
tx := transaction.New(script, 100*native.GASFactor)
tx.Nonce = Nonce()
tx.ValidUntilBlock = bc.BlockHeight() + 1
tx.Signers = []transaction.Signer{{
Account: signer.ScriptHash(),
Scopes: transaction.Global,
}}
AddNetworkFee(bc, tx, signer)
require.NoError(t, signer.SignTx(netmode.UnitTestNet, tx))
return tx
}
// AddSystemFee adds system fee to the transaction. If negative value specified,
// then system fee is defined by test invocation.
func AddSystemFee(bc *core.Blockchain, tx *transaction.Transaction, sysFee int64) {
if sysFee >= 0 {
tx.SystemFee = sysFee
return
}
v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions
tx.SystemFee = v.GasConsumed()
}
// AddNetworkFee adds network fee to the transaction.
func AddNetworkFee(bc *core.Blockchain, tx *transaction.Transaction, signers ...Signer) {
baseFee := bc.GetBaseExecFee()
size := io.GetVarSize(tx)
for _, sgr := range signers {
netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script())
tx.NetworkFee += netFee
size += sizeDelta
}
tx.NetworkFee += int64(size) * bc.FeePerByte()
}
// NewUnsignedBlock creates a new unsigned block from txs.
func (e *Executor) NewUnsignedBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
lastBlock := e.TopBlock(t)
b := &block.Block{
Header: block.Header{
NextConsensus: e.Validator.ScriptHash(),
Script: transaction.Witness{
VerificationScript: e.Validator.Script(),
},
Timestamp: lastBlock.Timestamp + 1,
},
Transactions: txs,
}
if e.Chain.GetConfig().StateRootInHeader {
b.StateRootEnabled = true
b.PrevStateRoot = e.Chain.GetStateModule().CurrentLocalStateRoot()
}
b.PrevHash = lastBlock.Hash()
b.Index = e.Chain.BlockHeight() + 1
b.RebuildMerkleRoot()
return b
}
// AddNewBlock creates a new block from the provided transactions and adds it on the bc.
func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
b := e.NewUnsignedBlock(t, txs...)
e.SignBlock(b)
require.NoError(t, e.Chain.AddBlock(b))
return b
}
// GenerateNewBlocks adds the specified number of empty blocks to the chain.
func (e *Executor) GenerateNewBlocks(t testing.TB, count int) []*block.Block {
blocks := make([]*block.Block, count)
for i := 0; i < count; i++ {
blocks[i] = e.AddNewBlock(t)
}
return blocks
}
// SignBlock add validators signature to b.
func (e *Executor) SignBlock(b *block.Block) *block.Block {
invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b)
b.Script.InvocationScript = invoc
return b
}
// AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt.
func (e *Executor) AddBlockCheckHalt(t testing.TB, txs ...*transaction.Transaction) *block.Block {
b := e.AddNewBlock(t, txs...)
for _, tx := range txs {
e.CheckHalt(t, tx.Hash())
}
return b
}
// TestInvoke creates a test VM with a dummy block and executes a transaction in it.
func TestInvoke(bc *core.Blockchain, tx *transaction.Transaction) (*vm.VM, error) {
lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
if err != nil {
return nil, err
}
b := &block.Block{
Header: block.Header{
Index: bc.BlockHeight() + 1,
Timestamp: lastBlock.Timestamp + 1,
},
}
// `GetTestVM` as well as `Run` can use a transaction hash which will set a cached value.
// This is unwanted behavior, so we explicitly copy the transaction to perform execution.
ttx := *tx
ic, _ := bc.GetTestVM(trigger.Application, &ttx, b)
defer ic.Finalize()
ic.VM.LoadWithFlags(tx.Script, callflag.All)
err = ic.VM.Run()
return ic.VM, err
}
// GetTransaction returns a transaction and its height by the specified hash.
func (e *Executor) GetTransaction(t testing.TB, h util.Uint256) (*transaction.Transaction, uint32) {
tx, height, err := e.Chain.GetTransaction(h)
require.NoError(t, err)
return tx, height
}
// GetBlockByIndex returns a block by the specified index.
func (e *Executor) GetBlockByIndex(t testing.TB, idx int) *block.Block {
h := e.Chain.GetHeaderHash(idx)
require.NotEmpty(t, h)
b, err := e.Chain.GetBlock(h)
require.NoError(t, err)
return b
}
// GetTxExecResult returns application execution results for the specified transaction.
func (e *Executor) GetTxExecResult(t testing.TB, h util.Uint256) *state.AppExecResult {
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
require.NoError(t, err)
require.Equal(t, 1, len(aer))
return &aer[0]
}