Add coroutine queue to scope context and add Spawn opcode to add coroutine/generator to the queue

This commit is contained in:
Brandon Roberts 2023-09-19 23:34:54 -07:00
parent 3f40e65c48
commit e959d6cee1
7 changed files with 209 additions and 0 deletions

109
core/vm/coroutine.go Normal file
View file

@ -0,0 +1,109 @@
package vm
import (
"log"
"github.com/ethereum/go-ethereum/common/math"
)
//TODO: Use references where possible
type Coroutine struct {
PC uint64
Stack Stack
}
func NewCoroutine(pc uint64, stack Stack) Coroutine {
newStack := deepCopyStack(&stack)
return Coroutine{pc, *newStack}
}
func (co *Coroutine) ExecuteCoroutine(interpreter *EVMInterpreter, scope *ScopeContext) (ret []byte, err error) {
log.Println("ExecuteCoroutine: ", co.PC, co.Stack)
// Don't bother with the execution if there's no code.
if len(scope.Contract.Code) == 0 {
return nil, nil
}
var (
op OpCode // current opcode
mem = scope.Memory // memory of the contract
stack = &co.Stack // stack of the contract
contract = scope.Contract
callContext = scope
pc = co.PC
cost uint64 // TODO: Bake in with other run gas
// copies used by tracer
res []byte // result of the opcode execution function
)
// Don't move this deferred function, it's placed before the capturestate-deferred method,
// so that it get's executed _after_: the capturestate needs the stacks before
// they are returned to the pools
defer func() {
// TODO: Think more about this
// callContext.AwaitTermination(interpreter)
returnStack(stack)
}()
callContext.Stack = stack
//TODO : Debug and other removed stuff?
// parent context.
for {
// Get the operation from the jump table and validate the stack to ensure there are
// enough stack items available to perform the operation.
op = contract.GetOp(pc)
operation := interpreter.table[op]
cost = operation.constantGas // For tracing
// Validate stack
if sLen := stack.len(); sLen < operation.minStack {
return nil, &ErrStackUnderflow{stackLen: sLen, required: operation.minStack}
} else if sLen > operation.maxStack {
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
if !contract.UseGas(cost) {
return nil, ErrOutOfGas
}
if operation.dynamicGas != nil {
// All ops with a dynamic memory usage also has a dynamic gas cost.
var memorySize uint64
// calculate the new memory size and expand the memory to fit
// the operation
// Memory check needs to be done prior to evaluating the dynamic gas portion,
// to detect calculation overflows
if operation.memorySize != nil {
memSize, overflow := operation.memorySize(stack)
if overflow {
return nil, ErrGasUintOverflow
}
// memory is expanded in words of 32 bytes. Gas
// is also calculated in words.
if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
return nil, ErrGasUintOverflow
}
}
// Consume the gas and return an error if not enough gas is available.
// cost is explicitly set so that the capture state defer method can get the proper cost
var dynamicCost uint64
dynamicCost, err = operation.dynamicGas(interpreter.evm, contract, stack, mem, memorySize)
cost += dynamicCost // for tracing
if err != nil || !contract.UseGas(dynamicCost) {
return nil, ErrOutOfGas
}
if memorySize > 0 {
mem.Resize(memorySize)
}
}
// execute the operation
res, err = operation.execute(&pc, interpreter, callContext)
if err != nil {
break
}
pc++
}
if err == errStopToken {
err = nil // clear stop token error
}
return res, err
}

View file

