mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-27 03:58:06 +00:00
bdb2d24a5a
VM always has istack and it doesn't even change, so doing this microallocation makes no sense. Notice that estack is a bit harder to change we do replace it in some cases and we compare pointers to it as well.
1807 lines
44 KiB
Go
1807 lines
44 KiB
Go
package vm
|
|
|
|
import (
|
|
"crypto/elliptic"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"math/big"
|
|
"os"
|
|
"text/tabwriter"
|
|
"unicode/utf8"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
"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/util/slice"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
)
|
|
|
|
type errorAtInstruct struct {
|
|
ip int
|
|
op opcode.Opcode
|
|
err interface{}
|
|
}
|
|
|
|
func (e *errorAtInstruct) Error() string {
|
|
return fmt.Sprintf("at instruction %d (%s): %s", e.ip, e.op, e.err)
|
|
}
|
|
|
|
func newError(ip int, op opcode.Opcode, err interface{}) *errorAtInstruct {
|
|
return &errorAtInstruct{ip: ip, op: op, err: err}
|
|
}
|
|
|
|
// StateMessage is a vm state message which could be used as additional info for example by cli.
|
|
type StateMessage string
|
|
|
|
const (
|
|
// MaxInvocationStackSize is the maximum size of an invocation stack.
|
|
MaxInvocationStackSize = 1024
|
|
|
|
// MaxTryNestingDepth is the maximum level of TRY nesting allowed,
|
|
// that is you can't have more exception handling contexts than this.
|
|
MaxTryNestingDepth = 16
|
|
|
|
// MaxStackSize is the maximum number of items allowed to be
|
|
// on all stacks at once.
|
|
MaxStackSize = 2 * 1024
|
|
|
|
maxSHLArg = stackitem.MaxBigIntegerSizeBits
|
|
)
|
|
|
|
// SyscallHandler is a type for syscall handler.
|
|
type SyscallHandler = func(*VM, uint32) error
|
|
|
|
// VM represents the virtual machine.
|
|
type VM struct {
|
|
state State
|
|
|
|
// callback to get interop price
|
|
getPrice func(opcode.Opcode, []byte) int64
|
|
|
|
istack Stack // invocation stack.
|
|
estack *Stack // execution stack.
|
|
|
|
uncaughtException stackitem.Item // exception being handled
|
|
|
|
refs refCounter
|
|
|
|
gasConsumed int64
|
|
GasLimit int64
|
|
|
|
// SyscallHandler handles SYSCALL opcode.
|
|
SyscallHandler func(v *VM, id uint32) error
|
|
|
|
// LoadToken handles CALLT opcode.
|
|
LoadToken func(id int32) error
|
|
|
|
trigger trigger.Type
|
|
|
|
// Invocations is a script invocation counter.
|
|
Invocations map[util.Uint160]int
|
|
}
|
|
|
|
// New returns a new VM object ready to load AVM bytecode scripts.
|
|
func New() *VM {
|
|
return NewWithTrigger(trigger.Application)
|
|
}
|
|
|
|
// NewWithTrigger returns a new VM for executions triggered by t.
|
|
func NewWithTrigger(t trigger.Type) *VM {
|
|
vm := &VM{
|
|
state: NoneState,
|
|
trigger: t,
|
|
|
|
SyscallHandler: defaultSyscallHandler,
|
|
Invocations: make(map[util.Uint160]int),
|
|
}
|
|
|
|
vm.refs.items = make(map[stackitem.Item]int)
|
|
initStack(&vm.istack, "invocation", nil)
|
|
vm.estack = newStack("evaluation", &vm.refs)
|
|
return vm
|
|
}
|
|
|
|
// SetPriceGetter registers the given PriceGetterFunc in v.
|
|
// f accepts vm's Context, current instruction and instruction parameter.
|
|
func (v *VM) SetPriceGetter(f func(opcode.Opcode, []byte) int64) {
|
|
v.getPrice = f
|
|
}
|
|
|
|
// GasConsumed returns the amount of GAS consumed during execution.
|
|
func (v *VM) GasConsumed() int64 {
|
|
return v.gasConsumed
|
|
}
|
|
|
|
// AddGas consumes specified amount of gas. It returns true iff gas limit wasn't exceeded.
|
|
func (v *VM) AddGas(gas int64) bool {
|
|
v.gasConsumed += gas
|
|
return v.GasLimit < 0 || v.gasConsumed <= v.GasLimit
|
|
}
|
|
|
|
// Estack returns the evaluation stack so interop hooks can utilize this.
|
|
func (v *VM) Estack() *Stack {
|
|
return v.estack
|
|
}
|
|
|
|
// Istack returns the invocation stack so interop hooks can utilize this.
|
|
func (v *VM) Istack() *Stack {
|
|
return &v.istack
|
|
}
|
|
|
|
// LoadArgs loads in the arguments used in the Mian entry point.
|
|
func (v *VM) LoadArgs(method []byte, args []stackitem.Item) {
|
|
if len(args) > 0 {
|
|
v.estack.PushVal(args)
|
|
}
|
|
if method != nil {
|
|
v.estack.PushVal(method)
|
|
}
|
|
}
|
|
|
|
// PrintOps prints the opcodes of the current loaded program to stdout.
|
|
func (v *VM) PrintOps(out io.Writer) {
|
|
if out == nil {
|
|
out = os.Stdout
|
|
}
|
|
w := tabwriter.NewWriter(out, 0, 0, 4, ' ', 0)
|
|
fmt.Fprintln(w, "INDEX\tOPCODE\tPARAMETER\t")
|
|
realctx := v.Context()
|
|
ctx := realctx.Copy()
|
|
ctx.ip = 0
|
|
ctx.nextip = 0
|
|
for {
|
|
cursor := ""
|
|
instr, parameter, err := ctx.Next()
|
|
if ctx.ip == realctx.ip {
|
|
cursor = "<<"
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(w, "%d\t%s\tERROR: %s\t%s\n", ctx.ip, instr, err, cursor)
|
|
break
|
|
}
|
|
var desc = ""
|
|
if parameter != nil {
|
|
switch instr {
|
|
case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT, opcode.CALL,
|
|
opcode.JMPEQ, opcode.JMPNE,
|
|
opcode.JMPGT, opcode.JMPGE, opcode.JMPLE, opcode.JMPLT,
|
|
opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL, opcode.CALLL,
|
|
opcode.JMPEQL, opcode.JMPNEL,
|
|
opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLEL, opcode.JMPLTL,
|
|
opcode.PUSHA, opcode.ENDTRY, opcode.ENDTRYL:
|
|
desc = getOffsetDesc(ctx, parameter)
|
|
case opcode.TRY, opcode.TRYL:
|
|
catchP, finallyP := getTryParams(instr, parameter)
|
|
desc = fmt.Sprintf("catch %s, finally %s",
|
|
getOffsetDesc(ctx, catchP), getOffsetDesc(ctx, finallyP))
|
|
case opcode.INITSSLOT:
|
|
desc = fmt.Sprint(parameter[0])
|
|
case opcode.CONVERT, opcode.ISTYPE:
|
|
typ := stackitem.Type(parameter[0])
|
|
desc = fmt.Sprintf("%s (%x)", typ, parameter[0])
|
|
case opcode.INITSLOT:
|
|
desc = fmt.Sprintf("%d local, %d arg", parameter[0], parameter[1])
|
|
case opcode.SYSCALL:
|
|
name, err := interopnames.FromID(GetInteropID(parameter))
|
|
if err != nil {
|
|
name = "not found"
|
|
}
|
|
desc = fmt.Sprintf("%s (%x)", name, parameter)
|
|
case opcode.PUSHINT8, opcode.PUSHINT16, opcode.PUSHINT32,
|
|
opcode.PUSHINT64, opcode.PUSHINT128, opcode.PUSHINT256:
|
|
val := bigint.FromBytes(parameter)
|
|
desc = fmt.Sprintf("%d (%x)", val, parameter)
|
|
case opcode.LDLOC, opcode.STLOC, opcode.LDARG, opcode.STARG, opcode.LDSFLD, opcode.STSFLD:
|
|
desc = fmt.Sprintf("%d (%x)", parameter[0], parameter)
|
|
default:
|
|
if utf8.Valid(parameter) {
|
|
desc = fmt.Sprintf("%x (%q)", parameter, parameter)
|
|
} else {
|
|
desc = fmt.Sprintf("%x", parameter)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", ctx.ip, instr, desc, cursor)
|
|
if ctx.nextip >= len(ctx.prog) {
|
|
break
|
|
}
|
|
}
|
|
w.Flush()
|
|
}
|
|
|
|
func getOffsetDesc(ctx *Context, parameter []byte) string {
|
|
offset, rOffset, err := calcJumpOffset(ctx, parameter)
|
|
if err != nil {
|
|
return fmt.Sprintf("ERROR: %v", err)
|
|
}
|
|
return fmt.Sprintf("%d (%d/%x)", offset, rOffset, parameter)
|
|
}
|
|
|
|
// AddBreakPoint adds a breakpoint to the current context.
|
|
func (v *VM) AddBreakPoint(n int) {
|
|
ctx := v.Context()
|
|
ctx.breakPoints = append(ctx.breakPoints, n)
|
|
}
|
|
|
|
// AddBreakPointRel adds a breakpoint relative to the current
|
|
// instruction pointer.
|
|
func (v *VM) AddBreakPointRel(n int) {
|
|
ctx := v.Context()
|
|
v.AddBreakPoint(ctx.nextip + n)
|
|
}
|
|
|
|
// LoadFileWithFlags loads a program in NEF format from the given path, ready to execute it.
|
|
func (v *VM) LoadFileWithFlags(path string, f callflag.CallFlag) error {
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nef, err := nef.FileFromBytes(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Load(nef.Script)
|
|
return nil
|
|
}
|
|
|
|
// Load initializes the VM with the program given.
|
|
func (v *VM) Load(prog []byte) {
|
|
v.LoadWithFlags(prog, callflag.NoneFlag)
|
|
}
|
|
|
|
// LoadWithFlags initializes the VM with the program and flags given.
|
|
func (v *VM) LoadWithFlags(prog []byte, f callflag.CallFlag) {
|
|
// Clear all stacks and state, it could be a reload.
|
|
v.istack.Clear()
|
|
v.estack.Clear()
|
|
v.state = NoneState
|
|
v.gasConsumed = 0
|
|
v.LoadScriptWithFlags(prog, f)
|
|
}
|
|
|
|
// LoadScript loads a script from the internal script table. It
|
|
// will immediately push a new context created from this script to
|
|
// the invocation stack and starts executing it.
|
|
func (v *VM) LoadScript(b []byte) {
|
|
v.LoadScriptWithFlags(b, callflag.NoneFlag)
|
|
}
|
|
|
|
// LoadScriptWithFlags loads script and sets call flag to f.
|
|
func (v *VM) LoadScriptWithFlags(b []byte, f callflag.CallFlag) {
|
|
v.checkInvocationStackSize()
|
|
ctx := NewContextWithParams(b, 0, -1, 0)
|
|
v.estack = newStack("evaluation", &v.refs)
|
|
ctx.estack = v.estack
|
|
ctx.tryStack = newStack("exception", nil)
|
|
ctx.callFlag = f
|
|
ctx.static = newSlot(&v.refs)
|
|
ctx.callingScriptHash = v.GetCurrentScriptHash()
|
|
v.istack.PushVal(ctx)
|
|
}
|
|
|
|
// LoadScriptWithHash if similar to the LoadScriptWithFlags method, but it also loads
|
|
// given script hash directly into the Context to avoid its recalculations and to make
|
|
// is possible to override it for deployed contracts with special hashes (the function
|
|
// assumes that it is used for deployed contracts setting context's parameters
|
|
// accordingly). It's up to user of this function to make sure the script and hash match
|
|
// each other.
|
|
func (v *VM) LoadScriptWithHash(b []byte, hash util.Uint160, f callflag.CallFlag) {
|
|
shash := v.GetCurrentScriptHash()
|
|
v.LoadScriptWithCallingHash(shash, b, hash, f, true, 0)
|
|
}
|
|
|
|
// LoadScriptWithCallingHash is similar to LoadScriptWithHash but sets calling hash explicitly.
|
|
// It should be used for calling from native contracts.
|
|
func (v *VM) LoadScriptWithCallingHash(caller util.Uint160, b []byte, hash util.Uint160,
|
|
f callflag.CallFlag, hasReturn bool, paramCount uint16) {
|
|
v.LoadScriptWithFlags(b, f)
|
|
ctx := v.Context()
|
|
ctx.scriptHash = hash
|
|
ctx.callingScriptHash = caller
|
|
if hasReturn {
|
|
ctx.RetCount = 1
|
|
} else {
|
|
ctx.RetCount = 0
|
|
}
|
|
ctx.ParamCount = int(paramCount)
|
|
}
|
|
|
|
// Context returns the current executed context. Nil if there is no context,
|
|
// which implies no program is loaded.
|
|
func (v *VM) Context() *Context {
|
|
if v.istack.Len() == 0 {
|
|
return nil
|
|
}
|
|
return v.istack.Peek(0).Value().(*Context)
|
|
}
|
|
|
|
// PopResult is used to pop the first item of the evaluation stack. This allows
|
|
// us to test compiler and vm in a bi-directional way.
|
|
func (v *VM) PopResult() interface{} {
|
|
e := v.estack.Pop()
|
|
if e != nil {
|
|
return e.Value()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stack returns json formatted representation of the given stack.
|
|
func (v *VM) Stack(n string) string {
|
|
var s *Stack
|
|
if n == "istack" {
|
|
s = &v.istack
|
|
}
|
|
if n == "estack" {
|
|
s = v.estack
|
|
}
|
|
b, _ := json.MarshalIndent(s, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
// State returns the state for the VM.
|
|
func (v *VM) State() State {
|
|
return v.state
|
|
}
|
|
|
|
// Ready returns true if the VM ready to execute the loaded program.
|
|
// Will return false if no program is loaded.
|
|
func (v *VM) Ready() bool {
|
|
return v.istack.Len() > 0
|
|
}
|
|
|
|
// Run starts the execution of the loaded program.
|
|
func (v *VM) Run() error {
|
|
if !v.Ready() {
|
|
v.state = FaultState
|
|
return errors.New("no program loaded")
|
|
}
|
|
|
|
if v.state.HasFlag(FaultState) {
|
|
// VM already ran something and failed, in general its state is
|
|
// undefined in this case so we can't run anything.
|
|
return errors.New("VM has failed")
|
|
}
|
|
// HaltState (the default) or BreakState are safe to continue.
|
|
v.state = NoneState
|
|
for {
|
|
switch {
|
|
case v.state.HasFlag(FaultState):
|
|
// Should be caught and reported already by the v.Step(),
|
|
// but we're checking here anyway just in case.
|
|
return errors.New("VM has failed")
|
|
case v.state.HasFlag(HaltState), v.state.HasFlag(BreakState):
|
|
// Normal exit from this loop.
|
|
return nil
|
|
case v.state == NoneState:
|
|
if err := v.Step(); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
v.state = FaultState
|
|
return errors.New("unknown state")
|
|
}
|
|
// check for breakpoint before executing the next instruction
|
|
ctx := v.Context()
|
|
if ctx != nil && ctx.atBreakPoint() {
|
|
v.state = BreakState
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 1 instruction in the program.
|
|
func (v *VM) Step() error {
|
|
ctx := v.Context()
|
|
op, param, err := ctx.Next()
|
|
if err != nil {
|
|
v.state = FaultState
|
|
return newError(ctx.ip, op, err)
|
|
}
|
|
return v.execute(ctx, op, param)
|
|
}
|
|
|
|
// StepInto behaves the same as “step over” in case if the line does not contain a function. Otherwise
|
|
// the debugger will enter the called function and continue line-by-line debugging there.
|
|
func (v *VM) StepInto() error {
|
|
ctx := v.Context()
|
|
|
|
if ctx == nil {
|
|
v.state = HaltState
|
|
}
|
|
|
|
if v.HasStopped() {
|
|
return nil
|
|
}
|
|
|
|
if ctx != nil && ctx.prog != nil {
|
|
op, param, err := ctx.Next()
|
|
if err != nil {
|
|
v.state = FaultState
|
|
return newError(ctx.ip, op, err)
|
|
}
|
|
vErr := v.execute(ctx, op, param)
|
|
if vErr != nil {
|
|
return vErr
|
|
}
|
|
}
|
|
|
|
cctx := v.Context()
|
|
if cctx != nil && cctx.atBreakPoint() {
|
|
v.state = BreakState
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StepOut takes the debugger to the line where the current function was called.
|
|
func (v *VM) StepOut() error {
|
|
var err error
|
|
if v.state == BreakState {
|
|
v.state = NoneState
|
|
}
|
|
|
|
expSize := v.istack.len
|
|
for v.state == NoneState && v.istack.len >= expSize {
|
|
err = v.StepInto()
|
|
}
|
|
if v.state == NoneState {
|
|
v.state = BreakState
|
|
}
|
|
return err
|
|
}
|
|
|
|
// StepOver takes the debugger to the line that will step over a given line.
|
|
// If the line contains a function the function will be executed and the result returned without debugging each line.
|
|
func (v *VM) StepOver() error {
|
|
var err error
|
|
if v.HasStopped() {
|
|
return err
|
|
}
|
|
|
|
if v.state == BreakState {
|
|
v.state = NoneState
|
|
}
|
|
|
|
expSize := v.istack.len
|
|
for {
|
|
err = v.StepInto()
|
|
if !(v.state == NoneState && v.istack.len > expSize) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if v.state == NoneState {
|
|
v.state = BreakState
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// HasFailed returns whether VM is in the failed state now. Usually used to
|
|
// check status after Run.
|
|
func (v *VM) HasFailed() bool {
|
|
return v.state.HasFlag(FaultState)
|
|
}
|
|
|
|
// HasStopped returns whether VM is in Halt or Failed state.
|
|
func (v *VM) HasStopped() bool {
|
|
return v.state.HasFlag(HaltState) || v.state.HasFlag(FaultState)
|
|
}
|
|
|
|
// HasHalted returns whether VM is in Halt state.
|
|
func (v *VM) HasHalted() bool {
|
|
return v.state.HasFlag(HaltState)
|
|
}
|
|
|
|
// AtBreakpoint returns whether VM is at breakpoint.
|
|
func (v *VM) AtBreakpoint() bool {
|
|
return v.state.HasFlag(BreakState)
|
|
}
|
|
|
|
// GetInteropID converts instruction parameter to an interop ID.
|
|
func GetInteropID(parameter []byte) uint32 {
|
|
return binary.LittleEndian.Uint32(parameter)
|
|
}
|
|
|
|
// execute performs an instruction cycle in the VM. Acting on the instruction (opcode).
|
|
func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err error) {
|
|
// Instead of polluting the whole VM logic with error handling, we will recover
|
|
// each panic at a central point, putting the VM in a fault state and setting error.
|
|
defer func() {
|
|
if errRecover := recover(); errRecover != nil {
|
|
v.state = FaultState
|
|
err = newError(ctx.ip, op, errRecover)
|
|
} else if v.refs.size > MaxStackSize {
|
|
v.state = FaultState
|
|
err = newError(ctx.ip, op, "stack is too big")
|
|
}
|
|
}()
|
|
|
|
if v.getPrice != nil && ctx.ip < len(ctx.prog) {
|
|
v.gasConsumed += v.getPrice(op, parameter)
|
|
if v.GasLimit >= 0 && v.gasConsumed > v.GasLimit {
|
|
panic("gas limit is exceeded")
|
|
}
|
|
}
|
|
|
|
if op <= opcode.PUSHINT256 {
|
|
v.estack.PushVal(bigint.FromBytes(parameter))
|
|
return
|
|
}
|
|
|
|
switch op {
|
|
case opcode.PUSHM1, opcode.PUSH0, opcode.PUSH1, opcode.PUSH2, opcode.PUSH3,
|
|
opcode.PUSH4, opcode.PUSH5, opcode.PUSH6, opcode.PUSH7,
|
|
opcode.PUSH8, opcode.PUSH9, opcode.PUSH10, opcode.PUSH11,
|
|
opcode.PUSH12, opcode.PUSH13, opcode.PUSH14, opcode.PUSH15,
|
|
opcode.PUSH16:
|
|
val := int(op) - int(opcode.PUSH0)
|
|
v.estack.PushVal(val)
|
|
|
|
case opcode.PUSHDATA1, opcode.PUSHDATA2, opcode.PUSHDATA4:
|
|
v.estack.PushVal(parameter)
|
|
|
|
case opcode.PUSHA:
|
|
n := getJumpOffset(ctx, parameter)
|
|
ptr := stackitem.NewPointerWithHash(n, ctx.prog, ctx.ScriptHash())
|
|
v.estack.PushVal(ptr)
|
|
|
|
case opcode.PUSHNULL:
|
|
v.estack.PushVal(stackitem.Null{})
|
|
|
|
case opcode.ISNULL:
|
|
_, ok := v.estack.Pop().value.(stackitem.Null)
|
|
v.estack.PushVal(ok)
|
|
|
|
case opcode.ISTYPE:
|
|
res := v.estack.Pop().Item()
|
|
v.estack.PushVal(res.Type() == stackitem.Type(parameter[0]))
|
|
|
|
case opcode.CONVERT:
|
|
typ := stackitem.Type(parameter[0])
|
|
item := v.estack.Pop().Item()
|
|
result, err := item.Convert(typ)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
v.estack.PushVal(result)
|
|
|
|
case opcode.INITSSLOT:
|
|
if parameter[0] == 0 {
|
|
panic("zero argument")
|
|
}
|
|
ctx.static.init(int(parameter[0]))
|
|
|
|
case opcode.INITSLOT:
|
|
if ctx.local != nil || ctx.arguments != nil {
|
|
panic("already initialized")
|
|
}
|
|
if parameter[0] == 0 && parameter[1] == 0 {
|
|
panic("zero argument")
|
|
}
|
|
if parameter[0] > 0 {
|
|
ctx.local = v.newSlot(int(parameter[0]))
|
|
}
|
|
if parameter[1] > 0 {
|
|
sz := int(parameter[1])
|
|
ctx.arguments = v.newSlot(sz)
|
|
for i := 0; i < sz; i++ {
|
|
ctx.arguments.Set(i, v.estack.Pop().Item())
|
|
}
|
|
}
|
|
|
|
case opcode.LDSFLD0, opcode.LDSFLD1, opcode.LDSFLD2, opcode.LDSFLD3, opcode.LDSFLD4, opcode.LDSFLD5, opcode.LDSFLD6:
|
|
item := ctx.static.Get(int(op - opcode.LDSFLD0))
|
|
v.estack.PushVal(item)
|
|
|
|
case opcode.LDSFLD:
|
|
item := ctx.static.Get(int(parameter[0]))
|
|
v.estack.PushVal(item)
|
|
|
|
case opcode.STSFLD0, opcode.STSFLD1, opcode.STSFLD2, opcode.STSFLD3, opcode.STSFLD4, opcode.STSFLD5, opcode.STSFLD6:
|
|
item := v.estack.Pop().Item()
|
|
ctx.static.Set(int(op-opcode.STSFLD0), item)
|
|
|
|
case opcode.STSFLD:
|
|
item := v.estack.Pop().Item()
|
|
ctx.static.Set(int(parameter[0]), item)
|
|
|
|
case opcode.LDLOC0, opcode.LDLOC1, opcode.LDLOC2, opcode.LDLOC3, opcode.LDLOC4, opcode.LDLOC5, opcode.LDLOC6:
|
|
item := ctx.local.Get(int(op - opcode.LDLOC0))
|
|
v.estack.PushVal(item)
|
|
|
|
case opcode.LDLOC:
|
|
item := ctx.local.Get(int(parameter[0]))
|
|
v.estack.PushVal(item)
|
|
|
|
case opcode.STLOC0, opcode.STLOC1, opcode.STLOC2, opcode.STLOC3, opcode.STLOC4, opcode.STLOC5, opcode.STLOC6:
|
|
item := v.estack.Pop().Item()
|
|
ctx.local.Set(int(op-opcode.STLOC0), item)
|
|
|
|
case opcode.STLOC:
|
|
item := v.estack.Pop().Item()
|
|
ctx.local.Set(int(parameter[0]), item)
|
|
|
|
case opcode.LDARG0, opcode.LDARG1, opcode.LDARG2, opcode.LDARG3, opcode.LDARG4, opcode.LDARG5, opcode.LDARG6:
|
|
item := ctx.arguments.Get(int(op - opcode.LDARG0))
|
|
v.estack.PushVal(item)
|
|
|
|
case opcode.LDARG:
|
|
item := ctx.arguments.Get(int(parameter[0]))
|
|
v.estack.PushVal(item)
|
|
|
|
case opcode.STARG0, opcode.STARG1, opcode.STARG2, opcode.STARG3, opcode.STARG4, opcode.STARG5, opcode.STARG6:
|
|
item := v.estack.Pop().Item()
|
|
ctx.arguments.Set(int(op-opcode.STARG0), item)
|
|
|
|
case opcode.STARG:
|
|
item := v.estack.Pop().Item()
|
|
ctx.arguments.Set(int(parameter[0]), item)
|
|
|
|
case opcode.NEWBUFFER:
|
|
n := toInt(v.estack.Pop().BigInt())
|
|
if n < 0 || n > stackitem.MaxSize {
|
|
panic("invalid size")
|
|
}
|
|
v.estack.PushVal(stackitem.NewBuffer(make([]byte, n)))
|
|
|
|
case opcode.MEMCPY:
|
|
n := toInt(v.estack.Pop().BigInt())
|
|
if n < 0 {
|
|
panic("invalid size")
|
|
}
|
|
si := toInt(v.estack.Pop().BigInt())
|
|
if si < 0 {
|
|
panic("invalid source index")
|
|
}
|
|
src := v.estack.Pop().Bytes()
|
|
if sum := si + n; sum < 0 || sum > len(src) {
|
|
panic("size is too big")
|
|
}
|
|
di := toInt(v.estack.Pop().BigInt())
|
|
if di < 0 {
|
|
panic("invalid destination index")
|
|
}
|
|
dst := v.estack.Pop().value.(*stackitem.Buffer).Value().([]byte)
|
|
if sum := di + n; sum < 0 || sum > len(dst) {
|
|
panic("size is too big")
|
|
}
|
|
copy(dst[di:], src[si:si+n])
|
|
|
|
case opcode.CAT:
|
|
b := v.estack.Pop().Bytes()
|
|
a := v.estack.Pop().Bytes()
|
|
l := len(a) + len(b)
|
|
if l > stackitem.MaxSize {
|
|
panic(fmt.Sprintf("too big item: %d", l))
|
|
}
|
|
ab := make([]byte, l)
|
|
copy(ab, a)
|
|
copy(ab[len(a):], b)
|
|
v.estack.PushVal(stackitem.NewBuffer(ab))
|
|
|
|
case opcode.SUBSTR:
|
|
l := int(v.estack.Pop().BigInt().Int64())
|
|
if l < 0 {
|
|
panic("negative length")
|
|
}
|
|
o := int(v.estack.Pop().BigInt().Int64())
|
|
if o < 0 {
|
|
panic("negative index")
|
|
}
|
|
s := v.estack.Pop().Bytes()
|
|
last := l + o
|
|
if last > len(s) {
|
|
panic("invalid offset")
|
|
}
|
|
res := make([]byte, l)
|
|
copy(res, s[o:last])
|
|
v.estack.PushVal(stackitem.NewBuffer(res))
|
|
|
|
case opcode.LEFT:
|
|
l := int(v.estack.Pop().BigInt().Int64())
|
|
if l < 0 {
|
|
panic("negative length")
|
|
}
|
|
s := v.estack.Pop().Bytes()
|
|
if t := len(s); l > t {
|
|
panic("size is too big")
|
|
}
|
|
res := make([]byte, l)
|
|
copy(res, s[:l])
|
|
v.estack.PushVal(stackitem.NewBuffer(res))
|
|
|
|
case opcode.RIGHT:
|
|
l := int(v.estack.Pop().BigInt().Int64())
|
|
if l < 0 {
|
|
panic("negative length")
|
|
}
|
|
s := v.estack.Pop().Bytes()
|
|
res := make([]byte, l)
|
|
copy(res, s[len(s)-l:])
|
|
v.estack.PushVal(stackitem.NewBuffer(res))
|
|
|
|
case opcode.DEPTH:
|
|
v.estack.PushVal(v.estack.Len())
|
|
|
|
case opcode.DROP:
|
|
if v.estack.Len() < 1 {
|
|
panic("stack is too small")
|
|
}
|
|
v.estack.Pop()
|
|
|
|
case opcode.NIP:
|
|
elem := v.estack.RemoveAt(1)
|
|
if elem == nil {
|
|
panic("no second element found")
|
|
}
|
|
|
|
case opcode.XDROP:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
if n < 0 {
|
|
panic("invalid length")
|
|
}
|
|
e := v.estack.RemoveAt(n)
|
|
if e == nil {
|
|
panic("bad index")
|
|
}
|
|
|
|
case opcode.CLEAR:
|
|
v.estack.Clear()
|
|
|
|
case opcode.DUP:
|
|
v.estack.Push(v.estack.Dup(0))
|
|
|
|
case opcode.OVER:
|
|
a := v.estack.Dup(1)
|
|
if a == nil {
|
|
panic("no second element found")
|
|
}
|
|
v.estack.Push(a)
|
|
|
|
case opcode.PICK:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
if n < 0 {
|
|
panic("negative stack item returned")
|
|
}
|
|
a := v.estack.Dup(n)
|
|
if a == nil {
|
|
panic("no nth element found")
|
|
}
|
|
v.estack.Push(a)
|
|
|
|
case opcode.TUCK:
|
|
a := v.estack.Dup(0)
|
|
if a == nil {
|
|
panic("no top-level element found")
|
|
}
|
|
if v.estack.Len() < 2 {
|
|
panic("can't TUCK with a one-element stack")
|
|
}
|
|
v.estack.InsertAt(a, 2)
|
|
|
|
case opcode.SWAP:
|
|
err := v.estack.Swap(1, 0)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
case opcode.ROT:
|
|
err := v.estack.Roll(2)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
case opcode.ROLL:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
err := v.estack.Roll(n)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
case opcode.REVERSE3, opcode.REVERSE4, opcode.REVERSEN:
|
|
n := 3
|
|
switch op {
|
|
case opcode.REVERSE4:
|
|
n = 4
|
|
case opcode.REVERSEN:
|
|
n = int(v.estack.Pop().BigInt().Int64())
|
|
}
|
|
if err := v.estack.ReverseTop(n); err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
// Bit operations.
|
|
case opcode.INVERT:
|
|
// inplace
|
|
e := v.estack.Peek(0)
|
|
i := e.BigInt()
|
|
e.value = stackitem.Make(new(big.Int).Not(i))
|
|
|
|
case opcode.AND:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(new(big.Int).And(b, a))
|
|
|
|
case opcode.OR:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(new(big.Int).Or(b, a))
|
|
|
|
case opcode.XOR:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(new(big.Int).Xor(b, a))
|
|
|
|
case opcode.EQUAL, opcode.NOTEQUAL:
|
|
b := v.estack.Pop()
|
|
if b == nil {
|
|
panic("no top-level element found")
|
|
}
|
|
a := v.estack.Pop()
|
|
if a == nil {
|
|
panic("no second-to-the-top element found")
|
|
}
|
|
v.estack.PushVal(a.value.Equals(b.value) == (op == opcode.EQUAL))
|
|
|
|
// Numeric operations.
|
|
case opcode.SIGN:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(x.Sign())
|
|
|
|
case opcode.ABS:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(new(big.Int).Abs(x))
|
|
|
|
case opcode.NEGATE:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(new(big.Int).Neg(x))
|
|
|
|
case opcode.INC:
|
|
x := v.estack.Pop().BigInt()
|
|
a := new(big.Int).Add(x, big.NewInt(1))
|
|
v.estack.PushVal(a)
|
|
|
|
case opcode.DEC:
|
|
x := v.estack.Pop().BigInt()
|
|
a := new(big.Int).Sub(x, big.NewInt(1))
|
|
v.estack.PushVal(a)
|
|
|
|
case opcode.ADD:
|
|
a := v.estack.Pop().BigInt()
|
|
b := v.estack.Pop().BigInt()
|
|
|
|
c := new(big.Int).Add(a, b)
|
|
v.estack.PushVal(c)
|
|
|
|
case opcode.SUB:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
|
|
c := new(big.Int).Sub(a, b)
|
|
v.estack.PushVal(c)
|
|
|
|
case opcode.MUL:
|
|
a := v.estack.Pop().BigInt()
|
|
b := v.estack.Pop().BigInt()
|
|
|
|
c := new(big.Int).Mul(a, b)
|
|
v.estack.PushVal(c)
|
|
|
|
case opcode.DIV:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
|
|
v.estack.PushVal(new(big.Int).Quo(a, b))
|
|
|
|
case opcode.MOD:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
|
|
v.estack.PushVal(new(big.Int).Rem(a, b))
|
|
|
|
case opcode.POW:
|
|
exp := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
if ei := exp.Uint64(); !exp.IsUint64() || ei > maxSHLArg {
|
|
panic("invalid exponent")
|
|
}
|
|
v.estack.PushVal(new(big.Int).Exp(a, exp, nil))
|
|
|
|
case opcode.SQRT:
|
|
a := v.estack.Pop().BigInt()
|
|
if a.Sign() == -1 {
|
|
panic("negative value")
|
|
}
|
|
|
|
v.estack.PushVal(new(big.Int).Sqrt(a))
|
|
|
|
case opcode.SHL, opcode.SHR:
|
|
b := v.estack.Pop().BigInt().Int64()
|
|
if b == 0 {
|
|
return
|
|
} else if b < 0 || b > maxSHLArg {
|
|
panic(fmt.Sprintf("operand must be between %d and %d", 0, maxSHLArg))
|
|
}
|
|
a := v.estack.Pop().BigInt()
|
|
|
|
var item big.Int
|
|
if op == opcode.SHL {
|
|
item.Lsh(a, uint(b))
|
|
} else {
|
|
item.Rsh(a, uint(b))
|
|
}
|
|
|
|
v.estack.PushVal(&item)
|
|
|
|
case opcode.NOT:
|
|
x := v.estack.Pop().Bool()
|
|
v.estack.PushVal(!x)
|
|
|
|
case opcode.BOOLAND:
|
|
b := v.estack.Pop().Bool()
|
|
a := v.estack.Pop().Bool()
|
|
v.estack.PushVal(a && b)
|
|
|
|
case opcode.BOOLOR:
|
|
b := v.estack.Pop().Bool()
|
|
a := v.estack.Pop().Bool()
|
|
v.estack.PushVal(a || b)
|
|
|
|
case opcode.NZ:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(x.Sign() != 0)
|
|
|
|
case opcode.NUMEQUAL:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(b) == 0)
|
|
|
|
case opcode.NUMNOTEQUAL:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(b) != 0)
|
|
|
|
case opcode.LT, opcode.LE, opcode.GT, opcode.GE:
|
|
eb := v.estack.Pop()
|
|
ea := v.estack.Pop()
|
|
_, aNil := ea.Item().(stackitem.Null)
|
|
_, bNil := eb.Item().(stackitem.Null)
|
|
|
|
res := !aNil && !bNil
|
|
if res {
|
|
cmp := ea.BigInt().Cmp(eb.BigInt())
|
|
switch op {
|
|
case opcode.LT:
|
|
res = cmp == -1
|
|
case opcode.LE:
|
|
res = cmp <= 0
|
|
case opcode.GT:
|
|
res = cmp == 1
|
|
case opcode.GE:
|
|
res = cmp >= 0
|
|
}
|
|
}
|
|
v.estack.PushVal(res)
|
|
|
|
case opcode.MIN:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
val := a
|
|
if a.Cmp(b) == 1 {
|
|
val = b
|
|
}
|
|
v.estack.PushVal(val)
|
|
|
|
case opcode.MAX:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
val := a
|
|
if a.Cmp(b) == -1 {
|
|
val = b
|
|
}
|
|
v.estack.PushVal(val)
|
|
|
|
case opcode.WITHIN:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(x) <= 0 && x.Cmp(b) == -1)
|
|
|
|
// Object operations
|
|
case opcode.NEWARRAY0:
|
|
v.estack.PushVal(stackitem.NewArray([]stackitem.Item{}))
|
|
|
|
case opcode.NEWARRAY, opcode.NEWARRAYT, opcode.NEWSTRUCT:
|
|
n := toInt(v.estack.Pop().BigInt())
|
|
if n < 0 || n > MaxStackSize {
|
|
panic("wrong number of elements")
|
|
}
|
|
typ := stackitem.AnyT
|
|
if op == opcode.NEWARRAYT {
|
|
typ = stackitem.Type(parameter[0])
|
|
}
|
|
items := makeArrayOfType(int(n), typ)
|
|
var res stackitem.Item
|
|
if op == opcode.NEWSTRUCT {
|
|
res = stackitem.NewStruct(items)
|
|
} else {
|
|
res = stackitem.NewArray(items)
|
|
}
|
|
v.estack.PushVal(res)
|
|
|
|
case opcode.NEWSTRUCT0:
|
|
v.estack.PushVal(stackitem.NewStruct([]stackitem.Item{}))
|
|
|
|
case opcode.APPEND:
|
|
itemElem := v.estack.Pop()
|
|
arrElem := v.estack.Pop()
|
|
|
|
val := cloneIfStruct(itemElem.value)
|
|
|
|
switch t := arrElem.value.(type) {
|
|
case *stackitem.Array:
|
|
t.Append(val)
|
|
case *stackitem.Struct:
|
|
t.Append(val)
|
|
default:
|
|
panic("APPEND: not of underlying type Array")
|
|
}
|
|
|
|
v.refs.Add(val)
|
|
|
|
case opcode.PACK:
|
|
n := toInt(v.estack.Pop().BigInt())
|
|
if n < 0 || n > v.estack.Len() {
|
|
panic("OPACK: invalid length")
|
|
}
|
|
|
|
items := make([]stackitem.Item, n)
|
|
for i := 0; i < n; i++ {
|
|
items[i] = v.estack.Pop().value
|
|
}
|
|
|
|
v.estack.PushVal(items)
|
|
|
|
case opcode.UNPACK:
|
|
a := v.estack.Pop().Array()
|
|
l := len(a)
|
|
for i := l - 1; i >= 0; i-- {
|
|
v.estack.PushVal(a[i])
|
|
}
|
|
v.estack.PushVal(l)
|
|
|
|
case opcode.PICKITEM:
|
|
key := v.estack.Pop()
|
|
validateMapKey(key)
|
|
|
|
obj := v.estack.Pop()
|
|
index := int(key.BigInt().Int64())
|
|
|
|
switch t := obj.value.(type) {
|
|
// Struct and Array items have their underlying value as []Item.
|
|
case *stackitem.Array, *stackitem.Struct:
|
|
arr := t.Value().([]stackitem.Item)
|
|
if index < 0 || index >= len(arr) {
|
|
panic("PICKITEM: invalid index")
|
|
}
|
|
item := arr[index].Dup()
|
|
v.estack.PushVal(item)
|
|
case *stackitem.Map:
|
|
index := t.Index(key.Item())
|
|
if index < 0 {
|
|
panic("invalid key")
|
|
}
|
|
v.estack.Push(&Element{value: t.Value().([]stackitem.MapElement)[index].Value.Dup()})
|
|
default:
|
|
arr := obj.Bytes()
|
|
if index < 0 || index >= len(arr) {
|
|
panic("PICKITEM: invalid index")
|
|
}
|
|
item := arr[index]
|
|
v.estack.PushVal(int(item))
|
|
}
|
|
|
|
case opcode.SETITEM:
|
|
item := v.estack.Pop().value
|
|
key := v.estack.Pop()
|
|
validateMapKey(key)
|
|
|
|
obj := v.estack.Pop()
|
|
|
|
switch t := obj.value.(type) {
|
|
// Struct and Array items have their underlying value as []Item.
|
|
case *stackitem.Array, *stackitem.Struct:
|
|
arr := t.Value().([]stackitem.Item)
|
|
index := int(key.BigInt().Int64())
|
|
if index < 0 || index >= len(arr) {
|
|
panic("SETITEM: invalid index")
|
|
}
|
|
v.refs.Remove(arr[index])
|
|
arr[index] = item
|
|
v.refs.Add(arr[index])
|
|
case *stackitem.Map:
|
|
if i := t.Index(key.value); i >= 0 {
|
|
v.refs.Remove(t.Value().([]stackitem.MapElement)[i].Value)
|
|
}
|
|
t.Add(key.value, item)
|
|
v.refs.Add(item)
|
|
|
|
case *stackitem.Buffer:
|
|
index := toInt(key.BigInt())
|
|
if index < 0 || index >= t.Len() {
|
|
panic("invalid index")
|
|
}
|
|
bi, err := item.TryInteger()
|
|
b := toInt(bi)
|
|
if err != nil || b < math.MinInt8 || b > math.MaxUint8 {
|
|
panic("invalid value")
|
|
}
|
|
t.Value().([]byte)[index] = byte(b)
|
|
|
|
default:
|
|
panic(fmt.Sprintf("SETITEM: invalid item type %s", t))
|
|
}
|
|
|
|
case opcode.REVERSEITEMS:
|
|
item := v.estack.Pop()
|
|
switch t := item.value.(type) {
|
|
case *stackitem.Array, *stackitem.Struct:
|
|
a := t.Value().([]stackitem.Item)
|
|
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
|
|
a[i], a[j] = a[j], a[i]
|
|
}
|
|
case *stackitem.Buffer:
|
|
b := t.Value().([]byte)
|
|
slice.Reverse(b)
|
|
default:
|
|
panic(fmt.Sprintf("invalid item type %s", t))
|
|
}
|
|
case opcode.REMOVE:
|
|
key := v.estack.Pop()
|
|
validateMapKey(key)
|
|
|
|
elem := v.estack.Pop()
|
|
switch t := elem.value.(type) {
|
|
case *stackitem.Array:
|
|
a := t.Value().([]stackitem.Item)
|
|
k := int(key.BigInt().Int64())
|
|
if k < 0 || k >= len(a) {
|
|
panic("REMOVE: invalid index")
|
|
}
|
|
v.refs.Remove(a[k])
|
|
t.Remove(k)
|
|
case *stackitem.Struct:
|
|
a := t.Value().([]stackitem.Item)
|
|
k := int(key.BigInt().Int64())
|
|
if k < 0 || k >= len(a) {
|
|
panic("REMOVE: invalid index")
|
|
}
|
|
v.refs.Remove(a[k])
|
|
t.Remove(k)
|
|
case *stackitem.Map:
|
|
index := t.Index(key.Item())
|
|
// NEO 2.0 doesn't error on missing key.
|
|
if index >= 0 {
|
|
v.refs.Remove(t.Value().([]stackitem.MapElement)[index].Value)
|
|
t.Drop(index)
|
|
}
|
|
default:
|
|
panic("REMOVE: invalid type")
|
|
}
|
|
|
|
case opcode.CLEARITEMS:
|
|
elem := v.estack.Pop()
|
|
switch t := elem.value.(type) {
|
|
case *stackitem.Array:
|
|
for _, item := range t.Value().([]stackitem.Item) {
|
|
v.refs.Remove(item)
|
|
}
|
|
t.Clear()
|
|
case *stackitem.Struct:
|
|
for _, item := range t.Value().([]stackitem.Item) {
|
|
v.refs.Remove(item)
|
|
}
|
|
t.Clear()
|
|
case *stackitem.Map:
|
|
for i := range t.Value().([]stackitem.MapElement) {
|
|
v.refs.Remove(t.Value().([]stackitem.MapElement)[i].Value)
|
|
}
|
|
t.Clear()
|
|
default:
|
|
panic("CLEARITEMS: invalid type")
|
|
}
|
|
|
|
case opcode.POPITEM:
|
|
arr := v.estack.Pop().Item()
|
|
elems := arr.Value().([]stackitem.Item)
|
|
index := len(elems) - 1
|
|
elem := elems[index]
|
|
switch item := arr.(type) {
|
|
case *stackitem.Array:
|
|
item.Remove(index)
|
|
case *stackitem.Struct:
|
|
item.Remove(index)
|
|
}
|
|
v.refs.Remove(elem)
|
|
v.estack.PushVal(elem)
|
|
|
|
case opcode.SIZE:
|
|
elem := v.estack.Pop()
|
|
// Cause there is no native (byte) item type here, hence we need to check
|
|
// the type of the item for array size operations.
|
|
switch t := elem.Value().(type) {
|
|
case []stackitem.Item:
|
|
v.estack.PushVal(len(t))
|
|
case []stackitem.MapElement:
|
|
v.estack.PushVal(len(t))
|
|
default:
|
|
v.estack.PushVal(len(elem.Bytes()))
|
|
}
|
|
|
|
case opcode.JMP, opcode.JMPL, opcode.JMPIF, opcode.JMPIFL, opcode.JMPIFNOT, opcode.JMPIFNOTL,
|
|
opcode.JMPEQ, opcode.JMPEQL, opcode.JMPNE, opcode.JMPNEL,
|
|
opcode.JMPGT, opcode.JMPGTL, opcode.JMPGE, opcode.JMPGEL,
|
|
opcode.JMPLT, opcode.JMPLTL, opcode.JMPLE, opcode.JMPLEL:
|
|
offset := getJumpOffset(ctx, parameter)
|
|
cond := true
|
|
switch op {
|
|
case opcode.JMP, opcode.JMPL:
|
|
case opcode.JMPIF, opcode.JMPIFL, opcode.JMPIFNOT, opcode.JMPIFNOTL:
|
|
cond = v.estack.Pop().Bool() == (op == opcode.JMPIF || op == opcode.JMPIFL)
|
|
default:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
cond = getJumpCondition(op, a, b)
|
|
}
|
|
|
|
if cond {
|
|
v.Jump(ctx, offset)
|
|
}
|
|
|
|
case opcode.CALL, opcode.CALLL:
|
|
// Note: jump offset must be calculated regarding to new context,
|
|
// but it is cloned and thus has the same script and instruction pointer.
|
|
v.call(ctx, getJumpOffset(ctx, parameter))
|
|
|
|
case opcode.CALLA:
|
|
ptr := v.estack.Pop().Item().(*stackitem.Pointer)
|
|
if ptr.ScriptHash() != ctx.ScriptHash() {
|
|
panic("invalid script in pointer")
|
|
}
|
|
|
|
v.call(ctx, ptr.Position())
|
|
|
|
case opcode.CALLT:
|
|
id := int32(binary.LittleEndian.Uint16(parameter))
|
|
if err := v.LoadToken(id); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
case opcode.SYSCALL:
|
|
interopID := GetInteropID(parameter)
|
|
err := v.SyscallHandler(v, interopID)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to invoke syscall %d: %s", interopID, err))
|
|
}
|
|
|
|
case opcode.RET:
|
|
oldCtx := v.istack.Pop().Value().(*Context)
|
|
oldEstack := v.estack
|
|
|
|
v.unloadContext(oldCtx)
|
|
if v.istack.Len() == 0 {
|
|
v.state = HaltState
|
|
break
|
|
}
|
|
|
|
newEstack := v.Context().estack
|
|
if oldEstack != newEstack {
|
|
if oldCtx.RetCount >= 0 && oldEstack.Len() != oldCtx.RetCount {
|
|
panic(fmt.Errorf("invalid return values count: expected %d, got %d",
|
|
oldCtx.RetCount, oldEstack.Len()))
|
|
}
|
|
rvcount := oldEstack.Len()
|
|
for i := rvcount; i > 0; i-- {
|
|
elem := oldEstack.RemoveAt(i - 1)
|
|
newEstack.Push(elem)
|
|
}
|
|
v.estack = newEstack
|
|
}
|
|
|
|
case opcode.NEWMAP:
|
|
v.estack.Push(&Element{value: stackitem.NewMap()})
|
|
|
|
case opcode.KEYS:
|
|
item := v.estack.Pop()
|
|
if item == nil {
|
|
panic("no argument")
|
|
}
|
|
|
|
m, ok := item.value.(*stackitem.Map)
|
|
if !ok {
|
|
panic("not a Map")
|
|
}
|
|
|
|
arr := make([]stackitem.Item, 0, m.Len())
|
|
for k := range m.Value().([]stackitem.MapElement) {
|
|
arr = append(arr, m.Value().([]stackitem.MapElement)[k].Key.Dup())
|
|
}
|
|
v.estack.PushVal(arr)
|
|
|
|
case opcode.VALUES:
|
|
item := v.estack.Pop()
|
|
if item == nil {
|
|
panic("no argument")
|
|
}
|
|
|
|
var arr []stackitem.Item
|
|
switch t := item.value.(type) {
|
|
case *stackitem.Array, *stackitem.Struct:
|
|
src := t.Value().([]stackitem.Item)
|
|
arr = make([]stackitem.Item, len(src))
|
|
for i := range src {
|
|
arr[i] = cloneIfStruct(src[i])
|
|
}
|
|
case *stackitem.Map:
|
|
arr = make([]stackitem.Item, 0, t.Len())
|
|
for k := range t.Value().([]stackitem.MapElement) {
|
|
arr = append(arr, cloneIfStruct(t.Value().([]stackitem.MapElement)[k].Value))
|
|
}
|
|
default:
|
|
panic("not a Map, Array or Struct")
|
|
}
|
|
|
|
v.estack.PushVal(arr)
|
|
|
|
case opcode.HASKEY:
|
|
key := v.estack.Pop()
|
|
validateMapKey(key)
|
|
|
|
c := v.estack.Pop()
|
|
if c == nil {
|
|
panic("no value found")
|
|
}
|
|
switch t := c.value.(type) {
|
|
case *stackitem.Array, *stackitem.Struct:
|
|
index := key.BigInt().Int64()
|
|
if index < 0 {
|
|
panic("negative index")
|
|
}
|
|
v.estack.PushVal(index < int64(len(c.Array())))
|
|
case *stackitem.Map:
|
|
v.estack.PushVal(t.Has(key.Item()))
|
|
case *stackitem.Buffer:
|
|
index := key.BigInt().Int64()
|
|
if index < 0 {
|
|
panic("negative index")
|
|
}
|
|
v.estack.PushVal(index < int64(t.Len()))
|
|
default:
|
|
panic("wrong collection type")
|
|
}
|
|
|
|
case opcode.NOP:
|
|
// unlucky ^^
|
|
|
|
case opcode.THROW:
|
|
v.throw(v.estack.Pop().Item())
|
|
|
|
case opcode.ABORT:
|
|
panic("ABORT")
|
|
|
|
case opcode.ASSERT:
|
|
if !v.estack.Pop().Bool() {
|
|
panic("ASSERT failed")
|
|
}
|
|
|
|
case opcode.TRY, opcode.TRYL:
|
|
catchP, finallyP := getTryParams(op, parameter)
|
|
if ctx.tryStack.Len() >= MaxTryNestingDepth {
|
|
panic("maximum TRY depth exceeded")
|
|
}
|
|
cOffset := getJumpOffset(ctx, catchP)
|
|
fOffset := getJumpOffset(ctx, finallyP)
|
|
if cOffset == ctx.ip && fOffset == ctx.ip {
|
|
panic("invalid offset for TRY*")
|
|
} else if cOffset == ctx.ip {
|
|
cOffset = -1
|
|
} else if fOffset == ctx.ip {
|
|
fOffset = -1
|
|
}
|
|
eCtx := newExceptionHandlingContext(cOffset, fOffset)
|
|
ctx.tryStack.PushVal(eCtx)
|
|
|
|
case opcode.ENDTRY, opcode.ENDTRYL:
|
|
eCtx := ctx.tryStack.Peek(0).Value().(*exceptionHandlingContext)
|
|
if eCtx.State == eFinally {
|
|
panic("invalid exception handling state during ENDTRY*")
|
|
}
|
|
eOffset := getJumpOffset(ctx, parameter)
|
|
if eCtx.HasFinally() {
|
|
eCtx.State = eFinally
|
|
eCtx.EndOffset = eOffset
|
|
eOffset = eCtx.FinallyOffset
|
|
} else {
|
|
ctx.tryStack.Pop()
|
|
}
|
|
v.Jump(ctx, eOffset)
|
|
|
|
case opcode.ENDFINALLY:
|
|
if v.uncaughtException != nil {
|
|
v.handleException()
|
|
return
|
|
}
|
|
eCtx := ctx.tryStack.Pop().Value().(*exceptionHandlingContext)
|
|
v.Jump(ctx, eCtx.EndOffset)
|
|
|
|
default:
|
|
panic(fmt.Sprintf("unknown opcode %s", op.String()))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (v *VM) unloadContext(ctx *Context) {
|
|
if ctx.local != nil {
|
|
ctx.local.Clear()
|
|
}
|
|
if ctx.arguments != nil {
|
|
ctx.arguments.Clear()
|
|
}
|
|
currCtx := v.Context()
|
|
if ctx.static != nil && currCtx != nil && ctx.static != currCtx.static {
|
|
ctx.static.Clear()
|
|
}
|
|
}
|
|
|
|
// getTryParams splits TRY(L) instruction parameter into offsets for catch and finally blocks.
|
|
func getTryParams(op opcode.Opcode, p []byte) ([]byte, []byte) {
|
|
i := 1
|
|
if op == opcode.TRYL {
|
|
i = 4
|
|
}
|
|
return p[:i], p[i:]
|
|
}
|
|
|
|
// getJumpCondition performs opcode specific comparison of a and b.
|
|
func getJumpCondition(op opcode.Opcode, a, b *big.Int) bool {
|
|
cmp := a.Cmp(b)
|
|
switch op {
|
|
case opcode.JMPEQ, opcode.JMPEQL:
|
|
return cmp == 0
|
|
case opcode.JMPNE, opcode.JMPNEL:
|
|
return cmp != 0
|
|
case opcode.JMPGT, opcode.JMPGTL:
|
|
return cmp > 0
|
|
case opcode.JMPGE, opcode.JMPGEL:
|
|
return cmp >= 0
|
|
case opcode.JMPLT, opcode.JMPLTL:
|
|
return cmp < 0
|
|
case opcode.JMPLE, opcode.JMPLEL:
|
|
return cmp <= 0
|
|
default:
|
|
panic(fmt.Sprintf("invalid JMP* opcode: %s", op))
|
|
}
|
|
}
|
|
|
|
func (v *VM) throw(item stackitem.Item) {
|
|
v.uncaughtException = item
|
|
v.handleException()
|
|
}
|
|
|
|
// Jump performs jump to the offset.
|
|
func (v *VM) Jump(ctx *Context, offset int) {
|
|
ctx.nextip = offset
|
|
}
|
|
|
|
// Call calls method by offset. It is similar to Jump but also
|
|
// pushes new context to the invocation stack and increments
|
|
// invocation counter for the corresponding context script hash.
|
|
func (v *VM) Call(ctx *Context, offset int) {
|
|
v.call(ctx, offset)
|
|
v.Invocations[ctx.ScriptHash()]++
|
|
}
|
|
|
|
// call is an internal representation of Call, which does not
|
|
// affect the invocation counter and is only being used by vm
|
|
// package.
|
|
func (v *VM) call(ctx *Context, offset int) {
|
|
v.checkInvocationStackSize()
|
|
newCtx := ctx.Copy()
|
|
newCtx.RetCount = -1
|
|
newCtx.local = nil
|
|
newCtx.arguments = nil
|
|
newCtx.tryStack = newStack("exception", nil)
|
|
newCtx.NEF = ctx.NEF
|
|
v.istack.PushVal(newCtx)
|
|
v.Jump(newCtx, offset)
|
|
}
|
|
|
|
// getJumpOffset returns instruction number in a current context
|
|
// to a which JMP should be performed.
|
|
// parameter should have length either 1 or 4 and
|
|
// is interpreted as little-endian.
|
|
func getJumpOffset(ctx *Context, parameter []byte) int {
|
|
offset, _, err := calcJumpOffset(ctx, parameter)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return offset
|
|
}
|
|
|
|
// calcJumpOffset returns absolute and relative offset of JMP/CALL/TRY instructions
|
|
// either in short (1-byte) or long (4-byte) form.
|
|
func calcJumpOffset(ctx *Context, parameter []byte) (int, int, error) {
|
|
var rOffset int32
|
|
switch l := len(parameter); l {
|
|
case 1:
|
|
rOffset = int32(int8(parameter[0]))
|
|
case 4:
|
|
rOffset = int32(binary.LittleEndian.Uint32(parameter))
|
|
default:
|
|
_, curr := ctx.CurrInstr()
|
|
return 0, 0, fmt.Errorf("invalid %s parameter length: %d", curr, l)
|
|
}
|
|
offset := ctx.ip + int(rOffset)
|
|
if offset < 0 || offset > len(ctx.prog) {
|
|
return 0, 0, fmt.Errorf("invalid offset %d ip at %d", offset, ctx.ip)
|
|
}
|
|
|
|
return offset, int(rOffset), nil
|
|
}
|
|
|
|
func (v *VM) handleException() {
|
|
pop := 0
|
|
ictxv := v.istack.Peek(0)
|
|
ictx := ictxv.Value().(*Context)
|
|
for ictx != nil {
|
|
e := ictx.tryStack.Peek(0)
|
|
for e != nil {
|
|
ectx := e.Value().(*exceptionHandlingContext)
|
|
if ectx.State == eFinally || (ectx.State == eCatch && !ectx.HasFinally()) {
|
|
ictx.tryStack.Pop()
|
|
e = ictx.tryStack.Peek(0)
|
|
continue
|
|
}
|
|
for i := 0; i < pop; i++ {
|
|
ctx := v.istack.Pop().Value().(*Context)
|
|
v.unloadContext(ctx)
|
|
}
|
|
if ectx.State == eTry && ectx.HasCatch() {
|
|
ectx.State = eCatch
|
|
v.estack.PushVal(v.uncaughtException)
|
|
v.uncaughtException = nil
|
|
v.Jump(ictx, ectx.CatchOffset)
|
|
} else {
|
|
ectx.State = eFinally
|
|
v.Jump(ictx, ectx.FinallyOffset)
|
|
}
|
|
return
|
|
}
|
|
pop++
|
|
ictxv = ictxv.Next()
|
|
if ictxv == nil {
|
|
break
|
|
}
|
|
ictx = ictxv.Value().(*Context)
|
|
}
|
|
throwUnhandledException(v.uncaughtException)
|
|
}
|
|
|
|
// throwUnhandledException gets exception message from the provided stackitem and panics.
|
|
func throwUnhandledException(item stackitem.Item) {
|
|
msg := "unhandled exception"
|
|
switch item.Type() {
|
|
case stackitem.ArrayT:
|
|
if arr := item.Value().([]stackitem.Item); len(arr) > 0 {
|
|
data, err := arr[0].TryBytes()
|
|
if err == nil {
|
|
msg = fmt.Sprintf("%s: %q", msg, string(data))
|
|
}
|
|
}
|
|
default:
|
|
data, err := item.TryBytes()
|
|
if err == nil {
|
|
msg = fmt.Sprintf("%s: %q", msg, string(data))
|
|
}
|
|
}
|
|
panic(msg)
|
|
}
|
|
|
|
// CheckMultisigPar checks if sigs contains sufficient valid signatures.
|
|
func CheckMultisigPar(v *VM, curve elliptic.Curve, h []byte, pkeys [][]byte, sigs [][]byte) bool {
|
|
if len(sigs) == 1 {
|
|
return checkMultisig1(v, curve, h, pkeys, sigs[0])
|
|
}
|
|
|
|
k1, k2 := 0, len(pkeys)-1
|
|
s1, s2 := 0, len(sigs)-1
|
|
|
|
type task struct {
|
|
pub *keys.PublicKey
|
|
signum int
|
|
}
|
|
|
|
type verify struct {
|
|
ok bool
|
|
signum int
|
|
}
|
|
|
|
worker := func(ch <-chan task, result chan verify) {
|
|
for {
|
|
t, ok := <-ch
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
result <- verify{
|
|
signum: t.signum,
|
|
ok: t.pub.Verify(sigs[t.signum], h),
|
|
}
|
|
}
|
|
}
|
|
|
|
const workerCount = 3
|
|
tasks := make(chan task, 2)
|
|
results := make(chan verify, len(sigs))
|
|
for i := 0; i < workerCount; i++ {
|
|
go worker(tasks, results)
|
|
}
|
|
|
|
tasks <- task{pub: bytesToPublicKey(pkeys[k1], curve), signum: s1}
|
|
tasks <- task{pub: bytesToPublicKey(pkeys[k2], curve), signum: s2}
|
|
|
|
sigok := true
|
|
taskCount := 2
|
|
|
|
loop:
|
|
for r := range results {
|
|
goingForward := true
|
|
|
|
taskCount--
|
|
if r.signum == s2 {
|
|
goingForward = false
|
|
}
|
|
if k1+1 == k2 {
|
|
sigok = r.ok && s1+1 == s2
|
|
if taskCount != 0 && sigok {
|
|
continue
|
|
}
|
|
break loop
|
|
} else if r.ok {
|
|
if s1+1 == s2 {
|
|
if taskCount != 0 && sigok {
|
|
continue
|
|
}
|
|
break loop
|
|
}
|
|
if goingForward {
|
|
s1++
|
|
} else {
|
|
s2--
|
|
}
|
|
}
|
|
|
|
var nextSig, nextKey int
|
|
if goingForward {
|
|
k1++
|
|
nextSig = s1
|
|
nextKey = k1
|
|
} else {
|
|
k2--
|
|
nextSig = s2
|
|
nextKey = k2
|
|
}
|
|
taskCount++
|
|
tasks <- task{pub: bytesToPublicKey(pkeys[nextKey], curve), signum: nextSig}
|
|
}
|
|
|
|
close(tasks)
|
|
|
|
return sigok
|
|
}
|
|
|
|
func checkMultisig1(v *VM, curve elliptic.Curve, h []byte, pkeys [][]byte, sig []byte) bool {
|
|
for i := range pkeys {
|
|
pkey := bytesToPublicKey(pkeys[i], curve)
|
|
if pkey.Verify(sig, h) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func cloneIfStruct(item stackitem.Item) stackitem.Item {
|
|
switch it := item.(type) {
|
|
case *stackitem.Struct:
|
|
ret, err := it.Clone()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
default:
|
|
return it
|
|
}
|
|
}
|
|
|
|
func makeArrayOfType(n int, typ stackitem.Type) []stackitem.Item {
|
|
if !typ.IsValid() {
|
|
panic(fmt.Sprintf("invalid stack item type: %d", typ))
|
|
}
|
|
items := make([]stackitem.Item, n)
|
|
for i := range items {
|
|
switch typ {
|
|
case stackitem.BooleanT:
|
|
items[i] = stackitem.NewBool(false)
|
|
case stackitem.IntegerT:
|
|
items[i] = stackitem.NewBigInteger(big.NewInt(0))
|
|
case stackitem.ByteArrayT:
|
|
items[i] = stackitem.NewByteArray([]byte{})
|
|
default:
|
|
items[i] = stackitem.Null{}
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func validateMapKey(key *Element) {
|
|
if key == nil {
|
|
panic("no key found")
|
|
}
|
|
if err := stackitem.IsValidMapKey(key.Item()); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (v *VM) checkInvocationStackSize() {
|
|
if v.istack.len >= MaxInvocationStackSize {
|
|
panic("invocation stack is too big")
|
|
}
|
|
}
|
|
|
|
// bytesToPublicKey is a helper deserializing keys using cache and panicing on
|
|
// error.
|
|
func bytesToPublicKey(b []byte, curve elliptic.Curve) *keys.PublicKey {
|
|
pkey, err := keys.NewPublicKeyFromBytes(b, curve)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
return pkey
|
|
}
|
|
|
|
// GetCallingScriptHash implements ScriptHashGetter interface.
|
|
func (v *VM) GetCallingScriptHash() util.Uint160 {
|
|
return v.Context().callingScriptHash
|
|
}
|
|
|
|
// GetEntryScriptHash implements ScriptHashGetter interface.
|
|
func (v *VM) GetEntryScriptHash() util.Uint160 {
|
|
return v.getContextScriptHash(v.istack.len - 1)
|
|
}
|
|
|
|
// GetCurrentScriptHash implements ScriptHashGetter interface.
|
|
func (v *VM) GetCurrentScriptHash() util.Uint160 {
|
|
return v.getContextScriptHash(0)
|
|
}
|
|
|
|
// toInt converts an item to a 32-bit int.
|
|
func toInt(i *big.Int) int {
|
|
if !i.IsInt64() {
|
|
panic("not an int32")
|
|
}
|
|
n := i.Int64()
|
|
if n < math.MinInt32 || n > math.MaxInt32 {
|
|
panic("not an int32")
|
|
}
|
|
return int(n)
|
|
}
|