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
This commit is contained in:
Charles Dusek 2026-03-27 13:43:44 -05:00
parent c3467dd8b5
commit a27bd32d26
2 changed files with 29 additions and 2 deletions

View file

@ -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

View file

@ -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)