From 2859da852d974cb90e89ae976cab31852cc130f7 Mon Sep 17 00:00:00 2001 From: lightclient Date: Sat, 3 Jan 2026 11:01:25 -0700 Subject: [PATCH 1/3] all: initial sketch of 7701 --- cmd/evm/internal/t8ntool/transaction.go | 2 +- core/bench_test.go | 2 +- core/bintrie_witness_test.go | 4 +- core/state_transition.go | 121 ++++++++++-- core/txpool/validation.go | 2 +- core/types/transaction.go | 25 +++ core/types/transaction_signing.go | 18 ++ core/types/tx_abstract.go | 155 ++++++++++++++++ core/vm/contract.go | 4 + core/vm/eips.go | 233 ++++++++++++++++++++++++ core/vm/errors.go | 1 + core/vm/evm.go | 89 ++++++++- core/vm/instructions.go | 2 +- core/vm/opcodes.go | 15 ++ core/vm/roles/roles.go | 50 +++++ params/forks/forks.go | 1 + params/protocol_params.go | 4 + tests/transaction_test_util.go | 2 +- 18 files changed, 703 insertions(+), 27 deletions(-) create mode 100644 core/types/tx_abstract.go create mode 100644 core/vm/roles/roles.go diff --git a/cmd/evm/internal/t8ntool/transaction.go b/cmd/evm/internal/t8ntool/transaction.go index 0f39df0753a..e4b2dc43e9a 100644 --- a/cmd/evm/internal/t8ntool/transaction.go +++ b/cmd/evm/internal/t8ntool/transaction.go @@ -134,7 +134,7 @@ func Transaction(ctx *cli.Context) error { } // Check intrinsic gas rules := chainConfig.Rules(common.Big0, true, 0) - gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, tx.SenderAuthorization() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) if err != nil { r.Error = err results = append(results, r) diff --git a/core/bench_test.go b/core/bench_test.go index 932188f8e20..6e7b6d779ae 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -89,7 +89,7 @@ func genValueTx(nbytes int) func(int, *BlockGen) { data := make([]byte, nbytes) return func(i int, gen *BlockGen) { toaddr := common.Address{} - gas, _ := IntrinsicGas(data, nil, nil, false, false, false, false) + gas, _ := IntrinsicGas(data, nil, nil, false, false, false, false, false) signer := gen.Signer() gasPrice := big.NewInt(0) if gen.header.BaseFee != nil { diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 7704ba41fb6..9d5d5e43e52 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -63,12 +63,12 @@ var ( func TestProcessVerkle(t *testing.T) { var ( code = common.FromHex(`6060604052600a8060106000396000f360606040526008565b00`) - intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true) + intrinsicContractCreationGas, _ = IntrinsicGas(code, nil, nil, true, true, true, true, false) // A contract creation that calls EXTCODECOPY in the constructor. Used to ensure that the witness // will not contain that copied data. // Source: https://gist.github.com/gballet/a23db1e1cb4ed105616b5920feb75985 codeWithExtCodeCopy = common.FromHex(`0x60806040526040516100109061017b565b604051809103906000f08015801561002c573d6000803e3d6000fd5b506000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555034801561007857600080fd5b5060008067ffffffffffffffff8111156100955761009461024a565b5b6040519080825280601f01601f1916602001820160405280156100c75781602001600182028036833780820191505090505b50905060008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1690506020600083833c81610101906101e3565b60405161010d90610187565b61011791906101a3565b604051809103906000f080158015610133573d6000803e3d6000fd5b50600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550505061029b565b60d58061046783390190565b6102068061053c83390190565b61019d816101d9565b82525050565b60006020820190506101b86000830184610194565b92915050565b6000819050602082019050919050565b600081519050919050565b6000819050919050565b60006101ee826101ce565b826101f8846101be565b905061020381610279565b925060208210156102435761023e7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8360200360080261028e565b831692505b5050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600061028582516101d9565b80915050919050565b600082821b905092915050565b6101bd806102aa6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f566852414610030575b600080fd5b61003861004e565b6040516100459190610146565b60405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166381ca91d36040518163ffffffff1660e01b815260040160206040518083038186803b1580156100b857600080fd5b505afa1580156100cc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100f0919061010a565b905090565b60008151905061010481610170565b92915050565b6000602082840312156101205761011f61016b565b5b600061012e848285016100f5565b91505092915050565b61014081610161565b82525050565b600060208201905061015b6000830184610137565b92915050565b6000819050919050565b600080fd5b61017981610161565b811461018457600080fd5b5056fea2646970667358221220a6a0e11af79f176f9c421b7b12f441356b25f6489b83d38cc828a701720b41f164736f6c63430008070033608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063ab5ed15014602d575b600080fd5b60336047565b604051603e9190605d565b60405180910390f35b60006001905090565b6057816076565b82525050565b6000602082019050607060008301846050565b92915050565b600081905091905056fea26469706673582212203a14eb0d5cd07c277d3e24912f110ddda3e553245a99afc4eeefb2fbae5327aa64736f6c63430008070033608060405234801561001057600080fd5b5060405161020638038061020683398181016040528101906100329190610063565b60018160001c6100429190610090565b60008190555050610145565b60008151905061005d8161012e565b92915050565b60006020828403121561007957610078610129565b5b60006100878482850161004e565b91505092915050565b600061009b826100f0565b91506100a6836100f0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156100db576100da6100fa565b5b828201905092915050565b6000819050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600080fd5b610137816100e6565b811461014257600080fd5b50565b60b3806101536000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806381ca91d314602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea26469706673582212209bff7098a2f526de1ad499866f27d6d0d6f17b74a413036d6063ca6a0998ca4264736f6c63430008070033`) - intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true) + intrinsicCodeWithExtCodeCopyGas, _ = IntrinsicGas(codeWithExtCodeCopy, nil, nil, true, true, true, true, false) signer = types.LatestSigner(testVerkleChainConfig) testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") bcdb = rawdb.NewMemoryDatabase() // Database for the blockchain diff --git a/core/state_transition.go b/core/state_transition.go index bf5ac07636d..f38afdf4047 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -26,6 +26,7 @@ import ( "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/core/vm/roles" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -68,11 +69,13 @@ func (result *ExecutionResult) Revert() []byte { } // IntrinsicGas computes the 'intrinsic gas' for a message with the given data. -func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860 bool) (uint64, error) { +func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.SetCodeAuthorization, isContractCreation, isHomestead, isEIP2028, isEIP3860 bool, isAbstract bool) (uint64, error) { // Set the starting gas for the raw transaction var gas uint64 if isContractCreation && isHomestead { gas = params.TxGasContractCreation + } else if isAbstract { + gas = params.TxGasAbstract } else { gas = params.TxGas } @@ -157,6 +160,15 @@ type Message struct { BlobHashes []common.Hash SetCodeAuthorizations []types.SetCodeAuthorization + Abstract bool + Sender *types.AbstractAuthorization + Deployer *types.AbstractAuthorization + Paymaster *types.PaymasterAuthorization + Payer common.Address + + AllData []byte + TotalGasLimit uint64 + // When SkipNonceChecks is true, the message nonce is not checked against the // account nonce in state. // @@ -189,6 +201,14 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In SkipTransactionChecks: false, BlobHashes: tx.BlobHashes(), BlobGasFeeCap: tx.BlobGasFeeCap(), + + Abstract: tx.SenderAuthorization() != nil, + Sender: tx.SenderAuthorization(), + Deployer: tx.DeployerAuthorization(), + Paymaster: tx.PaymasterAuthorization(), + + AllData: common.CopyBytes(tx.Data()), + TotalGasLimit: tx.Gas(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -199,6 +219,27 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In } var err error msg.From, err = types.Sender(s, tx) + + msg.Payer = msg.From + if msg.Abstract { + msg.Payer = msg.Paymaster.Target + msg.SkipNonceChecks = true + } + + // Sum all gas limits if AA. + if msg.Abstract { + msg.AllData = append(msg.AllData, msg.Sender.Data...) + msg.TotalGasLimit += msg.Sender.Gas + if msg.Deployer != nil { + msg.AllData = append(msg.AllData, msg.Deployer.Data...) + msg.TotalGasLimit += msg.Deployer.Gas + } + if msg.Paymaster != nil { + msg.AllData = append(msg.AllData, msg.Paymaster.Data...) + msg.TotalGasLimit += msg.Paymaster.Gas + } + } + return msg, err } @@ -264,11 +305,11 @@ func (st *stateTransition) to() common.Address { } func (st *stateTransition) buyGas() error { - mgval := new(big.Int).SetUint64(st.msg.GasLimit) + mgval := new(big.Int).SetUint64(st.msg.TotalGasLimit) mgval.Mul(mgval, st.msg.GasPrice) balanceCheck := new(big.Int).Set(mgval) if st.msg.GasFeeCap != nil { - balanceCheck.SetUint64(st.msg.GasLimit) + balanceCheck.SetUint64(st.msg.TotalGasLimit) balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap) } balanceCheck.Add(balanceCheck, st.msg.Value) @@ -289,21 +330,21 @@ func (st *stateTransition) buyGas() error { if overflow { return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) } - if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { - return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) + if have, want := st.state.GetBalance(st.msg.Payer), balanceCheckU256; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.Payer.Hex(), have, want) } - if err := st.gp.SubGas(st.msg.GasLimit); err != nil { + if err := st.gp.SubGas(st.msg.TotalGasLimit); err != nil { return err } if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil { - st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance) + st.evm.Config.Tracer.OnGasChange(0, st.msg.TotalGasLimit, tracing.GasChangeTxInitialBalance) } - st.gasRemaining = st.msg.GasLimit + st.gasRemaining = st.msg.TotalGasLimit - st.initialGas = st.msg.GasLimit + st.initialGas = st.msg.TotalGasLimit mgvalU256, _ := uint256.FromBig(mgval) - st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) + st.state.SubBalance(st.msg.Payer, mgvalU256, tracing.BalanceDecreaseGasBuy) return nil } @@ -327,8 +368,8 @@ func (st *stateTransition) preCheck() error { isOsaka := st.evm.ChainConfig().IsOsaka(st.evm.Context.BlockNumber, st.evm.Context.Time) if !msg.SkipTransactionChecks { // Verify tx gas limit does not exceed EIP-7825 cap. - if isOsaka && msg.GasLimit > params.MaxTxGas { - return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.GasLimit) + if isOsaka && msg.TotalGasLimit > params.MaxTxGas { + return fmt.Errorf("%w (cap: %d, tx: %d)", ErrGasLimitTooHigh, params.MaxTxGas, msg.TotalGasLimit) } // Make sure the sender is an EOA code := st.state.GetCode(msg.From) @@ -443,7 +484,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { ) // Check clauses 4-5, subtract intrinsic gas if everything is correct - gas, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + gas, err := IntrinsicGas(msg.AllData, msg.AccessList, msg.SetCodeAuthorizations, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, msg.Abstract) if err != nil { return nil, err } @@ -452,11 +493,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Gas limit suffices for the floor data cost (EIP-7623) if rules.IsPrague { - floorDataGas, err = FloorDataGas(msg.Data) + floorDataGas, err = FloorDataGas(msg.AllData) if err != nil { return nil, err } - if msg.GasLimit < floorDataGas { + if msg.TotalGasLimit < floorDataGas { return nil, fmt.Errorf("%w: have %d, want %d", ErrFloorDataGas, msg.GasLimit, floorDataGas) } } @@ -471,6 +512,14 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { if targetAddr := msg.To; targetAddr != nil { st.evm.AccessEvents.AddTxDestination(*targetAddr, msg.Value.Sign() != 0, !st.state.Exist(*targetAddr)) } + if msg.Deployer != nil { + target := msg.Deployer.Target + st.evm.AccessEvents.AddTxDestination(target, false, !st.state.Exist(target)) + } + if msg.Paymaster != nil { + target := msg.Paymaster.Target + st.evm.AccessEvents.AddTxDestination(target, false, !st.state.Exist(target)) + } } // Check clause 6 @@ -492,12 +541,44 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // - reset transient storage(eip 1153) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) + // Run the abstract validations: + // - account deployment, in case the user's account does not yet exist + // - standard validation, i.e. nonce and authorization checks + // - paymaster validation, when the user operation is sponsored + if msg.Abstract { + // Attempt to deploy user's smart account. + if msg.Deployer != nil && len(st.state.GetCode(msg.From)) != 0 { + remaining, err := st.evm.CallWithRole(msg.Deployer.Target, msg.Deployer.Gas, roles.SenderDeployment) + if err != nil { + return nil, fmt.Errorf("deployer failed: %v", err) + } + st.gasRemaining += remaining + } + + // Validate abstract tx. + remaining, err := st.evm.CallWithRole(msg.Sender.Target, msg.Sender.Gas, roles.SenderValidation) + if err != nil { + return nil, fmt.Errorf("sender validation failed: %v", err) + } + st.gasRemaining += remaining + + // Validate paymaster promise to sponsor. + if msg.Paymaster != nil { + remaining, err := st.evm.CallWithRole(msg.Paymaster.Target, msg.Paymaster.Gas, roles.PaymasterValidation) + if err != nil { + return nil, fmt.Errorf("paymaster failed: %v", err) + } + st.gasRemaining += remaining + } + } + var ( - ret []byte - vmerr error // vm errors do not effect consensus and are therefore not assigned to err + ret []byte + vmerr error // vm errors do not effect consensus and are therefore not assigned to err + remaining uint64 ) if contractCreation { - ret, _, st.gasRemaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) + ret, _, remaining, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining, value) } else { // Increment the nonce for the next transaction. st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) @@ -520,9 +601,11 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // Execute the transaction's call. - ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) + ret, remaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value) } + st.gasRemaining += remaining + // Record the gas used excluding gas refunds. This value represents the actual // gas allowance required to complete execution. peakGasUsed := st.gasUsed() diff --git a/core/txpool/validation.go b/core/txpool/validation.go index 4f985a8bd06..066a24cc242 100644 --- a/core/txpool/validation.go +++ b/core/txpool/validation.go @@ -122,7 +122,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types } // Ensure the transaction has more gas than the bare minimum needed to cover // the transaction metadata - intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai) + intrGas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, true, rules.IsIstanbul, rules.IsShanghai, tx.SenderAuthorization() == nil) if err != nil { return err } diff --git a/core/types/transaction.go b/core/types/transaction.go index 6af960b8c3e..51f39eb98f9 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -50,6 +50,7 @@ const ( DynamicFeeTxType = 0x02 BlobTxType = 0x03 SetCodeTxType = 0x04 + AbstractTxType = 0x06 // skip 0x05 since it magic for set code tx ) // Transaction is an Ethereum transaction. @@ -535,6 +536,30 @@ func (tx *Transaction) SetCodeAuthorities() []common.Address { return auths } +func (tx *Transaction) SenderAuthorization() *AbstractAuthorization { + abstracttx, ok := tx.inner.(*AbstractTx) + if !ok { + return nil + } + return abstracttx.Sender.copy() +} + +func (tx *Transaction) DeployerAuthorization() *AbstractAuthorization { + abstracttx, ok := tx.inner.(*AbstractTx) + if !ok { + return nil + } + return abstracttx.Deployer.copy() +} + +func (tx *Transaction) PaymasterAuthorization() *PaymasterAuthorization { + abstracttx, ok := tx.inner.(*AbstractTx) + if !ok { + return nil + } + return abstracttx.Paymaster.copy() +} + // SetTime sets the decoding time of a transaction. This is used by tests to set // arbitrary times and by persistent transaction pools when loading old txs from // disk. diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index ef8fb194d5f..98bc7f730a3 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -231,6 +231,9 @@ func newModernSigner(chainID *big.Int, fork forks.Fork) Signer { if fork >= forks.Prague { s.txtypes.set(SetCodeTxType) } + if fork >= forks.Bogota { + s.txtypes.set(AbstractTxType) + } return s } @@ -262,6 +265,9 @@ func (s *modernSigner) Sender(tx *Transaction) (common.Address, error) { if tx.ChainId().Cmp(s.chainID) != 0 { return common.Address{}, fmt.Errorf("%w: have %d want %d", ErrInvalidChainId, tx.ChainId(), s.chainID) } + if tt == AbstractTxType { + return tx.SenderAuthorization().Target, nil + } // 'modern' txs are defined to use 0 and 1 as their recovery // id, add 27 to become equivalent to unprotected Homestead signatures. V, R, S := tx.RawSignatureValues() @@ -287,6 +293,18 @@ func (s *modernSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *bi return R, S, V, nil } +// NewBogotaSigner returns a signer that accepts +// - EIP-7701 abstraction transactions +// - EIP-7702 set code transactions +// - EIP-4844 blob transactions +// - EIP-1559 dynamic fee transactions +// - EIP-2930 access list transactions, +// - EIP-155 replay protected transactions, and +// - legacy Homestead transactions. +func NewBogotaSigner(chainId *big.Int) Signer { + return newModernSigner(chainId, forks.Bogota) +} + // NewPragueSigner returns a signer that accepts // - EIP-7702 set code transactions // - EIP-4844 blob transactions diff --git a/core/types/tx_abstract.go b/core/types/tx_abstract.go new file mode 100644 index 00000000000..074f1df5f79 --- /dev/null +++ b/core/types/tx_abstract.go @@ -0,0 +1,155 @@ +// Copyright 2026 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 types + +import ( + "bytes" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" +) + +type AbstractAuthorization struct { + Target common.Address + Data []byte + Gas uint64 +} + +func (a *AbstractAuthorization) copy() *AbstractAuthorization { + if a == nil { + return nil + } + return &AbstractAuthorization{ + Target: a.Target, + Data: common.CopyBytes(a.Data), + Gas: a.Gas, + } +} + +type PaymasterAuthorization struct { + Target common.Address + Data []byte + Gas uint64 + PostOpGas uint64 +} + +func (a *PaymasterAuthorization) copy() *PaymasterAuthorization { + if a == nil { + return nil + } + return &PaymasterAuthorization{ + Target: a.Target, + Data: common.CopyBytes(a.Data), + Gas: a.Gas, + PostOpGas: a.PostOpGas, + } +} + +type AbstractTx struct { + ChainID *uint256.Int + Nonce uint64 + + Sender AbstractAuthorization + Deployer *AbstractAuthorization + Paymaster *PaymasterAuthorization + + Data []byte // FromExecutionData + Gas uint64 + + GasTipCap *uint256.Int // a.k.a. maxPriorityFeePerGas + GasFeeCap *uint256.Int // a.k.a. maxFeePerGas + + AccessList AccessList + AuthList []SetCodeAuthorization +} + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *AbstractTx) copy() TxData { + cpy := &AbstractTx{ + Nonce: tx.Nonce, + Sender: *tx.Sender.copy(), + Deployer: tx.Deployer.copy(), + Paymaster: tx.Paymaster.copy(), + Data: common.CopyBytes(tx.Data), + Gas: tx.Gas, + + // These are copied below. + AccessList: make(AccessList, len(tx.AccessList)), + AuthList: make([]SetCodeAuthorization, len(tx.AuthList)), + ChainID: new(uint256.Int), + GasTipCap: new(uint256.Int), + GasFeeCap: new(uint256.Int), + } + copy(cpy.AccessList, tx.AccessList) + copy(cpy.AuthList, tx.AuthList) + if tx.ChainID != nil { + cpy.ChainID.Set(tx.ChainID) + } + if tx.GasTipCap != nil { + cpy.GasTipCap.Set(tx.GasTipCap) + } + if tx.GasFeeCap != nil { + cpy.GasFeeCap.Set(tx.GasFeeCap) + } + return cpy +} + +// accessors for innerTx. +func (tx *AbstractTx) txType() byte { return AbstractTxType } +func (tx *AbstractTx) chainID() *big.Int { return tx.ChainID.ToBig() } +func (tx *AbstractTx) accessList() AccessList { return tx.AccessList } +func (tx *AbstractTx) data() []byte { return tx.Data } +func (tx *AbstractTx) gas() uint64 { return tx.Gas } +func (tx *AbstractTx) gasFeeCap() *big.Int { return tx.GasFeeCap.ToBig() } +func (tx *AbstractTx) gasTipCap() *big.Int { return tx.GasTipCap.ToBig() } +func (tx *AbstractTx) gasPrice() *big.Int { return tx.GasFeeCap.ToBig() } +func (tx *AbstractTx) value() *big.Int { return big.NewInt(0) } +func (tx *AbstractTx) nonce() uint64 { return tx.Nonce } +func (tx *AbstractTx) to() *common.Address { return nil } // TODO: is returning nil here correct? + +func (tx *AbstractTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { + if baseFee == nil { + return dst.Set(tx.GasFeeCap.ToBig()) + } + tip := dst.Sub(tx.GasFeeCap.ToBig(), baseFee) + if tip.Cmp(tx.GasTipCap.ToBig()) > 0 { + tip.Set(tx.GasTipCap.ToBig()) + } + return tip.Add(tip, baseFee) +} + +func (tx *AbstractTx) rawSignatureValues() (v, r, s *big.Int) { + panic("abstract tx does not have signature") +} + +func (tx *AbstractTx) setSignatureValues(chainID, v, r, s *big.Int) { + panic("abstract tx does not have signature") +} + +func (tx *AbstractTx) encode(b *bytes.Buffer) error { + return rlp.Encode(b, tx) +} + +func (tx *AbstractTx) decode(input []byte) error { + return rlp.DecodeBytes(input, tx) +} + +func (tx *AbstractTx) sigHash(chainID *big.Int) common.Hash { + panic("abstract tx does not have signature") +} diff --git a/core/vm/contract.go b/core/vm/contract.go index 165ca833f88..001d8f0cb05 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -19,6 +19,7 @@ package vm import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm/roles" "github.com/holiman/uint256" ) @@ -42,6 +43,8 @@ type Contract struct { IsDeployment bool IsSystemCall bool + Role roles.Role + Gas uint64 value *uint256.Int } @@ -58,6 +61,7 @@ func NewContract(caller common.Address, address common.Address, value *uint256.I jumpDests: jumpDests, Gas: gas, value: value, + Role: roles.SenderExecution, } } diff --git a/core/vm/eips.go b/core/vm/eips.go index dfcac4b9302..03d7f642c2c 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm/roles" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) @@ -40,6 +41,7 @@ var activators = map[int]func(*JumpTable){ 1344: enable1344, 1153: enable1153, 4762: enable4762, + 7701: enable7701, 7702: enable7702, 7939: enable7939, 8024: enable8024, @@ -579,3 +581,234 @@ func enable7702(jt *JumpTable) { jt[STATICCALL].dynamicGas = gasStaticCallEIP7702 jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP7702 } + +// opCurrentRole pushes the current execution role to the stack. +func opCurrentRole(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + role := uint256.NewInt(uint64(scope.Contract.Role)) + scope.Stack.push(role) + return nil, nil +} + +// opAcceptRole accepts the provided role from the stack. +func opAcceptRole(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + // TODO: should this return data also? What will consume it? + role := scope.Stack.pop() + evm.accepted = roles.Role(role.Uint64()) + return nil, errStopToken +} + +type paramLoader func(*EVM, uint64) []byte + +func loadType(evm *EVM, id uint64) []byte { + // TODO: plumb in type to TxContext + return nil +} + +func loadNonce(evm *EVM, id uint64) []byte { + // TODO: plumb in nonce to TxContext + return nil +} + +func loadSender(evm *EVM, id uint64) []byte { + return nil +} + +func loadSenderValidationData(evm *EVM, id uint64) []byte { + return nil +} + +func loadDeployer(evm *EVM, id uint64) []byte { + return nil +} + +func loadDeployerData(evm *EVM, id uint64) []byte { + return nil +} + +func loadPaymaster(evm *EVM, id uint64) []byte { + return nil +} + +func loadPaymasterData(evm *EVM, id uint64) []byte { + return nil +} + +func loadSenderExecutionData(evm *EVM, id uint64) []byte { + return nil +} + +func loadMaxPriorityFeePerGas(evm *EVM, id uint64) []byte { + return nil +} + +func loadMaxFeePerGas(evm *EVM, id uint64) []byte { + return nil +} + +func loadPaymasterValidationGas(evm *EVM, id uint64) []byte { + return nil +} + +func loadSenderExecutionGas(evm *EVM, id uint64) []byte { + return nil +} + +func loadPaymasterPostOpGas(evm *EVM, id uint64) []byte { + return nil +} + +func loadAccessListHash(evm *EVM, id uint64) []byte { + return nil +} + +func loadAuthorizationListHash(evm *EVM, id uint64) []byte { + return nil +} + +func loadExecutionStatus(evm *EVM, id uint64) []byte { + return nil +} + +func loadExecutionGasUsed(evm *EVM, id uint64) []byte { + return nil +} + +func loadTxHashForSignature(evm *EVM, id uint64) []byte { + return nil +} + +// idToParam is a map of identifier to transaction parameter loader. +var idToParam = map[uint64]paramLoader{ + 0x00: loadType, + 0x01: loadNonce, + 0x02: loadSender, + 0x03: loadSenderValidationData, + 0x04: loadDeployer, + 0x05: loadDeployerData, + 0x06: loadPaymaster, + 0x07: loadPaymasterData, + 0x08: loadSenderExecutionData, + 0x0B: loadMaxPriorityFeePerGas, + 0x0C: loadMaxFeePerGas, + 0x0D: loadSenderValidationData, + 0x0E: loadPaymasterValidationGas, + 0x0F: loadSenderExecutionGas, + 0x10: loadPaymasterPostOpGas, + 0x11: loadAccessListHash, + 0x12: loadAuthorizationListHash, + 0xf1: loadExecutionStatus, + 0xf2: loadExecutionGasUsed, + 0xff: loadTxHashForSignature, +} + +// opTxParamLoad will load the 32-byte word of a transaction parameter at the +// specified index. +func opTxParamLoad(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + var ( + stack = scope.Stack + idx = stack.pop() + id = stack.pop() + get paramLoader + ok bool + ) + id64, overflow := id.Uint64WithOverflow() + if get, ok = idToParam[id64]; !ok || overflow { + return nil, ErrTxParamIdInvalid + } + + param := get(evm, id.Uint64()) + value := new(uint256.Int) + idx64, overflow := idx.Uint64WithOverflow() + if !overflow { + value.SetBytes(getData(param, idx64, 32)) + } + stack.push(value) + return nil, nil +} + +// opTxParamSize loads the specified transaction parameter then computes its +// size and pushes the result to the stack. +func opTxParamSize(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + var ( + stack = scope.Stack + id = stack.peek() + get paramLoader + ok bool + ) + id64, overflow := id.Uint64WithOverflow() + if get, ok = idToParam[id64]; !ok || overflow { + return nil, ErrTxParamIdInvalid + } + + param := get(evm, id.Uint64()) + stack.push(id.SetUint64(uint64(len(param)))) + + return nil, nil +} + +// opTxParamCopy copies the specified transaction parameter into memory. +func opTxParamCopy(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { + var ( + stack = scope.Stack + memOffset = scope.Stack.pop() + dataOffset = scope.Stack.pop() + length = scope.Stack.pop() + id = stack.pop() + get paramLoader + ok bool + ) + + id64, overflow := id.Uint64WithOverflow() + if get, ok = idToParam[id64]; !ok || overflow { + return nil, ErrTxParamIdInvalid + } + param := get(evm, id.Uint64()) + + dataOffset64, overflow := dataOffset.Uint64WithOverflow() + if overflow { + dataOffset64 = math.MaxUint64 + } + + // These values are checked for overflow during gas cost calculation + memOffset64 := memOffset.Uint64() + length64 := length.Uint64() + scope.Memory.Set(memOffset64, length64, getData(param, dataOffset64, length64)) + return nil, nil +} + +// enable7701 enables the EIP-7701 changes to support native account +// abstraction. +func enable7701(jt *JumpTable) { + jt[CURRENTROLE] = &operation{ + execute: opCurrentRole, + constantGas: GasQuickStep, + minStack: minStack(0, 1), + maxStack: maxStack(0, 1), + } + jt[ACCEPTROLE] = &operation{ + execute: opAcceptRole, + constantGas: GasQuickStep, + minStack: minStack(1, 0), + maxStack: maxStack(1, 0), + } + jt[TXPARAMLOAD] = &operation{ + execute: opTxParamLoad, + constantGas: GasFastestStep, + minStack: minStack(2, 1), + maxStack: maxStack(2, 1), + } + jt[TXPARAMSIZE] = &operation{ + execute: opTxParamSize, + constantGas: GasQuickStep, + minStack: minStack(1, 1), + maxStack: maxStack(1, 1), + } + jt[TXPARAMCOPY] = &operation{ + execute: opTxParamCopy, + constantGas: GasFastestStep, + dynamicGas: gasCallDataCopy, + minStack: minStack(4, 0), + maxStack: maxStack(4, 0), + memorySize: memoryCallDataCopy, + } +} diff --git a/core/vm/errors.go b/core/vm/errors.go index e33c9fcb853..48526e6082e 100644 --- a/core/vm/errors.go +++ b/core/vm/errors.go @@ -38,6 +38,7 @@ var ( ErrGasUintOverflow = errors.New("gas uint64 overflow") ErrInvalidCode = errors.New("invalid code: must not begin with 0xef") ErrNonceUintOverflow = errors.New("nonce uint64 overflow") + ErrTxParamIdInvalid = errors.New("tx param id invalid") // errStopToken is an internal token indicating interpreter loop termination, // never returned to outside callers. diff --git a/core/vm/evm.go b/core/vm/evm.go index 8975c791c84..913ceb9fb1d 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -18,6 +18,7 @@ package vm import ( "errors" + "fmt" "math/big" "sync/atomic" @@ -25,6 +26,7 @@ import ( "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/roles" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -77,6 +79,8 @@ type TxContext struct { BlobHashes []common.Hash // Provides information for BLOBHASH BlobFeeCap *big.Int // Is used to zero the blobbasefee if NoBaseFee is set AccessEvents *state.AccessEvents // Capture all state accesses for this tx + + // TODO: add EIP-7701 fields } // EVM is the Ethereum Virtual Machine base object and provides @@ -130,6 +134,8 @@ type EVM struct { readOnly bool // Whether to throw on stateful modifications returnData []byte // Last CALL's return data for subsequent reuse + + accepted roles.Role // Last CALL's accepted role, if any } // NewEVM constructs an EVM instance with the supplied block context, state @@ -317,6 +323,86 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g return ret, gas, err } +// TODO: should this function take the data input? +func (evm *EVM) CallWithRole(addr common.Address, gas uint64, role roles.Role) (leftOverGas uint64, err error) { + // Capture the tracer start/end events in debug mode + var ret []byte + if evm.Config.Tracer != nil { + evm.captureBegin(evm.depth, CALL, params.AAEntryPoint, addr, nil, gas, common.Big0) + defer func(startGas uint64) { + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + }(gas) + } + // Fail if we're trying to execute above the call depth limit + if evm.depth > int(params.CallCreateDepth) { + return 0, ErrDepth + } + snapshot := evm.StateDB.Snapshot() + p, isPrecompile := evm.precompile(addr) + + if !evm.StateDB.Exist(addr) { + if !isPrecompile && evm.chainRules.IsEIP4762 { + // Add proof of absence to witness + // At this point, the read costs have already been charged, either because this + // is a direct tx call, in which case it's covered by the intrinsic gas, or because + // of a CALL instruction, in which case BASIC_DATA has been added to the access + // list in write mode. If there is enough gas paying for the addition of the code + // hash leaf to the access list, then account creation will proceed unimpaired. + // Thus, only pay for the creation of the code hash leaf here. + wgas := evm.AccessEvents.CodeHashGas(addr, true, gas, false) + if gas < wgas { + evm.StateDB.RevertToSnapshot(snapshot) + return gas, ErrOutOfGas + } + gas -= wgas + } + + if !isPrecompile && evm.chainRules.IsEIP158 { + // Calling a non-existing account, don't do anything. + return 0, nil + } + evm.StateDB.CreateAccount(addr) + } + + if isPrecompile { + ret, gas, err = RunPrecompiledContract(p, nil, gas, evm.Config.Tracer) + } else { + // Initialise a new contract and set the code that is to be used by the EVM. + code := evm.resolveCode(addr) + if len(code) == 0 { + ret, err = nil, nil // gas is unchanged + } else { + // The contract is a scoped environment for this execution context only. + contract := NewContract(params.AAEntryPoint, addr, common.U2560, gas, evm.jumpDests) + contract.IsSystemCall = false + contract.Role = role + contract.SetCallCode(evm.resolveCodeHash(addr), code) + ret, err = evm.Run(contract, nil, false) + gas = contract.Gas + } + } + // When an error was returned by the EVM or when setting the creation code + // above we revert to the snapshot and consume any gas remaining. Additionally, + // when we're in homestead this also counts for code storage gas errors. + if err != nil { + evm.StateDB.RevertToSnapshot(snapshot) + if err != ErrExecutionReverted { + if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil { + evm.Config.Tracer.OnGasChange(gas, 0, tracing.GasChangeCallFailedExecution) + } + + gas = 0 + } + // TODO: consider clearing up unused snapshots: + //} else { + // evm.StateDB.DiscardSnapshot(snapshot) + } + if evm.accepted != role { + return gas, fmt.Errorf("role %s not accepted, got %s", role, evm.accepted) + } + return gas, nil +} + // CallCode executes the contract associated with the addr with the given input // as parameters. It also handles any necessary value transfer required and takes // the necessary steps to create accounts and reverses the state in case of an @@ -373,7 +459,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // // DelegateCall differs from CallCode in the sense that it executes the given address' // code with the caller as context and the caller is set to the caller of the caller. -func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { +func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, addr common.Address, input []byte, gas uint64, value *uint256.Int, role roles.Role) (ret []byte, leftOverGas uint64, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { // DELEGATECALL inherits value from parent call @@ -397,6 +483,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, // Note: The value refers to the original value from the parent call. contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) + contract.Role = role ret, err = evm.Run(contract, input, false) gas = contract.Gas } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 6b04a2daff7..db96b4fc728 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -812,7 +812,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { // Get arguments from the memory. args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64()) - ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, gas, scope.Contract.value) + ret, returnGas, err := evm.DelegateCall(scope.Contract.Caller(), scope.Contract.Address(), toAddr, args, gas, scope.Contract.value, scope.Contract.Role) if err != nil { temp.Clear() } else { diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 9a32126a80b..0b57cbce564 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -220,6 +220,15 @@ const ( DATACOPY OpCode = 0xd3 ) +// EIP-7701 operations. +const ( + TXPARAMLOAD OpCode = 0xdb + TXPARAMSIZE OpCode = 0xdc + TXPARAMCOPY OpCode = 0xdd + CURRENTROLE OpCode = 0xde + ACCEPTROLE OpCode = 0xdf +) + // 0xe0 range - eof operations. const ( RJUMP OpCode = 0xe0 @@ -422,6 +431,10 @@ var opCodeToString = [256]string{ DATASIZE: "DATASIZE", DATACOPY: "DATACOPY", + // EIP-7701 ops. + CURRENTROLE: "CURRENTROLE", + ACCEPTROLE: "ACCEPTROLE", + // 0xe0 range. RJUMP: "RJUMP", RJUMPI: "RJUMPI", @@ -608,6 +621,8 @@ var stringToOp = map[string]OpCode{ "DATALOADN": DATALOADN, "DATASIZE": DATASIZE, "DATACOPY": DATACOPY, + "CURRENTROLE": CURRENTROLE, + "ACCEPTROLE": ACCEPTROLE, "RJUMP": RJUMP, "RJUMPI": RJUMPI, "RJUMPV": RJUMPV, diff --git a/core/vm/roles/roles.go b/core/vm/roles/roles.go new file mode 100644 index 00000000000..62dc5b259be --- /dev/null +++ b/core/vm/roles/roles.go @@ -0,0 +1,50 @@ +// +// 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 roles + +import "fmt" + +type Role byte + +const ( + SenderDeployment = 0xA0 + SenderValidation = 0xA1 + PaymasterValidation = 0xA2 + SenderExecution = 0xA3 + PaymasterPostOp = 0xA4 +) + +func (r Role) String() string { + if s := roleToString[r]; s != "" { + return s + } + return fmt.Sprintf("role %#x not defined", int(r)) +} + +var roleToString = map[Role]string{ + SenderDeployment: "SENDER_DEPLOYMENT", + SenderValidation: "SENDER_VALIDATION", + PaymasterValidation: "PAYMASTER_VALIDATION", + SenderExecution: "SENDER_EXECUTION", + PaymasterPostOp: "PAYMASTER_POST_OP", +} + +// var stringToRole = map[string]Role{ +// "SENDER_DEPLOYMENT": SenderDeployment, +// "SENDER_VALIDATION": SenderValidation, +// "PAYMASTER_VALIDATION": PaymasterValidation, +// "SENDER_EXECUTION": SenderExecution, +// "PAYMASTER_POST_OP": PaymasterPostOp, +// } diff --git a/params/forks/forks.go b/params/forks/forks.go index 641d59434b7..1484bdbc6aa 100644 --- a/params/forks/forks.go +++ b/params/forks/forks.go @@ -46,6 +46,7 @@ const ( BPO4 BPO5 Amsterdam + Bogota ) // String implements fmt.Stringer. diff --git a/params/protocol_params.go b/params/protocol_params.go index bb506af015c..3147a9a20bd 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -37,6 +37,7 @@ const ( CallNewAccountGas uint64 = 25000 // Paid for CALL when the destination address didn't exist prior. TxGas uint64 = 21000 // Per transaction not creating a contract. NOTE: Not payable on data of calls between transactions. TxGasContractCreation uint64 = 53000 // Per transaction that creates a contract. NOTE: Not payable on data of calls between transactions. + TxGasAbstract uint64 = 15000 // Per transaction that has abstract validation. TxDataZeroGas uint64 = 4 // Per byte of data attached to a transaction that equals zero. NOTE: Not payable on data of calls between transactions. QuadCoeffDiv uint64 = 512 // Divisor for the quadratic particle of the memory cost equation. LogDataGas uint64 = 8 // Per byte in a LOG* operation's data. @@ -204,6 +205,9 @@ var ( // SystemAddress is where the system-transaction is sent from as per EIP-4788 SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe") + // AAEntryPoint is the default caller for top level AbstractTx frames. + AAEntryPoint = common.HexToAddress("0x7701") + // EIP-4788 - Beacon block root in the EVM BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02") BeaconRootsCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500") diff --git a/tests/transaction_test_util.go b/tests/transaction_test_util.go index a90c2d522f6..484aa94f131 100644 --- a/tests/transaction_test_util.go +++ b/tests/transaction_test_util.go @@ -81,7 +81,7 @@ func (tt *TransactionTest) Run() error { return } // Intrinsic gas - requiredGas, err = core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) + requiredGas, err = core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai, tx.SenderAuthorization() == nil) if err != nil { return } From c051a69bfb5406bbf031ccdcba2167aad62b1905 Mon Sep 17 00:00:00 2001 From: lightclient Date: Sat, 3 Jan 2026 11:18:03 -0700 Subject: [PATCH 2/3] core/types: add docs for abstract tx --- core/types/tx_abstract.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/types/tx_abstract.go b/core/types/tx_abstract.go index 074f1df5f79..c19ee5dce6d 100644 --- a/core/types/tx_abstract.go +++ b/core/types/tx_abstract.go @@ -25,6 +25,8 @@ import ( "github.com/holiman/uint256" ) +// AbstractAuthorization is the input used to abstractly validate +// authorizations. type AbstractAuthorization struct { Target common.Address Data []byte @@ -42,6 +44,8 @@ func (a *AbstractAuthorization) copy() *AbstractAuthorization { } } +// PaymasterAuthorization includes the neccessary data to validate an +// authorization from a paymaster to sponsor the transaction. type PaymasterAuthorization struct { Target common.Address Data []byte @@ -61,6 +65,8 @@ func (a *PaymasterAuthorization) copy() *PaymasterAuthorization { } } +// AbstractTx implements the EIP-7701 transaction type that allows arbitrary +// implementation of sender validation and paymaster validation. type AbstractTx struct { ChainID *uint256.Int Nonce uint64 From 4e1fc9bc25891311e6351456d147001b079a9936 Mon Sep 17 00:00:00 2001 From: lightclient Date: Mon, 5 Jan 2026 06:55:18 -0700 Subject: [PATCH 3/3] core: extend TxContext to have all data related to TXPARAM* family --- core/evm.go | 44 ++++++++++++++++++++ core/state_transition.go | 2 + core/vm/eips.go | 90 ++++++++++++++++++++++++++++++---------- core/vm/evm.go | 24 ++++++++++- core/vm/runtime/env.go | 6 +++ 5 files changed, 143 insertions(+), 23 deletions(-) diff --git a/core/evm.go b/core/evm.go index 18d940fdd22..594d8d26d2b 100644 --- a/core/evm.go +++ b/core/evm.go @@ -25,6 +25,8 @@ import ( "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/crypto" + "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" ) @@ -82,13 +84,55 @@ func NewEVMTxContext(msg *Message) vm.TxContext { Origin: msg.From, GasPrice: new(big.Int).Set(msg.GasPrice), BlobHashes: msg.BlobHashes, + + // EIP-7701 fields + TxType: msg.TxType, + Nonce: msg.Nonce, + Sender: msg.Sender, + Deployer: msg.Deployer, + Paymaster: msg.Paymaster, + SenderExecutionData: msg.Data, + SenderExecutionGas: msg.GasLimit, } if msg.BlobGasFeeCap != nil { ctx.BlobFeeCap = new(big.Int).Set(msg.BlobGasFeeCap) } + if msg.GasTipCap != nil { + ctx.MaxPriorityFeePerGas = new(big.Int).Set(msg.GasTipCap) + } + if msg.GasFeeCap != nil { + ctx.MaxFeePerGas = new(big.Int).Set(msg.GasFeeCap) + } + + // Compute AccessListHash + if len(msg.AccessList) > 0 { + ctx.AccessListHash = rlpHash(msg.AccessList) + } + + // Compute AuthorizationListHash + if len(msg.SetCodeAuthorizations) > 0 { + ctx.AuthorizationListHash = rlpHash(msg.SetCodeAuthorizations) + } + + // TxHashForSignature is nil for AA transactions + // For non-AA transactions, we leave it nil as we don't have access to the + // original transaction here. If needed, this should be set by the caller. + if !msg.Abstract { + // TODO: compute TxHashForSignature for non-AA transactions if needed + } + return ctx } +// rlpHash computes the keccak256 hash of the RLP encoding of x. +func rlpHash(x interface{}) common.Hash { + encoded, err := rlp.EncodeToBytes(x) + if err != nil { + return common.Hash{} + } + return crypto.Keccak256Hash(encoded) +} + // GetHashFn returns a GetHashFunc which retrieves header hashes by number func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash { // Cache will initially contain [refHash.parent], diff --git a/core/state_transition.go b/core/state_transition.go index f38afdf4047..12eec70a8b7 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -146,6 +146,7 @@ func toWordSize(size uint64) uint64 { // A Message contains the data derived from a single transaction that is relevant to state // processing. type Message struct { + TxType uint8 To *common.Address From common.Address Nonce uint64 @@ -187,6 +188,7 @@ type Message struct { // TransactionToMessage converts a transaction into a Message. func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) { msg := &Message{ + TxType: tx.Type(), Nonce: tx.Nonce(), GasLimit: tx.Gas(), GasPrice: new(big.Int).Set(tx.GasPrice()), diff --git a/core/vm/eips.go b/core/vm/eips.go index 03d7f642c2c..d9870df5dcf 100644 --- a/core/vm/eips.go +++ b/core/vm/eips.go @@ -599,82 +599,128 @@ func opAcceptRole(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) { type paramLoader func(*EVM, uint64) []byte +// uint64ToBytes32 converts a uint64 to a 32-byte big-endian slice. +func uint64ToBytes32(v uint64) []byte { + return common.LeftPadBytes(new(uint256.Int).SetUint64(v).Bytes(), 32) +} + func loadType(evm *EVM, id uint64) []byte { - // TODO: plumb in type to TxContext - return nil + return uint64ToBytes32(uint64(evm.TxContext.TxType)) } func loadNonce(evm *EVM, id uint64) []byte { - // TODO: plumb in nonce to TxContext - return nil + return uint64ToBytes32(evm.TxContext.Nonce) } func loadSender(evm *EVM, id uint64) []byte { - return nil + return common.LeftPadBytes(evm.TxContext.Origin.Bytes(), 32) } func loadSenderValidationData(evm *EVM, id uint64) []byte { - return nil + if evm.TxContext.Sender == nil { + return nil + } + return evm.TxContext.Sender.Data } func loadDeployer(evm *EVM, id uint64) []byte { - return nil + // 0 or 32 bytes: return nil when not set + if evm.TxContext.Deployer == nil { + return nil + } + return common.LeftPadBytes(evm.TxContext.Deployer.Target.Bytes(), 32) } func loadDeployerData(evm *EVM, id uint64) []byte { - return nil + // dynamic, default empty array + if evm.TxContext.Deployer == nil { + return nil + } + return evm.TxContext.Deployer.Data } func loadPaymaster(evm *EVM, id uint64) []byte { - return nil + // 0 or 32 bytes: return nil when not set + if evm.TxContext.Paymaster == nil { + return nil + } + return common.LeftPadBytes(evm.TxContext.Paymaster.Target.Bytes(), 32) } func loadPaymasterData(evm *EVM, id uint64) []byte { - return nil + // dynamic, default empty array + if evm.TxContext.Paymaster == nil { + return nil + } + return evm.TxContext.Paymaster.Data } func loadSenderExecutionData(evm *EVM, id uint64) []byte { - return nil + return evm.TxContext.SenderExecutionData } func loadMaxPriorityFeePerGas(evm *EVM, id uint64) []byte { - return nil + if evm.TxContext.MaxPriorityFeePerGas == nil { + return make([]byte, 32) + } + return common.LeftPadBytes(evm.TxContext.MaxPriorityFeePerGas.Bytes(), 32) } func loadMaxFeePerGas(evm *EVM, id uint64) []byte { - return nil + if evm.TxContext.MaxFeePerGas == nil { + return make([]byte, 32) + } + return common.LeftPadBytes(evm.TxContext.MaxFeePerGas.Bytes(), 32) +} + +func loadSenderValidationGas(evm *EVM, id uint64) []byte { + if evm.TxContext.Sender == nil { + return uint64ToBytes32(0) + } + return uint64ToBytes32(evm.TxContext.Sender.Gas) } func loadPaymasterValidationGas(evm *EVM, id uint64) []byte { - return nil + // 32 bytes, default 0 + if evm.TxContext.Paymaster == nil { + return uint64ToBytes32(0) + } + return uint64ToBytes32(evm.TxContext.Paymaster.Gas) } func loadSenderExecutionGas(evm *EVM, id uint64) []byte { - return nil + return uint64ToBytes32(evm.TxContext.SenderExecutionGas) } func loadPaymasterPostOpGas(evm *EVM, id uint64) []byte { - return nil + // 32 bytes, default 0 + if evm.TxContext.Paymaster == nil { + return uint64ToBytes32(0) + } + return uint64ToBytes32(evm.TxContext.Paymaster.PostOpGas) } func loadAccessListHash(evm *EVM, id uint64) []byte { - return nil + return evm.TxContext.AccessListHash[:] } func loadAuthorizationListHash(evm *EVM, id uint64) []byte { - return nil + return evm.TxContext.AuthorizationListHash[:] } func loadExecutionStatus(evm *EVM, id uint64) []byte { - return nil + return uint64ToBytes32(evm.TxContext.ExecutionStatus) } func loadExecutionGasUsed(evm *EVM, id uint64) []byte { - return nil + return uint64ToBytes32(evm.TxContext.ExecutionGasUsed) } func loadTxHashForSignature(evm *EVM, id uint64) []byte { - return nil + if evm.TxContext.TxHashForSignature == nil { + return nil + } + return evm.TxContext.TxHashForSignature[:] } // idToParam is a map of identifier to transaction parameter loader. @@ -690,7 +736,7 @@ var idToParam = map[uint64]paramLoader{ 0x08: loadSenderExecutionData, 0x0B: loadMaxPriorityFeePerGas, 0x0C: loadMaxFeePerGas, - 0x0D: loadSenderValidationData, + 0x0D: loadSenderValidationGas, 0x0E: loadPaymasterValidationGas, 0x0F: loadSenderExecutionGas, 0x10: loadPaymasterPostOpGas, diff --git a/core/vm/evm.go b/core/vm/evm.go index 913ceb9fb1d..7f6809c2d78 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -80,7 +80,29 @@ type TxContext struct { BlobFeeCap *big.Int // Is used to zero the blobbasefee if NoBaseFee is set AccessEvents *state.AccessEvents // Capture all state accesses for this tx - // TODO: add EIP-7701 fields + // EIP-7701: Transaction parameters for TXPARAM* opcodes + TxType uint8 // 0x00: transaction type + Nonce uint64 // 0x01: transaction nonce + // Origin is sender (0x02) + + // AA authorization structs (nil for non-AA txs) + Sender *types.AbstractAuthorization // 0x03: Data, 0x0D: Gas + Deployer *types.AbstractAuthorization // 0x04: Target, 0x05: Data + Paymaster *types.PaymasterAuthorization // 0x06: Target, 0x07: Data, 0x0E: Gas, 0x10: PostOpGas + + SenderExecutionData []byte // 0x08: execution calldata + SenderExecutionGas uint64 // 0x0F: execution gas limit + MaxPriorityFeePerGas *big.Int // 0x0B: max priority fee per gas + MaxFeePerGas *big.Int // 0x0C: max fee per gas + + // Hashes (computed when creating TxContext) + AccessListHash common.Hash // 0x11: hash of access list + AuthorizationListHash common.Hash // 0x12: hash of authorization list + TxHashForSignature *common.Hash // 0xff: hash for signature (nil for AA txs) + + // Mutable execution state (set during AA tx processing) + ExecutionStatus uint64 // 0xf1: execution result status + ExecutionGasUsed uint64 // 0xf2: gas used during execution } // EVM is the Ethereum Virtual Machine base object and provides diff --git a/core/vm/runtime/env.go b/core/vm/runtime/env.go index e54041f7e27..3fb32d9f2e7 100644 --- a/core/vm/runtime/env.go +++ b/core/vm/runtime/env.go @@ -27,6 +27,12 @@ func NewEnv(cfg *Config) *vm.EVM { GasPrice: cfg.GasPrice, BlobHashes: cfg.BlobHashes, BlobFeeCap: cfg.BlobFeeCap, + + // EIP-7701: Set basic fields for non-AA transactions + // AA-specific fields (Sender, Deployer, Paymaster) remain nil + MaxPriorityFeePerGas: cfg.GasPrice, + MaxFeePerGas: cfg.GasPrice, + SenderExecutionGas: cfg.GasLimit, } blockContext := vm.BlockContext{ CanTransfer: core.CanTransfer,