Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a131831
all: implement EIP-8141 frame transactions
lightclient Jul 22, 2026
d17dc9f
core, core/vm: resolve empty signers and index default code by scope
lightclient Jul 22, 2026
cb229c5
core: renumber the skipped frame receipt status to 2
lightclient Jul 22, 2026
135bfc8
core, params: charge ARBITRARY signature entries 100 gas
lightclient Jul 22, 2026
2bbe2ce
core/types: include the mandatory costs in the frame calldata floor
lightclient Jul 22, 2026
d701b28
core/types: require canonical low-s P256 signature entries
lightclient Jul 22, 2026
8b6592a
core: credit cross-frame state-gas refunds to the transaction
lightclient Jul 22, 2026
3cd03f2
eth/catalyst: accept Bogota payloads on the Amsterdam engine methods
lightclient Jul 22, 2026
c6b7b3d
core/vm: journal the frame approval context with call snapshots
lightclient Jul 22, 2026
b72be58
core, core/types: enforce frame static constraints at validation time
lightclient Jul 22, 2026
f23a731
core, eth, miner: install the expiry verifier at the Bogota transition
lightclient Jul 22, 2026
598854f
core, core/vm: adapt the frame stack to the execution-gas rename
lightclient Aug 24, 2026
fb80d6e
core: install the expiry verifier code without touching the account
lightclient Aug 24, 2026
b239918
core, cmd/evm: account frame transaction blobs like blob transactions
lightclient Sep 7, 2026
ea43522
cmd/evm: add a state-test mode to t8n
lightclient Sep 7, 2026
31baa3c
tests: stop scheduling BPO3 and BPO4 in the Bogota fork config
lightclient Sep 7, 2026
817f512
params: align the frame transaction base cost with EIP-2780
lightclient Sep 7, 2026
a46e09a
core/vm: move the raw signature copy from SIGPARAM into SIGDATACOPY
lightclient Sep 7, 2026
0eaf092
core/types: keep VERIFY frames and approval scope out of atomic batches
lightclient Sep 7, 2026
4f23d82
core, core/types, core/vm: adopt two-dimensional gas for frame transa…
lightclient Sep 7, 2026
af7332e
core, core/vm: attribute state-gas refills to the frame that paid the…
lightclient Sep 7, 2026
63c4361
core/vm: expose the state-gas dimension through TXPARAM and FRAMEPARAM
lightclient Sep 7, 2026
b470d6d
tests: load frame transactions from state test fixtures
lightclient Sep 7, 2026
6bda3f6
core: record the expiry verifier install in the block access list
lightclient Sep 7, 2026
a00d0f1
core/txpool: admit frame transactions into the legacy pool
lightclient Sep 7, 2026
8f4289e
core/types: lead the frame transaction file with the transaction type
lightclient Sep 7, 2026
ce776c5
core/types: organize frame tx definition a bit
lightclient Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,10 @@ type rejectedTx struct {
Err string `json:"error"`
}

// Apply applies a set of transactions to a pre-state
func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, txIt txIterator, miningReward int64) (*state.StateDB, *ExecutionResult, []byte, error) {
// Apply applies a set of transactions to a pre-state. In state-test mode the
// transactions are applied without any block-level system operations,
// mirroring the reference state-test runner.
func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, txIt txIterator, miningReward int64, stateTest bool) (*state.StateDB, *ExecutionResult, []byte, error) {
// Capture errors for BLOCKHASH operation, if we haven't been supplied the
// required blockhashes
var hashError error
Expand Down Expand Up @@ -244,10 +246,10 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
misc.ApplyDAOHardFork(statedb)
}
evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig)
if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil {
if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil && !stateTest {
core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList)
}
if pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) {
if !stateTest && pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) {
var (
prevNumber = pre.Env.Number - 1
prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)]
Expand All @@ -261,7 +263,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()})
continue
}
if tx.Type() == types.BlobTxType && vmContext.BlobBaseFee == nil {
if tx.BlobGas() > 0 && vmContext.BlobBaseFee == nil {
errMsg := "blob tx used but field env.ExcessBlobGas missing"
log.Warn("rejected tx", "index", i, "hash", tx.Hash(), "error", errMsg)
rejectedTxs = append(rejectedTxs, &rejectedTx{i, errMsg})
Expand All @@ -273,9 +275,10 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()})
continue
}
txBlobGas := uint64(0)
if tx.Type() == types.BlobTxType {
txBlobGas = uint64(params.BlobTxBlobGasPerBlob * len(tx.BlobHashes()))
// Blob-carrying transactions (blob and frame txs) contribute their
// blob gas to the block's allowance.
txBlobGas := tx.BlobGas()
if txBlobGas > 0 {
max := eip4844.MaxBlobGasPerBlock(chainConfig, pre.Env.Timestamp)
if used := blobGasUsed + txBlobGas; used > max {
err := fmt.Errorf("blob gas (%d) would exceed maximum allowance %d", used, max)
Expand Down Expand Up @@ -358,16 +361,21 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
}
}

// Gather the execution-layer triggered requests.
var allLogs []*types.Log
for _, receipt := range receipts {
allLogs = append(allLogs, receipt.Logs...)
}
requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1))
if err != nil {
return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err))
// Gather the execution-layer triggered requests. A state-test
// invocation performs no system operations.
var requests [][]byte
if !stateTest {
var allLogs []*types.Log
for _, receipt := range receipts {
allLogs = append(allLogs, receipt.Logs...)
}
reqs, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1))
if err != nil {
return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err))
}
requests = reqs
blockAccessList.Merge(bal)
}
blockAccessList.Merge(bal)

