diff --git a/core/frame_tx_test.go b/core/frame_tx_test.go
new file mode 100644
index 0000000000..13828cb12b
--- /dev/null
+++ b/core/frame_tx_test.go
@@ -0,0 +1,253 @@
+// Copyright 2025 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 core
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/ethereum/go-ethereum/triedb"
+ "github.com/holiman/uint256"
+)
+
+// buildApproveCode returns EVM bytecode that replicates the EELS approve_bytecode
+// with a trampoline self-CALL pattern for the APPROVE caller check.
+// Thanks Claude for this fn :)
+func buildApproveCode(scope uint8) []byte {
+
+ code := []byte{}
+ b := func(ops ...byte) { code = append(code, ops...) }
+
+ // ---- Compute frame_target ----
+ // stack: []
+ // TXPARAMLOAD(selector=0x10, index=0, offset=0) → current_frame_index
+ b(byte(vm.PUSH1), 0x00) // offset=0
+ b(byte(vm.PUSH1), 0x00) // index=0
+ b(byte(vm.PUSH1), 0x10) // selector=0x10 (current frame index)
+ b(byte(vm.TXPARAMLOAD)) // → [frame_idx]
+
+ // TXPARAMLOAD(selector=0x11, index=frame_idx, offset=0) → frame target raw
+ b(byte(vm.PUSH1), 0x00) // offset=0
+ b(byte(vm.DUP2)) // frame_idx
+ b(byte(vm.PUSH1), 0x11) // selector=0x11 (frame target)
+ b(byte(vm.TXPARAMLOAD)) // → [frame_target_raw, frame_idx]
+ b(byte(vm.SWAP1)) // → [frame_idx, frame_target_raw]
+ b(byte(vm.POP)) // → [frame_target_raw]
+
+ // Resolve null target → sender:
+ // if frame_target_raw == 0: frame_target = TXPARAMLOAD(0x02, 0, 0) (= sender)
+ // frame_target = frame_target_raw | (iszero(frame_target_raw) * sender)
+ b(byte(vm.DUP1)) // → [frame_target_raw, frame_target_raw]
+ b(byte(vm.ISZERO)) // → [is_null, frame_target_raw]
+ // TXPARAMLOAD(0x02, 0, 0) → sender
+ b(byte(vm.PUSH1), 0x00) // offset
+ b(byte(vm.PUSH1), 0x00) // index
+ b(byte(vm.PUSH1), 0x02) // selector = sender
+ b(byte(vm.TXPARAMLOAD)) // → [sender, is_null, frame_target_raw]
+ b(byte(vm.MUL)) // → [sender*is_null, frame_target_raw]
+ b(byte(vm.OR)) // → [frame_target]
+
+ // ---- Check CALLER == frame_target OR CALLER == ADDRESS ----
+ b(byte(vm.CALLER)) // → [caller, frame_target]
+ b(byte(vm.DUP2)) // → [frame_target, caller, frame_target]
+ b(byte(vm.EQ)) // → [caller==frame_target, frame_target]
+ b(byte(vm.CALLER)) // → [caller, caller==ft, frame_target]
+ b(byte(vm.ADDRESS)) // → [address, caller, caller==ft, frame_target]
+ b(byte(vm.EQ)) // → [caller==addr, caller==ft, frame_target]
+ b(byte(vm.OR)) // → [condition, frame_target]
+ b(byte(vm.SWAP1)) // → [frame_target, condition]
+ b(byte(vm.POP)) // → [condition]
+
+ // ---- Branch: JUMPI to approve block ----
+ // We'll patch the jump destination after we know the layout
+ jumpiPos := len(code)
+ b(byte(vm.PUSH1), 0x00) // placeholder for approve_pc
+ b(byte(vm.JUMPI))
+
+ // ---- Else: trampoline self-CALL ----
+ // CALL(gas=100000, address=ADDRESS, value=0, argsOffset=0, argsSize=0, retOffset=0, retSize=0)
+ b(byte(vm.PUSH1), 0x00) // retSize=0
+ b(byte(vm.PUSH1), 0x00) // retOffset=0
+ b(byte(vm.PUSH1), 0x00) // argsSize=0
+ b(byte(vm.PUSH1), 0x00) // argsOffset=0
+ b(byte(vm.PUSH1), 0x00) // value=0
+ b(byte(vm.ADDRESS)) // to=self
+ b(byte(vm.PUSH3), 0x01, 0x86, 0xA0) // gas=100000
+ b(byte(vm.CALL))
+ b(byte(vm.POP)) // discard success flag
+
+ // RETURNDATACOPY(0, 0, RETURNDATASIZE)
+ b(byte(vm.RETURNDATASIZE))
+ b(byte(vm.PUSH1), 0x00)
+ b(byte(vm.PUSH1), 0x00)
+ b(byte(vm.RETURNDATACOPY))
+
+ // RETURN(0, RETURNDATASIZE)
+ b(byte(vm.RETURNDATASIZE))
+ b(byte(vm.PUSH1), 0x00)
+ b(byte(vm.RETURN))
+
+ // ---- Approve block ----
+ approvePC := len(code)
+ b(byte(vm.JUMPDEST))
+ // APPROVE(offset=0, length=0, scope=)
+ b(byte(vm.PUSH1), scope) // scope
+ b(byte(vm.PUSH1), 0x00) // length=0
+ b(byte(vm.PUSH1), 0x00) // offset=0
+ b(byte(vm.APPROVE))
+ b(byte(vm.STOP))
+
+ // Patch the JUMPI destination
+ code[jumpiPos+1] = byte(approvePC)
+
+ return code
+}
+
+// buildSstoreCode returns EVM bytecode: SSTORE(slot, value) + STOP
+func buildSstoreCode(slot, value byte) []byte {
+ return []byte{
+ byte(vm.PUSH1), value,
+ byte(vm.PUSH1), slot,
+ byte(vm.SSTORE),
+ byte(vm.STOP),
+ }
+}
+
+// TestFrameTxHappyPathSelfPaid mirrors the Python test_happy_path_self_paid:
+// Todo - add more tests , writing one tests only curretly to debug Errors arised
+func TestFrameTxHappyPathSelfPaid(t *testing.T) {
+ var (
+ config = params.AllDevChainProtocolChanges
+ sender = common.HexToAddress("0x1000000000000000000000000000000000001000")
+ executionTarget = common.HexToAddress("0x2000000000000000000000000000000000002000")
+ senderCode = buildApproveCode(0x02)
+ execCode = buildSstoreCode(0x01, 0x01)
+ baseFee = big.NewInt(7)
+ )
+
+ // --- State ---
+ db := state.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil)
+ statedb, _ := state.New(types.EmptyRootHash, db)
+
+ statedb.SetCode(sender, senderCode, tracing.CodeChangeUnspecified)
+ statedb.SetNonce(sender, 1, tracing.NonceChangeUnspecified)
+ statedb.SetBalance(sender,
+ uint256.NewInt(1_000_000_000_000_000_000),
+ tracing.BalanceChangeUnspecified,
+ )
+ statedb.SetCode(executionTarget, execCode, tracing.CodeChangeUnspecified)
+ statedb.SetNonce(executionTarget, 1, tracing.NonceChangeUnspecified)
+ statedb.Commit(0, false, false)
+
+ // --- Build tx ---
+ frames := []types.Frame{
+ {
+ Mode: types.FrameModeVerify,
+ Target: sender,
+ GasLimit: 50_000,
+ Data: []byte{},
+ },
+ {
+ Mode: types.FrameModeSender,
+ Target: executionTarget,
+ GasLimit: 50_000,
+ Data: []byte{},
+ },
+ }
+ frameTx := &types.FrameTx{
+ ChainID: uint256.NewInt(1),
+ Nonce: 1,
+ Sender: sender,
+ Frames: frames,
+ GasTipCap: uint256.NewInt(0),
+ GasFeeCap: uint256.NewInt(7),
+ }
+ tx := types.NewTx(frameTx)
+
+ rnd := common.BigToHash(big.NewInt(42))
+ header := &types.Header{
+ Number: big.NewInt(1),
+ Time: 0,
+ Difficulty: big.NewInt(0),
+ GasLimit: 10_000_000,
+ BaseFee: baseFee,
+ Extra: []byte{},
+ MixDigest: rnd,
+ }
+
+ blockCtx := vm.BlockContext{
+ CanTransfer: CanTransfer,
+ Transfer: Transfer,
+ GetHash: func(n uint64) common.Hash { return common.Hash{} },
+ Coinbase: common.HexToAddress("0xC014BA5E"),
+ GasLimit: header.GasLimit,
+ BlockNumber: header.Number,
+ Time: header.Time,
+ Difficulty: header.Difficulty,
+ BaseFee: baseFee,
+ Random: &rnd,
+ }
+ evm := vm.NewEVM(blockCtx, statedb, config, vm.Config{})
+
+ gp := new(GasPool).AddGas(header.GasLimit)
+ var usedGas uint64
+
+ receipt, err := ApplyTransaction(evm, gp, statedb, header, tx, &usedGas)
+ if err != nil {
+ t.Fatalf("ApplyTransaction failed: %v", err)
+ }
+ // jsonBytes, _ := json.MarshalIndent(receipt, "", " ")
+ // fmt.Printf("\n\nreceipt: ")
+ // fmt.Println(string(jsonBytes))
+ if receipt.Status != types.ReceiptStatusSuccessful {
+ t.Fatalf("receipt status: got %d want 1", receipt.Status)
+ }
+
+ // Sender nonce should be 2
+ if n := statedb.GetNonce(sender); n != 2 {
+ t.Errorf("sender nonce: got %d want 2", n)
+ }
+
+ // Execution target storage slot 1 should be 1
+ slot := statedb.GetState(executionTarget, common.BigToHash(big.NewInt(1)))
+ if slot != common.BigToHash(big.NewInt(1)) {
+ t.Errorf("slot 1: got %x want 1", slot)
+ }
+
+ // Payer should be sender
+ if receipt.Payer == nil || *receipt.Payer != sender {
+ t.Errorf("payer: got %v want %s", receipt.Payer, sender.Hex())
+ }
+
+ // Frame receipts should be [success, success]
+ if len(receipt.FrameReceipts) != 2 {
+ t.Fatalf("frame receipts: got %d want 2", len(receipt.FrameReceipts))
+ }
+ for i, fr := range receipt.FrameReceipts {
+ if fr.Status != types.ReceiptStatusSuccessful {
+ t.Errorf("frame %d status: got %d want 1", i, fr.Status)
+ }
+ t.Logf("FrameReceipt[%d]: status=%d gasUsed=%d logs=%d", i, fr.Status, fr.GasUsed, len(fr.Logs))
+ }
+}
diff --git a/tests/init.go b/tests/init.go
index d10b47986c..52aca5426f 100644
--- a/tests/init.go
+++ b/tests/init.go
@@ -720,6 +720,79 @@ var Forks = map[string]*params.ChainConfig{
BPO4: params.DefaultBPO4BlobConfig,
},
},
+ "Amsterdam": {
+ ChainID: big.NewInt(1),
+ HomesteadBlock: big.NewInt(0),
+ EIP150Block: big.NewInt(0),
+ EIP155Block: big.NewInt(0),
+ EIP158Block: big.NewInt(0),
+ ByzantiumBlock: big.NewInt(0),
+ ConstantinopleBlock: big.NewInt(0),
+ PetersburgBlock: big.NewInt(0),
+ IstanbulBlock: big.NewInt(0),
+ MuirGlacierBlock: big.NewInt(0),
+ BerlinBlock: big.NewInt(0),
+ LondonBlock: big.NewInt(0),
+ ArrowGlacierBlock: big.NewInt(0),
+ MergeNetsplitBlock: big.NewInt(0),
+ TerminalTotalDifficulty: big.NewInt(0),
+ ShanghaiTime: u64(0),
+ CancunTime: u64(0),
+ PragueTime: u64(0),
+ OsakaTime: u64(0),
+ AmsterdamTime: u64(0),
+ BPO1Time: u64(0),
+ BPO2Time: u64(0),
+ BPO3Time: u64(0),
+ BPO4Time: u64(15_000),
+ DepositContractAddress: params.MainnetChainConfig.DepositContractAddress,
+ BlobScheduleConfig: ¶ms.BlobScheduleConfig{
+ Cancun: params.DefaultCancunBlobConfig,
+ Prague: params.DefaultPragueBlobConfig,
+ Osaka: params.DefaultOsakaBlobConfig,
+ BPO1: bpo1BlobConfig,
+ BPO2: bpo2BlobConfig,
+ BPO3: params.DefaultBPO3BlobConfig,
+ BPO4: params.DefaultBPO4BlobConfig,
+ },
+ },
+ "Bogota": {
+ ChainID: big.NewInt(1),
+ HomesteadBlock: big.NewInt(0),
+ EIP150Block: big.NewInt(0),
+ EIP155Block: big.NewInt(0),
+ EIP158Block: big.NewInt(0),
+ ByzantiumBlock: big.NewInt(0),
+ ConstantinopleBlock: big.NewInt(0),
+ PetersburgBlock: big.NewInt(0),
+ IstanbulBlock: big.NewInt(0),
+ MuirGlacierBlock: big.NewInt(0),
+ BerlinBlock: big.NewInt(0),
+ LondonBlock: big.NewInt(0),
+ ArrowGlacierBlock: big.NewInt(0),
+ MergeNetsplitBlock: big.NewInt(0),
+ TerminalTotalDifficulty: big.NewInt(0),
+ ShanghaiTime: u64(0),
+ CancunTime: u64(0),
+ PragueTime: u64(0),
+ OsakaTime: u64(0),
+ AmsterdamTime: u64(0),
+ BogotaTime: u64(0),
+ BPO1Time: u64(0),
+ BPO2Time: u64(0),
+ BPO3Time: u64(0),
+ BPO4Time: u64(15_000),
+ DepositContractAddress: params.MainnetChainConfig.DepositContractAddress,
+ BlobScheduleConfig: ¶ms.BlobScheduleConfig{
+ Cancun: params.DefaultCancunBlobConfig,
+ Prague: params.DefaultPragueBlobConfig,
+ Osaka: params.DefaultOsakaBlobConfig,
+ BPO1: bpo1BlobConfig,
+ BPO2: bpo2BlobConfig,
+ BPO3: params.DefaultBPO3BlobConfig,
+ BPO4: params.DefaultBPO4BlobConfig,
+ },
+ },
"Verkle": {
ChainID: big.NewInt(1),
HomesteadBlock: big.NewInt(0),
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 7525081f84..3307732163 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -302,7 +302,8 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh
}
}
- // Try to recover tx with current signer
+ // Try to recover tx with current signer.
+ // For frame transactions (EIP-8141), use TransactionToMessage
if len(post.TxBytes) != 0 {
var ttx types.Transaction
err := ttx.UnmarshalBinary(post.TxBytes)
@@ -312,6 +313,15 @@ func (t *StateTest) RunNoVerify(subtest StateSubtest, vmconfig vm.Config, snapsh
if _, err := types.Sender(types.LatestSigner(config), &ttx); err != nil {
return st, common.Hash{}, 0, err
}
+ // Frame transactions need the full tx context (frames, sig hash, etc.)
+ // which is only set by TransactionToMessage, not by the generic toMessage.
+ // This impl might change
+ if ttx.Type() == types.FrameTxType {
+ msg, err = core.TransactionToMessage(&ttx, types.LatestSigner(config), baseFee)
+ if err != nil {
+ return st, common.Hash{}, 0, err
+ }
+ }
}
// Prepare the EVM.
diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go
index a90c2d522f..17fb5e9a47 100644
--- a/tests/transaction_test_util.go
+++ b/tests/transaction_test_util.go
@@ -119,6 +119,8 @@ func (tt *TransactionTest) Run() error {
{"Shanghai", true},
{"Cancun", true},
{"Prague", true},
+ {"Amsterdam", true},
+ {"Bogota", true},
} {
expected := tt.Result[testcase.name]
if expected == nil {