diff --git a/core/state_transition.go b/core/state_transition.go index bf5ac07636d..0d1327b3bc6 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -143,19 +143,22 @@ func toWordSize(size uint64) uint64 { // A Message contains the data derived from a single transaction that is relevant to state // processing. type Message struct { - To *common.Address - From common.Address - Nonce uint64 - Value *big.Int - GasLimit uint64 - GasPrice *big.Int - GasFeeCap *big.Int - GasTipCap *big.Int - Data []byte - AccessList types.AccessList - BlobGasFeeCap *big.Int - BlobHashes []common.Hash - SetCodeAuthorizations []types.SetCodeAuthorization + To *common.Address + From common.Address + Nonce uint64 + Value *big.Int + GasLimit uint64 + GasPrice *big.Int + GasFeeCap *big.Int + GasTipCap *big.Int + Data []byte + AccessList types.AccessList + BlobGasFeeCap *big.Int + BlobHashes []common.Hash + + // AuthList provides an abstraction over authorization handling. + // It handles both signed authorizations and unsigned authorizations + AuthList []types.SetCodeAuth // When SkipNonceChecks is true, the message nonce is not checked against the // account nonce in state. @@ -172,8 +175,30 @@ type Message struct { SkipTransactionChecks bool } +// getAuthorizationList extracts SetCodeAuthorization list from auth interfaces. +// This is used for intrinsic gas calculation and validation. +func (msg *Message) getAuthorizationList() []types.SetCodeAuthorization { + if msg.AuthList == nil { + return nil + } + authList := make([]types.SetCodeAuthorization, len(msg.AuthList)) + for i, auth := range msg.AuthList { + authList[i] = auth.AsSetCodeAuthorization() + } + return authList +} + // TransactionToMessage converts a transaction into a Message. func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.Int) (*Message, error) { + // Create authorization interfaces from transaction authorizations + var authList []types.SetCodeAuth + if auths := tx.SetCodeAuthorizations(); auths != nil { + authList = make([]types.SetCodeAuth, len(auths)) + for i, auth := range auths { + authList[i] = types.NewSignedAuthorization(auth) + } + } + msg := &Message{ Nonce: tx.Nonce(), GasLimit: tx.Gas(), @@ -184,7 +209,7 @@ func TransactionToMessage(tx *types.Transaction, s types.Signer, baseFee *big.In Value: tx.Value(), Data: tx.Data(), AccessList: tx.AccessList(), - SetCodeAuthorizations: tx.SetCodeAuthorizations(), + AuthList: authList, SkipNonceChecks: false, SkipTransactionChecks: false, BlobHashes: tx.BlobHashes(), @@ -398,11 +423,11 @@ func (st *stateTransition) preCheck() error { } } // Check that EIP-7702 authorization list signatures are well formed. - if msg.SetCodeAuthorizations != nil { + if msg.AuthList != nil { if msg.To == nil { return fmt.Errorf("%w (sender %v)", ErrSetCodeTxCreate, msg.From) } - if len(msg.SetCodeAuthorizations) == 0 { + if len(msg.AuthList) == 0 { return fmt.Errorf("%w (sender %v)", ErrEmptyAuthList, msg.From) } } @@ -443,7 +468,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.Data, msg.AccessList, msg.getAuthorizationList(), contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai) if err != nil { return nil, err } @@ -503,10 +528,10 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall) // Apply EIP-7702 authorizations. - if msg.SetCodeAuthorizations != nil { - for _, auth := range msg.SetCodeAuthorizations { + if msg.AuthList != nil { + for _, auth := range msg.AuthList { // Note errors are ignored, we simply skip invalid authorizations here. - st.applyAuthorization(&auth) + st.applyAuthorization(auth) } } @@ -574,17 +599,21 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { } // validateAuthorization validates an EIP-7702 authorization against the state. -func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) { +func (st *stateTransition) validateAuthorization(auth types.SetCodeAuth) (authority common.Address, err error) { + skipValidation := st.msg.SkipTransactionChecks + // Verify chain ID is null or equal to current chain ID. - if !auth.ChainID.IsZero() && auth.ChainID.CmpBig(st.evm.ChainConfig().ChainID) != 0 { + chainID := auth.GetChainID() + if !skipValidation && !chainID.IsZero() && chainID.CmpBig(st.evm.ChainConfig().ChainID) != 0 { return authority, ErrAuthorizationWrongChainID } // Limit nonce to 2^64-1 per EIP-2681. - if auth.Nonce+1 < auth.Nonce { + nonce := auth.GetNonce() + if !skipValidation && nonce+1 < nonce { return authority, ErrAuthorizationNonceOverflow } - // Validate signature values and recover authority. - authority, err = auth.Authority() + // Get authority from auth + authority, err = auth.GetAuthority() if err != nil { return authority, fmt.Errorf("%w: %v", ErrAuthorizationInvalidSignature, err) } @@ -598,14 +627,17 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio if _, ok := types.ParseDelegation(code); len(code) != 0 && !ok { return authority, ErrAuthorizationDestinationHasCode } - if have := st.state.GetNonce(authority); have != auth.Nonce { - return authority, ErrAuthorizationNonceMismatch + // Skip state checks during gas estimation + if !skipValidation { + if have := st.state.GetNonce(authority); have != nonce { + return authority, ErrAuthorizationNonceMismatch + } } return authority, nil } // applyAuthorization applies an EIP-7702 code delegation to the state. -func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { +func (st *stateTransition) applyAuthorization(auth types.SetCodeAuth) error { authority, err := st.validateAuthorization(auth) if err != nil { return err @@ -618,15 +650,17 @@ func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) } // Update nonce and account code. - st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization) - if auth.Address == (common.Address{}) { + nonce := auth.GetNonce() + address := auth.GetAddress() + st.state.SetNonce(authority, nonce+1, tracing.NonceChangeAuthorization) + if address == (common.Address{}) { // Delegation to zero address means clear. st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear) return nil } // Otherwise install delegation to auth.Address. - st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) + st.state.SetCode(authority, types.AddressToDelegation(address), tracing.CodeChangeAuthorization) return nil } diff --git a/core/types/tx_setcode.go b/core/types/tx_setcode.go index f2281d4ae76..8bfd6389931 100644 --- a/core/types/tx_setcode.go +++ b/core/types/tx_setcode.go @@ -68,6 +68,19 @@ type SetCodeTx struct { //go:generate go run github.com/fjl/gencodec -type SetCodeAuthorization -field-override authorizationMarshaling -out gen_authorization.go +// SetCodeAuth is an interface for getting authorization details. +// It abstracts over signed and unsigned authorizations, enabling +// gas estimation without valid signatures. +type SetCodeAuth interface { + GetChainID() uint256.Int + GetAddress() common.Address + GetNonce() uint64 + GetAuthority() (common.Address, error) + + // AsSetCodeAuthorization returns the underlying SetCodeAuthorization for encoding. + AsSetCodeAuthorization() SetCodeAuthorization +} + // SetCodeAuthorization is an authorization from an account to deploy code at its address. type SetCodeAuthorization struct { ChainID uint256.Int `json:"chainId" gencodec:"required"` @@ -241,3 +254,80 @@ func (tx *SetCodeTx) sigHash(chainID *big.Int) common.Hash { tx.AuthList, }) } + +// SignedAuthorization wraps a SetCodeAuthorization and recovers authority from signature. +type SignedAuthorization struct { + auth SetCodeAuthorization +} + +// NewSignedAuthorization creates a new SignedAuthorization. +func NewSignedAuthorization(auth SetCodeAuthorization) *SignedAuthorization { + return &SignedAuthorization{auth: auth} +} + +func (s *SignedAuthorization) GetChainID() uint256.Int { + return s.auth.ChainID +} + +func (s *SignedAuthorization) GetAddress() common.Address { + return s.auth.Address +} + +func (s *SignedAuthorization) GetNonce() uint64 { + return s.auth.Nonce +} + +func (s *SignedAuthorization) GetAuthority() (common.Address, error) { + return s.auth.Authority() +} + +func (s *SignedAuthorization) AsSetCodeAuthorization() SetCodeAuthorization { + return s.auth +} + +// UnsignedAuthorization represents an authorization with an authority address, +// used for gas estimation when a valid signature is not available. +type UnsignedAuthorization struct { + chainID uint256.Int + address common.Address + nonce uint64 + authority common.Address +} + +// NewUnsignedAuthorization creates a new UnsignedAuthorization. +func NewUnsignedAuthorization(chainID uint256.Int, address common.Address, nonce uint64, authority common.Address) *UnsignedAuthorization { + return &UnsignedAuthorization{ + chainID: chainID, + address: address, + nonce: nonce, + authority: authority, + } +} + +func (u *UnsignedAuthorization) GetChainID() uint256.Int { + return u.chainID +} + +func (u *UnsignedAuthorization) GetAddress() common.Address { + return u.address +} + +func (u *UnsignedAuthorization) GetNonce() uint64 { + return u.nonce +} + +func (u *UnsignedAuthorization) GetAuthority() (common.Address, error) { + return u.authority, nil +} + +func (u *UnsignedAuthorization) AsSetCodeAuthorization() SetCodeAuthorization { + // Return a zero-valued authorization with basic fields filled in + return SetCodeAuthorization{ + ChainID: u.chainID, + Address: u.address, + Nonce: u.nonce, + V: 0, + R: uint256.Int{}, + S: uint256.Int{}, + } +} diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index aaa002b5ec0..523719d6142 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -4054,3 +4054,91 @@ func TestSendRawTransactionSync_Timeout(t *testing.T) { t.Fatalf("expected ErrorData=%s, got %v", want, got) } } + +// TestEstimateGasWithAuthorityHints tests that gas estimation works with the +// optional authorityHints field in transaction args +func TestEstimateGasWithAuthorityHints(t *testing.T) { + t.Parallel() + accounts := newAccounts(3) + targetContract := accounts[2].addr + genesis := &core.Genesis{ + Config: params.AllDevChainProtocolChanges, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + targetContract: {Code: []byte{0x60, 0x01, 0x60, 0x00, 0x55}}, // PUSH1 1 PUSH1 0 SSTORE + }, + } + api := NewBlockChainAPI(newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + b.SetPoS() + })) + tests := []struct { + name string + setupArgs func() TransactionArgs + expectErr bool + }{ + { + name: "Gas estimation without signature)", + setupArgs: func() TransactionArgs { + auth := types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(1337), + Address: targetContract, + Nonce: 0, + V: 0, + R: *uint256.NewInt(0), + S: *uint256.NewInt(0), + } + return TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + AuthorizationList: []types.SetCodeAuthorization{auth}, + AuthorityHints: []common.Address{accounts[0].addr}, // Provide authority hint + } + }, + expectErr: false, + }, + { + name: "Gas estimation with valid signature", + setupArgs: func() TransactionArgs { + auth, err := types.SignSetCode(accounts[0].key, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(1337), + Address: targetContract, + Nonce: 0, + }) + if err != nil { + t.Fatalf("Failed to sign authorization: %v", err) + } + return TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + AuthorizationList: []types.SetCodeAuthorization{auth}, + } + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := tt.setupArgs() + // Perform gas estimation + blockNr := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + estimate, err := api.EstimateGas(context.Background(), args, &blockNr, nil, nil) + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error but got none") + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if estimate == 0 { + t.Errorf("Expected non-zero gas estimate") + } + } + }) + } +} diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 23aa8e5947a..f19879fccd1 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -70,6 +70,9 @@ type TransactionArgs struct { // For SetCodeTxType AuthorizationList []types.SetCodeAuthorization `json:"authorizationList"` + + // AuthorityHints provides authority addresses for gas estimation. + AuthorityHints []common.Address `json:"authorityHints,omitempty"` } // from retrieves the transaction sender address. @@ -476,6 +479,24 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c if args.AccessList != nil { accessList = *args.AccessList } + var authList []types.SetCodeAuth + if args.AuthorizationList != nil { + authList = make([]types.SetCodeAuth, len(args.AuthorizationList)) + for i, auth := range args.AuthorizationList { + // If authority hints are provided, use unsigned authorization + if i < len(args.AuthorityHints) { + authList[i] = types.NewUnsignedAuthorization( + auth.ChainID, + auth.Address, + auth.Nonce, + args.AuthorityHints[i], + ) + } else { + // Otherwise use signed authorization + authList[i] = types.NewSignedAuthorization(auth) + } + } + } return &core.Message{ From: args.from(), To: args.To, @@ -489,7 +510,7 @@ func (args *TransactionArgs) ToMessage(baseFee *big.Int, skipNonceCheck bool) *c AccessList: accessList, BlobGasFeeCap: (*big.Int)(args.BlobFeeCap), BlobHashes: args.BlobHashes, - SetCodeAuthorizations: args.AuthorizationList, + AuthList: authList, SkipNonceChecks: skipNonceCheck, SkipTransactionChecks: true, } diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 1d6cc8db70f..d17757362c8 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -448,35 +448,35 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (*core.Mess if gasPrice == nil { return nil, errors.New("no gas price provided") } - var authList []types.SetCodeAuthorization + var authList []types.SetCodeAuth if tx.AuthorizationList != nil { - authList = make([]types.SetCodeAuthorization, len(tx.AuthorizationList)) + authList = make([]types.SetCodeAuth, len(tx.AuthorizationList)) for i, auth := range tx.AuthorizationList { - authList[i] = types.SetCodeAuthorization{ + authList[i] = types.NewSignedAuthorization(types.SetCodeAuthorization{ ChainID: *uint256.MustFromBig(auth.ChainID), Address: auth.Address, Nonce: auth.Nonce, V: auth.V, R: *uint256.MustFromBig(auth.R), S: *uint256.MustFromBig(auth.S), - } + }) } } msg := &core.Message{ - From: from, - To: to, - Nonce: tx.Nonce, - Value: value, - GasLimit: gasLimit, - GasPrice: gasPrice, - GasFeeCap: tx.MaxFeePerGas, - GasTipCap: tx.MaxPriorityFeePerGas, - Data: data, - AccessList: accessList, - BlobHashes: tx.BlobVersionedHashes, - BlobGasFeeCap: tx.BlobGasFeeCap, - SetCodeAuthorizations: authList, + From: from, + To: to, + Nonce: tx.Nonce, + Value: value, + GasLimit: gasLimit, + GasPrice: gasPrice, + GasFeeCap: tx.MaxFeePerGas, + GasTipCap: tx.MaxPriorityFeePerGas, + Data: data, + AccessList: accessList, + BlobHashes: tx.BlobVersionedHashes, + BlobGasFeeCap: tx.BlobGasFeeCap, + AuthList: authList, } return msg, nil }