// Commit block
root, err := statedb.Commit(rules, vmContext.BlockNumber.Uint64())
Expand Down
5 changes: 5 additions & 0 deletions cmd/evm/internal/t8ntool/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ var (
Usage: "Mining reward. Set to -1 to disable",
Value: 0,
}
StateTestFlag = &cli.BoolFlag{
Name: "state.test",
Usage: "Run in state-test mode: apply the transactions to the pre-state " +
"without performing any block-level system operations.",
}
ChainIDFlag = &cli.Int64Flag{
Name: "state.chainid",
Usage: "ChainID to use",
Expand Down
7 changes: 6 additions & 1 deletion cmd/evm/internal/t8ntool/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,12 @@ func Transaction(ctx *cli.Context) error {
value = uint256.NewInt(1)
}
rules := chainConfig.Rules(common.Big0, true, 0)
cost, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), r.Address, tx.To(), value, rules)
var cost uint64
if tx.Type() == types.FrameTxType {
cost, err = core.FrameTxIntrinsicGas(tx.Frames(), tx.FrameSignatures(), r.Address)
} else {
cost, err = core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), r.Address, tx.To(), value, rules)
}
if err != nil {
r.Error = err
results = append(results, r)
Expand Down
2 changes: 1 addition & 1 deletion cmd/evm/internal/t8ntool/transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func Transition(ctx *cli.Context) error {
vmConfig.Tracer = tracer.Hooks
}
// Run the test and aggregate the result
s, result, body, err := prestate.Apply(vmConfig, chainConfig, txIt, ctx.Int64(RewardFlag.Name))
s, result, body, err := prestate.Apply(vmConfig, chainConfig, txIt, ctx.Int64(RewardFlag.Name), ctx.Bool(StateTestFlag.Name))
if err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions cmd/evm/internal/t8ntool/tx_iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ func (t *txWithKey) UnmarshalJSON(input []byte) error {
func signUnsignedTransactions(txs []*txWithKey, signer types.Signer) (types.Transactions, error) {
var signedTxs []*types.Transaction
for i, tx := range txs {
if tx.tx.Type() == types.FrameTxType {
// EIP-8141 frame transactions carry an explicit sender and a
// signature entry list instead of an ECDSA signature.
if tx.key != nil {
return nil, NewError(ErrorJson, fmt.Errorf("tx %d: frame transaction cannot be signed with secretKey", i))
}
signedTxs = append(signedTxs, tx.tx)
continue
}
var (
v, r, s = tx.tx.RawSignatureValues()
signed *types.Transaction
Expand Down
1 change: 1 addition & 0 deletions cmd/evm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ var (
t8ntool.ForknameFlag,
t8ntool.ChainIDFlag,
t8ntool.RewardFlag,
t8ntool.StateTestFlag,
t8ntool.OpcodeCountFlag,
},
}
Expand Down
191 changes: 191 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4189,6 +4189,197 @@ func TestEIP7702(t *testing.T) {
}
}

// TestEIP8141TransitionInstall checks that the Bogota transition block installs
// the expiry verifier code only, records the install in the block access list
// at the pre-execution index, and that a block carrying that access list
// imports through the BAL-driven processor with the same state root the
// builder computed.
func TestEIP8141TransitionInstall(t *testing.T) {
var (
config = *params.MergedTestChainConfig
engine = beacon.New(ethash.NewFaker())
zero = uint64(0)
bogota = uint64(20) // the chain maker spaces blocks 10s apart: block 2
)
config.AmsterdamTime = &zero
config.BogotaTime = &bogota
gspec := &Genesis{Config: &config, Alloc: SystemContractAllocs()}

_, blocks, _ := GenerateChainWithGenesis(gspec, engine, 3, func(i int, b *BlockGen) {
b.SetParentBeaconRoot(common.Hash{})
})
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
defer chain.Stop()
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}

installs := func(block *types.Block) int {
count := 0
for _, access := range *block.AccessList() {
if access.Address != params.FrameTxExpiryVerifier {
continue
}
for _, change := range access.CodeChanges {
if change.BlockAccessIndex != 0 {
t.Fatalf("block %d: verifier code change at index %d, want 0", block.NumberU64(), change.BlockAccessIndex)
}
if !bytes.Equal(change.NewCode, params.FrameTxExpiryVerifierCode) {
t.Fatalf("block %d: verifier code change carries unexpected code", block.NumberU64())
}
count++
}
if len(access.NonceChanges) != 0 || len(access.BalanceChanges) != 0 {
t.Fatalf("block %d: install touched the verifier's nonce or balance", block.NumberU64())
}
}
return count
}
for i, want := range []int{0, 1, 0} {
if got := installs(blocks[i]); got != want {
t.Fatalf("block %d: %d verifier code changes in the access list, want %d", i+1, got, want)
}
}
pre, err := chain.StateAt(blocks[0].Header())
if err != nil {
t.Fatalf("failed to open pre-transition state: %v", err)
}
if code := pre.GetCode(params.FrameTxExpiryVerifier); len(code) != 0 {
t.Fatalf("verifier code present before the transition")
}
post, _ := chain.State()
if code := post.GetCode(params.FrameTxExpiryVerifier); !bytes.Equal(code, params.FrameTxExpiryVerifierCode) {
t.Fatalf("verifier code not installed after the transition")
}
if nonce := post.GetNonce(params.FrameTxExpiryVerifier); nonce != 0 {
t.Fatalf("verifier nonce wrong: expected 0, got %d", nonce)
}
}

// TestEIP8141 inserts a block with an EIP-8141 frame transaction: a VERIFY
// frame authorizes execution and payment through the default code using the
// sender's signature entry, and a SENDER frame calls a storage-writing
// contract.
func TestEIP8141(t *testing.T) {
var (
config = *params.MergedTestChainConfig
engine = beacon.New(ethash.NewFaker())
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether))
zero = uint64(0)
)
config.AmsterdamTime = &zero
config.BogotaTime = &zero
alloc := SystemContractAllocs()
alloc[addr1] = types.Account{Balance: funds}
alloc[aa] = types.Account{ // The address 0xAAAA sstores 42 into slot 42.
Code: program.New().Sstore(0x42, 0x42).Bytes(),
Nonce: 0,
Balance: big.NewInt(0),
}
// Genesis-activated Bogota networks carry the expiry verifier code in
// the genesis allocation.
alloc[params.FrameTxExpiryVerifier] = types.Account{Code: params.FrameTxExpiryVerifierCode, Balance: big.NewInt(0)}
gspec := &Genesis{
Config: &config,
Alloc: alloc,
}
signer := types.LatestSigner(&config)

frametx := &types.FrameTx{
ChainID: uint256.MustFromBig(config.ChainID),
Nonce: 0,
Sender: addr1,
Frames: []types.Frame{
{
Mode: types.ModeVerify,
Flags: types.ApproveExecutionAndPayment,
GasLimits: types.Limits{Execution: 100_000},
Value: uint256.NewInt(0),
},
{
Mode: types.ModeSender,
Target: &aa,
GasLimits: types.Limits{Execution: 300_000, State: 200_000},
Value: uint256.NewInt(0),
},
},
Signatures: types.SignatureList{{
Scheme: types.FrameTxSchemeSecp256k1,
Signer: addr1.Bytes(),
}},
Fees: types.Fees{
MaxPriorityFeePerGas: uint256.NewInt(2),
MaxFeePerGas: uint256.MustFromBig(newGwei(5)),
MaxFeePerBlobGas: uint256.NewInt(0),
},
}
// Sign the canonical signature hash with the sender key and fill in the
// signature entry as v || r || s.
sigHash := signer.Hash(types.NewTx(frametx))
sig, err := crypto.Sign(sigHash[:], key1)
if err != nil {
t.Fatalf("failed to sign frame transaction: %v", err)
}
frametx.Signatures[0].Signature = append([]byte{sig[64]}, sig[:64]...)
tx := types.NewTx(frametx)

_, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) {
// Run the EIP-4788 system call with the zero root set by the chain
// maker, so the generated access list matches block processing.
b.SetParentBeaconRoot(common.Hash{})
b.AddTx(tx)
})
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
defer chain.Stop()
if n, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}

