Skip to content

Feat/erc6492 erc1271 validation#1109

Open
amalcaraz wants to merge 9 commits into
mainfrom
feat/erc6492-erc1271-validation
Open

Feat/erc6492 erc1271 validation#1109
amalcaraz wants to merge 9 commits into
mainfrom
feat/erc6492-erc1271-validation

Conversation

@amalcaraz
Copy link
Copy Markdown
Collaborator

Summary

Adds support for verifying Aleph messages signed by smart contract wallets (Safe, Kernel/ZeroDev, Privy embedded wallets, etc.), including counterfactual wallets that have not yet been deployed on chain.

Previously, EVMVerifier only ran ecrecover on the raw signature, which fails for any non-EOA signer. This is why messages from Privy users (who get a ZeroDev Kernel smart account by default) were being rejected as "The signature of the message is invalid" — e.g. message
6f699b25…1a26d.

Verification flow

EVMVerifier.verify_signature now picks a path based on the signature shape:

Path Trigger Cost
ERC-6492 counterfactual signature ends with 0x6492…6492 magic suffix 1 eth_call to UniversalSigValidator
Plain ECDSA 65-byte sig that ecrecovers to the sender 0 RPC calls
ERC-1271 deployed ECDSA fails AND sender has deployed bytecode 2 eth_call (get_code + isValidSignature)
  • ERC-6492 is detected upfront (cheap byte comparison) so smart-wallet sigs skip the useless ECDSA attempt.
  • Plain EOA messages still pay zero RPC calls — the common case stays free.
  • The ERC-6492 counterfactual path uses the canonical UniversalSigValidator at 0x0000000000002fd5Aeb385D324B580FCa7c83823, which atomically simulates factory deployment and calls isValidSignature in a single eth_call. See EIP-6492.

Scope: Ethereum mainnet only (for now)

Smart wallets use block.chainid in their EIP-712 domain separators, so verifying a Base/Arbitrum/etc. signature against the Ethereum mainnet RPC would produce false negatives. Until per-chain RPCs are wired through, only Chain.ETH gets the RPC-backed paths:

Chain Verifier Plain ECDSA ERC-1271 ERC-6492
ETH EthereumVerifier(rpc_url=…)
ETHERLINK EthereumVerifier() (no RPC)
All other EVM chains (Base, Arbitrum, Optimism, …) EVMVerifier() (no RPC)

Plain EOA messages on every EVM chain continue to work exactly as before — the scoping only affects smart contract wallet signatures. Expanding to more chains is a follow-up that needs per-chain RPC configuration.

Config wiring

  • EVMVerifier and SignatureVerifier now accept an optional rpc_url.
  • All 3 SignatureVerifier() call sites (api_entrypoint.py, jobs/process_pending_messages.py, jobs/fetch_pending_messages.py) pass config.ethereum.api_url.value.
  • If no RPC is configured, behavior is identical to before: only plain ECDSA works.

Security guarantees

For a signature from smart wallet 0xa9F3…1635 (Kernel, owner EOA 0xfFFE…4fD7):

  • Hash binds to the messagekeccak256(EIP-191(chain + sender + type + item_hash)). Any tampering invalidates.
  • Sender address is cryptographically bound to configsender = keccak256(0xff || factory || salt || keccak256(initcode))[12:]. The initcode embeds the owner; swapping the owner changes the address. Colliding on 160 bits is computationally infeasible.
  • Only the owner's private key could produce the inner ECDSA — Kernel's ECDSA validator recovers a key from the EIP-712-wrapped hash and requires it to equal the configured owner.
  • No cross-chain replay — Kernel's EIP-712 domain includes block.chainid, so a signature produced for a wallet on one chain cannot be validated on another. (Also why we scoped to ETH mainnet only.)

For deployed smart wallets (ERC-1271 path), verification is against live contract state, so key rotations / multisig additions / upgrades affect validity. Residual trust assumptions and what the verification does not guarantee are documented in docs/protocol/smart-wallet-signature-verification.md.

Files

  • src/aleph/chains/evm.py — ERC-6492 / ERC-1271 detection paths + AsyncWeb3 client
  • src/aleph/chains/signature_verifier.py — thread rpc_url through; scope to ETH only
  • src/aleph/api_entrypoint.py, src/aleph/jobs/{process,fetch}_pending_messages.py — pass config.ethereum.api_url to SignatureVerifier
  • tests/chains/test_evm.py — 10 new tests covering all paths (ECDSA, ERC-1271 valid/invalid/no-RPC/no-code, ERC-6492 valid/invalid/no-RPC, detection-skips-ECDSA, constructor)
  • docs/protocol/smart-wallet-signatures.md — signature format reference (ERC-6492 schema, all accounts, before/after deployment examples)
  • docs/protocol/smart-wallet-signature-verification.md — verification flow and security analysis

