forked from TrueCloudLab/neoneo-go
6b105e6d10
GAS and NEO tokens are sent to validators account (not the committee's one). For single-node chain they are the same, but for four-nodes chain they are different. Thus, use validators multisig address to create new accounts and to deploy contracts. Also, allow to provide desired account balance while creating new account.
368 lines
13 KiB
Go
368 lines
13 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/block"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
|
"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/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/emit"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Executor is a wrapper over chain state.
|
|
type Executor struct {
|
|
Chain blockchainer.Blockchainer
|
|
Validator Signer
|
|
Committee Signer
|
|
CommitteeHash util.Uint160
|
|
Contracts map[string]*Contract
|
|
}
|
|
|
|
// NewExecutor creates new executor instance from provided blockchain and committee.
|
|
func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, 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 block with the highest index.
|
|
func (e *Executor) TopBlock(t *testing.T) *block.Block {
|
|
b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(int(e.Chain.BlockHeight())))
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
// NativeHash returns native contract hash by name.
|
|
func (e *Executor) NativeHash(t *testing.T, name string) util.Uint160 {
|
|
h, err := e.Chain.GetNativeContractScriptHash(name)
|
|
require.NoError(t, err)
|
|
return h
|
|
}
|
|
|
|
// NativeID returns native contract ID by name.
|
|
func (e *Executor) NativeID(t *testing.T, name string) int32 {
|
|
h := e.NativeHash(t, name)
|
|
cs := e.Chain.GetContractState(h)
|
|
require.NotNil(t, cs)
|
|
return cs.ID
|
|
}
|
|
|
|
// NewUnsignedTx creates new unsigned transaction which invokes method of contract with hash.
|
|
func (e *Executor) NewUnsignedTx(t *testing.T, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
|
|
w := io.NewBufBinWriter()
|
|
emit.AppCall(w.BinWriter, hash, method, callflag.All, args...)
|
|
require.NoError(t, w.Err)
|
|
|
|
script := w.Bytes()
|
|
tx := transaction.New(script, 0)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
return tx
|
|
}
|
|
|
|
// NewTx creates new transaction which invokes contract method.
|
|
// Transaction is signed with signer.
|
|
func (e *Executor) NewTx(t *testing.T, 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 provided signers.
|
|
func (e *Executor) SignTx(t *testing.T, 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 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.T, 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 contract to bc. It also checks that
|
|
// precalculated contract hash matches the actual one.
|
|
// data is an optional argument to `_deploy`.
|
|
// Returns hash of the deploy transaction.
|
|
func (e *Executor) DeployContract(t *testing.T, c *Contract, data interface{}) util.Uint256 {
|
|
tx := e.NewDeployTx(t, e.Chain, c, data)
|
|
e.AddNewBlock(t, tx)
|
|
e.CheckHalt(t, tx.Hash())
|
|
|
|
// Check that 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 contract to bc. It checks that deploy
|
|
// transaction FAULTed with the specified error.
|
|
func (e *Executor) DeployContractCheckFAULT(t *testing.T, 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 transaction with the specified script to the chain and
|
|
// returns its hash. It does no faults check.
|
|
func (e *Executor) InvokeScript(t *testing.T, script []byte, signers []Signer) util.Uint256 {
|
|
tx := transaction.New(script, 0)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
e.SignTx(t, tx, -1, signers...)
|
|
e.AddNewBlock(t, tx)
|
|
return tx.Hash()
|
|
}
|
|
|
|
// InvokeScriptCheckHALT adds transaction with the specified script to the chain
|
|
// and checks it's HALTed with the specified items on stack.
|
|
func (e *Executor) InvokeScriptCheckHALT(t *testing.T, script []byte, signers []Signer, stack ...stackitem.Item) {
|
|
hash := e.InvokeScript(t, script, signers)
|
|
e.CheckHalt(t, hash, stack...)
|
|
}
|
|
|
|
// InvokeScriptCheckFAULT adds transaction with the specified script to the
|
|
// chain and checks it's FAULTed with the specified error.
|
|
func (e *Executor) InvokeScriptCheckFAULT(t *testing.T, script []byte, signers []Signer, errMessage string) {
|
|
hash := e.InvokeScript(t, script, signers)
|
|
e.CheckFault(t, hash, errMessage)
|
|
}
|
|
|
|
// CheckHalt checks that transaction persisted with HALT state.
|
|
func (e *Executor) CheckHalt(t *testing.T, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult {
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
require.NoError(t, err)
|
|
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
|
|
if len(stack) != 0 {
|
|
require.Equal(t, stack, aer[0].Stack)
|
|
}
|
|
return &aer[0]
|
|
}
|
|
|
|
// CheckFault checks that transaction persisted with FAULT state.
|
|
// Raised exception is also checked to contain s as a substring.
|
|
func (e *Executor) CheckFault(t *testing.T, h util.Uint256, s string) {
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
require.NoError(t, err)
|
|
require.Equal(t, vm.FaultState, aer[0].VMState)
|
|
require.True(t, strings.Contains(aer[0].FaultException, s),
|
|
"expected: %s, got: %s", s, aer[0].FaultException)
|
|
}
|
|
|
|
// CheckTxNotificationEvent checks that specified event was emitted at the specified position
|
|
// during transaction script execution. Negative index corresponds to backwards enumeration.
|
|
func (e *Executor) CheckTxNotificationEvent(t *testing.T, 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 provided account owns specified amount of GAS.
|
|
func (e *Executor) CheckGASBalance(t *testing.T, 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()))
|
|
}
|
|
|
|
// NewDeployTx returns new deployment tx for contract signed by committee.
|
|
func (e *Executor) NewDeployTx(t *testing.T, bc blockchainer.Blockchainer, 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)
|
|
|
|
buf := io.NewBufBinWriter()
|
|
emit.AppCall(buf.BinWriter, bc.ManagementContractHash(), "deploy", callflag.All, neb, rawManifest, data)
|
|
require.NoError(t, buf.Err)
|
|
|
|
tx := transaction.New(buf.Bytes(), 100*native.GASFactor)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: e.Validator.ScriptHash(),
|
|
Scopes: transaction.Global,
|
|
}}
|
|
addNetworkFee(bc, tx, e.Validator)
|
|
require.NoError(t, e.Validator.SignTx(netmode.UnitTestNet, tx))
|
|
return tx
|
|
}
|
|
|
|
func addSystemFee(bc blockchainer.Blockchainer, 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()
|
|
}
|
|
|
|
func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, signers ...Signer) {
|
|
baseFee := bc.GetPolicer().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 new unsigned block from txs.
|
|
func (e *Executor) NewUnsignedBlock(t *testing.T, 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 provided transactions and adds it on bc.
|
|
func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block {
|
|
b := e.NewUnsignedBlock(t, txs...)
|
|
e.SignBlock(b)
|
|
require.NoError(t, e.Chain.AddBlock(b))
|
|
return b
|
|
}
|
|
|
|
// GenerateNewBlocks adds specified number of empty blocks to the chain.
|
|
func (e *Executor) GenerateNewBlocks(t *testing.T, count int) {
|
|
for i := 0; i < count; i++ {
|
|
e.AddNewBlock(t)
|
|
}
|
|
}
|
|
|
|
// 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.T, 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 dummy block and executes transaction in it.
|
|
func TestInvoke(bc blockchainer.Blockchainer, 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 transaction hash which will set cached value.
|
|
// This is unwanted behaviour so we explicitly copy transaction to perform execution.
|
|
ttx := *tx
|
|
v, f := bc.GetTestVM(trigger.Application, &ttx, b)
|
|
defer f()
|
|
|
|
v.LoadWithFlags(tx.Script, callflag.All)
|
|
err = v.Run()
|
|
return v, err
|
|
}
|
|
|
|
// GetTransaction returns transaction and its height by the specified hash.
|
|
func (e *Executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) {
|
|
tx, height, err := e.Chain.GetTransaction(h)
|
|
require.NoError(t, err)
|
|
return tx, height
|
|
}
|
|
|
|
// GetBlockByIndex returns block by the specified index.
|
|
func (e *Executor) GetBlockByIndex(t *testing.T, 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.T, 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]
|
|
}
|