Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions core/vm/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uin

func (c *Contract) validJumpdest(dest *big.Int) bool {
udest := dest.Uint64()
// PC cannot go beyond len(code) and certainly can't be bigger than 63bits.
// PC cannot go beyond len(code) and certainly can't be bigger than 63 bits.
// Don't bother checking for JUMPDEST in that case.
if dest.BitLen() >= 63 || udest >= uint64(len(c.Code)) {
return false
Expand All @@ -92,16 +92,32 @@ func (c *Contract) validJumpdest(dest *big.Int) bool {
if OpCode(c.Code[udest]) != JUMPDEST {
return false
}
// Do we have it locally already?
if c.analysis != nil {
return c.analysis.codeSegment(udest)
return c.isCode(udest)
}

func (c *Contract) validJumpSubdest(udest uint64) bool {
// PC cannot go beyond len(code) and certainly can't be bigger than 63 bits.
// Don't bother checking for BEGINSUB in that case.
if int64(udest) < 0 || udest >= uint64(len(c.Code)) {
return false
}
// Only BEGINSUBs allowed for destinations
if OpCode(c.Code[udest]) != BEGINSUB {
return false
}
// If we have the code hash (but no analysis), we should look into the
// parent analysis map and see if the analysis has been made previously
return c.isCode(udest)
}

// isCode returns true if the provided PC location is an actual opcode, as
// opposed to a data-segment following a PUSHN operation.
func (c *Contract) isCode(udest uint64) bool {
// Do we have a contract hash already?
if c.CodeHash != (common.Hash{}) {
// Does parent context have the analysis?
analysis, exist := c.jumpdests[c.CodeHash]
if !exist {
// Do the analysis and save in parent context
// We do not need to store it in c.analysis
analysis = codeBitmap(c.Code)
c.jumpdests[c.CodeHash] = analysis
}
Expand All @@ -113,7 +129,9 @@ func (c *Contract) validJumpdest(dest *big.Int) bool {
// in state trie. In that case, we do an analysis, and save it locally, so
// we don't have to recalculate it for every JUMP instruction in the execution
// However, we don't save it within the parent context
c.analysis = codeBitmap(c.Code)
if c.analysis == nil {
c.analysis = codeBitmap(c.Code)
}
return c.analysis.codeSegment(udest)
}

Expand Down
33 changes: 33 additions & 0 deletions core/vm/eips.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func EnableEIP(eipNum int, jt *JumpTable) error {
enable1884(jt)
case 1344:
enable1344(jt)
case 2315:
enable2315(jt)
default:
return fmt.Errorf("undefined eip %d", eipNum)
}
Expand Down Expand Up @@ -91,3 +93,34 @@ func enable2200(jt *JumpTable) {
jt[SLOAD].constantGas = params.SloadGasEIP2200
jt[SSTORE].dynamicGas = gasSStoreEIP2200
}

// enable2315 applies EIP-2315 (Simple Subroutines)
// - Adds opcodes that jump to and return from subroutines
func enable2315(jt *JumpTable) {
// New opcode
jt[BEGINSUB] = operation{
execute: opBeginSub,
constantGas: GasQuickStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
valid: true,
}
// New opcode
jt[JUMPSUB] = operation{
execute: opJumpSub,
constantGas: GasSlowStep,
minStack: minStack(1, 0),
maxStack: maxStack(1, 0),
jumps: true,
valid: true,
}
// New opcode
jt[RETURNSUB] = operation{
execute: opReturnSub,
constantGas: GasFastStep,
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
valid: true,
jumps: true,
}
}
5 changes: 5 additions & 0 deletions core/vm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import (

// List evm execution errors
var (
// ErrInvalidSubroutineEntry means that a BEGINSUB was reached via iteration,
// as opposed to from a JUMPSUB instruction
ErrInvalidSubroutineEntry = errors.New("invalid subroutine entry")
ErrOutOfGas = errors.New("out of gas")
ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas")
ErrDepth = errors.New("max call depth exceeded")
Expand All @@ -34,6 +37,8 @@ var (
ErrWriteProtection = errors.New("write protection")
ErrReturnDataOutOfBounds = errors.New("return data out of bounds")
ErrGasUintOverflow = errors.New("gas uint64 overflow")
ErrInvalidRetsub = errors.New("invalid retsub")
ErrReturnStackExceeded = errors.New("return stack limit reached")
)

// ErrStackUnderflow wraps an evm error when the items on the stack less
Expand Down
14 changes: 14 additions & 0 deletions core/vm/gen_structlog.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions core/vm/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,39 @@ func opJumpdest(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) (
return nil, nil
}

func opBeginSub(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
return nil, ErrInvalidSubroutineEntry
}

func opJumpSub(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
if len(callContext.rstack.data) >= 1023 {
return nil, ErrReturnStackExceeded
}
pos := callContext.stack.pop()
if !pos.IsUint64() {
return nil, ErrInvalidJump
}
posU64 := pos.Uint64()
if !callContext.contract.validJumpSubdest(posU64) {
return nil, ErrInvalidJump
}
callContext.rstack.push(*pc)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not conform to the spec https://eips.ethereum.org/EIPS/eip-2315:

Push the current pc + 1 to the return stack.

Though neither the spec's own example, so something needs fixing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah:

Note 3: The description above lays out the semantics of this feature in terms of a return stack. But the actual state of the return stack is not observable by EVM code or consensus-critical to the protocol. (For example, a node implementor may code JUMPSUB to unobservably push pc on the return stack rather than pc + 1, which is allowed so long as RETURNSUB observably returns control to the pc + 1 location.)

Still, might be wise to standardize it, otherwise consensus diffing traces would be nasty

*pc = posU64 + 1
interpreter.intPool.put(pos)
return nil, nil
}

func opReturnSub(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
if len(callContext.rstack.data) == 0 {
return nil, ErrInvalidRetsub
}
// Other than the check that the return stack is not empty, there is no
// need to validate the pc from 'returns', since we only ever push valid
//values onto it via jumpsub.
*pc = callContext.rstack.pop() + 1
return nil, nil
}

func opPc(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
callContext.stack.push(interpreter.intPool.get().SetUint64(*pc))
return nil, nil
Expand Down
31 changes: 16 additions & 15 deletions core/vm/instructions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu
var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack()
rstack = newReturnStack()
pc = uint64(0)
evmInterpreter = env.interpreter.(*EVMInterpreter)
)
Expand All @@ -109,7 +110,7 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu
expected := new(big.Int).SetBytes(common.Hex2Bytes(test.Expected))
stack.push(x)
stack.push(y)
opFn(&pc, evmInterpreter, &callCtx{nil, stack, nil})
opFn(&pc, evmInterpreter, &callCtx{nil, stack, rstack, nil})
actual := stack.pop()

if actual.Cmp(expected) != 0 {
Expand Down Expand Up @@ -211,10 +212,10 @@ func TestSAR(t *testing.T) {
// getResult is a convenience function to generate the expected values
func getResult(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcase {
var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack()
pc = uint64(0)
interpreter = env.interpreter.(*EVMInterpreter)
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack, rstack = newstack(), newReturnStack()
pc = uint64(0)
interpreter = env.interpreter.(*EVMInterpreter)
)
interpreter.intPool = poolOfIntPools.get()
result := make([]TwoOperandTestcase, len(args))
Expand All @@ -223,7 +224,7 @@ func getResult(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcas
y := new(big.Int).SetBytes(common.Hex2Bytes(param.y))
stack.push(x)
stack.push(y)
opFn(&pc, interpreter, &callCtx{nil, stack, nil})
opFn(&pc, interpreter, &callCtx{nil, stack, rstack, nil})
actual := stack.pop()
result[i] = TwoOperandTestcase{param.x, param.y, fmt.Sprintf("%064x", actual)}
}
Expand Down Expand Up @@ -263,7 +264,7 @@ func TestJsonTestcases(t *testing.T) {
func opBenchmark(bench *testing.B, op executionFunc, args ...string) {
var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack()
stack, rstack = newstack(), newReturnStack()
evmInterpreter = NewEVMInterpreter(env, env.vmConfig)
)

Expand All @@ -281,7 +282,7 @@ func opBenchmark(bench *testing.B, op executionFunc, args ...string) {
a := new(big.Int).SetBytes(arg)
stack.push(a)
}
op(&pc, evmInterpreter, &callCtx{nil, stack, nil})
op(&pc, evmInterpreter, &callCtx{nil, stack, rstack, nil})
stack.pop()
}
poolOfIntPools.put(evmInterpreter.intPool)
Expand Down Expand Up @@ -498,7 +499,7 @@ func BenchmarkOpIsZero(b *testing.B) {
func TestOpMstore(t *testing.T) {
var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack()
stack, rstack = newstack(), newReturnStack()
mem = NewMemory()
evmInterpreter = NewEVMInterpreter(env, env.vmConfig)
)
Expand All @@ -509,12 +510,12 @@ func TestOpMstore(t *testing.T) {
pc := uint64(0)
v := "abcdef00000000000000abba000000000deaf000000c0de00100000000133700"
stack.pushN(new(big.Int).SetBytes(common.Hex2Bytes(v)), big.NewInt(0))
opMstore(&pc, evmInterpreter, &callCtx{mem, stack, nil})
opMstore(&pc, evmInterpreter, &callCtx{mem, stack, rstack, nil})
if got := common.Bytes2Hex(mem.GetCopy(0, 32)); got != v {
t.Fatalf("Mstore fail, got %v, expected %v", got, v)
}
stack.pushN(big.NewInt(0x1), big.NewInt(0))
opMstore(&pc, evmInterpreter, &callCtx{mem, stack, nil})
opMstore(&pc, evmInterpreter, &callCtx{mem, stack, rstack, nil})
if common.Bytes2Hex(mem.GetCopy(0, 32)) != "0000000000000000000000000000000000000000000000000000000000000001" {
t.Fatalf("Mstore failed to overwrite previous value")
}
Expand All @@ -524,7 +525,7 @@ func TestOpMstore(t *testing.T) {
func BenchmarkOpMstore(bench *testing.B) {
var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack()
stack, rstack = newstack(), newReturnStack()
mem = NewMemory()
evmInterpreter = NewEVMInterpreter(env, env.vmConfig)
)
Expand All @@ -539,15 +540,15 @@ func BenchmarkOpMstore(bench *testing.B) {
bench.ResetTimer()
for i := 0; i < bench.N; i++ {
stack.pushN(value, memStart)
opMstore(&pc, evmInterpreter, &callCtx{mem, stack, nil})
opMstore(&pc, evmInterpreter, &callCtx{mem, stack, rstack, nil})
}
poolOfIntPools.put(evmInterpreter.intPool)
}

func BenchmarkOpSHA3(bench *testing.B) {
var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack()
stack, rstack = newstack(), newReturnStack()
mem = NewMemory()
evmInterpreter = NewEVMInterpreter(env, env.vmConfig)
)
Expand All @@ -560,7 +561,7 @@ func BenchmarkOpSHA3(bench *testing.B) {
bench.ResetTimer()
for i := 0; i < bench.N; i++ {
stack.pushN(big.NewInt(32), start)
opSha3(&pc, evmInterpreter, &callCtx{mem, stack, nil})
opSha3(&pc, evmInterpreter, &callCtx{mem, stack, rstack, nil})
}
poolOfIntPools.put(evmInterpreter.intPool)
}
Expand Down
15 changes: 9 additions & 6 deletions core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Interpreter interface {
type callCtx struct {
memory *Memory
stack *Stack
rstack *ReturnStack
contract *Contract
}

Expand Down Expand Up @@ -167,12 +168,14 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
}

var (
op OpCode // current opcode
mem = NewMemory() // bound memory
stack = newstack() // local stack
op OpCode // current opcode
mem = NewMemory() // bound memory
stack = newstack() // local stack
returns = newReturnStack() // local returns stack
callContext = &callCtx{
memory: mem,
stack: stack,
rstack: returns,
contract: contract,
}
// For optimisation reason we're using uint64 as the program counter.
Expand All @@ -195,9 +198,9 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
defer func() {
if err != nil {
if !logged {
in.cfg.Tracer.CaptureState(in.evm, pcCopy, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
in.cfg.Tracer.CaptureState(in.evm, pcCopy, op, gasCopy, cost, mem, stack, returns, contract, in.evm.depth, err)
} else {
in.cfg.Tracer.CaptureFault(in.evm, pcCopy, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
in.cfg.Tracer.CaptureFault(in.evm, pcCopy, op, gasCopy, cost, mem, stack, returns, contract, in.evm.depth, err)
}
}
}()
Expand Down Expand Up @@ -279,7 +282,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
}

if in.cfg.Debug {
in.cfg.Tracer.CaptureState(in.evm, pc, op, gasCopy, cost, mem, stack, contract, in.evm.depth, err)
in.cfg.Tracer.CaptureState(in.evm, pc, op, gasCopy, cost, mem, stack, returns, contract, in.evm.depth, err)
logged = true
}

Expand Down
Loading