mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-23 13:38:35 +00:00
1599 lines
36 KiB
Go
1599 lines
36 KiB
Go
package vm
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"os"
|
|
"text/tabwriter"
|
|
"unicode/utf8"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type errorAtInstruct struct {
|
|
ip int
|
|
op opcode.Opcode
|
|
err interface{}
|
|
}
|
|
|
|
func (e *errorAtInstruct) Error() string {
|
|
return fmt.Sprintf("error encountered 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 (
|
|
// MaxArraySize is the maximum array size allowed in the VM.
|
|
MaxArraySize = 1024
|
|
|
|
// MaxItemSize is the maximum item size allowed in the VM.
|
|
MaxItemSize = 1024 * 1024
|
|
|
|
// MaxInvocationStackSize is the maximum size of an invocation stack.
|
|
MaxInvocationStackSize = 1024
|
|
|
|
// MaxBigIntegerSizeBits is the maximum size of BigInt item in bits.
|
|
MaxBigIntegerSizeBits = 32 * 8
|
|
|
|
// MaxStackSize is the maximum number of items allowed to be
|
|
// on all stacks at once.
|
|
MaxStackSize = 2 * 1024
|
|
|
|
maxSHLArg = MaxBigIntegerSizeBits
|
|
minSHLArg = -MaxBigIntegerSizeBits
|
|
)
|
|
|
|
// VM represents the virtual machine.
|
|
type VM struct {
|
|
state State
|
|
|
|
// callbacks to get interops.
|
|
getInterop []InteropGetterFunc
|
|
|
|
// callback to get interop price
|
|
getPrice func(*VM, opcode.Opcode, []byte) util.Fixed8
|
|
|
|
// callback to get scripts.
|
|
getScript func(util.Uint160) ([]byte, bool)
|
|
|
|
istack *Stack // invocation stack.
|
|
estack *Stack // execution stack.
|
|
astack *Stack // alt stack.
|
|
|
|
// Hash to verify in CHECKSIG/CHECKMULTISIG.
|
|
checkhash []byte
|
|
|
|
itemCount map[StackItem]int
|
|
size int
|
|
|
|
gasConsumed util.Fixed8
|
|
gasLimit util.Fixed8
|
|
|
|
// Public keys cache.
|
|
keys map[string]*keys.PublicKey
|
|
}
|
|
|
|
// New returns a new VM object ready to load .avm bytecode scripts.
|
|
func New() *VM {
|
|
vm := &VM{
|
|
getInterop: make([]InteropGetterFunc, 0, 3), // 3 functions is typical for our default usage.
|
|
getScript: nil,
|
|
state: haltState,
|
|
istack: NewStack("invocation"),
|
|
|
|
itemCount: make(map[StackItem]int),
|
|
keys: make(map[string]*keys.PublicKey),
|
|
}
|
|
|
|
vm.estack = vm.newItemStack("evaluation")
|
|
vm.astack = vm.newItemStack("alt")
|
|
|
|
vm.RegisterInteropGetter(getDefaultVMInterop)
|
|
return vm
|
|
}
|
|
|
|
func (v *VM) newItemStack(n string) *Stack {
|
|
s := NewStack(n)
|
|
s.size = &v.size
|
|
s.itemCount = v.itemCount
|
|
|
|
return s
|
|
}
|
|
|
|
// RegisterInteropGetter registers the given InteropGetterFunc into VM. There
|
|
// can be many interop getters and they're probed in LIFO order wrt their
|
|
// registration time.
|
|
func (v *VM) RegisterInteropGetter(f InteropGetterFunc) {
|
|
v.getInterop = append(v.getInterop, f)
|
|
}
|
|
|
|
// SetPriceGetter registers the given PriceGetterFunc in v.
|
|
// f accepts vm's Context, current instruction and instruction parameter.
|
|
func (v *VM) SetPriceGetter(f func(*VM, opcode.Opcode, []byte) util.Fixed8) {
|
|
v.getPrice = f
|
|
}
|
|
|
|
// GasConsumed returns the amount of GAS consumed during execution.
|
|
func (v *VM) GasConsumed() util.Fixed8 {
|
|
return v.gasConsumed
|
|
}
|
|
|
|
// SetGasLimit sets maximum amount of gas which v can spent.
|
|
// If max <= 0, no limit is imposed.
|
|
func (v *VM) SetGasLimit(max util.Fixed8) {
|
|
v.gasLimit = max
|
|
}
|
|
|
|
// Estack returns the evaluation stack so interop hooks can utilize this.
|
|
func (v *VM) Estack() *Stack {
|
|
return v.estack
|
|
}
|
|
|
|
// Astack returns the alt stack so interop hooks can utilize this.
|
|
func (v *VM) Astack() *Stack {
|
|
return v.astack
|
|
}
|
|
|
|
// Istack returns the invocation stack so interop hooks can utilize this.
|
|
func (v *VM) Istack() *Stack {
|
|
return v.istack
|
|
}
|
|
|
|
// SetPublicKeys sets internal key cache to the specified value (note
|
|
// that it doesn't copy them).
|
|
func (v *VM) SetPublicKeys(keys map[string]*keys.PublicKey) {
|
|
v.keys = keys
|
|
}
|
|
|
|
// GetPublicKeys returns internal key cache (note that it doesn't copy it).
|
|
func (v *VM) GetPublicKeys() map[string]*keys.PublicKey {
|
|
return v.keys
|
|
}
|
|
|
|
// LoadArgs loads in the arguments used in the Mian entry point.
|
|
func (v *VM) LoadArgs(method []byte, args []StackItem) {
|
|
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() {
|
|
w := tabwriter.NewWriter(os.Stdout, 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:
|
|
offset := int16(binary.LittleEndian.Uint16(parameter))
|
|
desc = fmt.Sprintf("%d (%d/%x)", ctx.ip+int(offset), offset, parameter)
|
|
case opcode.SYSCALL:
|
|
desc = fmt.Sprintf("%q", parameter)
|
|
case opcode.APPCALL, opcode.TAILCALL:
|
|
desc = fmt.Sprintf("%x", 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()
|
|
}
|
|
|
|
// 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.ip + n)
|
|
}
|
|
|
|
// LoadFile loads a program from the given path, ready to execute it.
|
|
func (v *VM) LoadFile(path string) error {
|
|
b, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
v.Load(b)
|
|
return nil
|
|
}
|
|
|
|
// Load initializes the VM with the program given.
|
|
func (v *VM) Load(prog []byte) {
|
|
// Clear all stacks and state, it could be a reload.
|
|
v.istack.Clear()
|
|
v.estack.Clear()
|
|
v.astack.Clear()
|
|
v.state = noneState
|
|
v.gasConsumed = 0
|
|
v.LoadScript(prog)
|
|
}
|
|
|
|
// 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) {
|
|
ctx := NewContext(b)
|
|
ctx.estack = v.estack
|
|
ctx.astack = v.astack
|
|
v.istack.PushVal(ctx)
|
|
}
|
|
|
|
// loadScriptWithHash if similar to the LoadScript method, but it also loads
|
|
// given script hash directly into the Context to avoid its recalculations. 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, hasDynamicInvoke bool) {
|
|
v.LoadScript(b)
|
|
ctx := v.Context()
|
|
ctx.scriptHash = hash
|
|
ctx.hasDynamicInvoke = hasDynamicInvoke
|
|
}
|
|
|
|
// 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 == "astack" {
|
|
s = v.astack
|
|
}
|
|
if n == "istack" {
|
|
s = v.istack
|
|
}
|
|
if n == "estack" {
|
|
s = v.estack
|
|
}
|
|
b, _ := json.MarshalIndent(s.ToContractParameters(), "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
// State returns string representation of the state for the VM.
|
|
func (v *VM) State() string {
|
|
return v.state.String()
|
|
}
|
|
|
|
// 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 {
|
|
// check for breakpoint before executing the next instruction
|
|
ctx := v.Context()
|
|
if ctx != nil && ctx.atBreakPoint() {
|
|
v.state |= breakState
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
} else {
|
|
v.state = breakState
|
|
}
|
|
|
|
expSize := v.istack.len
|
|
for v.state == noneState && v.istack.len >= expSize {
|
|
err = v.StepInto()
|
|
}
|
|
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
|
|
} else {
|
|
v.state = breakState
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// SetCheckedHash sets checked hash for CHECKSIG and CHECKMULTISIG instructions.
|
|
func (v *VM) SetCheckedHash(h []byte) {
|
|
v.checkhash = make([]byte, len(h))
|
|
copy(v.checkhash, h)
|
|
}
|
|
|
|
// SetScriptGetter sets the script getter for CALL instructions.
|
|
func (v *VM) SetScriptGetter(gs func(util.Uint160) ([]byte, bool)) {
|
|
v.getScript = gs
|
|
}
|
|
|
|
// GetInteropID converts instruction parameter to an interop ID.
|
|
func GetInteropID(parameter []byte) uint32 {
|
|
if len(parameter) == 4 {
|
|
return binary.LittleEndian.Uint32(parameter)
|
|
}
|
|
|
|
return InteropNameToID(parameter)
|
|
}
|
|
|
|
// GetInteropByID returns interop function together with price.
|
|
// Registered callbacks are checked in LIFO order.
|
|
func (v *VM) GetInteropByID(id uint32) *InteropFuncPrice {
|
|
for i := len(v.getInterop) - 1; i >= 0; i-- {
|
|
if ifunc := v.getInterop[i](id); ifunc != nil {
|
|
return ifunc
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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.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(v, op, parameter)
|
|
if v.gasLimit > 0 && v.gasConsumed > v.gasLimit {
|
|
panic("gas limit is exceeded")
|
|
}
|
|
}
|
|
|
|
switch op {
|
|
case opcode.APPCALL, opcode.TAILCALL:
|
|
isZero := true
|
|
for i := range parameter {
|
|
if parameter[i] != 0 {
|
|
isZero = false
|
|
break
|
|
}
|
|
}
|
|
if !isZero {
|
|
break
|
|
}
|
|
|
|
parameter = v.estack.Pop().Bytes()
|
|
fallthrough
|
|
case opcode.CALLED, opcode.CALLEDT:
|
|
if !ctx.hasDynamicInvoke {
|
|
panic("contract is not allowed to make dynamic invocations")
|
|
}
|
|
}
|
|
|
|
if op >= opcode.PUSHBYTES1 && op <= opcode.PUSHBYTES75 {
|
|
v.estack.PushVal(parameter)
|
|
return
|
|
}
|
|
|
|
switch op {
|
|
case opcode.PUSHM1, 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.PUSH1) + 1
|
|
v.estack.PushVal(val)
|
|
|
|
case opcode.PUSH0:
|
|
v.estack.PushVal([]byte{})
|
|
|
|
case opcode.PUSHDATA1, opcode.PUSHDATA2, opcode.PUSHDATA4:
|
|
v.estack.PushVal(parameter)
|
|
|
|
case opcode.PUSHNULL:
|
|
v.estack.PushVal(NullItem{})
|
|
|
|
case opcode.ISNULL:
|
|
res := v.estack.Pop().value.Equals(NullItem{})
|
|
v.estack.PushVal(res)
|
|
|
|
// Stack operations.
|
|
case opcode.TOALTSTACK:
|
|
v.astack.Push(v.estack.Pop())
|
|
|
|
case opcode.FROMALTSTACK:
|
|
v.estack.Push(v.astack.Pop())
|
|
|
|
case opcode.DUPFROMALTSTACK:
|
|
v.estack.Push(v.astack.Dup(0))
|
|
|
|
case opcode.DUP:
|
|
v.estack.Push(v.estack.Dup(0))
|
|
|
|
case opcode.SWAP:
|
|
err := v.estack.Swap(1, 0)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
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.CAT:
|
|
b := v.estack.Pop().Bytes()
|
|
a := v.estack.Pop().Bytes()
|
|
if l := len(a) + len(b); l > MaxItemSize {
|
|
panic(fmt.Sprintf("too big item: %d", l))
|
|
}
|
|
ab := append(a, b...)
|
|
v.estack.PushVal(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()
|
|
if o > len(s) {
|
|
// panic("invalid offset")
|
|
// FIXME revert when NEO 3.0 https://github.com/nspcc-dev/neo-go/issues/477
|
|
v.estack.PushVal("")
|
|
break
|
|
}
|
|
last := l + o
|
|
if last > len(s) {
|
|
last = len(s)
|
|
}
|
|
v.estack.PushVal(s[o:last])
|
|
|
|
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 {
|
|
l = t
|
|
}
|
|
v.estack.PushVal(s[:l])
|
|
|
|
case opcode.RIGHT:
|
|
l := int(v.estack.Pop().BigInt().Int64())
|
|
if l < 0 {
|
|
panic("negative length")
|
|
}
|
|
s := v.estack.Pop().Bytes()
|
|
v.estack.PushVal(s[len(s)-l:])
|
|
|
|
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.XSWAP:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
err := v.estack.Swap(n, 0)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
case opcode.XTUCK:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
if n <= 0 {
|
|
panic("XTUCK: invalid length")
|
|
}
|
|
a := v.estack.Dup(0)
|
|
if a == nil {
|
|
panic("no top-level element found")
|
|
}
|
|
if n > v.estack.Len() {
|
|
panic("can't push to the position specified")
|
|
}
|
|
v.estack.InsertAt(a, n)
|
|
|
|
case opcode.ROT:
|
|
err := v.estack.Roll(2)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
case opcode.DEPTH:
|
|
v.estack.PushVal(v.estack.Len())
|
|
|
|
case opcode.NIP:
|
|
elem := v.estack.RemoveAt(1)
|
|
if elem == nil {
|
|
panic("no second element found")
|
|
}
|
|
|
|
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.ROLL:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
err := v.estack.Roll(n)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
case opcode.DROP:
|
|
if v.estack.Len() < 1 {
|
|
panic("stack is too small")
|
|
}
|
|
v.estack.Pop()
|
|
|
|
case opcode.EQUAL:
|
|
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))
|
|
|
|
// Bit operations.
|
|
case opcode.INVERT:
|
|
// inplace
|
|
e := v.estack.Peek(0)
|
|
i := e.BigInt()
|
|
e.value = makeStackItem(i.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))
|
|
|
|
// Numeric operations.
|
|
case opcode.ADD:
|
|
a := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(a)
|
|
b := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(b)
|
|
|
|
c := new(big.Int).Add(a, b)
|
|
v.checkBigIntSize(c)
|
|
v.estack.PushVal(c)
|
|
|
|
case opcode.SUB:
|
|
b := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(b)
|
|
a := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(a)
|
|
|
|
c := new(big.Int).Sub(a, b)
|
|
v.checkBigIntSize(c)
|
|
v.estack.PushVal(c)
|
|
|
|
case opcode.DIV:
|
|
b := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(b)
|
|
a := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(a)
|
|
|
|
v.estack.PushVal(new(big.Int).Quo(a, b))
|
|
|
|
case opcode.MUL:
|
|
a := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(a)
|
|
b := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(b)
|
|
|
|
c := new(big.Int).Mul(a, b)
|
|
v.checkBigIntSize(c)
|
|
v.estack.PushVal(c)
|
|
|
|
case opcode.MOD:
|
|
b := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(b)
|
|
a := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(a)
|
|
|
|
v.estack.PushVal(new(big.Int).Rem(a, b))
|
|
|
|
case opcode.SHL, opcode.SHR:
|
|
b := v.estack.Pop().BigInt().Int64()
|
|
if b == 0 {
|
|
return
|
|
} else if b < minSHLArg || b > maxSHLArg {
|
|
panic(fmt.Sprintf("operand must be between %d and %d", minSHLArg, maxSHLArg))
|
|
}
|
|
a := v.estack.Pop().BigInt()
|
|
v.checkBigIntSize(a)
|
|
|
|
newOp := op
|
|
if b < 0 {
|
|
b = -b
|
|
if op == opcode.SHR {
|
|
newOp = opcode.SHL
|
|
} else {
|
|
newOp = opcode.SHR
|
|
}
|
|
}
|
|
var item big.Int
|
|
if newOp == opcode.SHL {
|
|
item.Lsh(a, uint(b))
|
|
} else {
|
|
item.Rsh(a, uint(b))
|
|
}
|
|
|
|
v.checkBigIntSize(&item)
|
|
v.estack.PushVal(&item)
|
|
|
|
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.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:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(b) == -1)
|
|
|
|
case opcode.GT:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(b) == 1)
|
|
|
|
case opcode.LTE:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(b) <= 0)
|
|
|
|
case opcode.GTE:
|
|
b := v.estack.Pop().BigInt()
|
|
a := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(a.Cmp(b) >= 0)
|
|
|
|
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)
|
|
|
|
case opcode.INC:
|
|
x := v.estack.Pop().BigInt()
|
|
a := new(big.Int).Add(x, big.NewInt(1))
|
|
v.checkBigIntSize(a)
|
|
v.estack.PushVal(a)
|
|
|
|
case opcode.DEC:
|
|
x := v.estack.Pop().BigInt()
|
|
a := new(big.Int).Sub(x, big.NewInt(1))
|
|
v.checkBigIntSize(a)
|
|
v.estack.PushVal(a)
|
|
|
|
case opcode.SIGN:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(x.Sign())
|
|
|
|
case opcode.NEGATE:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(x.Neg(x))
|
|
|
|
case opcode.ABS:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(x.Abs(x))
|
|
|
|
case opcode.NOT:
|
|
x := v.estack.Pop().Bool()
|
|
v.estack.PushVal(!x)
|
|
|
|
case opcode.NZ:
|
|
x := v.estack.Pop().BigInt()
|
|
v.estack.PushVal(x.Cmp(big.NewInt(0)) != 0)
|
|
|
|
// Object operations.
|
|
case opcode.NEWARRAY:
|
|
item := v.estack.Pop()
|
|
switch t := item.value.(type) {
|
|
case *StructItem:
|
|
arr := make([]StackItem, len(t.value))
|
|
copy(arr, t.value)
|
|
v.estack.PushVal(&ArrayItem{arr})
|
|
case *ArrayItem:
|
|
v.estack.PushVal(t)
|
|
default:
|
|
n := item.BigInt().Int64()
|
|
if n > MaxArraySize {
|
|
panic("too long array")
|
|
}
|
|
items := makeArrayOfFalses(int(n))
|
|
v.estack.PushVal(&ArrayItem{items})
|
|
}
|
|
|
|
case opcode.NEWSTRUCT:
|
|
item := v.estack.Pop()
|
|
switch t := item.value.(type) {
|
|
case *ArrayItem:
|
|
arr := make([]StackItem, len(t.value))
|
|
copy(arr, t.value)
|
|
v.estack.PushVal(&StructItem{arr})
|
|
case *StructItem:
|
|
v.estack.PushVal(t)
|
|
default:
|
|
n := item.BigInt().Int64()
|
|
if n > MaxArraySize {
|
|
panic("too long struct")
|
|
}
|
|
items := makeArrayOfFalses(int(n))
|
|
v.estack.PushVal(&StructItem{items})
|
|
}
|
|
|
|
case opcode.APPEND:
|
|
itemElem := v.estack.Pop()
|
|
arrElem := v.estack.Pop()
|
|
|
|
val := cloneIfStruct(itemElem.value)
|
|
|
|
switch t := arrElem.value.(type) {
|
|
case *ArrayItem:
|
|
arr := t.Value().([]StackItem)
|
|
if len(arr) >= MaxArraySize {
|
|
panic("too long array")
|
|
}
|
|
arr = append(arr, val)
|
|
t.value = arr
|
|
case *StructItem:
|
|
arr := t.Value().([]StackItem)
|
|
if len(arr) >= MaxArraySize {
|
|
panic("too long struct")
|
|
}
|
|
arr = append(arr, val)
|
|
t.value = arr
|
|
default:
|
|
panic("APPEND: not of underlying type Array")
|
|
}
|
|
|
|
v.estack.updateSizeAdd(val)
|
|
|
|
case opcode.PACK:
|
|
n := int(v.estack.Pop().BigInt().Int64())
|
|
if n < 0 || n > v.estack.Len() || n > MaxArraySize {
|
|
panic("OPACK: invalid length")
|
|
}
|
|
|
|
items := make([]StackItem, 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 []StackItem.
|
|
case *ArrayItem, *StructItem:
|
|
arr := t.Value().([]StackItem)
|
|
if index < 0 || index >= len(arr) {
|
|
panic("PICKITEM: invalid index")
|
|
}
|
|
item := arr[index].Dup()
|
|
v.estack.PushVal(item)
|
|
case *MapItem:
|
|
index := t.Index(key.Item())
|
|
if index < 0 {
|
|
panic("invalid key")
|
|
}
|
|
v.estack.Push(&Element{value: t.value[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 []StackItem.
|
|
case *ArrayItem, *StructItem:
|
|
arr := t.Value().([]StackItem)
|
|
index := int(key.BigInt().Int64())
|
|
if index < 0 || index >= len(arr) {
|
|
panic("SETITEM: invalid index")
|
|
}
|
|
v.estack.updateSizeRemove(arr[index])
|
|
arr[index] = item
|
|
v.estack.updateSizeAdd(arr[index])
|
|
case *MapItem:
|
|
if t.Has(key.value) {
|
|
v.estack.updateSizeRemove(item)
|
|
} else if len(t.value) >= MaxArraySize {
|
|
panic("too big map")
|
|
}
|
|
t.Add(key.value, item)
|
|
v.estack.updateSizeAdd(item)
|
|
|
|
default:
|
|
panic(fmt.Sprintf("SETITEM: invalid item type %s", t))
|
|
}
|
|
|
|
case opcode.REVERSE:
|
|
a := v.estack.Pop().Array()
|
|
if len(a) > 1 {
|
|
for i, j := 0, len(a)-1; i <= j; i, j = i+1, j-1 {
|
|
a[i], a[j] = a[j], a[i]
|
|
}
|
|
}
|
|
case opcode.REMOVE:
|
|
key := v.estack.Pop()
|
|
validateMapKey(key)
|
|
|
|
elem := v.estack.Pop()
|
|
switch t := elem.value.(type) {
|
|
case *ArrayItem:
|
|
a := t.value
|
|
k := int(key.BigInt().Int64())
|
|
if k < 0 || k >= len(a) {
|
|
panic("REMOVE: invalid index")
|
|
}
|
|
v.estack.updateSizeRemove(a[k])
|
|
a = append(a[:k], a[k+1:]...)
|
|
t.value = a
|
|
case *StructItem:
|
|
a := t.value
|
|
k := int(key.BigInt().Int64())
|
|
if k < 0 || k >= len(a) {
|
|
panic("REMOVE: invalid index")
|
|
}
|
|
v.estack.updateSizeRemove(a[k])
|
|
a = append(a[:k], a[k+1:]...)
|
|
t.value = a
|
|
case *MapItem:
|
|
index := t.Index(key.Item())
|
|
// NEO 2.0 doesn't error on missing key.
|
|
if index >= 0 {
|
|
v.estack.updateSizeRemove(t.value[index].Value)
|
|
t.Drop(index)
|
|
}
|
|
default:
|
|
panic("REMOVE: invalid type")
|
|
}
|
|
|
|
case opcode.ARRAYSIZE:
|
|
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:
|
|
v.estack.PushVal(len(t))
|
|
case []MapElement:
|
|
v.estack.PushVal(len(t))
|
|
default:
|
|
v.estack.PushVal(len(elem.Bytes()))
|
|
}
|
|
|
|
case opcode.SIZE:
|
|
elem := v.estack.Pop()
|
|
arr := elem.Bytes()
|
|
v.estack.PushVal(len(arr))
|
|
|
|
case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT:
|
|
offset := v.getJumpOffset(ctx, parameter, 0)
|
|
cond := true
|
|
if op != opcode.JMP {
|
|
cond = v.estack.Pop().Bool() == (op == opcode.JMPIF)
|
|
}
|
|
|
|
v.jumpIf(ctx, offset, cond)
|
|
|
|
case opcode.CALL:
|
|
v.checkInvocationStackSize()
|
|
|
|
newCtx := ctx.Copy()
|
|
newCtx.rvcount = -1
|
|
v.istack.PushVal(newCtx)
|
|
|
|
offset := v.getJumpOffset(newCtx, parameter, 0)
|
|
v.jumpIf(newCtx, offset, true)
|
|
|
|
case opcode.SYSCALL:
|
|
interopID := GetInteropID(parameter)
|
|
ifunc := v.GetInteropByID(interopID)
|
|
|
|
if ifunc == nil {
|
|
panic(fmt.Sprintf("interop hook (%q/0x%x) not registered", parameter, interopID))
|
|
}
|
|
if err := ifunc.Func(v); err != nil {
|
|
panic(fmt.Sprintf("failed to invoke syscall: %s", err))
|
|
}
|
|
|
|
case opcode.APPCALL, opcode.TAILCALL:
|
|
if v.getScript == nil {
|
|
panic("no getScript callback is set up")
|
|
}
|
|
|
|
if op == opcode.APPCALL {
|
|
v.checkInvocationStackSize()
|
|
}
|
|
|
|
hash, err := util.Uint160DecodeBytesBE(parameter)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
script, hasDynamicInvoke := v.getScript(hash)
|
|
if script == nil {
|
|
panic("could not find script")
|
|
}
|
|
|
|
if op == opcode.TAILCALL {
|
|
_ = v.istack.Pop()
|
|
}
|
|
|
|
v.loadScriptWithHash(script, hash, hasDynamicInvoke)
|
|
|
|
case opcode.RET:
|
|
oldCtx := v.istack.Pop().Value().(*Context)
|
|
rvcount := oldCtx.rvcount
|
|
oldEstack := v.estack
|
|
|
|
if rvcount > 0 && oldEstack.Len() < rvcount {
|
|
panic("missing some return elements")
|
|
}
|
|
if v.istack.Len() == 0 {
|
|
v.state = haltState
|
|
break
|
|
}
|
|
|
|
newEstack := v.Context().estack
|
|
if oldEstack != newEstack {
|
|
if rvcount < 0 {
|
|
rvcount = oldEstack.Len()
|
|
}
|
|
for i := rvcount; i > 0; i-- {
|
|
elem := oldEstack.RemoveAt(i - 1)
|
|
newEstack.Push(elem)
|
|
}
|
|
v.estack = newEstack
|
|
v.astack = v.Context().astack
|
|
}
|
|
|
|
case opcode.CHECKSIG, opcode.VERIFY:
|
|
var hashToCheck []byte
|
|
|
|
keyb := v.estack.Pop().Bytes()
|
|
signature := v.estack.Pop().Bytes()
|
|
if op == opcode.CHECKSIG {
|
|
if v.checkhash == nil {
|
|
panic("VM is not set up properly for signature checks")
|
|
}
|
|
hashToCheck = v.checkhash
|
|
} else { // VERIFY
|
|
msg := v.estack.Pop().Bytes()
|
|
hashToCheck = hash.Sha256(msg).BytesBE()
|
|
}
|
|
pkey := v.bytesToPublicKey(keyb)
|
|
res := pkey.Verify(signature, hashToCheck)
|
|
v.estack.PushVal(res)
|
|
|
|
case opcode.CHECKMULTISIG:
|
|
pkeys, err := v.estack.PopSigElements()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("wrong parameters: %s", err.Error()))
|
|
}
|
|
sigs, err := v.estack.PopSigElements()
|
|
if err != nil {
|
|
panic(fmt.Sprintf("wrong parameters: %s", err.Error()))
|
|
}
|
|
// It's ok to have more keys than there are signatures (it would
|
|
// just mean that some keys didn't sign), but not the other way around.
|
|
if len(pkeys) < len(sigs) {
|
|
panic("more signatures than there are keys")
|
|
}
|
|
if v.checkhash == nil {
|
|
panic("VM is not set up properly for signature checks")
|
|
}
|
|
|
|
sigok := CheckMultisigPar(v, pkeys, sigs)
|
|
v.estack.PushVal(sigok)
|
|
|
|
case opcode.NEWMAP:
|
|
v.estack.Push(&Element{value: NewMapItem()})
|
|
|
|
case opcode.KEYS:
|
|
item := v.estack.Pop()
|
|
if item == nil {
|
|
panic("no argument")
|
|
}
|
|
|
|
m, ok := item.value.(*MapItem)
|
|
if !ok {
|
|
panic("not a Map")
|
|
}
|
|
|
|
arr := make([]StackItem, 0, len(m.value))
|
|
for k := range m.value {
|
|
arr = append(arr, m.value[k].Key.Dup())
|
|
}
|
|
v.estack.PushVal(arr)
|
|
|
|
case opcode.VALUES:
|
|
item := v.estack.Pop()
|
|
if item == nil {
|
|
panic("no argument")
|
|
}
|
|
|
|
var arr []StackItem
|
|
switch t := item.value.(type) {
|
|
case *ArrayItem, *StructItem:
|
|
src := t.Value().([]StackItem)
|
|
arr = make([]StackItem, len(src))
|
|
for i := range src {
|
|
arr[i] = cloneIfStruct(src[i])
|
|
}
|
|
case *MapItem:
|
|
arr = make([]StackItem, 0, len(t.value))
|
|
for k := range t.value {
|
|
arr = append(arr, cloneIfStruct(t.value[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 *ArrayItem, *StructItem:
|
|
index := key.BigInt().Int64()
|
|
if index < 0 {
|
|
panic("negative index")
|
|
}
|
|
v.estack.PushVal(index < int64(len(c.Array())))
|
|
case *MapItem:
|
|
v.estack.PushVal(t.Has(key.Item()))
|
|
default:
|
|
panic("wrong collection type")
|
|
}
|
|
|
|
// Cryptographic operations.
|
|
case opcode.SHA1:
|
|
b := v.estack.Pop().Bytes()
|
|
sha := sha1.New()
|
|
sha.Write(b)
|
|
v.estack.PushVal(sha.Sum(nil))
|
|
|
|
case opcode.SHA256:
|
|
b := v.estack.Pop().Bytes()
|
|
v.estack.PushVal(hash.Sha256(b).BytesBE())
|
|
|
|
case opcode.NOP:
|
|
// unlucky ^^
|
|
|
|
case opcode.CALLI, opcode.CALLE, opcode.CALLED, opcode.CALLET, opcode.CALLEDT:
|
|
var (
|
|
tailCall = (op == opcode.CALLET || op == opcode.CALLEDT)
|
|
hashOnStack = (op == opcode.CALLED || op == opcode.CALLEDT)
|
|
addElement int
|
|
newCtx *Context
|
|
)
|
|
|
|
if hashOnStack {
|
|
addElement = 1
|
|
}
|
|
|
|
rvcount := int(parameter[0])
|
|
pcount := int(parameter[1])
|
|
if v.estack.Len() < pcount+addElement {
|
|
panic("missing some parameters")
|
|
}
|
|
if tailCall {
|
|
if ctx.rvcount != rvcount {
|
|
panic("context and parameter rvcount mismatch")
|
|
}
|
|
} else {
|
|
v.checkInvocationStackSize()
|
|
}
|
|
|
|
if op == opcode.CALLI {
|
|
newCtx = ctx.Copy()
|
|
} else {
|
|
var hashBytes []byte
|
|
|
|
if hashOnStack {
|
|
hashBytes = v.estack.Pop().Bytes()
|
|
} else {
|
|
hashBytes = parameter[2:]
|
|
}
|
|
|
|
hash, err := util.Uint160DecodeBytesBE(hashBytes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
script, hasDynamicInvoke := v.getScript(hash)
|
|
if script == nil {
|
|
panic(fmt.Sprintf("could not find script %s", hash))
|
|
}
|
|
newCtx = NewContext(script)
|
|
newCtx.scriptHash = hash
|
|
newCtx.hasDynamicInvoke = hasDynamicInvoke
|
|
}
|
|
newCtx.rvcount = rvcount
|
|
newCtx.estack = NewStack("evaluation")
|
|
newCtx.astack = NewStack("alt")
|
|
// Going backwards to naturally push things onto the new stack.
|
|
for i := pcount; i > 0; i-- {
|
|
elem := v.estack.RemoveAt(i - 1)
|
|
newCtx.estack.Push(elem)
|
|
}
|
|
if tailCall {
|
|
_ = v.istack.Pop()
|
|
}
|
|
v.istack.PushVal(newCtx)
|
|
v.estack = newCtx.estack
|
|
v.astack = newCtx.astack
|
|
if op == opcode.CALLI {
|
|
// CALLI is a bit different from other JMPs
|
|
// https://github.com/neo-project/neo-vm/blob/master-2.x/src/neo-vm/ExecutionEngine.cs#L1175
|
|
offset := v.getJumpOffset(newCtx, parameter[2:], 2)
|
|
v.jumpIf(newCtx, offset, true)
|
|
}
|
|
|
|
case opcode.THROW:
|
|
panic("THROW")
|
|
|
|
case opcode.THROWIFNOT:
|
|
if !v.estack.Pop().Bool() {
|
|
panic("THROWIFNOT")
|
|
}
|
|
|
|
default:
|
|
panic(fmt.Sprintf("unknown opcode %s", op.String()))
|
|
}
|
|
return
|
|
}
|
|
|
|
// jumpIf performs jump to offset if cond is true.
|
|
func (v *VM) jumpIf(ctx *Context, offset int, cond bool) {
|
|
if cond {
|
|
ctx.nextip = offset
|
|
}
|
|
}
|
|
|
|
// getJumpOffset returns instruction number in a current context
|
|
// to a which JMP should be performed.
|
|
// parameter is interpreted as little-endian int16.
|
|
func (v *VM) getJumpOffset(ctx *Context, parameter []byte, mod int) int {
|
|
rOffset := int16(binary.LittleEndian.Uint16(parameter))
|
|
offset := ctx.ip + int(rOffset) + mod
|
|
if offset < 0 || offset > len(ctx.prog) {
|
|
panic(fmt.Sprintf("JMP: invalid offset %d ip at %d", offset, ctx.ip))
|
|
}
|
|
|
|
return offset
|
|
}
|
|
|
|
// CheckMultisigPar checks if sigs contains sufficient valid signatures.
|
|
func CheckMultisigPar(v *VM, pkeys [][]byte, sigs [][]byte) bool {
|
|
if len(sigs) == 1 {
|
|
return checkMultisig1(v, 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], v.checkhash),
|
|
}
|
|
}
|
|
}
|
|
|
|
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: v.bytesToPublicKey(pkeys[k1]), signum: s1}
|
|
tasks <- task{pub: v.bytesToPublicKey(pkeys[k2]), 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: v.bytesToPublicKey(pkeys[nextKey]), signum: nextSig}
|
|
}
|
|
|
|
close(tasks)
|
|
|
|
return sigok
|
|
}
|
|
|
|
func checkMultisig1(v *VM, pkeys [][]byte, sig []byte) bool {
|
|
for i := range pkeys {
|
|
pkey := v.bytesToPublicKey(pkeys[i])
|
|
if pkey.Verify(sig, v.checkhash) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func cloneIfStruct(item StackItem) StackItem {
|
|
switch it := item.(type) {
|
|
case *StructItem:
|
|
return it.Clone()
|
|
default:
|
|
return it
|
|
}
|
|
}
|
|
|
|
func makeArrayOfFalses(n int) []StackItem {
|
|
items := make([]StackItem, n)
|
|
for i := range items {
|
|
items[i] = &BoolItem{false}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func validateMapKey(key *Element) {
|
|
if key == nil {
|
|
panic("no key found")
|
|
}
|
|
if !isValidMapKey(key.Item()) {
|
|
panic("key can't be a collection")
|
|
}
|
|
}
|
|
|
|
func (v *VM) checkInvocationStackSize() {
|
|
if v.istack.len >= MaxInvocationStackSize {
|
|
panic("invocation stack is too big")
|
|
}
|
|
}
|
|
|
|
func (v *VM) checkBigIntSize(a *big.Int) {
|
|
if a.BitLen() > MaxBigIntegerSizeBits {
|
|
panic("big integer is too big")
|
|
}
|
|
}
|
|
|
|
// bytesToPublicKey is a helper deserializing keys using cache and panicing on
|
|
// error.
|
|
func (v *VM) bytesToPublicKey(b []byte) *keys.PublicKey {
|
|
var pkey *keys.PublicKey
|
|
s := string(b)
|
|
if v.keys[s] != nil {
|
|
pkey = v.keys[s]
|
|
} else {
|
|
var err error
|
|
pkey, err = keys.NewPublicKeyFromBytes(b)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
v.keys[s] = pkey
|
|
}
|
|
return pkey
|
|
}
|