// Verify the SENDER frame executed the storage write.
state, _ := chain.State()
var (
fortyTwo = common.BytesToHash([]byte{0x42})
actual = state.GetState(aa, fortyTwo)
)
if actual.Cmp(fortyTwo) != 0 {
t.Fatalf("aa storage wrong: expected %d, got %d", fortyTwo, actual)
}
// Verify the sender's nonce was incremented by the payment approval and
// the sender paid for the transaction.
if nonce := state.GetNonce(addr1); nonce != 1 {
t.Fatalf("sender nonce wrong: expected 1, got %d", nonce)
}
if balance := state.GetBalance(addr1); balance.CmpBig(funds) >= 0 {
t.Fatalf("sender balance not charged: %v", balance)
}
// Verify the frame transaction receipt.
receipts := chain.GetReceiptsByHash(blocks[0].Hash())
if len(receipts) != 1 {
t.Fatalf("expected 1 receipt, got %d", len(receipts))
}
receipt := receipts[0]
if receipt.Payer == nil || *receipt.Payer != addr1 {
t.Fatalf("receipt payer wrong: expected %v, got %v", addr1, receipt.Payer)
}
if len(receipt.FrameReceipts) != 2 {
t.Fatalf("expected 2 frame receipts, got %d", len(receipt.FrameReceipts))
}
for i, frameReceipt := range receipt.FrameReceipts {
if frameReceipt.Status != 1 {
t.Fatalf("frame %d status wrong: expected 1, got %d", i, frameReceipt.Status)
}
}
}

