vm: implement Buffer stack item

Buffer is a stack item type introduced in NEO3
which represents mutable byte-array.
This commit is contained in:
Evgenii Stratonikov 2020-05-12 14:47:33 +03:00
parent ee9adcdc5c
commit c3f7832f3b
5 changed files with 166 additions and 11 deletions

View file

@ -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)

View file

@ -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
}
}

View file

@ -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{

View file

@ -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)
}

View file

@ -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) {