Feat/erc6492 erc1271 validation#1109
Conversation
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.
…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.
foxpatch-aleph
left a comment
There was a problem hiding this comment.
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.
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,
EVMVerifieronly ranecrecoveron 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. message6f699b25…1a26d.Verification flow
EVMVerifier.verify_signaturenow picks a path based on the signature shape:0x6492…6492magic suffixeth_calltoUniversalSigValidatorecrecovers to the sendereth_call(get_code+isValidSignature)UniversalSigValidatorat0x0000000000002fd5Aeb385D324B580FCa7c83823, which atomically simulates factory deployment and callsisValidSignaturein a singleeth_call. See EIP-6492.Scope: Ethereum mainnet only (for now)
Smart wallets use
block.chainidin 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, onlyChain.ETHgets the RPC-backed paths:ETHEthereumVerifier(rpc_url=…)ETHERLINKEthereumVerifier()(no RPC)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
EVMVerifierandSignatureVerifiernow accept an optionalrpc_url.SignatureVerifier()call sites (api_entrypoint.py,jobs/process_pending_messages.py,jobs/fetch_pending_messages.py) passconfig.ethereum.api_url.value.Security guarantees
For a signature from smart wallet
0xa9F3…1635(Kernel, owner EOA0xfFFE…4fD7):keccak256(EIP-191(chain + sender + type + item_hash)). Any tampering invalidates.sender = 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.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 +AsyncWeb3clientsrc/aleph/chains/signature_verifier.py— threadrpc_urlthrough; scope to ETH onlysrc/aleph/api_entrypoint.py,src/aleph/jobs/{process,fetch}_pending_messages.py— passconfig.ethereum.api_urltoSignatureVerifiertests/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 analysisTest Plan
hatch run testing:test tests/chains/test_evm.py -v→ 13/13 passinghatch 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 cleanethereum.api_urlpointing to a working mainnet RPC → should be acceptedChain.BASE→ confirm it is rejected (out of scope until per-chain RPCs land)Follow-ups (out of scope for this PR)
UniversalSigValidatoraddress on each target chain before enabling it there.