From e959d6cee1f15a34dd46b0c4efe770893b41263e Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Tue, 19 Sep 2023 23:34:54 -0700 Subject: [PATCH] Add coroutine queue to scope context and add Spawn opcode to add coroutine/generator to the queue --- core/vm/coroutine.go | 109 ++++++++++++++++++++++++++++++++++++++++ core/vm/instructions.go | 26 ++++++++++ core/vm/interpreter.go | 39 ++++++++++++++ core/vm/jump_table.go | 6 +++ core/vm/opcodes.go | 3 ++ core/vm/stack.go | 6 +++ functions.yul | 20 ++++++++ 7 files changed, 209 insertions(+) create mode 100644 core/vm/coroutine.go create mode 100644 functions.yul diff --git a/core/vm/coroutine.go b/core/vm/coroutine.go new file mode 100644 index 0000000000..3ec48a1268 --- /dev/null +++ b/core/vm/coroutine.go @@ -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 +} diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 2105201fce..5554ad5e31 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -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 diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 873337850e..2d4e2a7efd 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -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 } diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 702b186615..ce7bea33af 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -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, diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 2929b8ce92..1af864054a 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -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, diff --git a/core/vm/stack.go b/core/vm/stack.go index e1a957e244..52ebf3da87 100644 --- a/core/vm/stack.go +++ b/core/vm/stack.go @@ -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 diff --git a/functions.yul b/functions.yul new file mode 100644 index 0000000000..8795827ab7 --- /dev/null +++ b/functions.yul @@ -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) + } +}