// Tests the scenario that the synchronization target in snap sync has been changed
// with a chain reorg at the tip. In this case the reorg'd segment should be unmarked
// with canonical flags.
Expand Down
7 changes: 7 additions & 0 deletions core/chain_makers.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,13 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse
evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{})
ProcessParentBlockHash(b.header.ParentHash, evm, b.bal)
}
if config.BogotaTime != nil && b.header.Time >= *config.BogotaTime && parent.Time() < *config.BogotaTime {
// EIP-8141: install the expiry verifier at the Bogota transition.
blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase)
blockContext.Random = &common.Hash{} // enable post-merge instruction set
evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{})
ProcessExpiryVerifierDeploy(evm, b.bal)
}

// Execute any user modifications to the block
if gen != nil {
Expand Down
3 changes: 3 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ var (

// -- EIP-7825 errors --
ErrGasLimitTooHigh = errors.New("transaction gas limit too high")

// -- EIP-8141 errors --
ErrFrameTxInvalidExecution = errors.New("invalid frame execution")
)

// EIP-7702 state transition errors.
Expand Down
6 changes: 6 additions & 0 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,12 @@ func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common
return s.transientStorage.Get(addr, key)
}

// ClearTransientStorage clears the transient storage between the frames of
// an EIP-8141 frame transaction.
func (s *StateDB) ClearTransientStorage() {
s.transientStorage = newTransientStorage()
}

//
// Setting, updating & deleting state object methods.
//
Expand Down
4 changes: 4 additions & 0 deletions core/state/statedb_hooked.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func (s *hookedStateDB) GetTransientState(addr common.Address, key common.Hash)
return s.inner.GetTransientState(addr, key)
}

func (s *hookedStateDB) ClearTransientStorage() {
s.inner.ClearTransientStorage()
}

func (s *hookedStateDB) SetTransientState(addr common.Address, key, value common.Hash) {
s.inner.SetTransientState(addr, key, value)
}
Expand Down
Loading
Loading