@ -17,6 +17,8 @@
package vm
import (
"log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
@ -501,6 +503,9 @@ func opMstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b
// pop value of the stack
mStart, val := scope.Stack.pop(), scope.Stack.pop()
scope.Memory.Set32(mStart.Uint64(), &val)
// Temporary : Log the opMstore to easily see execution order
log.Printf("opMstore: mStart=%v, val=%v", mStart, val)
return nil, nil
}
@ -558,6 +563,27 @@ func opJumpdest(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([
return nil, nil
}
// TODO: Push end of program to stack before executing spawn and pop off after w/ other args?
func opSpawn(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
if interpreter.evm.abort.Load() {
return nil, errStopToken
}
pos := scope.Stack.pop()
if !scope.Contract.validJumpdest(&pos) {
return nil, ErrInvalidJump
}
// Save coroutine to memory ( Stack, PC )
coroutine := NewCoroutine(pos.Uint64(), *scope.Stack)
// Add coroutine to queue
scope.PushCoroutine(coroutine)
log.Printf("Spawned coroutine %v", coroutine)
return nil, nil
}
func opPc(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
scope.Stack.push(new(uint256.Int).SetUint64(*pc))
return nil, nil

View file

@ -17,6 +17,9 @@
package vm
import (
"errors"
log2 "log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
@ -37,6 +40,38 @@ type ScopeContext struct {
Memory *Memory
Stack *Stack
Contract *Contract
CoroutineQueue []Coroutine
}
func (scope *ScopeContext) PushCoroutine(coroutine Coroutine) {
scope.CoroutineQueue = append(scope.CoroutineQueue, coroutine)
}
func (scope *ScopeContext) PopCoroutine() (Coroutine, error) {
if len(scope.CoroutineQueue) == 0 {
return Coroutine{}, errors.New("Coroutine queue is empty")
}
coroutine := scope.CoroutineQueue[0]
scope.CoroutineQueue = scope.CoroutineQueue[1:]
return coroutine, nil
}
func (scope *ScopeContext) AwaitTermination(interpreter *EVMInterpreter) {
coroutine, err := scope.PopCoroutine()
for err == nil {
// TODO: Propogate returns and errors?
ret, err2 := coroutine.ExecuteCoroutine(interpreter, scope)
if err2 != nil {
log2.Println("Coroutine execution failed", "error", err2)
}
if ret != nil {
log2.Println("Coroutine returned", "ret", ret)
}
coroutine, err = scope.PopCoroutine()
log2.Println("Awaiting coroutine termination", "coroutine", coroutine, "err", err)
}
}
// EVMInterpreter represents an EVM interpreter
@ -133,6 +168,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
Memory: mem,
Stack: stack,
Contract: contract,
CoroutineQueue: []Coroutine{},
}
// For optimisation reason we're using uint64 as the program counter.
// It's theoretically possible to go above 2^64. The YP defines the PC
@ -150,6 +186,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
// so that it get's executed _after_: the capturestate needs the stacks before
// they are returned to the pools
defer func() {
callContext.AwaitTermination(in)
returnStack(stack)
}()
contract.Input = input
@ -238,5 +275,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
err = nil // clear stop token error
}
log2.Println("returning from run with", res, err)
// Res is array of bytes located in scope.Memory
return res, err
}

View file

@ -595,6 +595,12 @@ func newFrontierInstructionSet() JumpTable {
minStack: minStack(0, 0),
maxStack: maxStack(0, 0),
},
SPAWN: {
execute: opSpawn,
constantGas: GasSlowStep, // TODO
minStack: minStack(1, 0),
maxStack: maxStack(1, 0),
},
PUSH1: {
execute: opPush1,
constantGas: GasFastestStep,

View file

@ -218,6 +218,7 @@ const (
CREATE2 OpCode = 0xf5
STATICCALL OpCode = 0xfa
SPAWN OpCode = 0xfb
REVERT OpCode = 0xfd
INVALID OpCode = 0xfe
SELFDESTRUCT OpCode = 0xff
@ -304,6 +305,7 @@ var opCodeToString = map[OpCode]string{
TLOAD: "TLOAD",
TSTORE: "TSTORE",
MCOPY: "MCOPY",
SPAWN: "SPAWN",
PUSH0: "PUSH0",
// 0x60 range - pushes.
@ -476,6 +478,7 @@ var stringToOp = map[string]OpCode{
"TLOAD": TLOAD,
"TSTORE": TSTORE,
"MCOPY": MCOPY,
"SPAWN": SPAWN,
"PUSH0": PUSH0,
"PUSH1": PUSH1,
"PUSH2": PUSH2,

View file

@ -44,6 +44,12 @@ func returnStack(s *Stack) {
stackPool.Put(s)
}
func deepCopyStack(s *Stack) *Stack {
ret := newstack()
ret.data = append(ret.data, s.data...)
return ret
}
// Data returns the underlying uint256.Int array.
func (st *Stack) Data() []uint256.Int {
return st.data

20
functions.yul Normal file
View file

@ -0,0 +1,20 @@
{
// TODO: return vals, storage
function basic() {
mstore(0x80, 0x42)
}
function func1(val) {
mstore(0xa0, val)
}
function func2(val1, val2, str1) {
mstore(0xc0, add(val1, val2))
mstore(0xe0, str1)
}
function main() {
basic()
func1(0x32)
}
}