diff --git a/.gitignore b/.gitignore index e554088..85938e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ ## IR outputs -*.ll \ No newline at end of file +*.ll +## obj files +*.o +*.a +*.so +## vscode +.vscode \ No newline at end of file diff --git a/compiler/Makefile b/compiler/Makefile new file mode 100644 index 0000000..8c5a9d3 --- /dev/null +++ b/compiler/Makefile @@ -0,0 +1,4 @@ +CLIBPATH = ./lib/clib.o +clib: + gcc -O3 -march=native -funroll-loops -o $(CLIBPATH) -c ./lib/clib.c + @echo "Compiled clib to $(CLIBPATH)." \ No newline at end of file diff --git a/compiler/compiler.go b/compiler/compiler.go index 8f7d62e..bf717de 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -28,6 +28,7 @@ type EVMCompiler struct { hostFuncType llvm.Type hostFunc llvm.Value table *JumpTable + clibs map[string]CHostFunc copts *EVMCompilationOpts } @@ -73,6 +74,7 @@ type EVMCompilationOpts struct { DisableSectionGasOptimization bool DisableStackUnderflowOptimization bool DisableIROptimization bool + FreeMemory bool } func DefaultEVMCompilationOpts() *EVMCompilationOpts { @@ -225,18 +227,25 @@ func (c *EVMCompiler) pushStack(stack, stackIdxPtr, value llvm.Value) { c.builder.CreateStore(newStackIdxVal, stackIdxPtr) } -func (c *EVMCompiler) popStack(stack, stackIdxPtr llvm.Value) llvm.Value { - stackIdxVal := c.builder.CreateLoad(c.ctx.Int64Type(), stackIdxPtr, "stack_idx_val") - newStackIdxVal := c.builder.CreateSub(stackIdxVal, c.u64Const(1), "new_stack_idx_val") - c.builder.CreateStore(newStackIdxVal, stackIdxPtr) - stackElem := c.builder.CreateGEP(c.ctx.IntType(256), stack, []llvm.Value{newStackIdxVal}, "stack_elem") - return c.builder.CreateLoad(c.ctx.IntType(256), stackElem, "stack_value") +func popStack(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr llvm.Value) llvm.Value { + stackIdxVal := builder.CreateLoad(ctx.Int64Type(), stackIdxPtr, "stack_idx_val") + newStackIdxVal := builder.CreateSub(stackIdxVal, u64Const(ctx, 1), "new_stack_idx_val") + builder.CreateStore(newStackIdxVal, stackIdxPtr) + stackElem := builder.CreateGEP(ctx.IntType(256), stack, []llvm.Value{newStackIdxVal}, "stack_elem") + return builder.CreateLoad(ctx.IntType(256), stackElem, "stack_value") } -func (c *EVMCompiler) peekStackPtr(stack, stackIdxPtr llvm.Value) llvm.Value { - stackIdxVal := c.builder.CreateLoad(c.ctx.Int64Type(), stackIdxPtr, "stack_idx_val") - newStackIdxVal := c.builder.CreateSub(stackIdxVal, c.u64Const(1), "new_stack_idx_val") - return c.builder.CreateGEP(c.ctx.IntType(256), stack, []llvm.Value{newStackIdxVal}, "stack_elem") +func peekStackPtr(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr llvm.Value) llvm.Value { + stackIdxVal := builder.CreateLoad(ctx.Int64Type(), stackIdxPtr, "stack_idx_val") + newStackIdxVal := builder.CreateSub(stackIdxVal, u64Const(ctx, 1), "new_stack_idx_val") + return builder.CreateGEP(ctx.IntType(256), stack, []llvm.Value{newStackIdxVal}, "stack_elem") +} + +func peekStack(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr llvm.Value, pos uint64) llvm.Value { + stackIdxVal := builder.CreateLoad(ctx.Int64Type(), stackIdxPtr, "stack_idx_val") + newStackIdxVal := builder.CreateSub(stackIdxVal, u64Const(ctx, pos+1), "new_stack_idx_val") + stackElem := builder.CreateGEP(ctx.IntType(256), stack, []llvm.Value{newStackIdxVal}, "stack_elem") + return builder.CreateLoad(ctx.IntType(256), stackElem, "stack_value") } func (c *EVMCompiler) createUint256ConstantFromBytes(data []byte) llvm.Value { @@ -249,43 +258,45 @@ func (c *EVMCompiler) createUint256ConstantFromBytes(data []byte) llvm.Value { return llvm.ConstIntFromString(c.ctx.IntType(256), v.String(), 10) } -func (c *EVMCompiler) loadFromMemory(memory, offset llvm.Value) llvm.Value { - offsetTrunc := c.builder.CreateTrunc(offset, c.ctx.Int64Type(), "mem_offset") - memPtr := c.builder.CreateGEP(c.ctx.Int8Type(), memory, []llvm.Value{offsetTrunc}, "mem_ptr") - - value := llvm.ConstInt(c.ctx.IntType(256), 0, false) - for i := 0; i < 32; i++ { - byteOffset := c.u64Const(uint64(i)) - bytePtr := c.builder.CreateGEP(c.ctx.Int8Type(), memPtr, []llvm.Value{byteOffset}, "byte_ptr") - byteVal := c.builder.CreateLoad(c.ctx.Int8Type(), bytePtr, "byte_val") - byteValExt := c.builder.CreateZExt(byteVal, c.ctx.IntType(256), "byte_ext") - shift := c.builder.CreateShl(byteValExt, llvm.ConstInt(c.ctx.IntType(256), uint64(8*(31-i)), false), "byte_shift") - value = c.builder.CreateOr(value, shift, "mem_value") - } +func (c *EVMCompiler) checkU64Overflow(overflow, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) { + continueBlock := llvm.AddBasicBlock(c.builder.GetInsertBlock().Parent(), "continue") + u64OverflowBlock := llvm.AddBasicBlock(c.builder.GetInsertBlock().Parent(), "u64_overflow") + // Branch to stack overflow block if limit exceeded, otherwise continue + c.builder.CreateCondBr(overflow, u64OverflowBlock, continueBlock) - return value + c.builder.SetInsertPointAtEnd(u64OverflowBlock) + // Store error code and exit + c.builder.CreateStore(c.u64Const(uint64(VMErrorCodeGasUintOverflow)), errorCodePtr) + c.builder.CreateBr(errorBlock) + c.builder.SetInsertPointAtEnd(continueBlock) } -func (c *EVMCompiler) storeToMemory(memory, offset, value llvm.Value) { - offsetTrunc := c.builder.CreateTrunc(offset, c.ctx.Int64Type(), "mem_offset") - memPtr := c.builder.CreateGEP(c.ctx.Int8Type(), memory, []llvm.Value{offsetTrunc}, "mem_ptr") +func (c *EVMCompiler) safeMul(x, y, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) llvm.Value { + ctx := c.ctx + builder := c.builder + i64 := ctx.Int64Type() + i128 := ctx.IntType(128) - for i := 0; i < 32; i++ { - shift := llvm.ConstInt(c.ctx.IntType(256), uint64(8*(31-i)), false) - shiftedValue := c.builder.CreateLShr(value, shift, "shifted_value") - byteVal := c.builder.CreateTrunc(shiftedValue, c.ctx.Int8Type(), "byte_val") + // Extend to 128-bit + x128 := builder.CreateZExt(x, i128, "x128") + y128 := builder.CreateZExt(y, i128, "y128") - byteOffset := c.u64Const(uint64(i)) - bytePtr := c.builder.CreateGEP(c.ctx.Int8Type(), memPtr, []llvm.Value{byteOffset}, "byte_ptr") - c.builder.CreateStore(byteVal, bytePtr) - } -} + // 128-bit product + prod := builder.CreateMul(x128, y128, "prod") + + // Low 64 bits + lo := builder.CreateTrunc(prod, i64, "lo") + + // High 64 bits + hi := builder.CreateLShr(prod, llvm.ConstInt(i128, 64, false), "hi_shifted") + hi64 := builder.CreateTrunc(hi, i64, "hi") + + // Check overflow: hi != 0 + hasOverflow := builder.CreateICmp(llvm.IntNE, hi64, llvm.ConstInt(i64, 0, false), "u64_overflow_safeMul") -func (c *EVMCompiler) storeByteToMemory(memory, offset, value llvm.Value) { - offsetTrunc := c.builder.CreateTrunc(offset, c.ctx.Int64Type(), "mem_offset") - memPtr := c.builder.CreateGEP(c.ctx.Int8Type(), memory, []llvm.Value{offsetTrunc}, "mem_ptr") - byteVal := c.builder.CreateTrunc(value, c.ctx.Int8Type(), "byte_val") - c.builder.CreateStore(byteVal, memPtr) + // Branch on overflow + c.checkU64Overflow(hasOverflow, errorCodePtr, errorBlock) + return lo } func (c *EVMCompiler) EmitObjectFile(filename string) error { diff --git a/compiler/compiler_static.go b/compiler/compiler_static.go index 39b9cff..d9e34fb 100644 --- a/compiler/compiler_static.go +++ b/compiler/compiler_static.go @@ -18,6 +18,17 @@ const ( var ( INT256_NEGATIVE_1 = uint256.MustFromHex("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").String() // -1 INT256_NEGATIVE_MIN = uint256.MustFromHex("0x8000000000000000000000000000000000000000000000000000000000000000").String() // -2^255 + + BIT_SWAP256 = "llvm.bswap.i256" + MEMORY_NEW = "memory_new" + MEMORY_FREE = "memory_free" + MEMORY_RESIZE = "memory_resize" + MEMORY_SET = "memory_set" + MEMORY_COPY = "memory_copy" + MEMORY_PTR = "memory_getptr" + TO_WORD_SIZE = "to_word_size" + MEMORY_GAS_COST = "memory_gas_cost" + MEM_CP_GAS_COST = "memory_copier_gas" ) // PCAnalysis holds static program counter analysis results @@ -38,6 +49,93 @@ func (c *EVMCompiler) u64Const(value uint64) llvm.Value { return llvm.ConstInt(c.ctx.Int64Type(), value, false) } +type CHostFunc struct { + fnType llvm.Type + fn llvm.Value +} + +func Clibs(ctx llvm.Context, mod llvm.Module) map[string]CHostFunc { + // C types + i8ptr := llvm.PointerType(ctx.Int8Type(), 0) + i32 := ctx.Int32Type() + i64 := ctx.Int64Type() + i256 := ctx.IntType(256) + voidt := ctx.VoidType() + + bswap256FnType := llvm.FunctionType(i256, []llvm.Type{i256}, false) + bswap256Fn := llvm.AddFunction(mod, BIT_SWAP256, bswap256FnType) + + // Memory* type is treated as i8* + memPtrType := i8ptr + // Memory* memory_new(void) + memNewType := llvm.FunctionType(memPtrType, nil, false) + memNewFn := llvm.AddFunction(mod, MEMORY_NEW, memNewType) + + // void memory_free(Memory*) + memFreeType := llvm.FunctionType(voidt, []llvm.Type{memPtrType}, false) + memFreeFn := llvm.AddFunction(mod, MEMORY_FREE, memFreeType) + + // void memory_resize(Memory*, i64) + resizeType := llvm.FunctionType(voidt, []llvm.Type{memPtrType, i64}, false) + memResizeFn := llvm.AddFunction(mod, MEMORY_RESIZE, resizeType) + + // uint8_t* memory_getptr(Memory* m, uint64_t offset, uint64_t size) + memGetptrType := llvm.FunctionType(i8ptr, []llvm.Type{memPtrType, i64, i64}, false) + memGetptrFn := llvm.AddFunction(mod, MEMORY_PTR, memGetptrType) + + // void memory_set(Memory*, i64, i64, const i8*) + memSetType := llvm.FunctionType(voidt, []llvm.Type{memPtrType, i64, i64, i8ptr}, false) + memSetFn := llvm.AddFunction(mod, MEMORY_SET, memSetType) + + // void memory_copy(Memory *m, uint64_t dst, uint64_t src, uint64_t len) + memCpType := llvm.FunctionType(voidt, []llvm.Type{memPtrType, i64, i64, i64}, false) + memCpFn := llvm.AddFunction(mod, MEMORY_COPY, memCpType) + + // int memory_gas_cost(Memory *mem, uint64_t newMemSize, uint64_t *outFee) + memGasCostType := llvm.FunctionType( + i32, // return int + []llvm.Type{memPtrType, i64, llvm.PointerType(i64, 0)}, + false, + ) + memGasCostFn := llvm.AddFunction(mod, MEMORY_GAS_COST, memGasCostType) + + // int memory_copier_gas(Memory *mem, uint64_t words, uint64_t memorySize, uint64_t *outGas) + memCpGasCostType := llvm.FunctionType( + i32, // return int + []llvm.Type{memPtrType, i64, i64, llvm.PointerType(i64, 0)}, + false, + ) + memCpGasCostFn := llvm.AddFunction(mod, MEM_CP_GAS_COST, memCpGasCostType) + + // uint64_t to_word_size(uint64_t size) + toWordSizeType := llvm.FunctionType(i64, []llvm.Type{i64}, false) + toWordSizeFn := llvm.AddFunction(mod, "to_word_size", toWordSizeType) + + memNewFn.SetLinkage(llvm.ExternalLinkage) + memFreeFn.SetLinkage(llvm.ExternalLinkage) + memResizeFn.SetLinkage(llvm.ExternalLinkage) + memSetFn.SetLinkage(llvm.ExternalLinkage) + toWordSizeFn.SetLinkage(llvm.ExternalLinkage) + memGasCostFn.SetLinkage(llvm.ExternalLinkage) + memCpFn.SetLinkage(llvm.ExternalLinkage) + memCpGasCostFn.SetLinkage(llvm.ExternalLinkage) + + cHostFns := make(map[string]CHostFunc) + cHostFns[BIT_SWAP256] = CHostFunc{bswap256FnType, bswap256Fn} + cHostFns[MEMORY_NEW] = CHostFunc{memNewType, memNewFn} + cHostFns[MEMORY_FREE] = CHostFunc{memFreeType, memFreeFn} + cHostFns[MEMORY_RESIZE] = CHostFunc{resizeType, memResizeFn} + cHostFns[MEMORY_SET] = CHostFunc{memSetType, memSetFn} + cHostFns[MEMORY_COPY] = CHostFunc{memCpType, memCpFn} + cHostFns[MEMORY_PTR] = CHostFunc{memGetptrType, memGetptrFn} + cHostFns[TO_WORD_SIZE] = CHostFunc{toWordSizeType, toWordSizeFn} + + cHostFns[MEMORY_GAS_COST] = CHostFunc{memGasCostType, memGasCostFn} + cHostFns[MEM_CP_GAS_COST] = CHostFunc{memCpGasCostType, memCpGasCostFn} + + return cHostFns +} + // CompileBytecodeStatic compiles EVM bytecode using static PC analysis func (c *EVMCompiler) CompileBytecodeStatic(bytecode []byte, opts *EVMCompilationOpts) (llvm.Module, error) { if opts == nil { @@ -47,6 +145,7 @@ func (c *EVMCompiler) CompileBytecodeStatic(bytecode []byte, opts *EVMCompilatio if err != nil { return llvm.Module{}, err } + c.clibs = Clibs(c.ctx, c.module) // Perform static analysis analysis := c.analyzeProgram(instructions) @@ -56,7 +155,8 @@ func (c *EVMCompiler) CompileBytecodeStatic(bytecode []byte, opts *EVMCompilatio uint256PtrType := llvm.PointerType(uint256Type, 0) uint64PtrType := llvm.PointerType(c.ctx.Int64Type(), 0) - execType := llvm.FunctionType(c.ctx.VoidType(), []llvm.Type{ + memPtrType := llvm.PointerType(c.ctx.Int8Type(), 0) + execType := llvm.FunctionType(memPtrType, []llvm.Type{ c.ctx.Int64Type(), // inst uint256PtrType, // stack uint8PtrType, // code (unused but kept for signature) @@ -80,6 +180,9 @@ func (c *EVMCompiler) CompileBytecodeStatic(bytecode []byte, opts *EVMCompilatio stackIdxPtr := c.builder.CreateAlloca(c.ctx.Int64Type(), "stack_ptr") c.builder.CreateStore(c.u64Const(0), stackIdxPtr) + memNewFn := c.clibs[MEMORY_NEW] + memory := c.builder.CreateCall(memNewFn.fnType, memNewFn.fn, []llvm.Value{}, "call_memory_new") + // Initialize gasPtr tracking gasPtr := c.builder.CreateAlloca(c.ctx.Int64Type(), "gas_used") c.builder.CreateStore(gasLimitParam, gasPtr) @@ -130,7 +233,7 @@ func (c *EVMCompiler) CompileBytecodeStatic(bytecode []byte, opts *EVMCompilatio } } - c.compileInstructionStatic(instr, prevInstr, instParam, stackParam, stackIdxPtr, gasPtr, jumpTargetPtr, errorCodePtr, analysis, nextBlock, dynJumpBlock, exitBlock, errorBlock, opts) + c.compileInstructionStatic(instr, prevInstr, instParam, stackParam, stackIdxPtr, memory, gasPtr, jumpTargetPtr, errorCodePtr, analysis, nextBlock, dynJumpBlock, exitBlock, errorBlock, opts) prevInstr = &instr } @@ -152,14 +255,27 @@ func (c *EVMCompiler) CompileBytecodeStatic(bytecode []byte, opts *EVMCompilatio errorCode := c.builder.CreateLoad(c.ctx.Int64Type(), errorCodePtr, "") finalizeGas() c.setOutputValueAt(outputPtrParam, OUTPUT_IDX_ERROR_CODE, errorCode) - c.builder.CreateRetVoid() + + nullMem := llvm.ConstNull(memPtrType) + memFreeFn := c.clibs[MEMORY_FREE] + if opts.FreeMemory { + c.builder.CreateCall(memFreeFn.fnType, memFreeFn.fn, []llvm.Value{memory}, "") + c.builder.CreateRet(nullMem) + } else { + c.builder.CreateRet(memory) + } // Finalize exit block c.builder.SetInsertPointAtEnd(exitBlock) finalizeGas() stackDepth := c.builder.CreateLoad(c.ctx.Int64Type(), stackIdxPtr, "stack_depth") c.setOutputValueAt(outputPtrParam, OUTPUT_IDX_STACK_DEPTH, stackDepth) - c.builder.CreateRetVoid() + if opts.FreeMemory { + c.builder.CreateCall(memFreeFn.fnType, memFreeFn.fn, []llvm.Value{memory}, "") + c.builder.CreateRet(nullMem) + } else { + c.builder.CreateRet(memory) + } err = llvm.VerifyModule(c.module, llvm.ReturnStatusAction) if err != nil { @@ -257,7 +373,10 @@ func (c *EVMCompiler) checkHostReturn(ret, errorCodePtr llvm.Value, nextBlock, e } // compileInstructionStatic compiles an instruction using static analysis with gas metering -func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr *EVMInstruction, execInst, stack, stackIdxPtr, gasPtr, jumpTargetPtr, errorCodePtr llvm.Value, analysis *PCAnalysis, nextBlock, dynJumpBlock, exitBlock, errorBlock llvm.BasicBlock, opts *EVMCompilationOpts) { +func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr *EVMInstruction, execInst, stack, stackIdxPtr, memory, gasPtr, jumpTargetPtr, errorCodePtr llvm.Value, analysis *PCAnalysis, nextBlock, dynJumpBlock, exitBlock, errorBlock llvm.BasicBlock, opts *EVMCompilationOpts) { + clibs := c.clibs + ctx, builder := c.ctx, c.builder + uint256Type := c.ctx.IntType(256) // If the code is unsupported, return error @@ -287,10 +406,10 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * // Check if it is host function if c.table[instr.Opcode].execute != nil { - ret := c.builder.CreateCall(c.hostFuncType, c.hostFunc, []llvm.Value{execInst, c.u64Const(uint64(instr.Opcode)), c.u64Const(uint64(instr.PC)), gasPtr, stackIdxPtr}, "") + ret := c.builder.CreateCall(c.hostFuncType, c.hostFunc, []llvm.Value{execInst, c.u64Const(uint64(instr.Opcode)), c.u64Const(uint64(instr.PC)), gasPtr, stackIdxPtr, memory}, "") if c.table[instr.Opcode].diffStack < 0 { for i := 0; i < -c.table[instr.Opcode].diffStack; i++ { - c.popStack(stack, stackIdxPtr) + popStack(ctx, builder, stack, stackIdxPtr) } } else { for i := 0; i < c.table[instr.Opcode].diffStack; i++ { @@ -303,33 +422,57 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * } switch instr.Opcode { + case MLOAD: + memSizeFn := memoryMLoadC + gasFn := gasMLoadC + c.consumeDynGasAndResizeMem(stack, stackIdxPtr, memory, gasPtr, errorCodePtr, errorBlock, memSizeFn, gasFn) + + opMloadC(ctx, builder, memory, stack, stackIdxPtr, clibs) + c.builder.CreateBr(nextBlock) + + case MSTORE: + memSizeFn := memoryMStoreC + gasFn := gasMStoreC + c.consumeDynGasAndResizeMem(stack, stackIdxPtr, memory, gasPtr, errorCodePtr, errorBlock, memSizeFn, gasFn) + + opMstoreC(ctx, builder, memory, stack, stackIdxPtr, clibs) + c.builder.CreateBr(nextBlock) + + case MSTORE8: + memSizeFn := memoryMStore8C + gasFn := gasMStore8C + c.consumeDynGasAndResizeMem(stack, stackIdxPtr, memory, gasPtr, errorCodePtr, errorBlock, memSizeFn, gasFn) + + opMstore8C(ctx, builder, memory, stack, stackIdxPtr, clibs) + c.builder.CreateBr(nextBlock) + case STOP: c.builder.CreateBr(exitBlock) case ADD: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateAdd(a, b, "add_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case MUL: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateMul(a, b, "mul_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case SUB: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateSub(a, b, "sub_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case DIV: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) zero := llvm.ConstInt(uint256Type, 0, false) isZero := c.builder.CreateICmp(llvm.IntEQ, b, zero, "div_by_zero") result := c.builder.CreateSelect(isZero, zero, c.builder.CreateUDiv(a, b, "div_result"), "div_safe") @@ -337,8 +480,8 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case SDIV: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) zero := llvm.ConstInt(uint256Type, 0, false) int256Min := llvm.ConstIntFromString(uint256Type, INT256_NEGATIVE_MIN, 10) int256Negtive1 := llvm.ConstIntFromString(uint256Type, INT256_NEGATIVE_1, 10) @@ -361,8 +504,8 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case MOD: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) zero := llvm.ConstInt(uint256Type, 0, false) isZero := c.builder.CreateICmp(llvm.IntEQ, b, zero, "mod_by_zero") result := c.builder.CreateSelect(isZero, zero, c.builder.CreateURem(a, b, "mod_result"), "mod_safe") @@ -370,8 +513,8 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case SMOD: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) zero := llvm.ConstInt(uint256Type, 0, false) isZero := c.builder.CreateICmp(llvm.IntEQ, b, zero, "smod_by_zero") result := c.builder.CreateSelect(isZero, zero, c.builder.CreateSRem(a, b, "smod_result"), "smod_safe") @@ -379,16 +522,16 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) // case EXP: - // a := c.popStack(stack, stackPtr) - // b := c.popStack(stack, stackPtr) + // a := popStack(ctx, builder,stack, stackPtr) + // b := popStack(ctx, builder,stack, stackPtr) // result := c.builder. (a, b, "exp_result") // c.pushStack(stack, stackPtr, result) // c.builder.CreateBr(nextBlock) case SIGNEXTEND: // Adapted from revm: https://github.com/bluealloy/revm/blob/fda371f73aba2c30a83c639608be78145fd1123b/crates/interpreter/src/instructions/arithmetic.rs#L89 - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) cond := c.builder.CreateICmp(llvm.IntULT, a, llvm.ConstInt(a.Type(), 31, false), "a_lt_31") // helper: signextend calculation signExtendCalc := func() llvm.Value { @@ -417,8 +560,8 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case LT, GT, SLT, SGT, EQ: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) var pred llvm.IntPredicate var name string @@ -447,7 +590,7 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case ISZERO: - a := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) zero := llvm.ConstInt(uint256Type, 0, false) cmp := c.builder.CreateICmp(llvm.IntEQ, a, zero, "iszero_cmp") result := c.builder.CreateZExt(cmp, uint256Type, "iszero_result") @@ -455,36 +598,36 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case AND: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateAnd(a, b, "and_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case OR: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateOr(a, b, "or_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case XOR: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateXor(a, b, "xor_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case NOT: - a := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) allOnes := llvm.ConstIntFromString(uint256Type, INT256_NEGATIVE_1, 10) result := c.builder.CreateXor(a, allOnes, "not_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case BYTE: - a := c.popStack(stack, stackIdxPtr) - b := c.popStack(stack, stackIdxPtr) + a := popStack(ctx, builder, stack, stackIdxPtr) + b := popStack(ctx, builder, stack, stackIdxPtr) // Constants const32 := llvm.ConstInt(uint256Type, 32, false) // upper bound for valid index const31 := llvm.ConstInt(uint256Type, 31, false) // used for position calculation @@ -511,37 +654,37 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * c.builder.CreateBr(nextBlock) case SHL: - shift := c.popStack(stack, stackIdxPtr) - value := c.popStack(stack, stackIdxPtr) + shift := popStack(ctx, builder, stack, stackIdxPtr) + value := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateShl(value, shift, "shl_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case SHR: - shift := c.popStack(stack, stackIdxPtr) - value := c.popStack(stack, stackIdxPtr) + shift := popStack(ctx, builder, stack, stackIdxPtr) + value := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateLShr(value, shift, "shr_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case SAR: - shift := c.popStack(stack, stackIdxPtr) - value := c.popStack(stack, stackIdxPtr) + shift := popStack(ctx, builder, stack, stackIdxPtr) + value := popStack(ctx, builder, stack, stackIdxPtr) result := c.builder.CreateAShr(value, shift, "sar_result") c.pushStack(stack, stackIdxPtr, result) c.builder.CreateBr(nextBlock) case POP: - c.popStack(stack, stackIdxPtr) + popStack(ctx, builder, stack, stackIdxPtr) c.builder.CreateBr(nextBlock) case JUMP: - target := c.popStack(stack, stackIdxPtr) + target := popStack(ctx, builder, stack, stackIdxPtr) c.createJump(prevInstr, target, jumpTargetPtr, errorCodePtr, analysis, dynJumpBlock, errorBlock) case JUMPI: - target := c.popStack(stack, stackIdxPtr) - condition := c.popStack(stack, stackIdxPtr) + target := popStack(ctx, builder, stack, stackIdxPtr) + condition := popStack(ctx, builder, stack, stackIdxPtr) zero := llvm.ConstInt(uint256Type, 0, false) isNonZero := c.builder.CreateICmp(llvm.IntNE, condition, zero, "jumpi_cond") @@ -562,6 +705,23 @@ func (c *EVMCompiler) compileInstructionStatic(instr EVMInstruction, prevInstr * // JUMPDEST is a no-op, charge the section gas before continue to next instruction c.builder.CreateBr(nextBlock) + case MSIZE: + msizePtr := c.builder.CreateGEP(memory.Type(), memory, []llvm.Value{ + llvm.ConstInt(c.ctx.Int32Type(), 1, false), + }, "msize_ptr") + + msizeVal := c.builder.CreateLoad(llvm.PointerType(c.ctx.Int64Type(), 0), msizePtr, "msize_val") + c.pushStack(stack, stackIdxPtr, msizeVal) + c.builder.CreateBr(nextBlock) + + case MCOPY: + memSizeFn := memoryMcopyC + gasFn := memoryCopierGasC(2) + c.consumeDynGasAndResizeMem(stack, stackIdxPtr, memory, gasPtr, errorCodePtr, errorBlock, memSizeFn, gasFn) + + opMcopyC(ctx, builder, memory, stack, stackIdxPtr, clibs) + c.builder.CreateBr(nextBlock) + default: if instr.Opcode >= PUSH0 && instr.Opcode <= PUSH32 { c.compilePushStatic(instr, stack, stackIdxPtr, nextBlock) @@ -716,6 +876,31 @@ func (c *EVMCompiler) consumeGas(gasCost uint64, gasPtr, errorCodePtr llvm.Value c.builder.CreateStore(newGas, gasPtr) } +func (c *EVMCompiler) consumeDyncGas(gasCost, gasPtr, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) { + // Load current gas used + currentGas := c.builder.CreateLoad(c.ctx.Int64Type(), gasPtr, "gas_remaining") + + // Check if we exceed gas limit + notExceedsLimit := c.builder.CreateICmp(llvm.IntULE, gasCost, currentGas, "exceeds_gas_limit") + + // Create continuation block + continueBlock := llvm.AddBasicBlock(c.builder.GetInsertBlock().Parent(), "gas_check_continue") + outOfGasBlock := llvm.AddBasicBlock(c.builder.GetInsertBlock().Parent(), "out_of_gas") + + // Branch to out-of-gas block if limit exceeded, otherwise continue + c.builder.CreateCondBr(notExceedsLimit, continueBlock, outOfGasBlock) + + c.builder.SetInsertPointAtEnd(outOfGasBlock) + // Store error code and exit + c.builder.CreateStore(c.u64Const(uint64(VMErrorCodeOutOfGas)), errorCodePtr) + c.builder.CreateBr(errorBlock) + + c.builder.SetInsertPointAtEnd(continueBlock) + // Sub gas cost and store + newGas := c.builder.CreateSub(currentGas, gasCost, "new_gas_used") + c.builder.CreateStore(newGas, gasPtr) +} + // CompileAndOptimizeStatic compiles using static analysis func (c *EVMCompiler) CompileAndOptimizeStatic(bytecode []byte, opts *EVMCompilationOpts) error { c.initailizeHostFunctions() diff --git a/compiler/contract_test.go b/compiler/contract_test.go index 62eb4f5..29740b1 100644 --- a/compiler/contract_test.go +++ b/compiler/contract_test.go @@ -34,7 +34,7 @@ func BenchmarkContracts(b *testing.B) { {name: "hash_10k", calldata: hexutil.MustDecode("0x30627b7c")}, {name: "push0_proxy"}, {name: "usdc_proxy"}, - {name: "snailtracer", calldata: hexutil.MustDecode("0x30627b7c")}, + // {name: "snailtracer", calldata: hexutil.MustDecode("0x30627b7c")}, // The following contracts are expected to revert. // revmc also skips these benchmarks: @@ -43,8 +43,8 @@ func BenchmarkContracts(b *testing.B) { // {name: "uniswap_v2_pair"}, // {name: "univ2_router"}, - {name: "poseidon_t2", calldata: hexutil.MustDecode("0x9d036e710000000000000000000000000000000000000000000000000000000000000000")}, - {name: "poseidon_t5", calldata: hexutil.MustDecode("0x8709cd8c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003")}, + // {name: "poseidon_t2", calldata: hexutil.MustDecode("0x9d036e710000000000000000000000000000000000000000000000000000000000000000")}, + // {name: "poseidon_t5", calldata: hexutil.MustDecode("0x8709cd8c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003")}, } for _, tc := range testCases { bytecode, err := readHexFile(fmt.Sprintf("../testdata/ContractHex/%v.rt.hex", tc.name)) diff --git a/compiler/eips.go b/compiler/eips.go index 6ea2e14..5cbbe76 100644 --- a/compiler/eips.go +++ b/compiler/eips.go @@ -251,27 +251,27 @@ func enable3860(jt *JumpTable) { // https://eips.ethereum.org/EIPS/eip-5656 func enable5656(jt *JumpTable) { jt[MCOPY] = &operation{ - execute: opMcopy, + // execute: opMcopy, constantGas: GasFastestStep, - dynamicGas: gasMcopy, - minStack: minStack(3, 0), - diffStack: diffStack(3, 0), - memorySize: memoryMcopy, + // dynamicGas: gasMcopy, + minStack: minStack(3, 0), + diffStack: diffStack(3, 0), + // memorySize: memoryMcopy, } } // opMcopy implements the MCOPY opcode (https://eips.ethereum.org/EIPS/eip-5656) -func opMcopy(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { - var ( - dst = scope.Stack.pop() - src = scope.Stack.pop() - length = scope.Stack.pop() - ) - // These values are checked for overflow during memory expansion calculation - // (the memorySize function on the opcode). - scope.Memory.Copy(dst.Uint64(), src.Uint64(), length.Uint64()) - return nil, nil -} +// func opMcopy(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { +// var ( +// dst = scope.Stack.pop() +// src = scope.Stack.pop() +// length = scope.Stack.pop() +// ) +// // These values are checked for overflow during memory expansion calculation +// // (the memorySize function on the opcode). +// scope.Memory.Copy(dst.Uint64(), src.Uint64(), length.Uint64()) +// return nil, nil +// } // opBlobHash implements the BLOBHASH opcode func opBlobHash(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { diff --git a/compiler/executor.go b/compiler/executor.go index 5902d67..85c8273 100644 --- a/compiler/executor.go +++ b/compiler/executor.go @@ -1,13 +1,13 @@ package compiler // #include -// typedef void (*func)(uint64_t inst, void* stack, void* code, uint64_t gas, void* output); -// static void execute(uint64_t f, uint64_t inst, void* stack, void* code, uint64_t gas, void* output) { ((func)f)(inst, stack, code, gas, output); } +// #include "./lib/mem.h" +// typedef Memory* (*func)(uint64_t inst, void* stack, void* code, uint64_t gas, void* output); +// static Memory* execute(uint64_t f, uint64_t inst, void* stack, void* code, uint64_t gas, void* output) { return ((func)f)(inst, stack, code, gas, output); } import "C" import ( "encoding/binary" "fmt" - "runtime/cgo" "unsafe" "github.com/ethereum/go-ethereum/common" @@ -40,6 +40,7 @@ type EVMExecutor struct { readOnly bool // Whether to throw on stateful modifications returnData []byte // Last CALL's return data for subsequent reuse + keepMemory bool // Whether to free memory after callNativeFunction } // ScopeContext is the current context of the call. @@ -101,12 +102,12 @@ func (e *EVMExecutor) Run(contract *Contract, input []byte, readOnly bool) (ret prevCallContext := e.callContext // Prepare execution environment - memory := NewMemory() + memory := &Memory{} stack := newstack() callContext := &ScopeContext{ Contract: contract, - Memory: memory, Stack: stack, + Memory: memory, } e.callContext = callContext @@ -124,12 +125,19 @@ func (e *EVMExecutor) Run(contract *Contract, input []byte, readOnly bool) (ret inst := createExecutionInstance(e) defer removeExecutionInstance(inst) gas := contract.Gas + // TODO: passing uint256 pointer as stack to compiled code only works for little-endianess machine! - errorCode, gasRemainingResult, stackDepth := e.callNativeFunction(funcPtr, inst, unsafe.Pointer(&stack.data[0][0]), gas) + errorCode, gasRemainingResult, stackDepth, cmem := e.callNativeFunction(funcPtr, inst, unsafe.Pointer(&stack.data[0][0]), gas) if errorCode == int64(VMErrorCodeStopToken) { errorCode = int64(VMExecutionSuccess) // clear stop token error } + if cmem != nil { + memory.cmem = cmem + memory.store = unsafe.Slice((*byte)(cmem.store), cmem.len) + memory.lastGasCost = uint64(cmem.lastGasCost) + } + gasRemaining := uint64(gasRemainingResult) if errorCode != int64(VMExecutionSuccess) { @@ -166,11 +174,11 @@ func (e *EVMExecutor) Run(contract *Contract, input []byte, readOnly bool) (ret } // Helper function to call native function pointer (requires CGO) -func (e *EVMExecutor) callNativeFunction(funcPtr FuncPtr, inst cgo.Handle, stack unsafe.Pointer, gas uint64) (int64, int64, int64) { +func (e *EVMExecutor) callNativeFunction(funcPtr FuncPtr, inst uint64, stack unsafe.Pointer, gas uint64) (int64, int64, int64, *C.Memory) { var output [OUTPUT_SIZE]byte - C.execute(C.uint64_t(funcPtr), C.uintptr_t(inst), stack, nil, C.uint64_t(gas), unsafe.Pointer(&output[0])) + cmem := C.execute(C.uint64_t(funcPtr), C.uintptr_t(inst), stack, nil, C.uint64_t(gas), unsafe.Pointer(&output[0])) errorCode := binary.LittleEndian.Uint64(output[OUTPUT_IDX_ERROR_CODE*8:]) gasUsed := binary.LittleEndian.Uint64(output[OUTPUT_IDX_GAS*8:]) stackDepth := binary.LittleEndian.Uint64(output[OUTPUT_IDX_STACK_DEPTH*8:]) - return int64(errorCode), int64(gasUsed), int64(stackDepth) + return int64(errorCode), int64(gasUsed), int64(stackDepth), cmem } diff --git a/compiler/executor_engine.go b/compiler/executor_engine.go index 0a5e924..78b8d49 100644 --- a/compiler/executor_engine.go +++ b/compiler/executor_engine.go @@ -1,7 +1,8 @@ package compiler // #include -// extern int64_t callHostFunc(uintptr_t inst, uint64_t opcode, uint64_t pc, uint64_t* gas, uint32_t* stackIdx); +// #include "lib/mem.h" +// extern int64_t callHostFunc(uintptr_t inst, uint64_t opcode, uint64_t pc, uint64_t* gas, uint64_t* stackIdx, Memory* m); import "C" import ( "fmt" @@ -20,7 +21,7 @@ type CompiledLoader interface { type NativeEngine struct { engine llvm.ExecutionEngine compiledLoader CompiledLoader - loadedContracts map[common.Hash]bool + loadedContracts map[common.Hash]FuncPtr } var _ NativeLoader = (*NativeEngine)(nil) @@ -37,10 +38,12 @@ func NewNativeEngine(loader CompiledLoader) *NativeEngine { nativeEngine := &NativeEngine{ engine: engine, compiledLoader: loader, - loadedContracts: map[common.Hash]bool{}, + loadedContracts: map[common.Hash]FuncPtr{}, } _, hostFunc := initializeHostFunction(ctx, module) engine.AddGlobalMapping(hostFunc, unsafe.Pointer(C.callHostFunc)) + clibpath := "./lib/clib.o" + engine.AddObjectFileByFilename(clibpath) return nativeEngine } @@ -50,18 +53,19 @@ func (n *NativeEngine) CompiledFuncPtr(codeHash common.Hash, chainRules params.R return FuncPtr(0), "", fmt.Errorf("codeHash:%v is nil", codeHash) } var version CompiledCodeVersion - if _, ok := n.loadedContracts[codeHash]; !ok { + funcPtr, ok := n.loadedContracts[codeHash] + if !ok { compiledCode, ver, err := n.compiledLoader.LoadCompiledCode(codeHash, chainRules, extraEips) if err != nil { return 0, version, err } n.engine.AddObjectFileFromBuffer(compiledCode) - n.loadedContracts[codeHash] = true + ptr := n.engine.GetFunctionAddress(GetContractFunction(codeHash)) + funcPtr = FuncPtr(ptr) + n.loadedContracts[codeHash] = funcPtr version = ver } - funcPtr := n.engine.GetFunctionAddress(GetContractFunction(codeHash)) - if funcPtr == 0 { return FuncPtr(0), "", fmt.Errorf("compiled contract code not found") } diff --git a/compiler/gas_table_c.go b/compiler/gas_table_c.go new file mode 100644 index 0000000..26241c1 --- /dev/null +++ b/compiler/gas_table_c.go @@ -0,0 +1,84 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package compiler + +import ( + "tinygo.org/x/go-llvm" +) + +type ( + gasFuncC func(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, mem, newMemSize, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock, clibs map[string]CHostFunc) (gas llvm.Value) + memorySizeFuncC func(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) (size llvm.Value) +) + +var ( + gasMLoadC = memoryGasCostC + gasMStore8C = memoryGasCostC + gasMStoreC = memoryGasCostC +) + +func (c *EVMCompiler) consumeDynGasAndResizeMem(stack, stackIdxPtr, memory, gasPtr, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock, memSizeFn memorySizeFuncC, gasFn gasFuncC) { + clibs := c.clibs + ctx, builder := c.ctx, c.builder + memSize := memSizeFn(ctx, builder, stack, stackIdxPtr, errorCodePtr, errorBlock) + + // memory is expanded in words of 32 bytes. Gas + // is also calculated in words. + towordsizeFn := clibs[TO_WORD_SIZE] + memWordSize := builder.CreateCall(towordsizeFn.fnType, towordsizeFn.fn, []llvm.Value{memSize}, "call_mem_wordsize") + const32 := u64Const(ctx, 32) + newNemSize := c.safeMul(memWordSize, const32, errorCodePtr, errorBlock) + + // Consume the gas and return an error if not enough gas is available. + dynamicCost := gasFn(ctx, builder, stack, stackIdxPtr, memory, newNemSize, errorCodePtr, errorBlock, clibs) + c.consumeDyncGas(dynamicCost, gasPtr, errorCodePtr, errorBlock) + + // resize memory + memResizeFn := clibs[MEMORY_RESIZE] + builder.CreateCall(memResizeFn.fnType, memResizeFn.fn, []llvm.Value{memory, newNemSize}, "") +} + +func memoryGasCostC(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, mem, newMemSize, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock, clibs map[string]CHostFunc) llvm.Value { + i64 := ctx.Int64Type() + ptrToDynamicCost := builder.CreateAlloca(i64, "dyn_cost_ptr") + memDynGas := clibs[MEMORY_GAS_COST] + errCode := builder.CreateCall(memDynGas.fnType, memDynGas.fn, []llvm.Value{mem, newMemSize, ptrToDynamicCost}, "call_mem_gas_cost") + // Branch on overflow + zero := llvm.ConstInt(ctx.Int32Type(), 0, false) + hasOverflow := builder.CreateICmp(llvm.IntNE, errCode, zero, "u64_overflow") + checkU64Overflow(ctx, builder, hasOverflow, errorCodePtr, errorBlock) + + return builder.CreateLoad(i64, ptrToDynamicCost, "dyn_cost") +} + +func memoryCopierGasC(stackpos int) gasFuncC { + return func(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, mem, newMemSize, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock, clibs map[string]CHostFunc) llvm.Value { + words := peekStack(ctx, builder, stack, stackIdxPtr, 2) + words64 := uint64WithOverflow(ctx, builder, words, errorCodePtr, errorBlock) + + i64 := ctx.Int64Type() + ptrToDynamicCost := builder.CreateAlloca(i64, "dyn_cost_ptr") + memCpGasFn := clibs[MEM_CP_GAS_COST] + errCode := builder.CreateCall(memCpGasFn.fnType, memCpGasFn.fn, []llvm.Value{mem, words64, newMemSize, ptrToDynamicCost}, "call_memcp_gas_cost") + + zero := llvm.ConstInt(ctx.Int32Type(), 0, false) + hasOverflow := builder.CreateICmp(llvm.IntNE, errCode, zero, "u64_overflow") + checkU64Overflow(ctx, builder, hasOverflow, errorCodePtr, errorBlock) + + return builder.CreateLoad(i64, ptrToDynamicCost, "dyn_cost") + } +} diff --git a/compiler/host.go b/compiler/host.go index 32b5c8f..d6e9a2d 100644 --- a/compiler/host.go +++ b/compiler/host.go @@ -1,34 +1,53 @@ package compiler // #include -// extern int64_t callHostFunc(uintptr_t inst, uint64_t opcode, uint64_t pc, uint64_t* gas, uint64_t* stackIdx); +// #include "lib/mem.h" +// #include +// #include +// #include +// extern int64_t callHostFunc(uintptr_t inst, uint64_t opcode, uint64_t pc, uint64_t* gas, uint64_t* stackIdx, Memory* m); import "C" import ( "fmt" - "runtime/cgo" + "sync" "github.com/ethereum/go-ethereum/common/math" "tinygo.org/x/go-llvm" ) -type EVMHost interface { - GetHostFunc(opcode OpCode) HostFunc +// Host function handling +// - Find the EVMCompiler (or EVMExecutionInstance) from a global map +// This avoid using unsafe pointer to recoever the execution instance +var executionInstanceMap = make(map[uint64]*EVMExecutor) +var executionInstanceLock sync.Mutex +var executionInstanceCounter uint64 + +func createExecutionInstance(inst *EVMExecutor) uint64 { + executionInstanceLock.Lock() + defer executionInstanceLock.Unlock() + executionInstanceCounter++ + counter := executionInstanceCounter + executionInstanceMap[counter] = inst + return counter } -func createExecutionInstance(inst *EVMExecutor) cgo.Handle { - return cgo.NewHandle(inst) +func removeExecutionInstance(instId uint64) { + executionInstanceLock.Lock() + defer executionInstanceLock.Unlock() + delete(executionInstanceMap, instId) } -func removeExecutionInstance(h cgo.Handle) { - h.Delete() +func getExecutionInstance(instId uint64) (inst *EVMExecutor) { + executionInstanceLock.Lock() + defer executionInstanceLock.Unlock() + return executionInstanceMap[instId] } //export callHostFunc -func callHostFunc(inst C.uintptr_t, opcode C.uint64_t, pcC C.uint64_t, gas *C.uint64_t, stackIdx *C.uint64_t) C.int64_t { - h := cgo.Handle(inst) - e, ok := h.Value().(*EVMExecutor) - if !ok || e == nil { - panic("execution instance not found or type mismatch") +func callHostFunc(inst C.uintptr_t, opcode C.uint64_t, pcC C.uint64_t, gas, stackIdx *C.uint64_t, mem *C.Memory) C.int64_t { + e := getExecutionInstance(uint64(inst)) + if e == nil { + panic("execution instance not found") } instr := e.table[OpCode(opcode)] @@ -43,6 +62,10 @@ func callHostFunc(inst C.uintptr_t, opcode C.uint64_t, pcC C.uint64_t, gas *C.ui // Set the gas in the contract, which may be used in dynamic gas. e.callContext.Contract.Gas = uint64(*gas) + e.callContext.Memory.cmem = mem + e.callContext.Memory.lastGasCost = uint64(mem.lastGasCost) + defer func() { mem.lastGasCost = C.uint64_t(e.callContext.Memory.lastGasCost) }() + // All ops with a dynamic memory usage also has a dynamic gas cost. var memorySize uint64 if instr.dynamicGas != nil { @@ -75,7 +98,9 @@ func callHostFunc(inst C.uintptr_t, opcode C.uint64_t, pcC C.uint64_t, gas *C.ui } } - e.callContext.Memory.Resize(memorySize) + if memorySize > 0 { + e.callContext.Memory.Resize(memorySize) + } // pc is already handled in compiler, so there is no need to pass pc back to native. pc := uint64(pcC) @@ -88,10 +113,90 @@ func callHostFunc(inst C.uintptr_t, opcode C.uint64_t, pcC C.uint64_t, gas *C.ui return C.int64_t(errCode) } -func initializeHostFunction(ctx llvm.Context, module llvm.Module) (hostFuncType llvm.Type, hostFunc llvm.Value) { +// cgoHandle is less efficient than cgo with register. +// func createExecutionInstance(inst *EVMExecutor) cgo.Handle { +// return cgo.NewHandle(inst) +// } + +// func removeExecutionInstance(h cgo.Handle) { +// h.Delete() +// } + +// //export callHostFunc +// func callHostFunc(inst C.uintptr_t, opcode C.uint64_t, pcC C.uint64_t, gas, stackIdx *C.uint64_t, mem *C.Memory) C.int64_t { +// h := cgo.Handle(inst) +// e, ok := h.Value().(*EVMExecutor) +// if !ok || e == nil { +// panic("execution instance not found or type mismatch") +// } + +// instr := e.table[OpCode(opcode)] +// f := instr.execute +// if f == nil { +// panic(fmt.Sprintf("host function for opcode %d not found", opcode)) +// } + +// // Construct stack for inputs +// stack := e.callContext.Stack +// stack.resetLen(int(*stackIdx)) + +// // Set the gas in the contract, which may be used in dynamic gas. +// e.callContext.Contract.Gas = uint64(*gas) +// e.callContext.Memory.cmem = mem +// e.callContext.Memory.lastGasCost = uint64(mem.lastGasCost) +// defer func() { mem.lastGasCost = C.uint64_t(e.callContext.Memory.lastGasCost) }() + +// // All ops with a dynamic memory usage also has a dynamic gas cost. +// var memorySize uint64 +// if instr.dynamicGas != nil { +// // 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 instr.memorySize != nil { +// memSize, overflow := instr.memorySize(stack) +// if overflow { +// return C.int64_t(VMErrorCodeGasUintOverflow) +// } +// // memory is expanded in words of 32 bytes. Gas +// // is also calculated in words. +// if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow { +// return C.int64_t(VMErrorCodeGasUintOverflow) +// } +// } +// // 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 +// dynamicCost, err := instr.dynamicGas(e.evm, e.callContext.Contract, stack, e.callContext.Memory, memorySize) +// if err != nil { +// return C.int64_t(VMErrorCodeOutOfGas) +// } +// // for tracing: this gas consumption event is emitted below in the debug section. +// if e.callContext.Contract.Gas < dynamicCost { +// return C.int64_t(VMErrorCodeOutOfGas) +// } else { +// e.callContext.Contract.Gas -= dynamicCost +// } +// } + +// if memorySize > 0 { +// e.callContext.Memory.Resize(memorySize) +// } + +// // pc is already handled in compiler, so there is no need to pass pc back to native. +// pc := uint64(pcC) +// ret, err := f(&pc, e, e.callContext) +// errCode := vmErrorCodeFromErr(err) +// // Pass ret to parent context +// e.evm.internalRet = ret +// *gas = C.uint64_t(e.callContext.Contract.Gas) + +// return C.int64_t(errCode) +// } + +func initializeHostFunction(ctx llvm.Context, module llvm.Module) (llvm.Type, llvm.Value) { i64ptr := llvm.PointerType(ctx.Int64Type(), 0) - hostFuncType = llvm.FunctionType(ctx.Int64Type(), []llvm.Type{ctx.Int64Type(), ctx.Int64Type(), ctx.Int64Type(), i64ptr, i64ptr}, false) - hostFunc = llvm.AddFunction(module, "host_func", hostFuncType) + hostFuncType := llvm.FunctionType(ctx.Int64Type(), []llvm.Type{ctx.Int64Type(), ctx.Int64Type(), ctx.Int64Type(), i64ptr, i64ptr, i64ptr}, false) + hostFunc := llvm.AddFunction(module, "host_func", hostFuncType) // Set the host function's linkage to External. // This prevents LLVM from internalizing it during LTO/IPO passes, // ensuring it remains visible outside the module. diff --git a/compiler/instructions.go b/compiler/instructions.go index 053311e..32e28c7 100644 --- a/compiler/instructions.go +++ b/compiler/instructions.go @@ -491,18 +491,18 @@ func opGasLimit(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]by // return nil, nil // } -func opMload(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { - v := scope.Stack.peek() - offset := v.Uint64() - v.SetBytes(scope.Memory.GetPtr(offset, 32)) - return nil, nil -} +// func opMload(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { +// v := scope.Stack.peek() +// offset := v.Uint64() +// v.SetBytes(scope.Memory.GetPtr(offset, 32)) +// return nil, nil +// } -func opMstore(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { - mStart, val := scope.Stack.pop(), scope.Stack.pop() - scope.Memory.Set32(mStart.Uint64(), &val) - return nil, nil -} +// func opMstore(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { +// mStart, val := scope.Stack.pop(), scope.Stack.pop() +// scope.Memory.Set32(mStart.Uint64(), &val) +// return nil, nil +// } func opMstore8(pc *uint64, interpreter *EVMExecutor, scope *ScopeContext) ([]byte, error) { off, val := scope.Stack.pop(), scope.Stack.pop() diff --git a/compiler/instructions_c.go b/compiler/instructions_c.go new file mode 100644 index 0000000..c903831 --- /dev/null +++ b/compiler/instructions_c.go @@ -0,0 +1,107 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package compiler + +import "tinygo.org/x/go-llvm" + +func opMstoreC(ctx llvm.Context, builder llvm.Builder, memory, stack, stackIdxPtr llvm.Value, clibs map[string]CHostFunc) { + bswapFn := clibs[BIT_SWAP256] + // convert value from machine's endian(little-endian) to big-endian + offset := popStack(ctx, builder, stack, stackIdxPtr) + value := popStack(ctx, builder, stack, stackIdxPtr) + + value = builder.CreateCall(bswapFn.fnType, bswapFn.fn, []llvm.Value{value}, "mstore_val_be") + + // allocate stack space for int256 + valuePtr := builder.CreateAlloca(ctx.IntType(256), "int256_ptr") + builder.CreateStore(value, valuePtr) + valueBytesPtr := builder.CreateBitCast(valuePtr, llvm.PointerType(ctx.Int8Type(), 0), "value_bytes_ptr") + + memSetFn := clibs[MEMORY_SET] + offset64 := builder.CreateTrunc(offset, ctx.Int64Type(), "offset64") + size64 := u64Const(ctx, 32) + builder.CreateCall(memSetFn.fnType, memSetFn.fn, []llvm.Value{ + memory, // Memory* + offset64, // uint64_t offset + size64, // uint64_t size + valueBytesPtr, // const uint8_t* value + }, "") +} + +func opMstore8C(ctx llvm.Context, builder llvm.Builder, memory, stack, stackIdxPtr llvm.Value, clibs map[string]CHostFunc) { + offset := popStack(ctx, builder, stack, stackIdxPtr) + value := popStack(ctx, builder, stack, stackIdxPtr) + + valueBytesPtr := builder.CreateAlloca(ctx.Int8Type(), "int8_ptr") + builder.CreateStore(value, valueBytesPtr) + + memSetFn := clibs[MEMORY_SET] + offset64 := builder.CreateTrunc(offset, ctx.Int64Type(), "offset64") + size64 := u64Const(ctx, 1) + builder.CreateCall(memSetFn.fnType, memSetFn.fn, []llvm.Value{ + memory, // Memory* + offset64, // uint64_t offset + size64, // uint64_t size + valueBytesPtr, // const uint8_t* value + }, "") +} + +func opMloadC(ctx llvm.Context, builder llvm.Builder, memory, stack, stackIdxPtr llvm.Value, clibs map[string]CHostFunc) { + // Obtain stack value and store new stack idx + stackElem := peekStackPtr(ctx, builder, stack, stackIdxPtr) + + offset := builder.CreateLoad(ctx.IntType(256), stackElem, "mload_stack_value") + offset64 := builder.CreateTrunc(offset, ctx.Int64Type(), "offset64") + + // Get pointer to "store" field (index 0 in struct Memory) + storePtr := builder.CreateGEP(memory.Type(), memory, []llvm.Value{ + // offset64, + llvm.ConstInt(ctx.Int32Type(), 0, false), + }, "mload_store_ptr") + + // Load the actual `uint8_t*` + storeVal := builder.CreateLoad(llvm.PointerType(ctx.Int8Type(), 0), storePtr, "mload_store_val") + + // GEP into store[offset64] + bytePtr := builder.CreateGEP(ctx.Int8Type(), storeVal, []llvm.Value{offset64}, "mload_byte_ptr") + + // Step 4: Cast to i256* (assuming memory is large enough and properly aligned) + i256Ptr := builder.CreateBitCast(bytePtr, llvm.PointerType(ctx.IntType(256), 0), "mload_i256_ptr") + + // Load 256-bit big-endian value + value := builder.CreateLoad(ctx.IntType(256), i256Ptr, "mload_val") + // convert value to machine's endian(little-endian) + bswapsFn := clibs[BIT_SWAP256] + value = builder.CreateCall(bswapsFn.fnType, bswapsFn.fn, []llvm.Value{value}, "call_bitswap256") + + builder.CreateStore(value, stackElem) +} + +func opMcopyC(ctx llvm.Context, builder llvm.Builder, memory, stack, stackIdxPtr llvm.Value, clibs map[string]CHostFunc) { + dst := popStack(ctx, builder, stack, stackIdxPtr) + src := popStack(ctx, builder, stack, stackIdxPtr) + length := popStack(ctx, builder, stack, stackIdxPtr) + + i64 := ctx.Int64Type() + dst64 := builder.CreateTrunc(dst, i64, "dst_64") + src64 := builder.CreateTrunc(src, i64, "src_64") + length64 := builder.CreateTrunc(length, i64, "length_64") + // These values are checked for overflow during memory expansion calculation + // (the memorySize function on the opcode). + memCpFn := clibs[MEMORY_COPY] + builder.CreateCall(memCpFn.fnType, memCpFn.fn, []llvm.Value{memory, dst64, src64, length64}, "") +} diff --git a/compiler/jit_loader.go b/compiler/jit_loader.go index cd7c54b..d747efa 100644 --- a/compiler/jit_loader.go +++ b/compiler/jit_loader.go @@ -26,6 +26,9 @@ func (l *JITLoader) LoadCompiledCode(hash common.Hash, chainRules params.Rules, if err != nil || len(code) == 0 { return []byte{}, "", fmt.Errorf("code for codeHash:%v not found", hash) } - c.CompileAndOptimizeWithOpts(code, l.copts) + err = c.CompileAndOptimizeWithOpts(code, l.copts) + if err != nil { + return []byte{}, "", fmt.Errorf("compilation failed\n:%v", err) + } return c.GetCompiledCode(), "", nil } diff --git a/compiler/jump_table.go b/compiler/jump_table.go index 5285ef2..fad9869 100644 --- a/compiler/jump_table.go +++ b/compiler/jump_table.go @@ -551,28 +551,28 @@ func newFrontierInstructionSet() JumpTable { diffStack: diffStack(1, 0), }, MLOAD: { - execute: opMload, + // execute: opMload, constantGas: GasFastestStep, - dynamicGas: gasMLoad, - minStack: minStack(1, 1), - diffStack: diffStack(1, 1), - memorySize: memoryMLoad, + // dynamicGas: gasMLoad, + minStack: minStack(1, 1), + diffStack: diffStack(1, 1), + // memorySize: memoryMLoad, }, MSTORE: { - execute: opMstore, + // execute: opMstore, constantGas: GasFastestStep, - dynamicGas: gasMStore, - minStack: minStack(2, 0), - diffStack: diffStack(2, 0), - memorySize: memoryMStore, + // dynamicGas: gasMStore, + minStack: minStack(2, 0), + diffStack: diffStack(2, 0), + // memorySize: memoryMStore, }, MSTORE8: { - execute: opMstore8, + // execute: opMstore8, constantGas: GasFastestStep, - dynamicGas: gasMStore8, - memorySize: memoryMStore8, - minStack: minStack(2, 0), - diffStack: diffStack(2, 0), + // dynamicGas: gasMStore8, + // memorySize: memoryMStore8, + minStack: minStack(2, 0), + diffStack: diffStack(2, 0), }, SLOAD: { execute: opSload, @@ -605,7 +605,7 @@ func newFrontierInstructionSet() JumpTable { diffStack: diffStack(0, 1), }, MSIZE: { - execute: opMsize, + // execute: opMsize, constantGas: GasQuickStep, minStack: minStack(0, 1), diffStack: diffStack(0, 1), diff --git a/compiler/lib/clib.c b/compiler/lib/clib.c new file mode 100644 index 0000000..0c24721 --- /dev/null +++ b/compiler/lib/clib.c @@ -0,0 +1,2 @@ +#include "gastable.c" +#include "mem.c" \ No newline at end of file diff --git a/compiler/lib/gastable.c b/compiler/lib/gastable.c new file mode 100644 index 0000000..2929974 --- /dev/null +++ b/compiler/lib/gastable.c @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include +#include "mem.h" +#include "protocol_params.h" + +#define MEMORY_WORD_SIZE 32 +#define QUAD_COEFF_DIV 512 // params.QuadCoeffDiv +#define MAX_MEM_SIZE 0x1FFFFFFFE0ULL +#define VMErrorCode_GasUintOverflow 11 + +// Round byte size to words of 32 bytes +uint64_t to_word_size(uint64_t size) +{ + if (size > UINT64_MAX - 31) + { + return UINT64_MAX / 32 + 1; + } + return (size + MEMORY_WORD_SIZE - 1) / MEMORY_WORD_SIZE; +} + +static inline uint64_t safe_mul(uint64_t x, uint64_t y, bool *overflow) +{ + __uint128_t product = (__uint128_t)x * (__uint128_t)y; + uint64_t lo = (uint64_t)product; + uint64_t hi = (uint64_t)(product >> 64); + + *overflow = (hi != 0); + return lo; +} + +static inline uint64_t safe_add(uint64_t x, uint64_t y, bool *overflow) +{ + uint64_t sum = x + y; + *overflow = (sum < x); // true if overflow + return sum; +} + +// Gas calculation for memory expansion +int memory_gas_cost(Memory *mem, uint64_t newMemSize, uint64_t *outFee) +{ + if (!mem || !outFee) + { + fprintf(stderr, "memory_gas_cost: memory or outfee is nil\n"); + abort(); + } + + if (newMemSize == 0) + { + *outFee = 0; + return 0; + } + + if (newMemSize > MAX_MEM_SIZE) + { + return VMErrorCode_GasUintOverflow; // overflow + } + + uint64_t newMemSizeWords = to_word_size(newMemSize); + newMemSize = newMemSizeWords * MEMORY_WORD_SIZE; + + if (newMemSize > mem->len) + { + // quadratic gas calculation + uint64_t square = newMemSizeWords * newMemSizeWords; + uint64_t linCoef = newMemSizeWords * MEMORY_GAS; + uint64_t quadCoef = square / QUAD_COEFF_DIV; + uint64_t newTotalFee = linCoef + quadCoef; + + uint64_t fee = newTotalFee - mem->lastGasCost; + mem->lastGasCost = newTotalFee; + + *outFee = fee; + return 0; + } + + *outFee = 0; + return 0; +} + +int memory_copier_gas(Memory *mem, uint64_t words, uint64_t memorySize, uint64_t *outGas) +{ + uint64_t gas = 0; + + // 1. Gas for expanding memory + int errCode = memory_gas_cost(mem, memorySize, outGas); + if (errCode != 0) + { + return errCode; + } + + // 2. Safe multiply + uint64_t wsize = to_word_size(words); + bool overflow = false; + words = safe_mul(wsize, COPY_GAS, &overflow); + if (overflow) + { + return VMErrorCode_GasUintOverflow; + } + *outGas = safe_add(*outGas, words, &overflow); + if (overflow) + { + return VMErrorCode_GasUintOverflow; + } + return 0; +} \ No newline at end of file diff --git a/compiler/lib/mem.c b/compiler/lib/mem.c new file mode 100644 index 0000000..908f8ac --- /dev/null +++ b/compiler/lib/mem.c @@ -0,0 +1,162 @@ +#include "mem.h" +#include +#include +#include + +Memory *memory_new(void) +{ + Memory *m = (Memory *)malloc(sizeof(Memory)); + if (!m) + return NULL; + m->store = NULL; + m->len = 0; + m->cap = 0; + m->lastGasCost = 0; + return m; +} + +void memory_free(Memory *m) +{ + if (!m) + return; + if (m->store) + free(m->store); + free(m); +} + +static int ensure_capacity(Memory *m, size_t newlen) +{ + if (newlen <= m->cap) + { + if (newlen > m->len) + m->len = newlen; + return 1; + } + size_t newcap = m->cap ? m->cap : 1; + while (newcap < newlen) + newcap <<= 1; + uint8_t *newbuf = (uint8_t *)realloc(m->store, newcap); + if (!newbuf) + return 0; + + // zero the newly allocated region for safety + if (newcap > m->len) + { + memset(newbuf + m->len, 0, newcap - m->len); + } + + // here we left padding zeros to memory due to memory.store is littleEndian + // uint64_t offset = newlen - m->len; + // memcpy(newbuf + offset, newbuf, m->len); + // if (newcap > m->len) + // { + // memset(newbuf, 0, offset); + // } + m->store = newbuf; + m->cap = newcap; + m->len = newlen; + return 1; +} + +void memory_resize(Memory *m, uint64_t size) +{ + if (!m) + return; + if ((size_t)m->len < (size_t)size) + { + if (!ensure_capacity(m, (size_t)size)) + { + fprintf(stderr, "memory_resize: allocation failed\n"); + abort(); + } + } +} + +// memory and value is big-endian, offset is also for big-endian +void memory_set(Memory *m, uint64_t offset, uint64_t size, const uint8_t *value) +{ + if (!m) + return; + if (size == 0) + return; + size_t end = (size_t)offset + (size_t)size; + if ((size_t)m->len < end) + { + fprintf(stderr, "memory_set: out of bounds (did you Resize first?)\n"); + abort(); + } + memcpy(m->store + offset, value, (size_t)size); +} + +void memory_set32(Memory *m, uint64_t offset, const uint8_t val32[32]) +{ + if (!m) + return; + size_t end = (size_t)offset + 32; + if ((size_t)m->len < end) + { + fprintf(stderr, "memory_set32: out of bounds\n"); + abort(); + } + memcpy(m->store + offset, val32, 32); +} + +uint8_t *memory_getptr(Memory *m, uint64_t offset, uint64_t size) +{ + if (!m) + return NULL; + if (size == 0) + return NULL; + size_t end = (size_t)offset + (size_t)size; + if ((size_t)m->len < end) + { + fprintf(stderr, "memory_getptr: out of bounds\n"); + abort(); + } + return m->store + offset; +} + +uint8_t *memory_getcopy(Memory *m, uint64_t offset, uint64_t size) +{ + if (!m) + return NULL; + if (size == 0) + return NULL; + size_t end = (size_t)offset + (size_t)size; + if ((size_t)m->len < end) + { + fprintf(stderr, "memory_getcopy: out of bounds\n"); + abort(); + } + uint8_t *out = (uint8_t *)malloc((size_t)size); + if (!out) + { + fprintf(stderr, "memory_getcopy: malloc failed\n"); + abort(); + } + memcpy(out, m->store + offset, (size_t)size); + return out; +} + +uint64_t memory_len(Memory *m) +{ + if (!m) + return 0; + return (uint64_t)m->len; +} + +void memory_copy(Memory *m, uint64_t dst, uint64_t src, uint64_t len) +{ + if (!m) + return; + if (len == 0) + return; + size_t end1 = (size_t)dst + (size_t)len; + size_t end2 = (size_t)src + (size_t)len; + if ((size_t)m->len < end1 || (size_t)m->len < end2) + { + fprintf(stderr, "memory_copy: out of bounds\n"); + abort(); + } + memmove(m->store + dst, m->store + src, (size_t)len); +} diff --git a/compiler/lib/mem.h b/compiler/lib/mem.h new file mode 100644 index 0000000..5fb83ff --- /dev/null +++ b/compiler/lib/mem.h @@ -0,0 +1,43 @@ +#ifndef MEM_H +#define MEM_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint8_t *store; + size_t len; + size_t cap; + uint64_t lastGasCost; +} Memory; + +// lifecycle +Memory* memory_new(void); +void memory_free(Memory* m); + +// sizing / access +void memory_resize(Memory* m, uint64_t size); +void memory_set(Memory* m, uint64_t offset, uint64_t size, const uint8_t* value); +void memory_set32(Memory* m, uint64_t offset, const uint8_t val32[32]); + +// returns pointer into internal buffer (DO NOT free) +uint8_t* memory_getptr(Memory* m, uint64_t offset, uint64_t size); + +// returns newly allocated copy (caller must free) +uint8_t* memory_getcopy(Memory* m, uint64_t offset, uint64_t size); + +// len +uint64_t memory_len(Memory* m); + +// copy (may overlap) +void memory_copy(Memory* m, uint64_t dst, uint64_t src, uint64_t len); + +#ifdef __cplusplus +} +#endif + +#endif // MEM_H diff --git a/compiler/lib/protocol_params.h b/compiler/lib/protocol_params.h new file mode 100644 index 0000000..5b25988 --- /dev/null +++ b/compiler/lib/protocol_params.h @@ -0,0 +1,2 @@ +#define MEMORY_GAS 3 // params.MemoryGas +#define COPY_GAS 3 // params.CopyGas \ No newline at end of file diff --git a/compiler/lib/test_mem.c b/compiler/lib/test_mem.c new file mode 100644 index 0000000..b05ddbe --- /dev/null +++ b/compiler/lib/test_mem.c @@ -0,0 +1,105 @@ +#include "mem.h" +#include +#include +#include +#include + +#define CHECK(cond, msg) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", msg); \ + return 1; \ + } \ +} while(0) + +int test_new_free() { + Memory* m = memory_new(); + CHECK(m != NULL, "memory_new returned NULL"); + CHECK(memory_len(m) == 0, "new memory len should be 0"); + memory_free(m); + return 0; +} + +int test_resize() { + Memory* m = memory_new(); + memory_resize(m, 10); + CHECK(memory_len(m) == 10, "memory_len after resize 10"); + memory_resize(m, 5); + CHECK(memory_len(m) == 5, "memory_len after shrink 5"); + memory_free(m); + return 0; +} + +int test_set_getptr_getcopy() { + Memory* m = memory_new(); + memory_resize(m, 8); + + uint8_t data[8] = {1,2,3,4,5,6,7,8}; + memory_set(m, 0, 8, data); + + uint8_t* ptr = memory_getptr(m, 0, 8); + CHECK(ptr != NULL, "memory_getptr returned NULL"); + CHECK(memcmp(ptr, data, 8) == 0, "memory_getptr data mismatch"); + + uint8_t* copy = memory_getcopy(m, 0, 8); + CHECK(copy != NULL, "memory_getcopy returned NULL"); + CHECK(memcmp(copy, data, 8) == 0, "memory_getcopy data mismatch"); + free(copy); + + memory_free(m); + return 0; +} + +int test_set32() { + Memory* m = memory_new(); + memory_resize(m, 32); + + uint8_t val32[32]; + for (int i = 0; i < 32; i++) val32[i] = i; + memory_set32(m, 0, val32); + + uint8_t* ptr = memory_getptr(m, 0, 32); + CHECK(ptr != NULL, "memory_getptr for set32 returned NULL"); + CHECK(memcmp(ptr, val32, 32) == 0, "memory_set32 data mismatch"); + + memory_free(m); + return 0; +} + +int test_copy() { + Memory* m = memory_new(); + memory_resize(m, 16); + + uint8_t data[16]; + for (int i = 0; i < 16; i++) data[i] = i; + memory_set(m, 0, 16, data); + + // Copy 0..8 -> 8..16 + memory_copy(m, 8, 0, 8); + + uint8_t expected[16]; + for (int i = 0; i < 8; i++) expected[i] = i; + for (int i = 8; i < 16; i++) expected[i] = i - 8; + uint8_t* ptr = memory_getptr(m, 0, 16); + CHECK(memcmp(ptr, expected, 16) == 0, "memory_copy overlap mismatch"); + + memory_free(m); + return 0; +} + +int main() { + int failures = 0; + + if (test_new_free()) failures++; + if (test_resize()) failures++; + if (test_set_getptr_getcopy()) failures++; + if (test_set32()) failures++; + if (test_copy()) failures++; + + if (failures == 0) { + printf("All tests passed ✅\n"); + return 0; + } else { + printf("%d tests failed ❌\n", failures); + return 1; + } +} diff --git a/compiler/memory.go b/compiler/memory.go index 9d156aa..6240506 100644 --- a/compiler/memory.go +++ b/compiler/memory.go @@ -16,10 +16,12 @@ package compiler +// #include "lib/mem.h" +// #cgo LDFLAGS: ./lib/clib.o +import "C" import ( "sync" - - "github.com/holiman/uint256" + "unsafe" ) var memoryPool = sync.Pool{ @@ -32,54 +34,123 @@ var memoryPool = sync.Pool{ type Memory struct { store []byte lastGasCost uint64 + cmem *C.Memory } -// NewMemory returns a new memory model. -func NewMemory() *Memory { - return memoryPool.Get().(*Memory) -} - -// Free returns the memory to the pool. -func (m *Memory) Free() { - // To reduce peak allocation, return only smaller memory instances to the pool. - const maxBufferSize = 16 << 10 - if cap(m.store) <= maxBufferSize { - m.store = m.store[:0] - m.lastGasCost = 0 - memoryPool.Put(m) - } -} +// // NewMemory returns a new memory model. +// func NewMemory() *Memory { +// return memoryPool.Get().(*Memory) +// } + +// // Free returns the memory to the pool. +// func (m *Memory) Free() { +// // To reduce peak allocation, return only smaller memory instances to the pool. +// const maxBufferSize = 16 << 10 +// if cap(m.store) <= maxBufferSize { +// m.store = m.store[:0] +// m.lastGasCost = 0 +// memoryPool.Put(m) +// } +// } + +// // Set sets offset + size to value +// func (m *Memory) Set(offset, size uint64, value []byte) { +// // It's possible the offset is greater than 0 and size equals 0. This is because +// // the calcMemSize (common.go) could potentially return 0 when size is zero (NO-OP) +// if size > 0 { +// // length of store may never be less than offset + size. +// // The store should be resized PRIOR to setting the memory +// if offset+size > uint64(len(m.store)) { +// panic("invalid memory: store empty") +// } +// copy(m.store[offset:offset+size], value) +// } +// } + +// // Set32 sets the 32 bytes starting at offset to the value of val, left-padded with zeroes to +// // 32 bytes. +// func (m *Memory) Set32(offset uint64, val *uint256.Int) { +// // length of store may never be less than offset + size. +// // The store should be resized PRIOR to setting the memory +// if offset+32 > uint64(len(m.store)) { +// panic("invalid memory: store empty") +// } +// // Fill in relevant bits +// val.PutUint256(m.store[offset:]) +// } + +// // Resize resizes the memory to size +// func (m *Memory) Resize(size uint64) { +// if uint64(m.Len()) < size { +// m.store = append(m.store, make([]byte, size-uint64(m.Len()))...) +// } +// } + +// // GetCopy returns offset + size as a new slice +// func (m *Memory) GetCopy(offset, size uint64) (cpy []byte) { +// if size == 0 { +// return nil +// } + +// // memory is always resized before being accessed, no need to check bounds +// cpy = make([]byte, size) +// copy(cpy, m.store[offset:offset+size]) +// return +// } + +// // GetPtr returns the offset + size +// func (m *Memory) GetPtr(offset, size uint64) []byte { +// if size == 0 { +// return nil +// } + +// // memory is always resized before being accessed, no need to check bounds +// return m.store[offset : offset+size] +// } + +// // Len returns the length of the backing slice +// func (m *Memory) Len() int { +// return len(m.store) +// } + +// // Data returns the backing slice +// func (m *Memory) Data() []byte { +// return m.store +// } + +// // Copy copies data from the src position slice into the dst position. +// // The source and destination may overlap. +// // OBS: This operation assumes that any necessary memory expansion has already been performed, +// // and this method may panic otherwise. +// func (m *Memory) Copy(dst, src, len uint64) { +// if len == 0 { +// return +// } +// copy(m.store[dst:], m.store[src:src+len]) +// } -// Set sets offset + size to value func (m *Memory) Set(offset, size uint64, value []byte) { - // It's possible the offset is greater than 0 and size equals 0. This is because - // the calcMemSize (common.go) could potentially return 0 when size is zero (NO-OP) if size > 0 { - // length of store may never be less than offset + size. - // The store should be resized PRIOR to setting the memory - if offset+size > uint64(len(m.store)) { - panic("invalid memory: store empty") - } - copy(m.store[offset:offset+size], value) + C.memory_set(m.cmem, C.uint64_t(offset), C.uint64_t(size), (*C.uint8_t)(unsafe.Pointer(&value[0]))) } } // Set32 sets the 32 bytes starting at offset to the value of val, left-padded with zeroes to // 32 bytes. -func (m *Memory) Set32(offset uint64, val *uint256.Int) { - // length of store may never be less than offset + size. - // The store should be resized PRIOR to setting the memory - if offset+32 > uint64(len(m.store)) { - panic("invalid memory: store empty") - } - // Fill in relevant bits - val.PutUint256(m.store[offset:]) -} +// func (m *Memory) Set32(offset uint64, val *uint256.Int) { +// // length of store may never be less than offset + size. +// // The store should be resized PRIOR to setting the memory +// if offset+32 > uint64(len(m.store)) { +// panic("invalid memory: store empty") +// } +// // Fill in relevant bits +// val.PutUint256(m.store[offset:]) +// } // Resize resizes the memory to size func (m *Memory) Resize(size uint64) { if uint64(m.Len()) < size { - m.store = append(m.store, make([]byte, size-uint64(m.Len()))...) + C.memory_resize(m.cmem, C.uint64_t(size)) } } @@ -88,11 +159,8 @@ func (m *Memory) GetCopy(offset, size uint64) (cpy []byte) { if size == 0 { return nil } - - // memory is always resized before being accessed, no need to check bounds - cpy = make([]byte, size) - copy(cpy, m.store[offset:offset+size]) - return + ccpy := C.memory_getcopy(m.cmem, C.uint64_t(offset), C.uint64_t(size)) + return unsafe.Slice((*byte)(ccpy), size) } // GetPtr returns the offset + size @@ -100,28 +168,16 @@ func (m *Memory) GetPtr(offset, size uint64) []byte { if size == 0 { return nil } - - // memory is always resized before being accessed, no need to check bounds - return m.store[offset : offset+size] + ptr := C.memory_getptr(m.cmem, C.uint64_t(offset), C.uint64_t(size)) + return unsafe.Slice((*byte)(ptr), size) } // Len returns the length of the backing slice func (m *Memory) Len() int { - return len(m.store) + return int(m.cmem.len) } // Data returns the backing slice func (m *Memory) Data() []byte { - return m.store -} - -// Copy copies data from the src position slice into the dst position. -// The source and destination may overlap. -// OBS: This operation assumes that any necessary memory expansion has already been performed, -// and this method may panic otherwise. -func (m *Memory) Copy(dst, src, len uint64) { - if len == 0 { - return - } - copy(m.store[dst:], m.store[src:src+len]) + return unsafe.Slice((*byte)(m.cmem.store), int(m.cmem.len)) } diff --git a/compiler/memory_table_c.go b/compiler/memory_table_c.go new file mode 100644 index 0000000..64af5bf --- /dev/null +++ b/compiler/memory_table_c.go @@ -0,0 +1,89 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package compiler + +import "tinygo.org/x/go-llvm" + +func u64Const(ctx llvm.Context, value uint64) llvm.Value { + return llvm.ConstInt(ctx.Int64Type(), value, false) +} + +func calcMemSize64C(ctx llvm.Context, builder llvm.Builder, offset64, length64, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) llvm.Value { + val := builder.CreateAdd(offset64, length64, "val") + hasOverflow := builder.CreateICmp(llvm.IntULT, val, offset64, "u64_overflow") + + checkU64Overflow(ctx, builder, hasOverflow, errorCodePtr, errorBlock) + return val +} + +func uint64WithOverflow(ctx llvm.Context, builder llvm.Builder, value, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) llvm.Value { + i64Type := ctx.Int64Type() + i256Type := ctx.IntType(256) + valueU64 := builder.CreateTrunc(value, i64Type, "offset64") // lossy + back := builder.CreateZExt(valueU64, i256Type, "back") + notEqual := builder.CreateICmp(llvm.IntNE, back, value, "overflow256to64") + + checkU64Overflow(ctx, builder, notEqual, errorCodePtr, errorBlock) + return valueU64 +} + +func checkU64Overflow(ctx llvm.Context, builder llvm.Builder, overflow, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) { + continueBlock := llvm.AddBasicBlock(builder.GetInsertBlock().Parent(), "continue") + u64OverflowBlock := llvm.AddBasicBlock(builder.GetInsertBlock().Parent(), "u64_overflow") + // Branch to stack overflow block if limit exceeded, otherwise continue + builder.CreateCondBr(overflow, u64OverflowBlock, continueBlock) + + builder.SetInsertPointAtEnd(u64OverflowBlock) + // Store error code and exit + errCode := llvm.ConstInt(ctx.Int64Type(), uint64(VMErrorCodeGasUintOverflow), false) + builder.CreateStore(errCode, errorCodePtr) + builder.CreateBr(errorBlock) + builder.SetInsertPointAtEnd(continueBlock) +} + +func memoryMLoadC(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) llvm.Value { + stackElem := peekStackPtr(ctx, builder, stack, stackIdxPtr) + offset := builder.CreateLoad(ctx.IntType(256), stackElem, "mload_stack_value") + offset64 := uint64WithOverflow(ctx, builder, offset, errorCodePtr, errorBlock) + length64 := u64Const(ctx, 32) + return calcMemSize64C(ctx, builder, offset64, length64, errorCodePtr, errorBlock) +} + +func memoryMStore8C(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) llvm.Value { + stackElem := peekStackPtr(ctx, builder, stack, stackIdxPtr) + offset := builder.CreateLoad(ctx.IntType(256), stackElem, "mload_stack_value") + offset64 := uint64WithOverflow(ctx, builder, offset, errorCodePtr, errorBlock) + length64 := u64Const(ctx, 1) + return calcMemSize64C(ctx, builder, offset64, length64, errorCodePtr, errorBlock) +} + +func memoryMcopyC(ctx llvm.Context, builder llvm.Builder, stack, stackIdxPtr, errorCodePtr llvm.Value, errorBlock llvm.BasicBlock) llvm.Value { + mStart := peekStack(ctx, builder, stack, stackIdxPtr, 0) + s1 := peekStack(ctx, builder, stack, stackIdxPtr, 1) + + bitIsSet := builder.CreateICmp(llvm.IntUGT, s1, mStart, "gt_cmp") + offset := builder.CreateSelect(bitIsSet, s1, mStart, "signed_val") + + offset64 := uint64WithOverflow(ctx, builder, offset, errorCodePtr, errorBlock) + length := peekStack(ctx, builder, stack, stackIdxPtr, 2) + length64 := uint64WithOverflow(ctx, builder, length, errorCodePtr, errorBlock) + return calcMemSize64C(ctx, builder, offset64, length64, errorCodePtr, errorBlock) +} + +var ( + memoryMStoreC = memoryMLoadC +) diff --git a/compiler/opcodes_test.go b/compiler/opcodes_test.go index d9d3432..ef7f684 100644 --- a/compiler/opcodes_test.go +++ b/compiler/opcodes_test.go @@ -289,7 +289,7 @@ func runOpcodeTest(t *testing.T, testCase OpcodeTestCase) { if expectedMem.lastGasCost != mem.lastGasCost { t.Errorf("Expected memory's lastGasCost: %v, actual lastGasCost: %v", expectedMem.lastGasCost, mem.lastGasCost) } - if !bytes.Equal(expectedMem.store, mem.store) { + if !bytes.Equal(expectedMem.store, mem.Data()) { t.Errorf("Expected memory: %v, actual memory: %v", expectedMem.store, mem.store) } } @@ -1353,6 +1353,24 @@ func TestMemoryOpcodes(t *testing.T) { }, expectedGas: 3*6 + 2*3, }, + { + name: "MSTORE_MSTORE_RESIZE", + bytecode: []byte{ + 0x60, 0xFF, // PUSH1 0xFF (value) + 0x60, 0x01, // PUSH1 0x01 (offset) + 0x52, // MSTORE + 0x60, 0xFF, // PUSH1 0x40 (offset) + 0x60, 0x00, // PUSH1 0x00, memorySize is less than current memorySize, memory shouldn't shrink + 0x52, // MSTORE + 0x00, // STOP + }, + expectedStack: [][32]byte{}, + expectedMemory: &Memory{ + store: common.Hex2Bytes("00000000000000000000000000000000000000000000000000000000000000ffff00000000000000000000000000000000000000000000000000000000000000"), + lastGasCost: 2 * 3, // memory expansion cost for the last MSTORE + }, + expectedGas: 3*6 + 2*3, + }, { name: "MSTORE8_MLOAD", bytecode: []byte{