293 lines
9.4 KiB
Go
293 lines
9.4 KiB
Go
|
package tests
|
||
|
|
||
|
import (
|
||
|
"encoding/hex"
|
||
|
"encoding/json"
|
||
|
"path"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
|
||
|
"github.com/nspcc-dev/neo-go/cli/smartcontract"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||
|
"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/state"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||
|
"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/manifest"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
||
|
"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/opcode"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
"go.uber.org/zap/zaptest"
|
||
|
)
|
||
|
|
||
|
const validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
|
||
|
|
||
|
// CommitteeAcc is an account used to sign tx as a committee.
|
||
|
var CommitteeAcc *wallet.Account
|
||
|
|
||
|
func init() {
|
||
|
CommitteeAcc, _ = wallet.NewAccountFromWIF(validatorWIF)
|
||
|
pubs := keys.PublicKeys{CommitteeAcc.PrivateKey().PublicKey()}
|
||
|
err := CommitteeAcc.ConvertMultisig(1, pubs)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var _nonce uint32
|
||
|
|
||
|
func nonce() uint32 {
|
||
|
_nonce++
|
||
|
return _nonce
|
||
|
}
|
||
|
|
||
|
// NewChain creates new blockchain instance with a single validator and
|
||
|
// setups cleanup functions.
|
||
|
func NewChain(t *testing.T) *core.Blockchain {
|
||
|
protoCfg := config.ProtocolConfiguration{
|
||
|
Magic: netmode.UnitTestNet,
|
||
|
P2PSigExtensions: true,
|
||
|
SecondsPerBlock: 1,
|
||
|
StandbyCommittee: []string{hex.EncodeToString(CommitteeAcc.PrivateKey().PublicKey().Bytes())},
|
||
|
ValidatorsCount: 1,
|
||
|
VerifyBlocks: true,
|
||
|
VerifyTransactions: true,
|
||
|
}
|
||
|
|
||
|
st := storage.NewMemoryStore()
|
||
|
log := zaptest.NewLogger(t)
|
||
|
bc, err := core.NewBlockchain(st, protoCfg, log)
|
||
|
require.NoError(t, err)
|
||
|
go bc.Run()
|
||
|
t.Cleanup(bc.Close)
|
||
|
return bc
|
||
|
}
|
||
|
|
||
|
// PrepareInvoke creates new invocation transaction.
|
||
|
// Signer can be either bool or *wallet.Account.
|
||
|
// In the first case `true` means sign by committee, `false` means sign by validators.
|
||
|
func PrepareInvoke(t *testing.T, bc *core.Blockchain, signer interface{},
|
||
|
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 = bc.BlockHeight() + 1
|
||
|
|
||
|
switch s := signer.(type) {
|
||
|
case *wallet.Account:
|
||
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
||
|
Account: s.Contract.ScriptHash(),
|
||
|
Scopes: transaction.Global,
|
||
|
})
|
||
|
require.NoError(t, addNetworkFee(bc, tx, s))
|
||
|
require.NoError(t, addSystemFee(bc, tx))
|
||
|
require.NoError(t, s.SignTx(netmode.UnitTestNet, tx))
|
||
|
case []*wallet.Account:
|
||
|
for _, acc := range s {
|
||
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
||
|
Account: acc.Contract.ScriptHash(),
|
||
|
Scopes: transaction.Global,
|
||
|
})
|
||
|
require.NoError(t, addNetworkFee(bc, tx, acc))
|
||
|
}
|
||
|
require.NoError(t, addSystemFee(bc, tx))
|
||
|
for _, acc := range s {
|
||
|
require.NoError(t, acc.SignTx(netmode.UnitTestNet, tx))
|
||
|
}
|
||
|
default:
|
||
|
panic("invalid signer")
|
||
|
}
|
||
|
|
||
|
return tx
|
||
|
}
|
||
|
|
||
|
// NewAccount creates new account and transfers 100.0 GAS to it.
|
||
|
func NewAccount(t *testing.T, bc *core.Blockchain) *wallet.Account {
|
||
|
acc, err := wallet.NewAccount()
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
tx := PrepareInvoke(t, bc, CommitteeAcc,
|
||
|
bc.UtilityTokenHash(), "transfer",
|
||
|
CommitteeAcc.Contract.ScriptHash(), acc.Contract.ScriptHash(), int64(100_0000_0000), nil)
|
||
|
AddBlock(t, bc, tx)
|
||
|
CheckHalt(t, bc, tx.Hash())
|
||
|
return acc
|
||
|
}
|
||
|
|
||
|
// DeployContract compiles and deploys contract to bc.
|
||
|
// path should contain Go source files.
|
||
|
// data is an optional argument to `_deploy`.
|
||
|
func DeployContract(t *testing.T, bc *core.Blockchain, path string, data interface{}) util.Uint160 {
|
||
|
tx, h := newDeployTx(t, bc, path, data)
|
||
|
AddBlock(t, bc, tx)
|
||
|
CheckHalt(t, bc, tx.Hash())
|
||
|
return h
|
||
|
}
|
||
|
|
||
|
// CheckHalt checks that transaction persisted with HALT state.
|
||
|
func CheckHalt(t *testing.T, bc *core.Blockchain, h util.Uint256, stack ...stackitem.Item) {
|
||
|
aer, err := bc.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)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// CheckFault checks that transaction persisted with FAULT state.
|
||
|
// Raised exception is also checked to contain s as a substring.
|
||
|
func CheckFault(t *testing.T, bc *core.Blockchain, h util.Uint256, s string) {
|
||
|
aer, err := bc.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)
|
||
|
}
|
||
|
|
||
|
// newDeployTx returns new deployment tx for contract.
|
||
|
func newDeployTx(t *testing.T, bc *core.Blockchain, ctrPath string, data interface{}) (*transaction.Transaction, util.Uint160) {
|
||
|
// nef.NewFile() cares about version a lot.
|
||
|
config.Version = "0.90.0-test"
|
||
|
|
||
|
avm, di, err := compiler.CompileWithDebugInfo(ctrPath, nil)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
ne, err := nef.NewFile(avm)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
conf, err := smartcontract.ParseContractConfig(path.Join(ctrPath, "config.yml"))
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
o := &compiler.Options{}
|
||
|
o.Name = conf.Name
|
||
|
o.ContractEvents = conf.Events
|
||
|
o.ContractSupportedStandards = conf.SupportedStandards
|
||
|
o.Permissions = make([]manifest.Permission, len(conf.Permissions))
|
||
|
for i := range conf.Permissions {
|
||
|
o.Permissions[i] = manifest.Permission(conf.Permissions[i])
|
||
|
}
|
||
|
o.SafeMethods = conf.SafeMethods
|
||
|
m, err := compiler.CreateManifest(di, o)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
rawManifest, err := json.Marshal(m)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
neb, err := ne.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: CommitteeAcc.Contract.ScriptHash()}}
|
||
|
require.NoError(t, addNetworkFee(bc, tx, CommitteeAcc))
|
||
|
require.NoError(t, CommitteeAcc.SignTx(netmode.UnitTestNet, tx))
|
||
|
|
||
|
h := state.CreateContractHash(tx.Sender(), ne.Checksum, m.Name)
|
||
|
return tx, h
|
||
|
}
|
||
|
|
||
|
func addSystemFee(bc *core.Blockchain, tx *transaction.Transaction) error {
|
||
|
v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions
|
||
|
tx.SystemFee = v.GasConsumed()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func addNetworkFee(bc *core.Blockchain, tx *transaction.Transaction, sender *wallet.Account) error {
|
||
|
size := io.GetVarSize(tx)
|
||
|
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), sender.Contract.Script)
|
||
|
tx.NetworkFee += netFee
|
||
|
size += sizeDelta
|
||
|
for _, cosigner := range tx.Signers {
|
||
|
contract := bc.GetContractState(cosigner.Account)
|
||
|
if contract != nil {
|
||
|
netFee, sizeDelta = fee.Calculate(bc.GetBaseExecFee(), contract.NEF.Script)
|
||
|
tx.NetworkFee += netFee
|
||
|
size += sizeDelta
|
||
|
}
|
||
|
}
|
||
|
tx.NetworkFee += int64(size) * bc.FeePerByte()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// AddBlock creates a new block from provided transactions and adds it on bc.
|
||
|
func AddBlock(t *testing.T, bc *core.Blockchain, txs ...*transaction.Transaction) *block.Block {
|
||
|
lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
|
||
|
require.NoError(t, err)
|
||
|
b := &block.Block{
|
||
|
Header: block.Header{
|
||
|
NextConsensus: CommitteeAcc.Contract.ScriptHash(),
|
||
|
Script: transaction.Witness{
|
||
|
VerificationScript: CommitteeAcc.Contract.Script,
|
||
|
},
|
||
|
Timestamp: lastBlock.Timestamp + 1,
|
||
|
},
|
||
|
Transactions: txs,
|
||
|
}
|
||
|
b.PrevHash = lastBlock.Hash()
|
||
|
b.Index = bc.BlockHeight() + 1
|
||
|
b.RebuildMerkleRoot()
|
||
|
|
||
|
sign := CommitteeAcc.PrivateKey().SignHashable(uint32(netmode.UnitTestNet), b)
|
||
|
b.Script.InvocationScript = append([]byte{byte(opcode.PUSHDATA1), 64}, sign...)
|
||
|
require.NoError(t, bc.AddBlock(b))
|
||
|
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt.
|
||
|
func AddBlockCheckHalt(t *testing.T, bc *core.Blockchain, txs ...*transaction.Transaction) *block.Block {
|
||
|
b := AddBlock(t, bc, txs...)
|
||
|
for _, tx := range txs {
|
||
|
CheckHalt(t, bc, tx.Hash())
|
||
|
}
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
// CheckTestInvoke executes transaction without persisting it's state and
|
||
|
// compares the result with the expected.
|
||
|
func CheckTestInvoke(t *testing.T, bc *core.Blockchain, tx *transaction.Transaction, expected interface{}) {
|
||
|
v, err := TestInvoke(bc, tx)
|
||
|
require.NoError(t, err)
|
||
|
require.Equal(t, 1, v.Estack().Len())
|
||
|
require.Equal(t, stackitem.Make(expected), v.Estack().Pop().Item())
|
||
|
}
|
||
|
|
||
|
// TestInvoke creates a test VM with dummy block and executes 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,
|
||
|
}}
|
||
|
v := bc.GetTestVM(trigger.Application, tx, b)
|
||
|
v.LoadWithFlags(tx.Script, callflag.All)
|
||
|
err = v.Run()
|
||
|
return v, err
|
||
|
}
|