diff --git a/core/vm/eips.go b/core/vm/eips.go index dfcac4b930..95140b68af 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -38,6 +38,7 @@ var activators = map[int]func(*JumpTable){ 2200: enable2200, 1884: enable1884, 1344: enable1344, + 8141: enable8141, 1153: enable1153, 4762: enable4762, 7702: enable7702, @@ -579,3 +580,37 @@ func enable7702(jt *JumpTable) { jt[STATICCALL].dynamicGas = gasStaticCallEIP7702 jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP7702 } + +func enable8141(jt *JumpTable) { + jt[APPROVE] = &operation{ + execute: opApprove, + constantGas: 0, + dynamicGas: gasApprove, + minStack: minStack(3, 0), + maxStack: maxStack(3, 0), + memorySize: memoryApprove, + } + + jt[TXPARAMLOAD] = &operation{ + execute: opTxParamLoad, + constantGas: GasFastestStep, + minStack: minStack(3, 1), + maxStack: maxStack(3, 1), + } + + jt[TXPARAMSIZE] = &operation{ + execute: opTxParamSize, + constantGas: GasQuickStep, + minStack: minStack(2, 1), + maxStack: maxStack(2, 1), + } + + jt[TXPARAMCOPY] = &operation{ + execute: opTxParamCopy, + constantGas: GasFastestStep, + dynamicGas: gasTxParamCopy, + minStack: minStack(5, 0), + maxStack: maxStack(5, 0), + memorySize: memoryTxParamCopy, + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 503e25d427..dd30b9918f 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -126,6 +126,36 @@ type EVM struct { readOnly bool // Whether to throw on stateful modifications returnData []byte // Last CALL's return data for subsequent reuse + + // FrameTxContext holds frame transaction state, set during frame tx execution. + FrameTxCtx *FrameTxContext +} + +type FrameTxContext struct { + Sender common.Address + Frames []FrameInfo + FrameStatuses []uint8 + SenderApproved bool + PayerApproved bool + Payer common.Address + SigHash common.Hash + CurrentFrame int + Nonce uint64 + GasTipCap *big.Int + GasFeeCap *big.Int + TxFee *big.Int + ApproveCalledInFrame bool + + BlobFeeCap *big.Int + BlobVersionedHashes []common.Hash +} + +// FrameInfo holds the definition of a single frame within a FrameTx. +type FrameInfo struct { + Mode uint8 + Target common.Address + GasLimit uint64 + Data []byte } // NewEVM constructs an EVM instance with the supplied block context, state @@ -144,6 +174,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon evm.precompiles = activePrecompiledContracts(evm.chainRules) switch { + case evm.chainRules.IsBogota: + evm.table = &bogotaInstructionSet case evm.chainRules.IsOsaka: evm.table = &osakaInstructionSet case evm.chainRules.IsVerkle: diff --git a/core/vm/instructions.go b/core/vm/instructions.go index a4c4b0703b..6a39cb05e4 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -1144,3 +1144,344 @@ func makeDup(size int) executionFunc { return nil, nil } } + +// 8141 + +func getTxParamData(evm *EVM, ftx *FrameTxContext, selector, frameIdx uint64) ([]byte, bool) { + switch selector { + + case 0x00: // tx type = 6 + val := new(uint256.Int).SetUint64(6) + b32 := val.Bytes32() + return b32[:], true + + case 0x01: // nonce + val := new(uint256.Int).SetUint64(ftx.Nonce) + b32 := val.Bytes32() + return b32[:], true + + case 0x02: // sender (left-padded to 32 bytes) + return common.LeftPadBytes(ftx.Sender.Bytes(), 32), true + + case 0x03: // max_priority_fee_per_gas + val, _ := uint256.FromBig(ftx.GasTipCap) + b32 := val.Bytes32() + return b32[:], true + + case 0x04: // max_fee_per_gas + val, _ := uint256.FromBig(ftx.GasFeeCap) + b32 := val.Bytes32() + return b32[:], true + + case 0x05: // max_fee_per_blob_gas + if ftx.BlobFeeCap == nil { + b32 := new(uint256.Int).Bytes32() + return b32[:], true + } + val, _ := uint256.FromBig(ftx.BlobFeeCap) + b32 := val.Bytes32() + return b32[:], true + + case 0x06: // max cost + val, _ := uint256.FromBig(ftx.TxFee) + b32 := val.Bytes32() + return b32[:], true + + case 0x07: // len(blob_versioned_hashes) + count := uint64(len(ftx.BlobVersionedHashes)) + val := new(uint256.Int).SetUint64(count) + b32 := val.Bytes32() + return b32[:], true + + case 0x08: // compute_sig_hash(tx) + return ftx.SigHash[:], true + + case 0x09: // len(frames) + val := new(uint256.Int).SetUint64(uint64(len(ftx.Frames))) + b32 := val.Bytes32() + return b32[:], true + + case 0x10: // current frame index + if ftx.CurrentFrame < 0 { + return nil, false + } + val := new(uint256.Int).SetUint64(uint64(ftx.CurrentFrame)) + b32 := val.Bytes32() + return b32[:], true + + case 0x11: // frame[index].target + if int(frameIdx) >= len(ftx.Frames) { + return nil, false // TxParamOutOfBounds → exceptional halt + } + f := ftx.Frames[frameIdx] + if f.Target == (common.Address{}) { + // null target + return make([]byte, 32), true + } + return common.LeftPadBytes(f.Target.Bytes(), 32), true + + case 0x12: // frame[index].data + // EELS: + // if index >= len(tx.frames): raise TxParamOutOfBounds + // if frame.mode == FRAME_MODE_VERIFY: return (b"", Uint(0)) + // data = bytes(frame.data) + // return (data, Uint(len(data))) + if int(frameIdx) >= len(ftx.Frames) { + return nil, false // TxParamOutOfBounds + } + f := ftx.Frames[frameIdx] + if f.Mode == types.FrameModeVerify { + return []byte{}, true + } + return f.Data, true + + case 0x13: // frame[index].gas_limit + if int(frameIdx) >= len(ftx.Frames) { + return nil, false + } + val := new(uint256.Int).SetUint64(ftx.Frames[frameIdx].GasLimit) + b32 := val.Bytes32() + return b32[:], true + + case 0x14: // frame[index].mode + if int(frameIdx) >= len(ftx.Frames) { + return nil, false + } + val := new(uint256.Int).SetUint64(uint64(ftx.Frames[frameIdx].Mode)) + b32 := val.Bytes32() + return b32[:], true + + case 0x15: // frame[index].status + if int(frameIdx) >= len(ftx.Frames) { + return nil, false + } + if int(frameIdx) >= ftx.CurrentFrame { + return nil, false + } + if int(frameIdx) >= len(ftx.FrameStatuses) { + return nil, false + } + val := new(uint256.Int).SetUint64(uint64(ftx.FrameStatuses[frameIdx])) + b32 := val.Bytes32() + return b32[:], true + + default: + return nil, false + } +} + +// TXPARAMLOAD (0xB0) +// Stack: [selector, index, offset] → [value] +// Gas: GAS_VERY_LOW (3) + +func opTxParamLoad(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + ftx := evm.FrameTxCtx + if ftx == nil { + return nil, ErrExecutionReverted + } + selector := scope.Stack.pop() + index := scope.Stack.pop() + offset := scope.Stack.pop() + data, ok := getTxParamData(evm, ftx, selector.Uint64(), index.Uint64()) + if !ok { + return nil, ErrExecutionReverted + } + var result [32]byte + offsetU64 := offset.Uint64() + if offsetU64 < uint64(len(data)) { + copy(result[:], data[offsetU64:]) + } + scope.Stack.push(new(uint256.Int).SetBytes(result[:])) + return nil, nil +} + +// TXPARAMSIZE (0xB1) +// Stack: [selector, index] → [size] +// Gas: GAS_BASE (2) + +func opTxParamSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + ftx := evm.FrameTxCtx + if ftx == nil { + return nil, ErrExecutionReverted + } + selector := scope.Stack.pop() + index := scope.Stack.pop() + data, ok := getTxParamData(evm, ftx, selector.Uint64(), index.Uint64()) + if !ok { + return nil, ErrExecutionReverted + } + + scope.Stack.push(new(uint256.Int).SetUint64(uint64(len(data)))) + return nil, nil +} + +// TXPARAMCOPY (0xB2) +// Stack: [selector, index, dest_offset, src_offset, length] → [] +// Gas: GAS_VERY_LOW + GAS_VERY_LOW * words + memory_expansion + +func opTxParamCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + selector := scope.Stack.pop() + index := scope.Stack.pop() + memOffset := scope.Stack.pop() + dataOffset := scope.Stack.pop() + length := scope.Stack.pop() + lengthU64 := length.Uint64() + if lengthU64 == 0 { + return nil, nil + } + ftx := evm.FrameTxCtx + if ftx == nil { + return nil, ErrExecutionReverted + } + data, ok := getTxParamData(evm, ftx, selector.Uint64(), index.Uint64()) + if !ok { + return nil, ErrExecutionReverted + } + dataOffsetU64 := dataOffset.Uint64() + dataCopy := make([]byte, lengthU64) + if dataOffsetU64 < uint64(len(data)) { + end := dataOffsetU64 + lengthU64 + if end > uint64(len(data)) { + end = uint64(len(data)) + } + copy(dataCopy, data[dataOffsetU64:end]) + } + scope.Memory.Set(memOffset.Uint64(), lengthU64, dataCopy) + return nil, nil +} + +// APPROVE (0xAA) +// Stack: [offset, length, scope] -> [] (terminates frame) +// Gas: GAS_ZERO + memory_expansion + +func opApprove(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + offset := scope.Stack.pop() + length := scope.Stack.pop() + scp := scope.Stack.pop() + + // GAS already charged + memory already extended by gasApprove() + ftx := evm.FrameTxCtx + if ftx == nil { + return nil, ErrExecutionReverted + } + + // EELS: tx_approval = _get_frame_tx_approval(evm) + s := scp.Uint64() + if s > 2 { + return nil, ErrExecutionReverted + } + + if ftx.CurrentFrame < 0 || ftx.CurrentFrame >= len(ftx.Frames) { + return nil, ErrExecutionReverted + } + frame := ftx.Frames[ftx.CurrentFrame] + + var frameTarget common.Address + if frame.Target == (common.Address{}) { + frameTarget = ftx.Sender + } else { + frameTarget = frame.Target + } + + if scope.Contract.Caller() != frameTarget { + return nil, ErrExecutionReverted + } + + switch s { + + case 0x0: + if ftx.SenderApproved { + return nil, ErrExecutionReverted + } + if scope.Contract.Caller() != ftx.Sender { + return nil, ErrExecutionReverted + } + ftx.SenderApproved = true + + case 0x1: + if ftx.PayerApproved { + return nil, ErrExecutionReverted + } + if !ftx.SenderApproved { + return nil, ErrExecutionReverted + } + balance := evm.StateDB.GetBalance(frameTarget).ToBig() + if balance.Cmp(ftx.TxFee) < 0 { + return nil, ErrExecutionReverted + } + fee, _ := uint256.FromBig(ftx.TxFee) + evm.StateDB.SubBalance(frameTarget, fee, tracing.BalanceDecreaseGasBuy) + evm.StateDB.SetNonce(ftx.Sender, evm.StateDB.GetNonce(ftx.Sender)+1, tracing.NonceChangeEoACall) + ftx.PayerApproved = true + ftx.Payer = frameTarget + + case 0x2: + if ftx.SenderApproved || ftx.PayerApproved { + return nil, ErrExecutionReverted + } + if scope.Contract.Caller() != ftx.Sender { + return nil, ErrExecutionReverted + } + balance := evm.StateDB.GetBalance(frameTarget).ToBig() + if balance.Cmp(ftx.TxFee) < 0 { + return nil, ErrExecutionReverted + } + fee, _ := uint256.FromBig(ftx.TxFee) + evm.StateDB.SubBalance(frameTarget, fee, tracing.BalanceDecreaseGasBuy) + evm.StateDB.SetNonce(ftx.Sender, evm.StateDB.GetNonce(ftx.Sender)+1, tracing.NonceChangeEoACall) + ftx.SenderApproved = true + ftx.PayerApproved = true + ftx.Payer = frameTarget + } + + ftx.ApproveCalledInFrame = true + + // Mirror opReturn + ret := scope.Memory.GetCopy(offset.Uint64(), length.Uint64()) + return ret, errStopToken +} + +// Todo(jvn): All these Memory extension and Gas Estimation verify correctness across EELS + +// memoryApprove computes memory size for APPROVE dynamic gas. +func memoryApprove(stack *Stack) (uint64, bool) { + // Stack at this point (before pops): [offset, length, scope] + return calcMemSize64(stack.Back(0), stack.Back(1)) +} + +// gasApprove computes dynamic gas for APPROVE (memory expansion only). +// EELS: charge_gas(evm, GAS_ZERO + extend_memory.cost) +func gasApprove(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + return memoryGasCost(mem, memorySize) +} + +// memoryTxParamCopy computes memory size for TXPARAMCOPY dynamic gas. +func memoryTxParamCopy(stack *Stack) (uint64, bool) { + // destOffset is Back(2), length is Back(4) + return calcMemSize64(stack.Back(2), stack.Back(4)) +} + +// gasTxParamCopy computes dynamic gas for TXPARAMCOPY. +func gasTxParamCopy(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { + gas, err := memoryGasCost(mem, memorySize) + if err != nil { + return 0, err + } + length := stack.Back(4) + words, overflow := length.Uint64WithOverflow() + if overflow { + return 0, ErrGasUintOverflow + } + // words = ceil(length / 32) + wordSize := toWordSize(words) + copyGas := wordSize * params.CopyGas + // overflow check + if wordSize != 0 && copyGas/wordSize != params.CopyGas { + return 0, ErrGasUintOverflow + } + total := gas + copyGas + if total < gas { + return 0, ErrGasUintOverflow + } + return total, nil +} diff --git a/core/vm/interface.go b/core/vm/interface.go index e285b18b0f..b55007d48f 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -56,6 +56,7 @@ type StateDB interface { GetTransientState(addr common.Address, key common.Hash) common.Hash SetTransientState(addr common.Address, key, value common.Hash) + ClearTransientStorage() SelfDestruct(common.Address) HasSelfDestructed(common.Address) bool diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index d7a4d9da1d..7d1df1ff2f 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -63,6 +63,7 @@ var ( verkleInstructionSet = newVerkleInstructionSet() pragueInstructionSet = newPragueInstructionSet() osakaInstructionSet = newOsakaInstructionSet() + bogotaInstructionSet = newBogotaInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -92,6 +93,12 @@ func newVerkleInstructionSet() JumpTable { return validate(instructionSet) } +func newBogotaInstructionSet() JumpTable { + instructionSet := newOsakaInstructionSet() + enable8141(&instructionSet) // EIP-7939 (CLZ opcode) + return validate(instructionSet) +} + func newOsakaInstructionSet() JumpTable { instructionSet := newPragueInstructionSet() enable7939(&instructionSet) // EIP-7939 (CLZ opcode) diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 9a32126a80..ba072705fe 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -212,6 +212,18 @@ const ( LOG4 ) +// 0xaa range - Frame Transaction opcodes. +const ( + APPROVE OpCode = 0xaa //Approve execution/payment in VERIFY frame. +) + +// 0xb0 range - transaction parameter opcodes. +const ( + TXPARAMLOAD OpCode = 0xb0 // Load 32-byte word from tx parameter. + TXPARAMSIZE OpCode = 0xb1 // Get size of tx parameter. + TXPARAMCOPY OpCode = 0xb2 // Copy tx parameter data to memory. +) + // 0xd0 range - eof operations. const ( DATALOAD OpCode = 0xd0 @@ -416,6 +428,14 @@ var opCodeToString = [256]string{ LOG3: "LOG3", LOG4: "LOG4", + // 0xaa - opcodes. + APPROVE: "APPROVE", + + // 0xb0 range - tx param opcodes. + TXPARAMLOAD: "TXPARAMLOAD", + TXPARAMSIZE: "TXPARAMSIZE", + TXPARAMCOPY: "TXPARAMCOPY", + // 0xd range - eof ops. DATALOAD: "DATALOAD", DATALOADN: "DATALOADN", @@ -631,6 +651,10 @@ var stringToOp = map[string]OpCode{ "REVERT": REVERT, "INVALID": INVALID, "SELFDESTRUCT": SELFDESTRUCT, + "APPROVE": APPROVE, + "TXPARAMLOAD": TXPARAMLOAD, + "TXPARAMSIZE": TXPARAMSIZE, + "TXPARAMCOPY": TXPARAMCOPY, } // StringToOp finds the opcode whose name is stored in `str`.