diff --git a/pkg/vm/stack/context.go b/pkg/vm/stack/context.go index 0b8ed8bd2..855c58f73 100644 --- a/pkg/vm/stack/context.go +++ b/pkg/vm/stack/context.go @@ -121,6 +121,11 @@ func (c *Context) ReadUint16() uint16 { return val } +// ReadInt16 reads a int16 from the script +func (c *Context) ReadInt16() int16 { + return int16(c.ReadUint16()) +} + // ReadByte reads one byte from the script func (c *Context) ReadByte() (byte, error) { byt, err := c.ReadBytes(1) @@ -152,6 +157,16 @@ func (c *Context) readVarBytes() ([]byte, error) { return c.ReadBytes(int(n)) } +// SetIP sets the instruction pointer ip to a given integer. +// Returns an error if ip is less than -1 or greater than LenInstr. +func (c *Context) SetIP(ip int) error { + if ok := ip < -1 || ip > c.LenInstr(); ok { + return errors.New("invalid instruction pointer") + } + c.ip = ip + return nil +} + // Hash overrides the default abstract hash method. func (c *Context) Hash() (string, error) { data := c.String() + fmt.Sprintf(" %v-%v-%v-%v-%v", c.ip, c.prog, c.breakPoints, c.Estack, c.Astack) diff --git a/pkg/vm/vm_ops.go b/pkg/vm/vm_ops.go index 32b938992..fd427fa9a 100644 --- a/pkg/vm/vm_ops.go +++ b/pkg/vm/vm_ops.go @@ -5,6 +5,10 @@ import "github.com/CityOfZion/neo-go/pkg/vm/stack" type stackInfo func(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) var opFunc = map[stack.Instruction]stackInfo{ + stack.JMPIFNOT: JMPIFNOT, + stack.JMPIF: JMPIF, + stack.JMP: JMP, + stack.NOP: NOP, stack.HASH256: HASH256, stack.HASH160: HASH160, stack.SHA256: SHA256, diff --git a/pkg/vm/vm_ops_flow.go b/pkg/vm/vm_ops_flow.go index 67ca4f825..71f32d42d 100644 --- a/pkg/vm/vm_ops_flow.go +++ b/pkg/vm/vm_ops_flow.go @@ -25,3 +25,71 @@ func RET(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rst return NONE, nil } + +// NOP Returns NONE VMState. +func NOP(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + return NONE, nil +} + +// JMP moves the instruction pointer to an offset which is +// calculated base on the instructionPointerOffset method. +// Returns and error if the offset is out of range. +func JMP(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + offset := instructionPointerOffset(ctx) + if err := ctx.SetIP(offset); err != nil { + return FAULT, err + + } + + return NONE, nil +} + +// JMPIF pops a boolean off of the stack and, +// if the the boolean's value is true, it +// moves the instruction pointer to an offset which is +// calculated base on the instructionPointerOffset method. +// Returns and error if the offset is out of range or +// the popped item is not a boolean. +func JMPIF(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + b, err := ctx.Estack.PopBoolean() + if err != nil { + return FAULT, err + } + + if b.Value() { + offset := instructionPointerOffset(ctx) + if err := ctx.SetIP(offset); err != nil { + return FAULT, err + } + + } + + return NONE, nil +} + +// JMPIFNOT pops a boolean off of the stack and, +// if the the boolean's value is false, it +// moves the instruction pointer to an offset which is +// calculated base on the instructionPointerOffset method. +// Returns and error if the offset is out of range or +// the popped item is not a boolean. +func JMPIFNOT(op stack.Instruction, ctx *stack.Context, istack *stack.Invocation, rstack *stack.RandomAccess) (Vmstate, error) { + b, err := ctx.Estack.PopBoolean() + if err != nil { + return FAULT, err + } + + if !b.Value() { + offset := instructionPointerOffset(ctx) + if err := ctx.SetIP(offset); err != nil { + return FAULT, err + } + + } + + return NONE, nil +} + +func instructionPointerOffset(ctx *stack.Context) int { + return ctx.IP() + int(ctx.ReadInt16()) - 3 +} diff --git a/pkg/vm/vm_ops_flow_test.go b/pkg/vm/vm_ops_flow_test.go new file mode 100644 index 000000000..2801e51a4 --- /dev/null +++ b/pkg/vm/vm_ops_flow_test.go @@ -0,0 +1,168 @@ +package vm + +import ( + "math/big" + "testing" + + "github.com/CityOfZion/neo-go/pkg/vm/stack" + "github.com/stretchr/testify/assert" +) + +func TestNopOp(t *testing.T) { + + v := VM{} + + a, err := stack.NewInt(big.NewInt(10)) + assert.Nil(t, err) + + ctx := stack.NewContext([]byte{}) + ctx.Estack.Push(a) + + v.executeOp(stack.NOP, ctx) + + // Stack should have one item + assert.Equal(t, 1, ctx.Estack.Len()) + + item, err := ctx.Estack.PopInt() + assert.Nil(t, err) + + assert.Equal(t, int64(10), item.Value().Int64()) +} + +func TestJmpOp(t *testing.T) { + + v := VM{} + + a, err := stack.NewInt(big.NewInt(10)) + assert.Nil(t, err) + + ctx := stack.NewContext([]byte{5, 0, 2, 3, 4}) + ctx.Estack.Push(a) + + // ctx.ip = -1 + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) + + // ctx.ip will be set to offset. + // offset = ctx.IP() + int(ctx.ReadInt16()) - 3 + // = 0 + 5 -3 = 2 + v.executeOp(stack.JMP, ctx) + + // Stack should have one item + assert.Equal(t, 1, ctx.Estack.Len()) + + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 3, ctx.IP()) +} + +// test JMPIF instruction with true boolean +// on top of the stack +func TestJmpIfOp1(t *testing.T) { + + v := VM{} + + a := stack.NewBoolean(true) + + ctx := stack.NewContext([]byte{5, 0, 2, 3, 4}) + ctx.Estack.Push(a) + + // ctx.ip = -1 + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) + + // ctx.ip will be set to offset + // because the there is a true boolean + // on top of the stack. + // offset = ctx.IP() + int(ctx.ReadInt16()) - 3 + // = 0 + 5 -3 = 2 + v.executeOp(stack.JMPIF, ctx) + + // Stack should have 0 item + assert.Equal(t, 0, ctx.Estack.Len()) + + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 3, ctx.IP()) +} + +// test JMPIF instruction with false boolean +// on top of the stack +func TestJmpIfOp2(t *testing.T) { + + v := VM{} + + a := stack.NewBoolean(false) + + ctx := stack.NewContext([]byte{5, 0, 2, 3, 4}) + ctx.Estack.Push(a) + + // ctx.ip = -1 + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) + + // nothing will happen because + // the value of the boolean on top of the stack + // is false + v.executeOp(stack.JMPIF, ctx) + + // Stack should have 0 item + assert.Equal(t, 0, ctx.Estack.Len()) + + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) +} + +// test JMPIFNOT instruction with true boolean +// on top of the stack +func TestJmpIfNotOp1(t *testing.T) { + + v := VM{} + + a := stack.NewBoolean(true) + + ctx := stack.NewContext([]byte{5, 0, 2, 3, 4}) + ctx.Estack.Push(a) + + // ctx.ip = -1 + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) + + // nothing will happen because + // the value of the boolean on top of the stack + // is true + v.executeOp(stack.JMPIFNOT, ctx) + + // Stack should have 0 item + assert.Equal(t, 0, ctx.Estack.Len()) + + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) +} + +// test JMPIFNOT instruction with false boolean +// on top of the stack +func TestJmpIfNotOp2(t *testing.T) { + + v := VM{} + + a := stack.NewBoolean(false) + + ctx := stack.NewContext([]byte{5, 0, 2, 3, 4}) + ctx.Estack.Push(a) + + // ctx.ip = -1 + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 0, ctx.IP()) + + // ctx.ip will be set to offset + // because the there is a false boolean + // on top of the stack. + // offset = ctx.IP() + int(ctx.ReadInt16()) - 3 + // = 0 + 5 -3 = 2 + v.executeOp(stack.JMPIFNOT, ctx) + + // Stack should have one item + assert.Equal(t, 0, ctx.Estack.Len()) + + // ctx.IP() = ctx.ip + 1 + assert.Equal(t, 3, ctx.IP()) +}