neo-go/cli/vm/cli_test.go
Roman Khimov e2cf5b868a
Merge pull request #2941 from nspcc-dev/drop-deprecated-0.102.0
Drop some deprecated things in 0.102.0
2023-04-06 10:42:26 +03:00

1219 lines
39 KiB
Go

package vm
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
gio "io"
"math/big"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/chzyer/readline"
"github.com/nspcc-dev/neo-go/cli/paramcontext"
"github.com/nspcc-dev/neo-go/internal/basicchain"
"github.com/nspcc-dev/neo-go/internal/random"
"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/interop/interopnames"
"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/storage/dbconfig"
"github.com/nspcc-dev/neo-go/pkg/core/storage/dboper"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"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/stretchr/testify/require"
"go.uber.org/atomic"
)
type readCloser struct {
sync.Mutex
bytes.Buffer
}
func (r *readCloser) Close() error {
return nil
}
func (r *readCloser) Read(p []byte) (int, error) {
r.Lock()
defer r.Unlock()
return r.Buffer.Read(p)
}
func (r *readCloser) WriteString(s string) {
r.Lock()
defer r.Unlock()
r.Buffer.WriteString(s)
}
type executor struct {
in *readCloser
out *bytes.Buffer
cli *CLI
ch chan struct{}
exit atomic.Bool
}
func newTestVMCLI(t *testing.T) *executor {
return newTestVMCLIWithLogo(t, false)
}
func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor {
return newTestVMCLIWithLogoAndCustomConfig(t, printLogo, nil)
}
func newTestVMCLIWithLogoAndCustomConfig(t *testing.T, printLogo bool, cfg *config.Config) *executor {
e := &executor{
in: &readCloser{Buffer: *bytes.NewBuffer(nil)},
out: bytes.NewBuffer(nil),
ch: make(chan struct{}),
}
var c config.Config
if cfg == nil {
configPath := "../../config/protocol.unit_testnet.single.yml"
var err error
c, err = config.LoadFile(configPath)
require.NoError(t, err, "could not load chain config")
c.ApplicationConfiguration.DBConfiguration.Type = dbconfig.InMemoryDB
} else {
c = *cfg
}
var err error
e.cli, err = NewWithConfig(printLogo,
func(int) { e.exit.Store(true) },
&readline.Config{
Prompt: "",
Stdin: e.in,
Stderr: e.out,
Stdout: e.out,
FuncIsTerminal: func() bool {
return false
},
}, c)
require.NoError(t, err)
return e
}
// newTestVMClIWithState creates executor backed by level DB filled by simple chain.
// LevelDB-backed CLI must be exited on cleanup.
func newTestVMClIWithState(t *testing.T) *executor {
// Firstly create a DB with chain, save and close it.
path := t.TempDir()
opts := dbconfig.LevelDBOptions{
DataDirectoryPath: path,
}
store, err := storage.NewLevelDBStore(opts)
require.NoError(t, err)
customConfig := func(c *config.Blockchain) {
c.StateRootInHeader = true // Need for P2PStateExchangeExtensions check.
c.P2PSigExtensions = true // Need for basic chain initializer.
}
bc, validators, committee, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, store)
require.NoError(t, err)
go bc.Run()
e := neotest.NewExecutor(t, bc, validators, committee)
basicchain.InitSimple(t, "../../", e)
bc.Close()
// After that create CLI backed by created chain.
configPath := "../../config/protocol.unit_testnet.yml"
cfg, err := config.LoadFile(configPath)
require.NoError(t, err)
cfg.ApplicationConfiguration.DBConfiguration.Type = dbconfig.LevelDB
cfg.ApplicationConfiguration.DBConfiguration.LevelDBOptions = opts
cfg.ProtocolConfiguration.StateRootInHeader = true
return newTestVMCLIWithLogoAndCustomConfig(t, false, &cfg)
}
func (e *executor) runProg(t *testing.T, commands ...string) {
e.runProgWithTimeout(t, 4*time.Second, commands...)
}
func (e *executor) runProgWithTimeout(t *testing.T, timeout time.Duration, commands ...string) {
cmd := strings.Join(commands, "\n") + "\n"
e.in.WriteString(cmd + "\n")
go func() {
require.NoError(t, e.cli.Run())
close(e.ch)
}()
select {
case <-e.ch:
case <-time.After(timeout):
require.Fail(t, "command took too long time")
}
}
func (e *executor) checkNextLine(t *testing.T, expected string) {
line, err := e.out.ReadString('\n')
require.NoError(t, err)
require.Regexp(t, expected, line)
}
func (e *executor) checkNextLineExact(t *testing.T, expected string) {
line, err := e.out.ReadString('\n')
require.NoError(t, err)
require.Equal(t, expected, line)
}
func (e *executor) checkError(t *testing.T, expectedErr error) {
line, err := e.out.ReadString('\n')
require.NoError(t, err)
expected := "Error: " + expectedErr.Error()
require.True(t, strings.HasPrefix(line, expected), fmt.Errorf("expected `%s`, got `%s`", expected, line))
}
func (e *executor) checkStack(t *testing.T, items ...any) {
d := json.NewDecoder(e.out)
var actual any
require.NoError(t, d.Decode(&actual))
rawActual, err := json.Marshal(actual)
require.NoError(t, err)
expected := vm.NewStack("")
for i := range items {
expected.PushVal(items[i])
}
rawExpected, err := json.Marshal(expected)
require.NoError(t, err)
require.JSONEq(t, string(rawExpected), string(rawActual))
// Decoder has it's own buffer, we need to return unread part to the output.
outRemain := e.out.String()
e.out.Reset()
_, err = gio.Copy(e.out, d.Buffered())
require.NoError(t, err)
e.out.WriteString(outRemain)
_, err = e.out.ReadString('\n')
require.NoError(t, err)
}
func (e *executor) checkEvents(t *testing.T, isKeywordExpected bool, events ...state.NotificationEvent) {
if isKeywordExpected {
e.checkNextLine(t, "Events:")
}
d := json.NewDecoder(e.out)
var actual any
require.NoError(t, d.Decode(&actual))
rawActual, err := json.Marshal(actual)
require.NoError(t, err)
rawExpected, err := json.Marshal(events)
require.NoError(t, err)
require.JSONEq(t, string(rawExpected), string(rawActual))
// Decoder has it's own buffer, we need to return unread part to the output.
outRemain := e.out.String()
e.out.Reset()
_, err = gio.Copy(e.out, d.Buffered())
require.NoError(t, err)
e.out.WriteString(outRemain)
_, err = e.out.ReadString('\n')
require.NoError(t, err)
}
func (e *executor) checkStorage(t *testing.T, kvs ...storage.KeyValue) {
for _, kv := range kvs {
e.checkNextLine(t, fmt.Sprintf("%s: %s", hex.EncodeToString(kv.Key), hex.EncodeToString(kv.Value)))
}
}
type storageChange struct {
ContractID int32
dboper.Operation
}
func (e *executor) checkChange(t *testing.T, c storageChange) {
e.checkNextLine(t, fmt.Sprintf("Contract ID: %d", c.ContractID))
e.checkNextLine(t, fmt.Sprintf("State: %s", c.State))
e.checkNextLine(t, fmt.Sprintf("Key: %s", hex.EncodeToString(c.Key)))
if c.Value != nil {
e.checkNextLine(t, fmt.Sprintf("Value: %s", hex.EncodeToString(c.Value)))
}
}
func (e *executor) checkSlot(t *testing.T, items ...any) {
d := json.NewDecoder(e.out)
var actual any
require.NoError(t, d.Decode(&actual))
rawActual, err := json.Marshal(actual)
require.NoError(t, err)
expected := make([]json.RawMessage, len(items))
for i := range items {
if items[i] == nil {
expected[i] = []byte("null")
continue
}
data, err := stackitem.ToJSONWithTypes(stackitem.Make(items[i]))
require.NoError(t, err)
expected[i] = data
}
rawExpected, err := json.MarshalIndent(expected, "", " ")
require.NoError(t, err)
require.JSONEq(t, string(rawExpected), string(rawActual))
// Decoder has it's own buffer, we need to return unread part to the output.
outRemain := e.out.String()
e.out.Reset()
_, err = gio.Copy(e.out, d.Buffered())
require.NoError(t, err)
e.out.WriteString(outRemain)
_, err = e.out.ReadString('\n')
require.NoError(t, err)
}
func TestLoad(t *testing.T) {
script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)}
ownerAddress := "NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB"
ownerAcc, err := address.StringToUint160(ownerAddress)
require.NoError(t, err)
sideAcc := util.Uint160{1, 2, 3}
buff := io.NewBufBinWriter()
emit.Bytes(buff.BinWriter, ownerAcc.BytesBE())
emit.Syscall(buff.BinWriter, interopnames.SystemRuntimeCheckWitness)
checkWitnessScript := buff.Bytes()
t.Run("loadhex", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"loadhex",
"loadhex notahex",
"loadhex "+hex.EncodeToString(script),
"loadhex "+hex.EncodeToString(checkWitnessScript)+" "+ownerAddress, // owner:DefaultScope => true
"run",
"loadhex "+hex.EncodeToString(checkWitnessScript)+" "+ownerAddress+":None", // owner:None => false
"run",
"loadhex "+hex.EncodeToString(checkWitnessScript)+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true
"run",
"loadhex "+hex.EncodeToString(checkWitnessScript)+" 0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true
"run",
"loadhex "+hex.EncodeToString(checkWitnessScript)+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false
"run",
)
e.checkError(t, ErrMissingParameter)
e.checkError(t, ErrInvalidParameter)
e.checkNextLine(t, "READY: loaded 3 instructions")
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, false)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, false)
})
t.Run("loadbase64", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"loadbase64",
"loadbase64 not_a_base64",
"loadbase64 "+base64.StdEncoding.EncodeToString(script),
"loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+ownerAddress, // owner:DefaultScope => true
"run",
"loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+ownerAddress+":None", // owner:None => false
"run",
"loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true
"run",
"loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" 0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true
"run",
"loadbase64 "+base64.StdEncoding.EncodeToString(checkWitnessScript)+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false
"run",
)
e.checkError(t, ErrMissingParameter)
e.checkError(t, ErrInvalidParameter)
e.checkNextLine(t, "READY: loaded 3 instructions")
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, false)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, false)
})
src := `package kek
func Main(op string, a, b int) int {
if op == "add" {
return a + b
} else {
return a * b
}
}`
tmpDir := t.TempDir()
checkLoadgo := func(t *testing.T, tName, cName, cErrName string) {
t.Run("loadgo "+tName, func(t *testing.T) {
filename := filepath.Join(tmpDir, cName)
require.NoError(t, os.WriteFile(filename, []byte(src), os.ModePerm))
filename = "'" + filename + "'"
filenameErr := filepath.Join(tmpDir, cErrName)
require.NoError(t, os.WriteFile(filenameErr, []byte(src+"invalid_token"), os.ModePerm))
filenameErr = "'" + filenameErr + "'"
goMod := []byte(`module test.example/vmcli
go 1.18`)
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm))
e := newTestVMCLI(t)
e.runProgWithTimeout(t, 10*time.Second,
"loadgo",
"loadgo "+filenameErr,
"loadgo "+filename,
"run main add 3 5")
e.checkError(t, ErrMissingParameter)
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "READY: loaded \\d* instructions")
e.checkStack(t, 8)
})
}
checkLoadgo(t, "simple", "vmtestcontract.go", "vmtestcontract_err.go")
checkLoadgo(t, "utf-8 with spaces", "тестовый контракт.go", "тестовый контракт с ошибкой.go")
prepareLoadgoSrc := func(t *testing.T, srcAllowNotify string) string {
filename := filepath.Join(tmpDir, "vmtestcontract.go")
require.NoError(t, os.WriteFile(filename, []byte(srcAllowNotify), os.ModePerm))
filename = "'" + filename + "'"
wd, err := os.Getwd()
require.NoError(t, err)
goMod := []byte(`module test.example/kek
require (
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0
)
replace github.com/nspcc-dev/neo-go/pkg/interop => ` + filepath.Join(wd, "../../pkg/interop") + `
go 1.18`)
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm))
return filename
}
t.Run("loadgo, check calling flags", func(t *testing.T) {
srcAllowNotify := `package kek
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main() int {
runtime.Log("Hello, world!")
return 1
}
`
filename := prepareLoadgoSrc(t, srcAllowNotify)
e := newTestVMCLI(t)
e.runProg(t,
"loadgo "+filename,
"run main")
e.checkNextLine(t, "READY: loaded \\d* instructions")
e.checkStack(t, 1)
})
t.Run("loadgo, check signers", func(t *testing.T) {
srcCheckWitness := `package kek
import (
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
)
func Main() bool {
var owner = address.ToHash160("` + ownerAddress + `")
return runtime.CheckWitness(owner)
}
`
filename := prepareLoadgoSrc(t, srcCheckWitness)
t.Run("address", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"loadgo "+filename+" "+ownerAddress, // owner:DefaultScope => true
"run main",
"loadgo "+filename+" "+ownerAddress+":None", // owner:None => false
"run main")
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, false)
})
t.Run("string LE", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"loadgo "+filename+" "+ownerAcc.StringLE(), // ownerLE:DefaultScope => true
"run main",
"loadgo "+filename+" 0x"+ownerAcc.StringLE(), // owner0xLE:DefaultScope => true
"run main")
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, true)
})
t.Run("nonwitnessed signer", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"loadgo "+filename+" "+sideAcc.StringLE(), // sideLE:DefaultScope => false
"run main")
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, false)
})
})
t.Run("loadnef", func(t *testing.T) {
config.Version = "0.92.0-test"
nefFile, di, err := compiler.CompileWithOptions("test.go", strings.NewReader(src), nil)
require.NoError(t, err)
filename := filepath.Join(tmpDir, "vmtestcontract.nef")
rawNef, err := nefFile.Bytes()
require.NoError(t, err)
require.NoError(t, os.WriteFile(filename, rawNef, os.ModePerm))
m, err := di.ConvertToManifest(&compiler.Options{})
require.NoError(t, err)
manifestFile := filepath.Join(tmpDir, "vmtestcontract.manifest.json")
rawManifest, err := json.Marshal(m)
require.NoError(t, err)
require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm))
filenameErr := filepath.Join(tmpDir, "vmtestcontract_err.nef")
require.NoError(t, os.WriteFile(filenameErr, append([]byte{1, 2, 3, 4}, rawNef...), os.ModePerm))
notExists := filepath.Join(tmpDir, "notexists.json")
manifestFile = "'" + manifestFile + "'"
filename = "'" + filename + "'"
filenameErr = "'" + filenameErr + "'"
e := newTestVMCLI(t)
e.runProg(t,
"loadnef",
"loadnef "+filenameErr+" "+manifestFile,
"loadnef "+filename+" "+notExists,
"loadnef "+filename+" "+filename,
"loadnef "+filename+" "+manifestFile,
"run main add 3 5")
e.checkError(t, ErrMissingParameter)
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "READY: loaded \\d* instructions")
e.checkStack(t, 8)
})
}
func TestRunWithDifferentArguments(t *testing.T) {
src := `package kek
var a = 1
func init() {
a += 1
}
func InitHasRun() bool {
return a == 2
}
func Negate(arg bool) bool {
return !arg
}
func GetInt(arg int) int {
return arg
}
func GetString(arg string) string {
return arg
}
func GetArr(arg []any) []any{
return arg
}`
tmpDir := t.TempDir()
filename := filepath.Join(tmpDir, "run_vmtestcontract.go")
require.NoError(t, os.WriteFile(filename, []byte(src), os.ModePerm))
filename = "'" + filename + "'"
e := newTestVMCLI(t)
e.runProgWithTimeout(t, 30*time.Second,
"loadgo "+filename, "run notexists",
"loadgo "+filename, "run negate false",
"loadgo "+filename, "run negate true",
"loadgo "+filename, "run negate bool:invalid",
"loadgo "+filename, "run getInt 123",
"loadgo "+filename, "run getInt int:invalid",
"loadgo "+filename, "run getString validstring",
"loadgo "+filename, "run initHasRun",
"loadhex "+hex.EncodeToString([]byte{byte(opcode.ADD)}),
"run _ 1 2",
"loadbase64 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.MUL)}),
"run _ 21 2",
"loadgo "+filename, "run getArr [ 1 2 3 ]",
)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkNextLine(t, "Error:")
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, false)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkError(t, ErrInvalidParameter)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, 123)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkError(t, ErrInvalidParameter)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, "validstring")
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, 3)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, 42)
e.checkNextLine(t, "READY: loaded \\d.* instructions")
e.checkStack(t, []stackitem.Item{
stackitem.NewBigInteger(big.NewInt(1)),
stackitem.NewBigInteger(big.NewInt(2)),
stackitem.NewBigInteger(big.NewInt(3)),
})
}
func TestPrintOps(t *testing.T) {
w := io.NewBufBinWriter()
emit.String(w.BinWriter, "log")
emit.Syscall(w.BinWriter, interopnames.SystemRuntimeLog)
emit.Instruction(w.BinWriter, opcode.PUSHDATA1, []byte{3, 1, 2, 3})
script := w.Bytes()
e := newTestVMCLI(t)
e.runProg(t,
"ops",
"loadhex "+hex.EncodeToString(script),
"ops")
e.checkNextLine(t, ".*no program loaded")
e.checkNextLine(t, fmt.Sprintf("READY: loaded %d instructions", len(script)))
e.checkNextLine(t, "INDEX.*OPCODE.*PARAMETER")
e.checkNextLine(t, "0.*PUSHDATA1.*6c6f67")
e.checkNextLine(t, "5.*SYSCALL.*System\\.Runtime\\.Log")
e.checkNextLine(t, "10.*PUSHDATA1.*010203")
}
func TestLoadAbort(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"loadhex "+hex.EncodeToString([]byte{byte(opcode.PUSH1), byte(opcode.ABORT)}),
"run",
)
e.checkNextLine(t, "READY: loaded 2 instructions")
e.checkNextLine(t, "Error:.*at instruction 1.*ABORT")
}
func TestBreakpoint(t *testing.T) {
w := io.NewBufBinWriter()
emit.Opcodes(w.BinWriter, opcode.PUSH1, opcode.PUSH2, opcode.ADD, opcode.PUSH6, opcode.ADD)
e := newTestVMCLI(t)
e.runProg(t,
"break 3",
"cont",
"ip",
"loadhex "+hex.EncodeToString(w.Bytes()),
"break",
"break second",
"break 2",
"break 4",
"cont", "estack",
"run", "estack",
"cont",
)
e.checkNextLine(t, "no program loaded")
e.checkNextLine(t, "no program loaded")
e.checkNextLine(t, "no program loaded")
e.checkNextLine(t, "READY: loaded 5 instructions")
e.checkError(t, ErrMissingParameter)
e.checkError(t, ErrInvalidParameter)
e.checkNextLine(t, "breakpoint added at instruction 2")
e.checkNextLine(t, "breakpoint added at instruction 4")
e.checkNextLine(t, "at breakpoint 2.*ADD")
e.checkStack(t, 1, 2)
e.checkNextLine(t, "at breakpoint 4.*ADD")
e.checkStack(t, 3, 6)
e.checkStack(t, 9)
}
func TestDumpSSlot(t *testing.T) {
w := io.NewBufBinWriter()
emit.Opcodes(w.BinWriter, opcode.INITSSLOT, 2, // init static slot with size=2
opcode.PUSH5, opcode.STSFLD, 1, // put `int(5)` to sslot[1]; sslot[0] is nil
opcode.LDSFLD1) // put sslot[1] to the top of estack
e := newTestVMCLI(t)
e.runProg(t,
"loadhex "+hex.EncodeToString(w.Bytes()),
"break 5",
"step", "sslot",
"cont", "estack",
)
e.checkNextLine(t, "READY: loaded 6 instructions")
e.checkNextLine(t, "breakpoint added at instruction 5")
e.checkNextLine(t, "at breakpoint 5.*LDSFLD1")
e.checkSlot(t, nil, 5)
e.checkStack(t, 5)
}
func TestDumpLSlot_DumpASlot(t *testing.T) {
w := io.NewBufBinWriter()
emit.Opcodes(w.BinWriter, opcode.PUSH4, opcode.PUSH5, opcode.PUSH6, // items for args slot
opcode.INITSLOT, 2, 3, // init local slot with size=2 and args slot with size 3
opcode.PUSH7, opcode.STLOC1, // put `int(7)` to lslot[1]; lslot[0] is nil
opcode.LDLOC, 1) // put lslot[1] to the top of estack
e := newTestVMCLI(t)
e.runProg(t,
"loadhex "+hex.EncodeToString(w.Bytes()),
"break 6",
"break 8",
"cont", "aslot",
"cont", "lslot",
"cont", "estack",
)
e.checkNextLine(t, "READY: loaded 10 instructions")
e.checkNextLine(t, "breakpoint added at instruction 6")
e.checkNextLine(t, "breakpoint added at instruction 8")
e.checkNextLine(t, "at breakpoint 6.*PUSH7")
e.checkSlot(t, 6, 5, 4) // args slot
e.checkNextLine(t, "at breakpoint 8.*LDLOC")
e.checkSlot(t, nil, 7) // local slot
e.checkStack(t, 7)
}
func TestStep(t *testing.T) {
script := hex.EncodeToString([]byte{
byte(opcode.PUSH0), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.PUSH3),
})
e := newTestVMCLI(t)
e.runProg(t,
"step",
"loadhex "+script,
"step invalid",
"step",
"step 2",
"ip", "step", "ip")
e.checkNextLine(t, "no program loaded")
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkError(t, ErrInvalidParameter)
e.checkNextLine(t, "at breakpoint 1.*PUSH1")
e.checkNextLine(t, "at breakpoint 3.*PUSH3")
e.checkNextLine(t, "instruction pointer at 3.*PUSH3")
e.checkNextLine(t, "execution has finished")
e.checkNextLine(t, "execution has finished")
}
func TestErrorOnStepInto(t *testing.T) {
script := hex.EncodeToString([]byte{byte(opcode.ADD)})
e := newTestVMCLI(t)
e.runProg(t,
"stepover",
"loadhex "+script,
"stepover")
e.checkNextLine(t, "Error:.*no program loaded")
e.checkNextLine(t, "READY: loaded 1 instructions")
e.checkNextLine(t, "Error:")
}
func TestStepIntoOverOut(t *testing.T) {
script := hex.EncodeToString([]byte{
byte(opcode.PUSH2), byte(opcode.CALL), 4, byte(opcode.NOP), byte(opcode.RET),
byte(opcode.PUSH3), byte(opcode.ADD), byte(opcode.RET),
})
e := newTestVMCLI(t)
e.runProg(t,
"loadhex "+script,
"step", "stepover", "run",
"loadhex "+script,
"step", "stepinto", "step", "estack", "run",
"loadhex "+script,
"step", "stepinto", "stepout", "run")
e.checkNextLine(t, "READY: loaded 8 instructions")
e.checkNextLine(t, "at breakpoint 1.*CALL")
e.checkNextLine(t, "instruction pointer at.*NOP")
e.checkStack(t, 5)
e.checkNextLine(t, "READY: loaded 8 instructions")
e.checkNextLine(t, "at breakpoint.*CALL")
e.checkNextLine(t, "instruction pointer at.*PUSH3")
e.checkNextLine(t, "at breakpoint.*ADD")
e.checkStack(t, 2, 3)
e.checkStack(t, 5)
e.checkNextLine(t, "READY: loaded 8 instructions")
e.checkNextLine(t, "at breakpoint 1.*CALL")
e.checkNextLine(t, "instruction pointer at.*PUSH3")
e.checkNextLine(t, "instruction pointer at.*NOP")
e.checkStack(t, 5)
}
// `Parse` output is written via `tabwriter` so if any problems
// are encountered in this test, try to replace ' ' with '\\s+'.
func TestParse(t *testing.T) {
t.Run("Integer", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t,
"parse",
"parse 6667")
e.checkError(t, ErrMissingParameter)
e.checkNextLine(t, "Integer to Hex.*0b1a")
e.checkNextLine(t, "Integer to Base64.*Cxo=")
e.checkNextLine(t, "Hex to String.*\"fg\"")
e.checkNextLine(t, "Hex to Integer.*26470")
e.checkNextLine(t, "Swap Endianness.*6766")
e.checkNextLine(t, "Base64 to String.*\"뮻\"")
e.checkNextLine(t, "Base64 to BigInteger.*-4477205")
e.checkNextLine(t, "String to Hex.*36363637")
e.checkNextLine(t, "String to Base64.*NjY2Nw==")
})
t.Run("Address", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t, "parse "+"NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc")
e.checkNextLine(t, "Address to BE ScriptHash.*aa8acf859d4fe402b34e673f2156821796a488eb")
e.checkNextLine(t, "Address to LE ScriptHash.*eb88a496178256213f674eb302e44f9d85cf8aaa")
e.checkNextLine(t, "Address to Base64.*(BE).*qorPhZ1P5AKzTmc/IVaCF5akiOs=")
e.checkNextLine(t, "Address to Base64.*(LE).*64iklheCViE/Z06zAuRPnYXPiqo=")
e.checkNextLine(t, "String to Hex.*4e6254694d3668387239396b70527462343238586373556b31547a4b656432675463")
e.checkNextLine(t, "String to Base64.*TmJUaU02aDhyOTlrcFJ0YjQyOFhjc1VrMVR6S2VkMmdUYw==")
})
t.Run("Uint160", func(t *testing.T) {
u := util.Uint160{66, 67, 68}
e := newTestVMCLI(t)
e.runProg(t, "parse "+u.StringLE())
e.checkNextLine(t, "Integer to Hex.*b6c706")
e.checkNextLine(t, "Integer to Base64.*tscG")
e.checkNextLine(t, "BE ScriptHash to Address.*NKuyBkoGdZZSLyPbJEetheRhQKhATAzN2A")
e.checkNextLine(t, "LE ScriptHash to Address.*NRxLN7apYwKJihzMt4eSSnU9BJ77dp2TNj")
e.checkNextLine(t, "Hex to String")
e.checkNextLine(t, "Hex to Integer.*378293464438118320046642359484100328446970822656")
e.checkNextLine(t, "Swap Endianness.*4243440000000000000000000000000000000000")
e.checkNextLine(t, "Base64 to String.*")
e.checkNextLine(t, "Base64 to BigInteger.*376115185060690908522683414825349447309891933036899526770189324554358227")
e.checkNextLine(t, "String to Hex.*30303030303030303030303030303030303030303030303030303030303030303030343434333432")
e.checkNextLine(t, "String to Base64.*MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQ0NDM0Mg==")
})
t.Run("public key", func(t *testing.T) {
pub := "02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2"
e := newTestVMCLI(t)
e.runProg(t, "parse "+pub)
e.checkNextLine(t, "Public key to BE ScriptHash.*ee9ea22c27e34bd0148fc4108e08f74e8f5048b2")
e.checkNextLine(t, "Public key to LE ScriptHash.*b248508f4ef7088e10c48f14d04be3272ca29eee")
e.checkNextLine(t, "Public key to Address.*Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn")
e.checkNextLine(t, "Hex to String")
e.checkNextLine(t, "Hex to Integer.*-7115107707948693452214836319400158580475150561081357074343221218306172781415678")
e.checkNextLine(t, "Swap Endianness.*c28d7fbfc4bb74d7a76f0496b87d6b203f754c5fed8ac517e3df7b01f42b62b302")
e.checkNextLine(t, "String to Hex.*303262333632326266343031376264666533313763353861656435663463373533663230366237646238393630343666613764373734626263346266376638646332")
e.checkNextLine(t, "String to Base64.*MDJiMzYyMmJmNDAxN2JkZmUzMTdjNThhZWQ1ZjRjNzUzZjIwNmI3ZGI4OTYwNDZmYTdkNzc0YmJjNGJmN2Y4ZGMy")
})
t.Run("base64", func(t *testing.T) {
e := newTestVMCLI(t)
u := random.Uint160()
e.runProg(t, "parse "+base64.StdEncoding.EncodeToString(u.BytesBE()))
e.checkNextLine(t, "Base64 to String\\s+")
e.checkNextLine(t, "Base64 to BigInteger\\s+")
e.checkNextLine(t, "Base64 to BE ScriptHash\\s+"+u.StringBE())
e.checkNextLine(t, "Base64 to LE ScriptHash\\s+"+u.StringLE())
e.checkNextLine(t, "Base64 to Address \\(BE\\)\\s+"+address.Uint160ToString(u))
e.checkNextLine(t, "Base64 to Address \\(LE\\)\\s+"+address.Uint160ToString(u.Reverse()))
e.checkNextLine(t, "String to Hex\\s+")
e.checkNextLine(t, "String to Base64\\s+")
})
}
func TestPrintLogo(t *testing.T) {
e := newTestVMCLIWithLogo(t, true)
e.runProg(t)
require.True(t, strings.HasPrefix(e.out.String(), logo))
require.False(t, e.exit.Load())
}
func TestExit(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t, "exit")
require.True(t, e.exit.Load())
}
func TestReset(t *testing.T) {
script := []byte{byte(opcode.PUSH1)}
e := newTestVMCLI(t)
e.runProg(t,
"loadhex "+hex.EncodeToString(script),
"ops",
"reset",
"ops")
e.checkNextLine(t, "READY: loaded 1 instructions")
e.checkNextLine(t, "INDEX.*OPCODE.*PARAMETER")
e.checkNextLine(t, "0.*PUSH1.*")
e.checkNextLine(t, "")
e.checkError(t, fmt.Errorf("VM is not ready: no program loaded"))
}
func TestRunWithState(t *testing.T) {
e := newTestVMClIWithState(t)
// Ensure that state is properly loaded and on-chain contract can be called.
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "put", callflag.All, 3, 3)
e.runProg(t,
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"exit")
e.checkNextLine(t, "READY: loaded 37 instructions")
e.checkStack(t, 3)
}
func TestRunWithHistoricState(t *testing.T) {
e := newTestVMClIWithState(t)
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "get", callflag.All, 1)
b := script.Bytes()
e.runProg(t,
"loadhex "+hex.EncodeToString(b), // normal invocation
"run",
"loadhex --historic 3 "+hex.EncodeToString(b), // historic invocation, old value should be retrieved
"run",
"loadhex --historic 0 "+hex.EncodeToString(b), // historic invocation, contract is not deployed yet
"run",
"exit",
)
e.checkNextLine(t, "READY: loaded 36 instructions")
e.checkStack(t, []byte{2})
e.checkNextLine(t, "READY: loaded 36 instructions")
e.checkStack(t, []byte{1})
e.checkNextLine(t, "READY: loaded 36 instructions")
e.checkNextLineExact(t, "Error: at instruction 31 (SYSCALL): System.Contract.Call failed: called contract 73a23e915b66ae406866787f4a6c1c517dc981e2 not found: key not found\n")
}
func TestEvents(t *testing.T) {
e := newTestVMClIWithState(t)
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(2) // examples/runtime/runtime.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "notify", callflag.All, []any{true, 5})
e.runProg(t,
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"events",
"exit")
expectedEvent := state.NotificationEvent{
ScriptHash: h,
Name: "Event",
Item: stackitem.NewArray([]stackitem.Item{
stackitem.NewArray([]stackitem.Item{
stackitem.Make(true),
stackitem.Make(5),
}),
}),
}
e.checkNextLine(t, "READY: loaded 42 instructions")
e.checkStack(t, stackitem.Null{})
e.checkEvents(t, true, expectedEvent) // automatically printed after `run` command
e.checkEvents(t, false, expectedEvent) // printed after `events` command
}
func TestEnv(t *testing.T) {
t.Run("default setup", func(t *testing.T) {
e := newTestVMCLI(t)
e.runProg(t, "env")
e.checkNextLine(t, "Chain height: 0")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 0\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: inmemory")
})
t.Run("setup with state", func(t *testing.T) {
e := newTestVMClIWithState(t)
e.runProg(t, "env", "exit")
e.checkNextLine(t, "Chain height: 5")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 5\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: leveldb")
})
t.Run("setup with historic state", func(t *testing.T) {
e := newTestVMClIWithState(t)
e.runProg(t, "loadbase64 --historic 3 "+base64.StdEncoding.EncodeToString([]byte{byte(opcode.PUSH1)}),
"env", "exit")
e.checkNextLine(t, "READY: loaded 1 instructions")
e.checkNextLine(t, "Chain height: 5")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 3\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: leveldb")
})
t.Run("verbose", func(t *testing.T) {
e := newTestVMClIWithState(t)
e.runProg(t, "env -v", "exit")
e.checkNextLine(t, "Chain height: 5")
e.checkNextLineExact(t, "VM height (may differ from chain height in case of historic call): 5\n")
e.checkNextLine(t, "Network magic: 42")
e.checkNextLine(t, "DB type: leveldb")
e.checkNextLine(t, "Node config:") // Do not check exact node config.
})
}
func TestDumpStorage(t *testing.T) {
e := newTestVMClIWithState(t)
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
expected := []storage.KeyValue{
{Key: []byte{1}, Value: []byte{2}},
{Key: []byte{2}, Value: []byte{2}},
}
e.runProg(t,
"storage "+h.StringLE(),
"storage 0x"+h.StringLE(),
"storage "+address.Uint160ToString(h),
"storage 1",
"storage 1 "+hex.EncodeToString(expected[0].Key),
"storage 1 --backwards",
"exit",
)
e.checkStorage(t, expected...)
e.checkStorage(t, expected...)
e.checkStorage(t, expected...)
e.checkStorage(t, expected...)
e.checkStorage(t, storage.KeyValue{Key: nil, Value: []byte{2}}) // empty key because search prefix is trimmed
e.checkStorage(t, expected[1], expected[0])
}
func TestDumpStorageDiff(t *testing.T) {
e := newTestVMClIWithState(t)
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "put", callflag.All, 3, 3)
expected := []storage.KeyValue{
{Key: []byte{1}, Value: []byte{2}},
{Key: []byte{2}, Value: []byte{2}},
}
diff := storage.KeyValue{Key: []byte{3}, Value: []byte{3}}
e.runProg(t,
"storage 1",
"storage 1 --diff",
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"storage 1",
"storage 1 --diff",
"exit",
)
e.checkStorage(t, expected...)
// no script is executed => no diff
e.checkNextLine(t, "READY: loaded 37 instructions")
e.checkStack(t, 3)
e.checkStorage(t, append(expected, diff)...)
e.checkStorage(t, diff)
}
func TestDumpChanges(t *testing.T) {
e := newTestVMClIWithState(t)
script := io.NewBufBinWriter()
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
emit.AppCall(script.BinWriter, h, "put", callflag.All, 3, 4) // add
emit.AppCall(script.BinWriter, h, "delete", callflag.All, 1) // remove
emit.AppCall(script.BinWriter, h, "put", callflag.All, 2, 5) // update
expected := []storageChange{
{
ContractID: 1,
Operation: dboper.Operation{
State: "Deleted",
Key: []byte{1},
},
},
{
ContractID: 1,
Operation: dboper.Operation{
State: "Changed",
Key: []byte{2},
Value: []byte{5},
},
},
{
ContractID: 1,
Operation: dboper.Operation{
State: "Added",
Key: []byte{3},
Value: []byte{4},
},
},
}
e.runProg(t,
"changes",
"changes 1",
"loadhex "+hex.EncodeToString(script.Bytes()),
"run",
"changes 1 "+hex.EncodeToString([]byte{1}),
"changes 1 "+hex.EncodeToString([]byte{2}),
"changes 1 "+hex.EncodeToString([]byte{3}),
"exit",
)
// no script is executed => no diff
e.checkNextLine(t, "READY: loaded 113 instructions")
e.checkStack(t, 3, true, 2)
e.checkChange(t, expected[0])
e.checkChange(t, expected[1])
e.checkChange(t, expected[2])
}
func TestLoadtx(t *testing.T) {
e := newTestVMClIWithState(t)
b, err := e.cli.chain.GetBlock(e.cli.chain.GetHeaderHash(2)) // Block #2 contains transaction that puts (1,1) pair to storage contract.
require.NoError(t, err)
require.Equal(t, 1, len(b.Transactions))
tx := b.Transactions[0]
tmp := filepath.Join(t.TempDir(), "tx.json")
require.NoError(t, paramcontext.InitAndSave(netmode.UnitTestNet, tx, nil, tmp))
e.runProg(t,
"loadtx "+tx.Hash().StringLE(), // hash LE
"run",
"loadtx --gas 10000 "+tx.Hash().StringLE(), // with GAS
"run",
"loadtx 0x"+tx.Hash().StringLE(), // hash LE with 0x prefix
"run",
"loadtx '"+tmp+"'", // Tx from parameter context file.
"run",
"loadtx", // missing argument
"exit",
)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, 1)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkError(t, errors.New("at instruction 3 (PACK): gas limit is exceeded"))
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, 1)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, 1)
e.checkError(t, errors.New("missing argument: <file-or-hash>"))
}
func TestLoaddeployed(t *testing.T) {
e := newTestVMClIWithState(t)
h, err := e.cli.chain.GetContractScriptHash(1) // examples/storage/storage.go
require.NoError(t, err)
ownerAddress := "NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB" // owner of examples/runtime/runtime.go (taken from deployed contract with ID=2)
ownerAcc, err := address.StringToUint160(ownerAddress)
require.NoError(t, err)
sideAcc := util.Uint160{1, 2, 3}
e.runProg(t,
"loaddeployed "+h.StringLE(), // hash LE
"run get 1",
"loaddeployed --gas 420000 "+h.StringLE(), // gas-limited
"run get 1",
"loaddeployed 0x"+h.StringLE(), // hash LE with 0x prefix
"run get 1",
"loaddeployed 1", // contract ID
"run get 1",
"loaddeployed --historic 2 1", // historic state, check that hash is properly set
"run get 1",
// Check signers parsing:
"loaddeployed 2 "+ownerAddress, // check witness (owner:DefautScope => true)
"run checkWitness",
"loaddeployed 2 "+ownerAddress+":None", // check witness (owner:None => false)
"run checkWitness",
"loaddeployed 2 "+ownerAddress+":CalledByEntry", // check witness (owner:CalledByEntry => true)
"run checkWitness",
"loaddeployed 2 "+ownerAcc.StringLE()+":CalledByEntry", // check witness (ownerLE:CalledByEntry => true)
"run checkWitness",
"loaddeployed 2 0x"+ownerAcc.StringLE()+":CalledByEntry", // check witness (owner0xLE:CalledByEntry => true)
"run checkWitness",
"loaddeployed 2 "+sideAcc.StringLE()+":Global", // check witness (sideLE:Global => false)
"run checkWitness",
"loaddeployed", // missing argument
"exit",
)
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, []byte{2})
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkError(t, errors.New("at instruction 63 (SYSCALL): System.Storage.Get failed: insufficient amount of gas"))
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, []byte{2})
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, []byte{2})
e.checkNextLine(t, "READY: loaded \\d+ instructions")
e.checkStack(t, []byte{1})
// Check signers parsing:
e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner:DefaultScope
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner:None
e.checkStack(t, false)
e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner:CalledByEntry
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of ownerLE:CalledByEntry
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner0xLE:CalledByEntry
e.checkStack(t, true)
e.checkNextLine(t, "READY: loaded \\d+ instructions") // check witness of owner0xLE:CalledByEntry
e.checkStack(t, false)
e.checkError(t, errors.New("contract hash, address or ID is mandatory argument"))
}
func TestJump(t *testing.T) {
buf := io.NewBufBinWriter()
emit.Opcodes(buf.BinWriter, opcode.PUSH1, opcode.PUSH2, opcode.ABORT) // some garbage
jmpTo := buf.Len()
emit.Opcodes(buf.BinWriter, opcode.PUSH4, opcode.PUSH5, opcode.ADD) // useful script
e := newTestVMCLI(t)
e.runProg(t,
"loadhex "+hex.EncodeToString(buf.Bytes()),
"jump "+strconv.Itoa(jmpTo),
"run",
)
e.checkNextLine(t, "READY: loaded 6 instructions")
e.checkNextLine(t, fmt.Sprintf("jumped to instruction %d", jmpTo))
e.checkStack(t, 9)
}