Skip to content

Commit

Permalink
core/vm: EIP-2315, JUMPSUB for the EVM (#20619)
Browse files Browse the repository at this point in the history
* core/vm: implement EIP 2315, subroutines for the EVM

* core/vm: eip 2315 - lintfix + check jump dest validity + check ret stack size constraints

  logger: markdown-friendly traces, validate jumpdest, more testcase, correct opcodes

* core/vm: update subroutines acc to eip: disallow walk-into

* core/vm/eips: gas cost changes for subroutines

* core/vm: update opcodes for EIP-2315

* core/vm: define RETURNSUB as a 'jumping' operation + review concerns

Co-authored-by: Martin Holst Swende <martin@swende.se>
  • Loading branch information
gcolvin and holiman authored Jun 2, 2020
1 parent a35382d commit cd57d5c
Show file tree
Hide file tree
Showing 15 changed files with 529 additions and 52 deletions.
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)
*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

0 comments on commit cd57d5c

Please sign in to comment.