Skip to content

feat(eip8130): single k1 path, EIP-2 low-s, DEFAULT_EOA_REVOKED flag#3605

Open
chunter-cb wants to merge 5 commits into
mainfrom
hh/eip-8130-recover-once-low-s
Open

feat(eip8130): single k1 path, EIP-2 low-s, DEFAULT_EOA_REVOKED flag#3605
chunter-cb wants to merge 5 commits into
mainfrom
hh/eip-8130-recover-once-low-s

Conversation

@chunter-cb

@chunter-cb chunter-cb commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

The EIP-8130 secp256k1 authentication model for base-execution-eip8130, matching the spec + contract reference in ethereum/EIPs#11815 and base/eip-8130#18.

secp256k1 authentication

K1_AUTHENTICATOR (address(1)) is the single secp256k1 identity: the implicit default EOA and every explicitly-registered k1 actor authenticate through it. address(0) is the empty / "no actor configured" sentinel and is not an authenticator selector — it is rejected as non-canonical by dispatch, and structurally below the txpool's K1_AUTHENTICATOR floor.

dispatch::ecrecover enforces EIP-2 low-s (and v ∈ {27,28}), so every authorizing surface that feeds the transaction id commits to one signature encoding. This is byte-parity with the deployed AccountConfiguration._recoverSigner.

Implicit default EOA and revocation

The implicit default EOA is live when its actor_config slot is empty and the DEFAULT_EOA_REVOKED flag is unset. The flag is 0x01 in a flags byte in the packed AccountState slot (bits 184..192); there is no separate revoked-authenticator sentinel.

authorize_k1 mirrors _authenticateK1 after recovery:

  • recovered == account && !DEFAULT_EOA_REVOKED → unrestricted owner, from one account-state read.
  • otherwise the signer must carry an explicit k1 actor_config entry (a scoped / re-enabled self key, or any other k1 actor).

This relies on the contract invariant an explicit self-actor entry implies the flag is set, so a live self never has a config to shadow.

Recover the EOA sender once

The empty-sender verifier path recovers the signer a single time (checked, low-s) and resolves the implicit owner directly via authorize_k1, rather than synthesizing an address(0) || sig blob and re-recovering through dispatch.

Layer-by-layer

  • base-common-consensus: K1_AUTHENTICATOR, DEFAULT_EOA_REVOKED constants.
  • base-execution-eip8130: AccountState.flags + default_eoa_revoked(); flag-aware is_actor; authorize_k1; dispatch k1 routing + address(0) reject.
  • base-execution-txpool: structural authenticator floor rejects address(0) (< K1_AUTHENTICATOR).

Parity note

