From c3f7832f3b8107c77e8502ff09b9f1dcf1f08786 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 12 May 2020 14:47:33 +0300 Subject: [PATCH] vm: implement Buffer stack item Buffer is a stack item type introduced in NEO3 which represents mutable byte-array. --- pkg/vm/serialization.go | 3 ++ pkg/vm/stack_item.go | 93 +++++++++++++++++++++++++++++++++++++-- pkg/vm/stack_item_test.go | 8 ++++ pkg/vm/vm.go | 45 +++++++++++++++++-- pkg/vm/vm_test.go | 28 +++++++++--- 5 files changed, 166 insertions(+), 11 deletions(-) diff --git a/pkg/vm/serialization.go b/pkg/vm/serialization.go index 23c68f494..52bd38445 100644 --- a/pkg/vm/serialization.go +++ b/pkg/vm/serialization.go @@ -89,6 +89,9 @@ func serializeItemTo(item StackItem, w *io.BinWriter, seen map[StackItem]bool) { case *ByteArrayItem: w.WriteBytes([]byte{byte(ByteArrayT)}) w.WriteVarBytes(t.value) + case *BufferItem: + w.WriteBytes([]byte{byte(BufferT)}) + w.WriteVarBytes(t.value) case *BoolItem: w.WriteBytes([]byte{byte(BooleanT)}) w.WriteBool(t.value) diff --git a/pkg/vm/stack_item.go b/pkg/vm/stack_item.go index d47225d47..462984c3d 100644 --- a/pkg/vm/stack_item.go +++ b/pkg/vm/stack_item.go @@ -124,14 +124,15 @@ func convertPrimitive(item StackItem, typ StackItemType) (StackItem, error) { return nil, err } return NewBigIntegerItem(bi), nil - case ByteArrayT: + case ByteArrayT, BufferT: b, err := item.TryBytes() if err != nil { return nil, err } + if typ == BufferT { + return NewBufferItem(b), nil + } return NewByteArrayItem(b), nil - case BufferT: - panic("TODO") // #877 case BooleanT: return NewBoolItem(item.Bool()), nil default: @@ -941,3 +942,89 @@ func (p *PointerItem) Convert(typ StackItemType) (StackItem, error) { return nil, errInvalidConversion } } + +// BufferItem represents represents Buffer stack item. +type BufferItem struct { + value []byte +} + +// NewBufferItem returns a new BufferItem object. +func NewBufferItem(b []byte) *BufferItem { + return &BufferItem{ + value: b, + } +} + +// Value implements StackItem interface. +func (i *BufferItem) Value() interface{} { + return i.value +} + +// String implements fmt.Stringer interface. +func (i *BufferItem) String() string { + return "Buffer" +} + +// Bool implements StackItem interface. +func (i *BufferItem) Bool() bool { + return true +} + +// TryBytes implements StackItem interface. +func (i *BufferItem) TryBytes() ([]byte, error) { + val := make([]byte, len(i.value)) + copy(val, i.value) + return val, nil +} + +// TryInteger implements StackItem interface. +func (i *BufferItem) TryInteger() (*big.Int, error) { + return nil, errors.New("can't convert Buffer to Integer") +} + +// Equals implements StackItem interface. +func (i *BufferItem) Equals(s StackItem) bool { + return i == s +} + +// Dup implements StackItem interface. +func (i *BufferItem) Dup() StackItem { + return i +} + +// MarshalJSON implements the json.Marshaler interface. +func (i *BufferItem) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(i.value)) +} + +// ToContractParameter implements StackItem interface. +func (i *BufferItem) ToContractParameter(map[StackItem]bool) smartcontract.Parameter { + return smartcontract.Parameter{ + Type: smartcontract.ByteArrayType, + Value: i.value, + } +} + +// Type implements StackItem interface. +func (i *BufferItem) Type() StackItemType { return BufferT } + +// Convert implements StackItem interface. +func (i *BufferItem) Convert(typ StackItemType) (StackItem, error) { + switch typ { + case BooleanT: + return NewBoolItem(i.Bool()), nil + case BufferT: + return i, nil + case ByteArrayT: + val := make([]byte, len(i.value)) + copy(val, i.value) + return NewByteArrayItem(val), nil + case IntegerT: + if len(i.value) > MaxBigIntegerSizeBits/8 { + return nil, errInvalidConversion + } + return NewBigIntegerItem(emit.BytesToInt(i.value)), nil + default: + return nil, errInvalidConversion + } +} diff --git a/pkg/vm/stack_item_test.go b/pkg/vm/stack_item_test.go index fb2f3633f..06db6ee61 100644 --- a/pkg/vm/stack_item_test.go +++ b/pkg/vm/stack_item_test.go @@ -374,6 +374,10 @@ var marshalJSONTestCases = []struct { input: NewByteArrayItem([]byte{1, 2, 3}), result: []byte(`"010203"`), }, + { + input: NewBufferItem([]byte{1, 2, 3}), + result: []byte(`"010203"`), + }, { input: &ArrayItem{value: []StackItem{&BigIntegerItem{value: big.NewInt(3)}, &ByteArrayItem{value: []byte{1, 2, 3}}}}, result: []byte(`[3,"010203"]`), @@ -432,6 +436,10 @@ var toContractParameterTestCases = []struct { input: NewByteArrayItem([]byte{0x01, 0x02, 0x03}), result: smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, }, + { + input: NewBufferItem([]byte{0x01, 0x02, 0x03}), + result: smartcontract.Parameter{Type: smartcontract.ByteArrayType, Value: []byte{0x01, 0x02, 0x03}}, + }, { input: NewArrayItem([]StackItem{NewBigIntegerItem(big.NewInt(2)), NewBoolItem(true)}), result: smartcontract.Parameter{Type: smartcontract.ArrayType, Value: []smartcontract.Parameter{ diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 07ca81046..7681d436a 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math" "math/big" "os" "text/tabwriter" @@ -1100,16 +1101,36 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro t.Add(key.value, item) v.refs.Add(item) + case *BufferItem: + index := toInt(key.BigInt()) + if index < 0 || index >= len(t.value) { + panic("invalid index") + } + bi, err := item.TryInteger() + b := toInt(bi) + if err != nil || b < math.MinInt8 || b > math.MaxUint8 { + panic("invalid value") + } + t.value[index] = byte(b) + default: panic(fmt.Sprintf("SETITEM: invalid item type %s", t)) } case opcode.REVERSEITEMS: - a := v.estack.Pop().Array() - if len(a) > 1 { - for i, j := 0, len(a)-1; i <= j; i, j = i+1, j-1 { + item := v.estack.Pop() + switch t := item.value.(type) { + case *ArrayItem, *StructItem: + a := t.Value().([]StackItem) + for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] } + case *BufferItem: + for i, j := 0, len(t.value)-1; i < j; i, j = i+1, j-1 { + t.value[i], t.value[j] = t.value[j], t.value[i] + } + default: + panic(fmt.Sprintf("invalid item type %s", t)) } case opcode.REMOVE: key := v.estack.Pop() @@ -1322,6 +1343,12 @@ func (v *VM) execute(ctx *Context, op opcode.Opcode, parameter []byte) (err erro v.estack.PushVal(index < int64(len(c.Array()))) case *MapItem: v.estack.PushVal(t.Has(key.Item())) + case *BufferItem: + index := key.BigInt().Int64() + if index < 0 { + panic("negative index") + } + v.estack.PushVal(index < int64(len(t.value))) default: panic("wrong collection type") } @@ -1576,3 +1603,15 @@ func (v *VM) GetEntryScriptHash() util.Uint160 { 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) +} diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index a8d08c23d..912c1b5da 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -937,6 +937,8 @@ func TestNOT(t *testing.T) { t.Run("ByteArray0", getTestFuncForVM(prog, true, []byte{0, 0})) t.Run("ByteArray1", getTestFuncForVM(prog, false, []byte{0, 1})) t.Run("NoArgument", getTestFuncForVM(prog, nil)) + t.Run("Buffer0", getTestFuncForVM(prog, false, NewBufferItem([]byte{}))) + t.Run("Buffer1", getTestFuncForVM(prog, false, NewBufferItem([]byte{1}))) } // getBigInt returns 2^a+b @@ -1056,6 +1058,7 @@ func TestEQUALTrue(t *testing.T) { prog := makeProgram(opcode.DUP, opcode.EQUAL) t.Run("Array", getTestFuncForVM(prog, true, []StackItem{})) t.Run("Map", getTestFuncForVM(prog, true, NewMapItem())) + t.Run("Buffer", getTestFuncForVM(prog, true, NewBufferItem([]byte{1, 2}))) } func TestEQUAL(t *testing.T) { @@ -1066,6 +1069,7 @@ func TestEQUAL(t *testing.T) { t.Run("IntegerByteArray", getTestFuncForVM(prog, true, []byte{16}, 16)) t.Run("Map", getTestFuncForVM(prog, false, NewMapItem(), NewMapItem())) t.Run("Array", getTestFuncForVM(prog, false, []StackItem{}, []StackItem{})) + t.Run("Buffer", getTestFuncForVM(prog, false, NewBufferItem([]byte{42}), NewBufferItem([]byte{42}))) } func runWithArgs(t *testing.T, prog []byte, result interface{}, args ...interface{}) { @@ -1270,6 +1274,7 @@ func TestPICKITEM(t *testing.T) { t.Run("bad index", getTestFuncForVM(prog, nil, []StackItem{}, 0)) t.Run("Array", getTestFuncForVM(prog, 2, []StackItem{makeStackItem(1), makeStackItem(2)}, 1)) t.Run("ByteArray", getTestFuncForVM(prog, 2, []byte{1, 2}, 1)) + t.Run("Buffer", getTestFuncForVM(prog, 2, NewBufferItem([]byte{1, 2}), 1)) } func TestPICKITEMDupArray(t *testing.T) { @@ -1305,6 +1310,13 @@ func TestPICKITEMMap(t *testing.T) { runWithArgs(t, prog, 3, m, 5) } +func TestSETITEMBuffer(t *testing.T) { + prog := makeProgram(opcode.DUP, opcode.REVERSE4, opcode.SETITEM) + t.Run("Good", getTestFuncForVM(prog, NewBufferItem([]byte{0, 42, 2}), 42, 1, NewBufferItem([]byte{0, 1, 2}))) + t.Run("BadIndex", getTestFuncForVM(prog, nil, 42, -1, NewBufferItem([]byte{0, 1, 2}))) + t.Run("BadValue", getTestFuncForVM(prog, nil, 256, 1, NewBufferItem([]byte{0, 1, 2}))) +} + func TestSETITEMMap(t *testing.T) { prog := makeProgram(opcode.SETITEM, opcode.PICKITEM) m := NewMapItem() @@ -1341,6 +1353,7 @@ func TestSIZE(t *testing.T) { prog := makeProgram(opcode.SIZE) t.Run("NoArgument", getTestFuncForVM(prog, nil)) t.Run("ByteArray", getTestFuncForVM(prog, 2, []byte{0, 1})) + t.Run("Buffer", getTestFuncForVM(prog, 2, NewBufferItem([]byte{0, 1}))) t.Run("Bool", getTestFuncForVM(prog, 1, false)) t.Run("Array", getTestFuncForVM(prog, 2, []StackItem{makeStackItem(1), makeStackItem([]byte{})})) t.Run("Map", func(t *testing.T) { @@ -1416,6 +1429,12 @@ func TestHASKEY(t *testing.T) { t.Run("True", getTestFuncForVM(prog, true, NewStructItem(arr), 4)) t.Run("False", getTestFuncForVM(prog, false, NewStructItem(arr), 5)) }) + + t.Run("Buffer", func(t *testing.T) { + t.Run("True", getTestFuncForVM(prog, true, NewBufferItem([]byte{5, 5, 5}), 2)) + t.Run("False", getTestFuncForVM(prog, false, NewBufferItem([]byte{5, 5, 5}), 3)) + t.Run("Negative", getTestFuncForVM(prog, nil, NewBufferItem([]byte{5, 5, 5}), -1)) + }) } func TestHASKEYMap(t *testing.T) { @@ -1838,11 +1857,10 @@ func TestUNPACKGood(t *testing.T) { assert.Equal(t, int64(1), vm.estack.Peek(len(elements)+1).BigInt().Int64()) } -func TestREVERSEITEMSBadNotArray(t *testing.T) { - prog := makeProgram(opcode.REVERSEITEMS) - vm := load(prog) - vm.estack.PushVal(1) - checkVMFailed(t, vm) +func TestREVERSEITEMS(t *testing.T) { + prog := makeProgram(opcode.DUP, opcode.REVERSEITEMS) + t.Run("InvalidItem", getTestFuncForVM(prog, nil, 1)) + t.Run("Buffer", getTestFuncForVM(prog, NewBufferItem([]byte{3, 2, 1}), NewBufferItem([]byte{1, 2, 3}))) } func testREVERSEITEMSIssue437(t *testing.T, i1, i2 opcode.Opcode, reversed bool) {