Test Plan

  • hatch run testing:test tests/chains/test_evm.py -v → 13/13 passing
  • hatch run testing:test → full suite 676 passed, 10 skipped (no regressions)
  • hatch run linting:all → ruff, black, isort, mypy, yamlfix, pyproject-fmt, check-sdist all clean
  • Manual check: submit an ERC-6492 signed message from Privy against staging, with ethereum.api_url pointing to a working mainnet RPC → should be accepted
  • Manual check: submit a plain EOA-signed message → confirm no RPC call happens (packet capture / logs)
  • Manual check: submit a signature from an already-deployed Safe on mainnet → confirm ERC-1271 path accepts it
  • Manual check: submit a smart-wallet signature on Chain.BASE → confirm it is rejected (out of scope until per-chain RPCs land)

Follow-ups (out of scope for this PR)

  • Per-chain RPC configuration so we can expand ERC-1271 / ERC-6492 to Base, Arbitrum, Optimism, etc.
  • Verify the UniversalSigValidator address on each target chain before enabling it there.

Prepares SignatureVerifier to use an Ethereum RPC client for upcoming
ERC-1271 and ERC-6492 smart wallet signature validation. Adds an optional
rpc_url parameter to EVMVerifier and SignatureVerifier, and wires
config.ethereum.api_url to all 3 SignatureVerifier instantiation sites.
Extends EVMVerifier with a fallback path that calls isValidSignature
on deployed smart contract wallets when plain ECDSA recovery does not
match the sender. Falls back only when an rpc_url is configured and
the sender has deployed bytecode, so plain EOAs remain zero-cost.
When a signature ends with the ERC-6492 magic suffix, skip ECDSA
recovery and route validation through the UniversalSigValidator
contract, which simulates factory deployment and calls isValidSignature
in a single eth_call. This enables message verification from Privy /
ZeroDev / Safe smart accounts that have not yet been deployed on chain.
@amalcaraz amalcaraz self-assigned this Apr 23, 2026
…r address

The previous implementation called a hallucinated UniversalSigValidator at
0x0000...3823, which is not deployed anywhere. Every ERC-6492 signature
silently failed because the eth_call returned empty bytes.

EIP-6492 actually uses a contract-creation trick: send the
ValidateSigOffchain deployer bytecode as 'data' in an eth_call with no 'to'
field. The bytecode runs as a constructor, deploys the UniversalSigValidator
inline, simulates factory deployment, calls isValidSignature, and returns a
single byte (0x01 valid / 0x00 invalid) - all in one eth_call.

The canonical bytecode (reference impl from AmbireTech/signature-validator)
is shipped as an asset at src/aleph/chains/assets/erc6492_validator_bytecode.hex.

Verified live against Ethereum mainnet with the real Aleph message
f4daf9c0dadd7aa89c37e62e24f90a032183ba3b829b2bd2cf87568a940fd0a8 - now
validates correctly.
Adds tests/chains/test_evm_integration.py with a single test that runs the
real rejected Aleph message (f4daf9c0...fd0a8) end-to-end against a public
Ethereum mainnet RPC, verifying the EIP-6492 contract-creation pattern
actually works (not just against mocks).

Uses a new 'network' pytest marker, excluded from the default run via
addopts. Enable explicitly with:

    hatch run testing:test tests/chains/test_evm_integration.py -m network -v

RPC URL is configurable via the ALEPH_TEST_ETH_RPC env var; defaults to
ethereum-rpc.publicnode.com.
@amalcaraz amalcaraz requested review from aliel and odesenfans April 23, 2026 15:58
@amalcaraz amalcaraz marked this pull request as ready for review April 23, 2026 15:58
Copy link
Copy Markdown

@foxpatch-aleph foxpatch-aleph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR correctly implements ERC-6492 counterfactual and ERC-1271 deployed smart wallet signature verification. The detection/fallback logic is sound: ERC-6492 magic suffix is detected upfront (avoiding the useless ECDSA attempt), plain EOA messages pay zero RPC calls, and ERC-1271 is a clean fallback when ECDSA fails and the sender has deployed bytecode. The chain scoping to ETH mainnet only is correctly implemented. Tests cover all paths including a real mainnet integration test. Documentation is thorough. No bugs or security issues found.

src/aleph/chains/evm.py (line 155): The LOGGER.warning on ECDSA mismatch will fire for every smart wallet signature that falls back to ERC-1271. Consider using LOGGER.info or LOGGER.debug here since this is expected behavior for smart wallet signatures, not an anomaly worth warning about.

src/aleph/chains/evm.py (line 24): ERC1271_MAGIC and IS_VALID_SIGNATURE_SELECTOR have the same value but serve different purposes (response validation vs. request building). Consider adding a comment clarifying this, e.g. ERC1271_MAGIC = bytes.fromhex('1626ba7e') # Also used as isValidSignature selector.

src/aleph/chains/assets/erc6492_validator_bytecode.hex (line 1): Missing trailing newline. While strip() handles this in _universal_validator_bytecode(), it's best practice for text assets to end with a newline. Consider adding one.

src/aleph/chains/signature_verifier.py (line 29): Note: all non-ETH EVM chains share the same evm = EVMVerifier() instance. This is fine since EVMVerifier is stateless (no RPC), but worth being aware of in case per-chain RPCs are added later — each chain would need its own instance.

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