The native low-s enforcement and the k1 / flag model stay byte-parity with the deployed AccountConfiguration (base/eip-8130#18). A divergence re-pins the canonical CREATE2 address, caught by the registry drift test.

Standardise on a single non-malleable recovery for every transaction-
authorizing surface:

- `dispatch::ecrecover` (the `ECRECOVER_AUTHENTICATOR` sentinel) now
  rejects malleable upper-half `s` (EIP-2) instead of canonicalizing and
  accepting it, so the recovered signer commits to one signature encoding.
  Must stay in lockstep with the deployed `AccountConfiguration` reference.
- Split a shadow-only `ActorAuthorizer::implicit_eoa_owner` helper. The
  empty-`sender` path in the verifier recovers the signer exactly once
  (checked, low-s) and authorizes the implicit owner directly, instead of
  synthesizing an `address(0) || sig` blob and re-recovering through
  dispatch. The wire `address(0)` form still recovers, since there the
  account is wire-supplied and the signature must be bound to it.
@cb-heimdall

cb-heimdall commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

Comment thread crates/execution/eip8130/src/authorize.rs Outdated
Comment thread crates/execution/eip8130/src/verify.rs Outdated
Track the EIP-8130 / contract rework (ethereum/EIPs#11815, base/eip-8130#18)
that unifies the secp256k1 authentication path and removes the revoked
authenticator sentinel.

- Rename ECRECOVER_AUTHENTICATOR -> K1_AUTHENTICATOR (still address(1)): the
  single secp256k1 identity for both the implicit default EOA and any explicit
  k1 actor. `address(0)` is now purely the empty "no actor configured" sentinel
  and is never a valid authenticator selector (rejected as non-canonical).
- Delete REVOKED_AUTHENTICATOR (type(uint160).max). Revocation folds into a new
  DEFAULT_EOA_REVOKED (0x01) bit in a `flags` byte appended to the packed
  AccountState slot (bits 184..192).
- Replace the address(0) implicit-EOA route with a single `authorize_k1`
  resolver mirroring `_authenticateK1`: a live default EOA (recovered == account
  and the flag unset) is an unrestricted owner from one account-state read;
  otherwise the signer must carry an explicit k1 actor_config entry. The
  empty-`sender` verifier path feeds this resolver after its single recovery.
- Drop the now-dead Revoked / ImplicitEoa* error variants.
- txpool: collapse the structural authenticator floor to reject only address(0)
  (< K1_AUTHENTICATOR); drop the obsolete REVOKED-sentinel rejection tests.
@chunter-cb chunter-cb changed the title feat(eip8130): enforce EIP-2 low-s and recover the EOA sender once feat(eip8130): single k1 path, EIP-2 low-s, DEFAULT_EOA_REVOKED flag Jun 18, 2026
@chunter-cb chunter-cb marked this pull request as ready for review June 18, 2026 10:09
Comment thread crates/execution/eip8130/src/authorize.rs
Adopts the inline self-key model (base/eip-8130#20, ethereum/EIPs#11816):
the account's own secp256k1 ("self") scope/policyType/expiry now live in the
packed account-state slot alongside DEFAULT_EOA_REVOKED, so the self key —
full owner or scoped — resolves in a single SLOAD. The actor_config(self)
slot is reserved for a mutually-exclusive non-k1 self authenticator.

- AccountState: decode inline default_eoa_{scope,policy_type,expiry}.
- authorize_k1: resolve the self path from the inline config; a set
  DEFAULT_EOA_REVOKED flag rejects with the new DefaultEoaRevoked error
  (revoked, or a non-k1 self is live) rather than falling through to a bound
  lookup; honor inline expiry and gate.
- get_policy: mirror the contract's (policy_type, target, commitment) shape
  with inline-self resolution.
authorize_k1 granted owner access based on a caller-supplied B256 recovered
signer; as a pub entrypoint that trusted its caller, it was a risky surface on
a security-critical path (the only caller, verify_sender, recovers correctly,
but nothing enforced that).

Introduce RecoveredActorId, a newtype whose private address is only producible
via a recovery constructor (recover_k1 for the K1_AUTHENTICATOR wire form;
recover_eoa_sender for the empty-sender EOA path). authorize_k1 now consumes
the token, lifting the "must have recovered first" precondition into the type
system without re-recovering. The k1 ecrecover is consolidated into
RecoveredActorId::recover_k1 (dispatch delegates to it), so the dispatch path
and the token cannot drift.
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

Clean PR. The RecoveredActorId newtype pattern addresses the previous review concern about authorize_k1 accepting a bare B256 — the private address field and recovery-only constructors enforce the authentication invariant at the type level.

Reviewed all 12 changed files. No new issues found.

Verified:

  • RecoveredActorId is not constructible outside recovery constructors (private field, #[non_exhaustive] not needed since the struct literal form is already blocked).
  • recover_eoa_sender delegates to alloy's checked recover_signer, so the EIP-2 low-s claim holds for both the recover_k1 (explicit rejection of normalize_s().is_some()) and the recover_eoa_sender path.
  • AccountState::from_word correctly unpacks the new flags/defaultEOAScope/defaultEOAPolicyType/defaultEOAExpiry fields at their bit offsets, cross-checked against the pack_account_state test helper.
  • get_policy return type change (Address, B256)(u8, Address, B256) has no external callers.
  • REVOKED_AUTHENTICATOR, ECRECOVER_AUTHENTICATOR, ImplicitEoaShadowed, ImplicitEoaMismatch are fully removed with zero dangling references.
  • The scope check in verify_sender (verify.rs:107) is now reachable (unlike the old code) since authorize_k1 can return a scoped inline self.

@github-actions

Copy link
Copy Markdown
Contributor

✅ base-std fork tests: all 616 passed

base/base is fully in sync with the base-std spec.

Dependency Ref Commit
base-std main d4b531cd
base-anvil 0092692587d8d064dd2c6923ce26a682c58f3694 00926925

@chunter-cb chunter-cb enabled auto-merge June 18, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants