diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 131c2e06c..4d1b9c00e 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -101,7 +101,7 @@ func (c *Context) Next() (opcode.Opcode, []byte, error) { } case opcode.JMP, opcode.JMPIF, opcode.JMPIFNOT, opcode.JMPEQ, opcode.JMPNE, opcode.JMPGT, opcode.JMPGE, opcode.JMPLT, opcode.JMPLE, - opcode.CALL, opcode.ISTYPE, opcode.NEWARRAYT: + opcode.CALL, opcode.ISTYPE, opcode.CONVERT, opcode.NEWARRAYT: numtoread = 1 case opcode.JMPL, opcode.JMPIFL, opcode.JMPIFNOTL, opcode.JMPEQL, opcode.JMPNEL, opcode.JMPGTL, opcode.JMPGEL, opcode.JMPLTL, opcode.JMPLEL, @@ -192,6 +192,11 @@ func (c *Context) TryInteger() (*big.Int, error) { // Type implements StackItem interface. func (c *Context) Type() StackItemType { panic("Context cannot appear on evaluation stack") } +// Convert implements StackItem interface. +func (c *Context) Convert(_ StackItemType) (StackItem, error) { + panic("Context cannot be converted to anything") +} + // Equals implements StackItem interface. func (c *Context) Equals(s StackItem) bool { return c == s diff --git a/pkg/vm/opcode/opcode.go b/pkg/vm/opcode/opcode.go index 57ed13553..d918e8d87 100644 --- a/pkg/vm/opcode/opcode.go +++ b/pkg/vm/opcode/opcode.go @@ -161,8 +161,9 @@ const ( CLEARITEMS Opcode = 0xD3 // Types - ISNULL Opcode = 0xD8 - ISTYPE Opcode = 0xD9 + ISNULL Opcode = 0xD8 + ISTYPE Opcode = 0xD9 + CONVERT Opcode = 0xDB // Exceptions THROW Opcode = 0xF0 diff --git a/pkg/vm/opcode/opcode_string.go b/pkg/vm/opcode/opcode_string.go index 637497604..2447a7dca 100644 --- a/pkg/vm/opcode/opcode_string.go +++ b/pkg/vm/opcode/opcode_string.go @@ -141,11 +141,12 @@ func _() { _ = x[CLEARITEMS-211] _ = x[ISNULL-216] _ = x[ISTYPE-217] + _ = x[CONVERT-219] _ = x[THROW-240] _ = x[THROWIFNOT-241] } -const _Opcode_name = "PUSHINT8PUSHINT16PUSHINT32PUSHINT64PUSHINT128PUSHINT256PUSHNULLPUSHDATA1PUSHDATA2PUSHDATA4PUSHM1PUSH0PUSH1PUSH2PUSH3PUSH4PUSH5PUSH6PUSH7PUSH8PUSH9PUSH10PUSH11PUSH12PUSH13PUSH14PUSH15PUSH16NOPJMPJMPLJMPIFJMPIFLJMPIFNOTJMPIFNOTLJMPEQJMPEQLJMPNEJMPNELJMPGTJMPGTLJMPGEJMPGELJMPLTJMPLTLJMPLEJMPLELCALLCALLLOLDPUSH1RETAPPCALLSYSCALLTAILCALLDUPFROMALTSTACKTOALTSTACKFROMALTSTACKXDROPXSWAPXTUCKDEPTHDROPDUPNIPOVERPICKROLLROTSWAPTUCKCATSUBSTRLEFTRIGHTINVERTANDORXOREQUALINCDECSIGNNEGATEABSNOTNZADDSUBMULDIVMODSHLSHRBOOLANDBOOLORNUMEQUALNUMNOTEQUALLTGTLTEGTEMINMAXWITHINSHA1SHA256HASH160HASH256CHECKSIGVERIFYCHECKMULTISIGPACKUNPACKNEWARRAY0NEWARRAYNEWARRAYTNEWSTRUCT0NEWSTRUCTNEWMAPSIZEHASKEYKEYSVALUESPICKITEMAPPENDSETITEMREVERSEITEMSREMOVECLEARITEMSISNULLISTYPETHROWTHROWIFNOT" +const _Opcode_name = "PUSHINT8PUSHINT16PUSHINT32PUSHINT64PUSHINT128PUSHINT256PUSHNULLPUSHDATA1PUSHDATA2PUSHDATA4PUSHM1PUSH0PUSH1PUSH2PUSH3PUSH4PUSH5PUSH6PUSH7PUSH8PUSH9PUSH10PUSH11PUSH12PUSH13PUSH14PUSH15PUSH16NOPJMPJMPLJMPIFJMPIFLJMPIFNOTJMPIFNOTLJMPEQJMPEQLJMPNEJMPNELJMPGTJMPGTLJMPGEJMPGELJMPLTJMPLTLJMPLEJMPLELCALLCALLLOLDPUSH1RETAPPCALLSYSCALLTAILCALLDUPFROMALTSTACKTOALTSTACKFROMALTSTACKXDROPXSWAPXTUCKDEPTHDROPDUPNIPOVERPICKROLLROTSWAPTUCKCATSUBSTRLEFTRIGHTINVERTANDORXOREQUALINCDECSIGNNEGATEABSNOTNZADDSUBMULDIVMODSHLSHRBOOLANDBOOLORNUMEQUALNUMNOTEQUALLTGTLTEGTEMINMAXWITHINSHA1SHA256HASH160HASH256CHECKSIGVERIFYCHECKMULTISIGPACKUNPACKNEWARRAY0NEWARRAYNEWARRAYTNEWSTRUCT0NEWSTRUCTNEWMAPSIZEHASKEYKEYSVALUESPICKITEMAPPENDSETITEMREVERSEITEMSREMOVECLEARITEMSISNULLISTYPECONVERTTHROWTHROWIFNOT" var _Opcode_map = map[Opcode]string{ 0: _Opcode_name[0:8], @@ -279,8 +280,9 @@ var _Opcode_map = map[Opcode]string{ 211: _Opcode_name[731:741], 216: _Opcode_name[741:747], 217: _Opcode_name[747:753], - 240: _Opcode_name[753:758], - 241: _Opcode_name[758:768], + 219: _Opcode_name[753:760], + 240: _Opcode_name[760:765], + 241: _Opcode_name[765:775], } func (i Opcode) String() string { diff --git a/pkg/vm/stack.go b/pkg/vm/stack.go index d24993057..fd779e941 100644 --- a/pkg/vm/stack.go +++ b/pkg/vm/stack.go @@ -80,8 +80,7 @@ func (e *Element) BigInt() *big.Int { return val } -// Bool attempts to get the underlying value of the element as a boolean. -// Will panic if the assertion failed which will be caught by the VM. +// Bool converts an underlying value of the element to a boolean. func (e *Element) Bool() bool { return e.value.Bool() } diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index bff691ab9..bca4e2745 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -32,8 +32,12 @@ type StackItem interface { ToContractParameter(map[StackItem]bool) smartcontract.Parameter // Type returns stack item type. Type() StackItemType + // Convert converts StackItem to another type. + Convert(StackItemType) (StackItem, error) } +var errInvalidConversion = errors.New("invalid conversion type") + func makeStackItem(v interface{}) StackItem { switch val := v.(type) { case int: @@ -108,6 +112,33 @@ func makeStackItem(v interface{}) StackItem { } } +// convertPrimitive converts primitive item to a specified type. +func convertPrimitive(item StackItem, typ StackItemType) (StackItem, error) { + if item.Type() == typ { + return item, nil + } + switch typ { + case IntegerT: + bi, err := item.TryInteger() + if err != nil { + return nil, err + } + return NewBigIntegerItem(bi), nil + case ByteArrayT: + b, err := item.TryBytes() + if err != nil { + return nil, err + } + return NewByteArrayItem(b), nil + case BufferT: + panic("TODO") // #877 + case BooleanT: + return NewBoolItem(item.Bool()), nil + default: + return nil, errInvalidConversion + } +} + // StructItem represents a struct on the stack. type StructItem struct { value []StackItem @@ -187,6 +218,20 @@ func (i *StructItem) ToContractParameter(seen map[StackItem]bool) smartcontract. // Type implements StackItem interface. func (i *StructItem) Type() StackItemType { return StructT } +// Convert implements StackItem interface. +func (i *StructItem) Convert(typ StackItemType) (StackItem, error) { + switch typ { + case StructT: + return i, nil + case ArrayT: + return NewArrayItem(i.value), nil + case BooleanT: + return NewBoolItem(i.Bool()), nil + default: + return nil, errInvalidConversion + } +} + // Clone returns a Struct with all Struct fields copied by value. // Array fields are still copied by reference. func (i *StructItem) Clone() *StructItem { @@ -251,6 +296,14 @@ func (i NullItem) ToContractParameter(map[StackItem]bool) smartcontract.Paramete // Type implements StackItem interface. func (i NullItem) Type() StackItemType { return AnyT } +// Convert implements StackItem interface. +func (i NullItem) Convert(typ StackItemType) (StackItem, error) { + if typ == AnyT || !typ.IsValid() { + return nil, errInvalidConversion + } + return i, nil +} + // BigIntegerItem represents a big integer on the stack. type BigIntegerItem struct { value *big.Int @@ -324,6 +377,11 @@ func (i *BigIntegerItem) ToContractParameter(map[StackItem]bool) smartcontract.P // Type implements StackItem interface. func (i *BigIntegerItem) Type() StackItemType { return IntegerT } +// Convert implements StackItem interface. +func (i *BigIntegerItem) Convert(typ StackItemType) (StackItem, error) { + return convertPrimitive(i, typ) +} + // MarshalJSON implements the json.Marshaler interface. func (i *BigIntegerItem) MarshalJSON() ([]byte, error) { return json.Marshal(i.value) @@ -410,6 +468,11 @@ func (i *BoolItem) ToContractParameter(map[StackItem]bool) smartcontract.Paramet // Type implements StackItem interface. func (i *BoolItem) Type() StackItemType { return BooleanT } +// Convert implements StackItem interface. +func (i *BoolItem) Convert(typ StackItemType) (StackItem, error) { + return convertPrimitive(i, typ) +} + // ByteArrayItem represents a byte array on the stack. type ByteArrayItem struct { value []byte @@ -488,6 +551,11 @@ func (i *ByteArrayItem) ToContractParameter(map[StackItem]bool) smartcontract.Pa // Type implements StackItem interface. func (i *ByteArrayItem) Type() StackItemType { return ByteArrayT } +// Convert implements StackItem interface. +func (i *ByteArrayItem) Convert(typ StackItemType) (StackItem, error) { + return convertPrimitive(i, typ) +} + // ArrayItem represents a new ArrayItem object. type ArrayItem struct { value []StackItem @@ -558,6 +626,20 @@ func (i *ArrayItem) ToContractParameter(seen map[StackItem]bool) smartcontract.P // Type implements StackItem interface. func (i *ArrayItem) Type() StackItemType { return ArrayT } +// Convert implements StackItem interface. +func (i *ArrayItem) Convert(typ StackItemType) (StackItem, error) { + switch typ { + case ArrayT: + return i, nil + case StructT: + return NewStructItem(i.value), nil + case BooleanT: + return NewBoolItem(i.Bool()), nil + default: + return nil, errInvalidConversion + } +} + // MapElement is a key-value pair of StackItems. type MapElement struct { Key StackItem @@ -649,6 +731,18 @@ func (i *MapItem) ToContractParameter(seen map[StackItem]bool) smartcontract.Par // Type implements StackItem interface. func (i *MapItem) Type() StackItemType { return MapT } +// Convert implements StackItem interface. +func (i *MapItem) Convert(typ StackItemType) (StackItem, error) { + switch typ { + case MapT: + return i, nil + case BooleanT: + return NewBoolItem(i.Bool()), nil + default: + return nil, errInvalidConversion + } +} + // Add adds key-value pair to the map. func (i *MapItem) Add(key, value StackItem) { if !isValidMapKey(key) { @@ -742,6 +836,18 @@ func (i *InteropItem) ToContractParameter(map[StackItem]bool) smartcontract.Para // Type implements StackItem interface. func (i *InteropItem) Type() StackItemType { return InteropT } +// Convert implements StackItem interface. +func (i *InteropItem) Convert(typ StackItemType) (StackItem, error) { + switch typ { + case InteropT: + return i, nil + case BooleanT: + return NewBoolItem(i.Bool()), nil + default: + return nil, errInvalidConversion + } +} + // MarshalJSON implements the json.Marshaler interface. func (i *InteropItem) MarshalJSON() ([]byte, error) { return json.Marshal(i.value) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index e09663755..a645ee48c 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -569,6 +569,15 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro res := v.estack.Pop().Item() v.estack.PushVal(res.Type() == StackItemType(parameter[0])) + case opcode.CONVERT: + typ := StackItemType(parameter[0]) + item := v.estack.Pop().Item() + result, err := item.Convert(typ) + if err != nil { + panic(err) + } + v.estack.PushVal(result) + // Stack operations. case opcode.TOALTSTACK: v.astack.Push(v.estack.Pop()) diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 90d08df36..6bba08c69 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "fmt" "math/big" "math/rand" "testing" @@ -256,6 +257,128 @@ func TestISTYPE(t *testing.T) { }) } +func testCONVERT(isErr bool, to StackItemType, item, res StackItem) func(t *testing.T) { + return func(t *testing.T) { + prog := []byte{byte(opcode.CONVERT), byte(to)} + v := load(prog) + v.estack.PushVal(item) + if isErr { + checkVMFailed(t, v) + return + } + runVM(t, v) + require.Equal(t, 1, v.estack.Len()) + require.Equal(t, res, v.estack.Pop().value) + } +} + +func TestCONVERT(t *testing.T) { + type convertTC struct { + item, res StackItem + } + arr := []StackItem{ + NewBigIntegerItem(big.NewInt(7)), + NewByteArrayItem([]byte{4, 8, 15}), + } + m := NewMapItem() + m.Add(NewByteArrayItem([]byte{1}), NewByteArrayItem([]byte{2})) + + getName := func(item StackItem, typ StackItemType) string { return fmt.Sprintf("%s->%s", item, typ) } + + t.Run("->Bool", func(t *testing.T) { + testBool := func(a, b StackItem) func(t *testing.T) { + return testCONVERT(false, BooleanT, a, b) + } + + trueCases := []StackItem{ + NewBoolItem(true), NewBigIntegerItem(big.NewInt(11)), NewByteArrayItem([]byte{1, 2, 3}), + NewArrayItem(arr), NewArrayItem(nil), + NewStructItem(arr), NewStructItem(nil), + NewMapItem(), m, NewInteropItem(struct{}{}), + } + for i := range trueCases { + t.Run(getName(trueCases[i], BooleanT), testBool(trueCases[i], NewBoolItem(true))) + } + + falseCases := []StackItem{ + NewBigIntegerItem(big.NewInt(0)), NewByteArrayItem([]byte{0, 0}), NewBoolItem(false), + } + for i := range falseCases { + testBool(falseCases[i], NewBoolItem(false)) + } + }) + + t.Run("compound/interop -> basic", func(t *testing.T) { + types := []StackItemType{IntegerT, ByteArrayT} + items := []StackItem{NewArrayItem(nil), NewStructItem(nil), NewMapItem(), NewInteropItem(struct{}{})} + for _, typ := range types { + for j := range items { + t.Run(getName(items[j], typ), testCONVERT(true, typ, items[j], nil)) + } + } + }) + + t.Run("primitive -> Integer/ByteArray", func(t *testing.T) { + n := big.NewInt(42) + b := emit.IntToBytes(n) + + itemInt := NewBigIntegerItem(n) + itemBytes := NewByteArrayItem(b) + + trueCases := map[StackItemType][]convertTC{ + IntegerT: { + {itemInt, itemInt}, + {itemBytes, itemInt}, + {NewBoolItem(true), NewBigIntegerItem(big.NewInt(1))}, + {NewBoolItem(false), NewBigIntegerItem(big.NewInt(0))}, + }, + ByteArrayT: { + {itemInt, itemBytes}, + {itemBytes, itemBytes}, + {NewBoolItem(true), NewByteArrayItem([]byte{1})}, + {NewBoolItem(false), NewByteArrayItem([]byte{0})}, + }, + } + + for typ := range trueCases { + for _, tc := range trueCases[typ] { + t.Run(getName(tc.item, typ), testCONVERT(false, typ, tc.item, tc.res)) + } + } + }) + + t.Run("Struct<->Array", func(t *testing.T) { + arrayItem := NewArrayItem(arr) + structItem := NewStructItem(arr) + t.Run("Array->Array", testCONVERT(false, ArrayT, arrayItem, arrayItem)) + t.Run("Array->Struct", testCONVERT(false, StructT, arrayItem, structItem)) + t.Run("Struct->Array", testCONVERT(false, ArrayT, structItem, arrayItem)) + t.Run("Struct->Struct", testCONVERT(false, StructT, structItem, structItem)) + }) + + t.Run("Map->Map", testCONVERT(false, MapT, m, m)) + + t.Run("Null->", func(t *testing.T) { + types := []StackItemType{ + BooleanT, ByteArrayT, IntegerT, ArrayT, StructT, MapT, InteropT, + } + for i := range types { + t.Run(types[i].String(), testCONVERT(false, types[i], NullItem{}, NullItem{})) + } + }) + + t.Run("->Any", func(t *testing.T) { + items := []StackItem{ + NewBigIntegerItem(big.NewInt(1)), NewByteArrayItem([]byte{1}), NewBoolItem(true), + NewArrayItem(arr), NewStructItem(arr), m, NewInteropItem(struct{}{}), + } + + for i := range items { + t.Run(items[i].String(), testCONVERT(true, AnyT, items[i], nil)) + } + }) +} + // appendBigStruct returns a program which: // 1. pushes size Structs on stack // 2. packs them into a new struct