f(eip-8141): add opcodes

This commit is contained in:
0xjvn 2026-03-05 01:20:05 +05:30
parent 23c6a6c174
commit 0b11863ec4
6 changed files with 440 additions and 0 deletions

View file

@ -38,6 +38,7 @@ var activators = map[int]func(*JumpTable){
2200: enable2200, 2200: enable2200,
1884: enable1884, 1884: enable1884,
1344: enable1344, 1344: enable1344,
8141: enable8141,
1153: enable1153, 1153: enable1153,
4762: enable4762, 4762: enable4762,
7702: enable7702, 7702: enable7702,
@ -579,3 +580,37 @@ func enable7702(jt *JumpTable) {
jt[STATICCALL].dynamicGas = gasStaticCallEIP7702 jt[STATICCALL].dynamicGas = gasStaticCallEIP7702
jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP7702 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,
}
}

View file

@ -126,6 +126,36 @@ type EVM struct {
readOnly bool // Whether to throw on stateful modifications readOnly bool // Whether to throw on stateful modifications
returnData []byte // Last CALL's return data for subsequent reuse 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 // 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) evm.precompiles = activePrecompiledContracts(evm.chainRules)
switch { switch {
case evm.chainRules.IsBogota:
evm.table = &bogotaInstructionSet
case evm.chainRules.IsOsaka: case evm.chainRules.IsOsaka:
evm.table = &osakaInstructionSet evm.table = &osakaInstructionSet
case evm.chainRules.IsVerkle: case evm.chainRules.IsVerkle:

View file

@ -1144,3 +1144,344 @@ func makeDup(size int) executionFunc {
return nil, nil 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
}

View file

@ -56,6 +56,7 @@ type StateDB interface {
GetTransientState(addr common.Address, key common.Hash) common.Hash GetTransientState(addr common.Address, key common.Hash) common.Hash
SetTransientState(addr common.Address, key, value common.Hash) SetTransientState(addr common.Address, key, value common.Hash)
ClearTransientStorage()
SelfDestruct(common.Address) SelfDestruct(common.Address)
HasSelfDestructed(common.Address) bool HasSelfDestructed(common.Address) bool

View file

@ -63,6 +63,7 @@ var (
verkleInstructionSet = newVerkleInstructionSet() verkleInstructionSet = newVerkleInstructionSet()
pragueInstructionSet = newPragueInstructionSet() pragueInstructionSet = newPragueInstructionSet()
osakaInstructionSet = newOsakaInstructionSet() osakaInstructionSet = newOsakaInstructionSet()
bogotaInstructionSet = newBogotaInstructionSet()
) )
// JumpTable contains the EVM opcodes supported at a given fork. // JumpTable contains the EVM opcodes supported at a given fork.
@ -92,6 +93,12 @@ func newVerkleInstructionSet() JumpTable {
return validate(instructionSet) return validate(instructionSet)
} }
func newBogotaInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet()
enable8141(&instructionSet) // EIP-7939 (CLZ opcode)
return validate(instructionSet)
}
func newOsakaInstructionSet() JumpTable { func newOsakaInstructionSet() JumpTable {
instructionSet := newPragueInstructionSet() instructionSet := newPragueInstructionSet()
enable7939(&instructionSet) // EIP-7939 (CLZ opcode) enable7939(&instructionSet) // EIP-7939 (CLZ opcode)

View file

@ -212,6 +212,18 @@ const (
LOG4 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. // 0xd0 range - eof operations.
const ( const (
DATALOAD OpCode = 0xd0 DATALOAD OpCode = 0xd0
@ -416,6 +428,14 @@ var opCodeToString = [256]string{
LOG3: "LOG3", LOG3: "LOG3",
LOG4: "LOG4", LOG4: "LOG4",
// 0xaa - opcodes.
APPROVE: "APPROVE",
// 0xb0 range - tx param opcodes.
TXPARAMLOAD: "TXPARAMLOAD",
TXPARAMSIZE: "TXPARAMSIZE",
TXPARAMCOPY: "TXPARAMCOPY",
// 0xd range - eof ops. // 0xd range - eof ops.
DATALOAD: "DATALOAD", DATALOAD: "DATALOAD",
DATALOADN: "DATALOADN", DATALOADN: "DATALOADN",
@ -631,6 +651,10 @@ var stringToOp = map[string]OpCode{
"REVERT": REVERT, "REVERT": REVERT,
"INVALID": INVALID, "INVALID": INVALID,
"SELFDESTRUCT": SELFDESTRUCT, "SELFDESTRUCT": SELFDESTRUCT,
"APPROVE": APPROVE,
"TXPARAMLOAD": TXPARAMLOAD,
"TXPARAMSIZE": TXPARAMSIZE,
"TXPARAMCOPY": TXPARAMCOPY,
} }
// StringToOp finds the opcode whose name is stored in `str`. // StringToOp finds the opcode whose name is stored in `str`.