From a27bd32d260c94711ac41e46b6e517cf7363db49 Mon Sep 17 00:00:00 2001 From: Charles Dusek Date: Fri, 27 Mar 2026 13:43:44 -0500 Subject: [PATCH] core, internal/ethapi: skip EIP-7702 auth signature validation in simulation mode When eth_estimateGas or eth_call is invoked with an authorizationList containing unsigned (zero-signature) entries, the signature recovery fails and the authorization is silently skipped. This means the sender's account never receives the delegated code, producing an incorrect gas estimate. Fix this by falling back to msg.From as the authority when signature recovery fails in simulation mode (SkipTransactionChecks). Also skip authorization nonce validation when SkipNonceChecks is set, consistent with the existing sender nonce skip behaviour. Fixes #31617 --- core/state_transition.go | 14 ++++++++++++-- internal/ethapi/api_test.go | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/state_transition.go b/core/state_transition.go index 52375bedaa..8cea986f81 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -607,7 +607,15 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio // Validate signature values and recover authority. authority, err = auth.Authority() if err != nil { - return authority, fmt.Errorf("%w: %v", ErrAuthorizationInvalidSignature, err) + // In simulation mode (e.g. eth_estimateGas, eth_call), allow + // authorizations with invalid signatures by assuming the + // transaction sender is the authority. This lets callers estimate + // gas for EIP-7702 transactions before signing the authorization. + if st.msg.SkipTransactionChecks { + authority = st.msg.From + } else { + return authority, fmt.Errorf("%w: %v", ErrAuthorizationInvalidSignature, err) + } } // Check the authority account // 1) doesn't have code or has exisiting delegation @@ -619,7 +627,9 @@ 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 { + // Skip nonce validation in simulation mode, consistent with the + // top-level SkipNonceChecks behaviour for the sender account. + if have := st.state.GetNonce(authority); have != auth.Nonce && !st.msg.SkipNonceChecks { return authority, ErrAuthorizationNonceMismatch } return authority, nil diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 62e9979d3d..77df0008bd 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -972,6 +972,23 @@ func TestEstimateGas(t *testing.T) { }, expectErr: core.ErrSetCodeTxCreate, }, + // EstimateGas with unsigned (zero-signature) EIP-7702 authorization + // should succeed, using the sender as the authority. This allows + // callers to estimate gas before signing the authorization. + { + blockNumber: rpc.LatestBlockNumber, + call: TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(0)), + AuthorizationList: []types.SetCodeAuthorization{{ + ChainID: *uint256.NewInt(0), + Address: accounts[0].addr, + Nonce: uint64(genBlocks + 1), + }}, + }, + want: 46000, + }, } for i, tc := range testSuite { result, err := api.EstimateGas(context.Background(), tc.call, &rpc.BlockNumberOrHash{BlockNumber: &tc.blockNumber}, &tc.overrides, &tc.blockOverrides)