From d27de5c0c52b0d318c03504719810ed0428c0d60 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 03:40:29 +0200 Subject: [PATCH 1/9] feat: thread rpc_url through EVMVerifier and SignatureVerifier 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. --- src/aleph/api_entrypoint.py | 2 +- src/aleph/chains/evm.py | 4 ++ src/aleph/chains/signature_verifier.py | 52 +++++++++++----------- src/aleph/jobs/fetch_pending_messages.py | 2 +- src/aleph/jobs/process_pending_messages.py | 2 +- tests/chains/test_evm.py | 12 +++++ 6 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/aleph/api_entrypoint.py b/src/aleph/api_entrypoint.py index b4d0428ed..aa0a6659f 100644 --- a/src/aleph/api_entrypoint.py +++ b/src/aleph/api_entrypoint.py @@ -60,7 +60,7 @@ async def configure_aiohttp_app( ipfs_service=ipfs_service, node_cache=node_cache, ) - signature_verifier = SignatureVerifier() + signature_verifier = SignatureVerifier(rpc_url=config.ethereum.api_url.value) app = create_aiohttp_app( max_file_size=config.storage.max_file_size.value, diff --git a/src/aleph/chains/evm.py b/src/aleph/chains/evm.py index 2de64a91e..d5a101c4d 100644 --- a/src/aleph/chains/evm.py +++ b/src/aleph/chains/evm.py @@ -1,5 +1,6 @@ import functools import logging +from typing import Optional from eth_account import Account from eth_account.messages import encode_defunct @@ -14,6 +15,9 @@ class EVMVerifier(Verifier): + def __init__(self, rpc_url: Optional[str] = None): + self.rpc_url = rpc_url + async def verify_signature(self, message: BasePendingMessage) -> bool: """Verifies a signature of a message, return True if verified, false if not""" diff --git a/src/aleph/chains/signature_verifier.py b/src/aleph/chains/signature_verifier.py index 447c9cab6..601d30c73 100644 --- a/src/aleph/chains/signature_verifier.py +++ b/src/aleph/chains/signature_verifier.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional from aleph_message.models import Chain @@ -19,39 +19,41 @@ class SignatureVerifier: verifiers: Dict[Chain, Verifier] - def __init__(self): + def __init__(self, rpc_url: Optional[str] = None): + evm = EVMVerifier(rpc_url=rpc_url) + eth = EthereumVerifier(rpc_url=rpc_url) self.verifiers = { - Chain.ARBITRUM: EVMVerifier(), + Chain.ARBITRUM: evm, Chain.AVAX: AvalancheConnector(), - Chain.BASE: EVMVerifier(), - Chain.BLAST: EVMVerifier(), - Chain.BOB: EVMVerifier(), - Chain.BSC: EVMVerifier(), - Chain.CYBER: EVMVerifier(), + Chain.BASE: evm, + Chain.BLAST: evm, + Chain.BOB: evm, + Chain.BSC: evm, + Chain.CYBER: evm, Chain.CSDK: CosmosConnector(), Chain.DOT: SubstrateConnector(), Chain.ECLIPSE: SolanaConnector(), - Chain.ETH: EthereumVerifier(), - Chain.ETHERLINK: EthereumVerifier(), - Chain.FRAXTAL: EVMVerifier(), - Chain.HYPE: EVMVerifier(), - Chain.INK: EVMVerifier(), - Chain.LENS: EVMVerifier(), - Chain.METIS: EVMVerifier(), - Chain.MODE: EVMVerifier(), - Chain.NEO: EVMVerifier(), + Chain.ETH: eth, + Chain.ETHERLINK: eth, + Chain.FRAXTAL: evm, + Chain.HYPE: evm, + Chain.INK: evm, + Chain.LENS: evm, + Chain.METIS: evm, + Chain.MODE: evm, + Chain.NEO: evm, Chain.NULS: NulsConnector(), Chain.NULS2: Nuls2Verifier(), - Chain.LINEA: EVMVerifier(), - Chain.LISK: EVMVerifier(), - Chain.OPTIMISM: EVMVerifier(), - Chain.POL: EVMVerifier(), + Chain.LINEA: evm, + Chain.LISK: evm, + Chain.OPTIMISM: evm, + Chain.POL: evm, Chain.SOL: SolanaConnector(), - Chain.SONIC: EVMVerifier(), - Chain.UNICHAIN: EVMVerifier(), + Chain.SONIC: evm, + Chain.UNICHAIN: evm, Chain.TEZOS: TezosVerifier(), - Chain.WORLDCHAIN: EVMVerifier(), - Chain.ZORA: EVMVerifier(), + Chain.WORLDCHAIN: evm, + Chain.ZORA: evm, } async def verify_signature(self, message: BasePendingMessage) -> None: diff --git a/src/aleph/jobs/fetch_pending_messages.py b/src/aleph/jobs/fetch_pending_messages.py index 6fa60e04a..5a24355ae 100644 --- a/src/aleph/jobs/fetch_pending_messages.py +++ b/src/aleph/jobs/fetch_pending_messages.py @@ -309,7 +309,7 @@ async def fetch_messages_task(config: Config): ipfs_service=ipfs_service, node_cache=node_cache, ) - signature_verifier = SignatureVerifier() + signature_verifier = SignatureVerifier(rpc_url=config.ethereum.api_url.value) message_handler = MessageHandler( signature_verifier=signature_verifier, storage_service=storage_service, diff --git a/src/aleph/jobs/process_pending_messages.py b/src/aleph/jobs/process_pending_messages.py index 4bf5819a1..c9492f842 100644 --- a/src/aleph/jobs/process_pending_messages.py +++ b/src/aleph/jobs/process_pending_messages.py @@ -172,7 +172,7 @@ async def fetch_and_process_messages_task(config: Config): ipfs_service=ipfs_service, node_cache=node_cache, ) - signature_verifier = SignatureVerifier() + signature_verifier = SignatureVerifier(rpc_url=config.ethereum.api_url.value) message_handler = MessageHandler( signature_verifier=signature_verifier, storage_service=storage_service, diff --git a/tests/chains/test_evm.py b/tests/chains/test_evm.py index f187fc7f7..cd53b3d36 100644 --- a/tests/chains/test_evm.py +++ b/tests/chains/test_evm.py @@ -52,3 +52,15 @@ async def test_verify_bad_evm_signature(evm_message: BasePendingMessage): evm_message.signature = "baba" result = await verifier.verify_signature(evm_message) assert result is False + + +def test_evm_verifier_accepts_rpc_url(): + """EVMVerifier can be constructed with an rpc_url without errors.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + assert verifier.rpc_url == "http://localhost:8545" + + +def test_evm_verifier_no_rpc_url(): + """EVMVerifier can still be constructed with no rpc_url.""" + verifier = EVMVerifier() + assert verifier.rpc_url is None From 718a343791844baad4bca4ed951264c648dfd7ab Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 03:44:37 +0200 Subject: [PATCH 2/9] feat: add ERC-1271 validation for deployed smart contract wallets 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. --- src/aleph/chains/evm.py | 99 +++++++++++++++++++++++++++++++++------- tests/chains/test_evm.py | 77 ++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 18 deletions(-) diff --git a/src/aleph/chains/evm.py b/src/aleph/chains/evm.py index d5a101c4d..f91fd9821 100644 --- a/src/aleph/chains/evm.py +++ b/src/aleph/chains/evm.py @@ -2,8 +2,11 @@ import logging from typing import Optional +from eth_abi.abi import encode from eth_account import Account -from eth_account.messages import encode_defunct +from eth_account.messages import _hash_eip191_message, encode_defunct +from eth_utils.address import to_checksum_address +from web3 import AsyncHTTPProvider, AsyncWeb3 from aleph.chains.common import get_verification_buffer from aleph.schemas.pending_messages import BasePendingMessage @@ -13,39 +16,101 @@ LOGGER = logging.getLogger("chains.evm") +ERC1271_MAGIC = bytes.fromhex("1626ba7e") +IS_VALID_SIGNATURE_SELECTOR = bytes.fromhex("1626ba7e") + class EVMVerifier(Verifier): def __init__(self, rpc_url: Optional[str] = None): self.rpc_url = rpc_url + self._w3: Optional[AsyncWeb3] = None + + def _get_web3_client(self) -> Optional[AsyncWeb3]: + if self.rpc_url is None: + return None + if self._w3 is None: + self._w3 = AsyncWeb3(AsyncHTTPProvider(self.rpc_url)) + return self._w3 + + async def _verify_erc1271( + self, + w3: AsyncWeb3, + sender: str, + message_hash: bytes, + signature: bytes, + ) -> bool: + """Call isValidSignature on a deployed ERC-1271 contract.""" + try: + calldata = IS_VALID_SIGNATURE_SELECTOR + encode( + ["bytes32", "bytes"], [message_hash, signature] + ) + result = await w3.eth.call({"to": sender, "data": calldata}) + return result[:4] == ERC1271_MAGIC + except Exception: + LOGGER.exception("Error calling isValidSignature on %s", sender) + return False async def verify_signature(self, message: BasePendingMessage) -> bool: - """Verifies a signature of a message, return True if verified, false if not""" + """Verifies a signature of a message, return True if verified, false if not. + Order (cheapest first): + 1. Plain ECDSA ecrecover (0 RPC calls) + 2. ERC-1271 isValidSignature on deployed contract (1 eth_call) + """ verification = get_verification_buffer(message) - - message_hash = await run_in_executor( - None, functools.partial(encode_defunct, text=verification.decode("utf-8")) + message_hash_obj = await run_in_executor( + None, + functools.partial(encode_defunct, text=verification.decode("utf-8")), ) + raw_hash: bytes = _hash_eip191_message(message_hash_obj) + + if not message.signature: + return False - verified = False try: - # we assume the signature is a valid string + sig_bytes = bytes.fromhex(message.signature.removeprefix("0x")) + except ValueError: + sig_bytes = b"" + + # Path 1: plain ECDSA + try: address = await run_in_executor( None, functools.partial( - Account.recover_message, message_hash, signature=message.signature + Account.recover_message, + message_hash_obj, + signature=message.signature, ), ) if address.lower() == message.sender.lower(): - verified = True - else: - LOGGER.warning( - "Received bad signature from %s for %s" % (address, message.sender) - ) - return False + return True + LOGGER.warning( + "ECDSA recovered %s != sender %s, falling back to ERC-1271", + address, + message.sender, + ) + except Exception: + LOGGER.debug( + "ECDSA recovery failed for %s, trying ERC-1271", message.sender + ) + # Path 2: ERC-1271 (deployed contract) + w3 = self._get_web3_client() + if w3 is None: + LOGGER.warning( + "Signature for %s failed ECDSA and no rpc_url configured for ERC-1271", + message.sender, + ) + return False + + sender_checksum = to_checksum_address(message.sender) + try: + code = await w3.eth.get_code(sender_checksum) except Exception: - LOGGER.exception("Error processing signature for %s" % message.sender) - verified = False + LOGGER.exception("Error checking contract code for %s", message.sender) + return False + + if not code or code == b"0x": + return False - return verified + return await self._verify_erc1271(w3, sender_checksum, raw_hash, sig_bytes) diff --git a/tests/chains/test_evm.py b/tests/chains/test_evm.py index cd53b3d36..de1122d69 100644 --- a/tests/chains/test_evm.py +++ b/tests/chains/test_evm.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -64,3 +64,78 @@ def test_evm_verifier_no_rpc_url(): """EVMVerifier can still be constructed with no rpc_url.""" verifier = EVMVerifier() assert verifier.rpc_url is None + + +@pytest.fixture +def erc1271_message() -> BasePendingMessage: + """Message signed by a deployed Kernel smart wallet (86-byte inner sig, no ERC-6492 wrapper).""" + return parse_message( + { + "item_hash": "442b2570512753ed1b41f84e8202023f19fd5d5ba31117c8319ea173a92488bd", + "type": "POST", + "chain": "ETH", + "sender": "0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635", + "signature": "0x01845adb2c711129d4f3966735ed98a9f09fc4ce5794f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe3731c", + "time": 1730410918.0, + "item_type": "inline", + "item_content": '{"address":"0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635","time":1730410918.0,"content":{},"type":"test"}', + "channel": "TEST", + } + ) + + +@pytest.mark.asyncio +async def test_verify_erc1271_deployed_valid(erc1271_message: BasePendingMessage): + """ERC-1271: valid sig from a deployed smart wallet returns True.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + + mock_w3 = AsyncMock() + mock_w3.eth.get_code = AsyncMock(return_value=b"\x60\x80") + mock_w3.eth.call = AsyncMock(return_value=bytes.fromhex("1626ba7e" + "00" * 28)) + + with patch.object(verifier, "_get_web3_client", return_value=mock_w3): + result = await verifier.verify_signature(erc1271_message) + + assert result is True + + +@pytest.mark.asyncio +async def test_verify_erc1271_deployed_invalid(erc1271_message: BasePendingMessage): + """ERC-1271: wrong response from isValidSignature returns False.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + + mock_w3 = AsyncMock() + mock_w3.eth.get_code = AsyncMock(return_value=b"\x60\x80") + mock_w3.eth.call = AsyncMock(return_value=bytes.fromhex("deadbeef" + "00" * 28)) + + with patch.object(verifier, "_get_web3_client", return_value=mock_w3): + result = await verifier.verify_signature(erc1271_message) + + assert result is False + + +@pytest.mark.asyncio +async def test_verify_erc1271_no_rpc_falls_back_to_ecdsa( + erc1271_message: BasePendingMessage, +): + """Without rpc_url, smart wallet sigs fail gracefully (no RPC available).""" + verifier = EVMVerifier() + result = await verifier.verify_signature(erc1271_message) + assert result is False + + +@pytest.mark.asyncio +async def test_verify_erc1271_skipped_when_no_code( + erc1271_message: BasePendingMessage, +): + """If sender has no deployed code, ERC-1271 path is skipped.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + + mock_w3 = AsyncMock() + mock_w3.eth.get_code = AsyncMock(return_value=b"") + + with patch.object(verifier, "_get_web3_client", return_value=mock_w3): + result = await verifier.verify_signature(erc1271_message) + + assert result is False + mock_w3.eth.call.assert_not_called() From 02cc8b74f0f7011b48ef6d5fb31b1d850a0bb4c3 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 03:53:25 +0200 Subject: [PATCH 3/9] feat: add ERC-6492 validation for counterfactual smart contract wallets 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. --- src/aleph/chains/evm.py | 58 ++++++++++++++++++--- tests/chains/test_evm.py | 107 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 6 deletions(-) diff --git a/src/aleph/chains/evm.py b/src/aleph/chains/evm.py index f91fd9821..34fe4ce29 100644 --- a/src/aleph/chains/evm.py +++ b/src/aleph/chains/evm.py @@ -16,8 +16,16 @@ LOGGER = logging.getLogger("chains.evm") +ERC6492_MAGIC = bytes.fromhex( + "6492649264926492649264926492649264926492649264926492649264926492" +) ERC1271_MAGIC = bytes.fromhex("1626ba7e") IS_VALID_SIGNATURE_SELECTOR = bytes.fromhex("1626ba7e") +# isValidSigWithSideEffects(address,bytes32,bytes) +UNIVERSAL_VALIDATOR_SELECTOR = bytes.fromhex("8dca4bea") +# ERC-6492 UniversalSigValidator (deterministic CREATE2 address) +# See: https://eips.ethereum.org/EIPS/eip-6492 +UNIVERSAL_VALIDATOR_ADDRESS = "0x0000000000002fd5Aeb385D324B580FCa7c83823" class EVMVerifier(Verifier): @@ -32,6 +40,31 @@ def _get_web3_client(self) -> Optional[AsyncWeb3]: self._w3 = AsyncWeb3(AsyncHTTPProvider(self.rpc_url)) return self._w3 + @staticmethod + def _is_erc6492(sig_bytes: bytes) -> bool: + return len(sig_bytes) >= 32 and sig_bytes[-32:] == ERC6492_MAGIC + + async def _verify_erc6492( + self, + w3: AsyncWeb3, + sender: str, + message_hash: bytes, + signature: bytes, + ) -> bool: + """Validate an ERC-6492 counterfactual signature via UniversalSigValidator.""" + try: + calldata = UNIVERSAL_VALIDATOR_SELECTOR + encode( + ["address", "bytes32", "bytes"], + [sender, message_hash, signature], + ) + result = await w3.eth.call( + {"to": UNIVERSAL_VALIDATOR_ADDRESS, "data": calldata} + ) + return bool(int.from_bytes(result, "big")) + except Exception: + LOGGER.exception("Error calling UniversalSigValidator for %s", sender) + return False + async def _verify_erc1271( self, w3: AsyncWeb3, @@ -53,9 +86,10 @@ async def _verify_erc1271( async def verify_signature(self, message: BasePendingMessage) -> bool: """Verifies a signature of a message, return True if verified, false if not. - Order (cheapest first): - 1. Plain ECDSA ecrecover (0 RPC calls) - 2. ERC-1271 isValidSignature on deployed contract (1 eth_call) + Detection / fallback order (cheapest first, except ERC-6492 which is detected upfront): + - ERC-6492 magic suffix → UniversalSigValidator eth_call (skip ECDSA) + - Plain ECDSA ecrecover (0 RPC calls) + - ERC-1271 isValidSignature on deployed contract (1 eth_call) """ verification = get_verification_buffer(message) message_hash_obj = await run_in_executor( @@ -72,7 +106,20 @@ async def verify_signature(self, message: BasePendingMessage) -> bool: except ValueError: sig_bytes = b"" - # Path 1: plain ECDSA + sender_checksum = to_checksum_address(message.sender) + + # Path 1: ERC-6492 counterfactual (magic suffix detected, skip ECDSA) + if self._is_erc6492(sig_bytes): + w3 = self._get_web3_client() + if w3 is None: + LOGGER.warning( + "ERC-6492 signature for %s but no rpc_url configured", + message.sender, + ) + return False + return await self._verify_erc6492(w3, sender_checksum, raw_hash, sig_bytes) + + # Path 2: plain ECDSA try: address = await run_in_executor( None, @@ -94,7 +141,7 @@ async def verify_signature(self, message: BasePendingMessage) -> bool: "ECDSA recovery failed for %s, trying ERC-1271", message.sender ) - # Path 2: ERC-1271 (deployed contract) + # Path 3: ERC-1271 (deployed contract) w3 = self._get_web3_client() if w3 is None: LOGGER.warning( @@ -103,7 +150,6 @@ async def verify_signature(self, message: BasePendingMessage) -> bool: ) return False - sender_checksum = to_checksum_address(message.sender) try: code = await w3.eth.get_code(sender_checksum) except Exception: diff --git a/tests/chains/test_evm.py b/tests/chains/test_evm.py index de1122d69..8f0b7ab7f 100644 --- a/tests/chains/test_evm.py +++ b/tests/chains/test_evm.py @@ -139,3 +139,110 @@ async def test_verify_erc1271_skipped_when_no_code( assert result is False mock_w3.eth.call.assert_not_called() + + +# The real ERC-6492 signature from message 6f699b25... +ERC6492_SIG = ( + "0x000000000000000000000000d703aae79538628d27099b8c4f621be4ccd142d5" + "0000000000000000000000000000000000000000000000000000000000000060" + "0000000000000000000000000000000000000000000000000000000000000260" + "00000000000000000000000000000000000000000000000000000000000001c4" + "c5265d5d000000000000000000000000aac5d4240af87249b3f71bc8e4a2cae074a3e419" + "0000000000000000000000000000000000000000000000000000000000000060" + "0000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000001243c3b752b" + "01845ADb2C711129d4f3966735eD98a9F09fC4cE57" + "0000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000" + "0000a000000000000000000000000000000000000000000000000000000000000000e0" + "0000000000000000000000000000000000000000000000000000000000000100" + "0000000000000000000000000000000000000000000000000000000000000014" + "fFFEfCDE25e1d00474530f1A7b90D02CEda94fD7" + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000056" + "01845ADb2C711129d4f3966735eD98a9F09fC4cE57" + "94f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae" + "316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe373" + "1c000000000000000000" + "006492649264926492649264926492649264926492649264926492649264926492" +) + + +@pytest.fixture +def erc6492_message() -> BasePendingMessage: + """Message with ERC-6492 counterfactual smart wallet signature.""" + return parse_message( + { + "item_hash": "316b07861dee3fcaa40d10a563fec5e8d6b4a81514b1265941bec861ce3e95ae", + "type": "POST", + "chain": "ETH", + "sender": "0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635", + "signature": ERC6492_SIG, + "time": 1745362074.0, + "item_type": "inline", + "item_content": '{"address":"0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635","time":1745362074.0,"content":{"key":"test2","label":"2222"},"type":"ALEPH-SSH"}', + "channel": "ALEPH-CLOUDSOLUTIONS", + } + ) + + +@pytest.mark.asyncio +async def test_verify_erc6492_counterfactual_valid( + erc6492_message: BasePendingMessage, +): + """ERC-6492: valid sig from a counterfactual smart wallet returns True.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + + mock_w3 = AsyncMock() + mock_w3.eth.call = AsyncMock(return_value=(1).to_bytes(32, "big")) + + with patch.object(verifier, "_get_web3_client", return_value=mock_w3): + result = await verifier.verify_signature(erc6492_message) + + assert result is True + call_args = mock_w3.eth.call.call_args[0][0] + assert call_args["to"].lower() == "0x0000000000002fd5aeb385d324b580fca7c83823" + + +@pytest.mark.asyncio +async def test_verify_erc6492_counterfactual_invalid( + erc6492_message: BasePendingMessage, +): + """ERC-6492: UniversalSigValidator returns 0 → invalid.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + + mock_w3 = AsyncMock() + mock_w3.eth.call = AsyncMock(return_value=(0).to_bytes(32, "big")) + + with patch.object(verifier, "_get_web3_client", return_value=mock_w3): + result = await verifier.verify_signature(erc6492_message) + + assert result is False + + +@pytest.mark.asyncio +async def test_erc6492_detection_skips_ecdsa( + erc6492_message: BasePendingMessage, +): + """ERC-6492 signatures skip ECDSA entirely and go straight to UniversalSigValidator.""" + verifier = EVMVerifier(rpc_url="http://localhost:8545") + + mock_w3 = AsyncMock() + mock_w3.eth.call = AsyncMock(return_value=(1).to_bytes(32, "big")) + + with patch.object(verifier, "_get_web3_client", return_value=mock_w3): + with patch("aleph.chains.evm.Account.recover_message") as mock_recover: + await verifier.verify_signature(erc6492_message) + mock_recover.assert_not_called() + + +@pytest.mark.asyncio +async def test_verify_erc6492_no_rpc_fails(erc6492_message: BasePendingMessage): + """Without rpc_url, ERC-6492 sigs fail (cannot call UniversalSigValidator).""" + verifier = EVMVerifier() + result = await verifier.verify_signature(erc6492_message) + assert result is False From 6c1bfb3bf71abf41f80b3fd9b4d019a906c1e908 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 13:36:31 +0200 Subject: [PATCH 4/9] docs: add ERC-6492/ERC-1271 smart wallet signatures reference --- docs/protocol/smart-wallet-signatures.md | 241 +++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 docs/protocol/smart-wallet-signatures.md diff --git a/docs/protocol/smart-wallet-signatures.md b/docs/protocol/smart-wallet-signatures.md new file mode 100644 index 000000000..26d1634d9 --- /dev/null +++ b/docs/protocol/smart-wallet-signatures.md @@ -0,0 +1,241 @@ +# ERC-6492 / ERC-1271 Smart Wallet Signatures in Aleph + +> Technical reference for smart contract wallet signature verification, generated from +> analysis of the real Aleph message +> `6f699b252db10e65e8651a77289a7789e2da77ce26c4f4ad247fbe9bd1e1a26d`. + +--- + +## 1. Context: why these signatures appear + +Providers such as **Privy** create **smart account** wallets (ZeroDev Kernel) for +users. These contracts can exist **counterfactually** — the address is deterministic +(CREATE2), but the bytecode is not on chain until someone pays the gas for the first +deployment. + +When a user signs an Aleph message with Privy before their wallet has been deployed: + +- Privy produces an **ERC-6492** signature that bundles the deployment instructions + for the wallet together with the inner ECDSA signature. +- Before this change, pyaleph did not understand this format and rejected the signature + as invalid. + +--- + +## 2. Contract stack involved (real-world example) + +| Address | Role | +|---|---| +| `0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635` | **Kernel smart wallet** (Aleph sender). Not yet deployed. | +| `0xd703aae79538628d27099b8c4f621be4ccd142d5` | **Factory** — deploys the smart wallet via `createAccount()` | +| `0xaac5d4240af87249b3f71bc8e4a2cae074a3e419` | **Kernel implementation** — the logic singleton | +| `0x845ADb2C711129d4f3966735eD98a9F09fC4cE57` | **ECDSA Validator plugin** — verifies that the owner signed | +| `0xfFFEfCDE25e1d00474530f1A7b90D02CEda94fD7` | **Owner EOA** — the private key that actually produces the ECDSA signature | + +--- + +## 3. Full schema of an ERC-6492 signature + +``` +FULL SIGNATURE (hex) +│ +├── ABI-encoded(address factory, bytes calldata, bytes innerSig) +│ │ +│ ├── [0:32] factory = 0xd703aae79538628d27099b8c4f621be4ccd142d5 (padded) +│ ├── [32:64] offset of calldata = 0x60 (96) +│ ├── [64:96] offset of innerSig = 0x260 (608) +│ │ +│ ├── CALLDATA (452 bytes) — instructions for the factory +│ │ ├── selector: 0xc5265d5d → createAccount(address impl, bytes initData, uint256 index) +│ │ ├── impl: 0xaac5d4240af87249b3f71bc8e4a2cae074a3e419 +│ │ ├── index: 0 +│ │ └── initData (Kernel initialize, 292 bytes): +│ │ ├── selector: 0x3c3b752b → initialize(...) +│ │ ├── validator type: 0x01 (root ECDSA validator) +│ │ ├── validator plugin: 0x845ADb2C711129d4f3966735eD98a9F09fC4cE57 +│ │ └── owner EOA: 0xfFFEfCDE25e1d00474530f1A7b90D02CEda94fD7 ← real private key +│ │ +│ └── INNER SIGNATURE (86 bytes) — signature produced by the owner EOA +│ ├── [0] type byte: 0x01 (root validator) +│ ├── [1:21] validator: 0x845ADb2C711129d4f3966735eD98a9F09fC4cE57 +│ └── [21:86] ECDSA (65 bytes): +│ ├── r: 0x94f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae +│ ├── s: 0x316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe373 +│ └── v: 28 (0x1c) +│ +└── MAGIC SUFFIX (32 bytes): 0x6492649264926492649264926492649264926492649264926492649264926492 +``` + +--- + +## 4. The message being signed in Aleph + +Pyaleph builds the verification buffer in `src/aleph/chains/common.py`: + +```python +buffer = f"{message.chain.value}\n{message.sender}\n{message.type.value}\n{message.item_hash}" +# Real example: +# "ETH\n0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635\nPOST\n6f699b252db..." +``` + +It then wraps the buffer with the EIP-191 personal sign prefix in +`src/aleph/chains/evm.py`: + +```python +message_hash = encode_defunct(text=verification.decode("utf-8")) +# Equivalent to eth_sign(buffer) = keccak256("\x19Ethereum Signed Message:\n" + len + buffer) +``` + +**Important:** the owner EOA (`0xfFFE…`) does **not** sign this hash directly. Kernel +wraps it in its own EIP-712 hash before presenting it to the EOA. This is why a direct +`ecrecover` over the inner ECDSA bytes returns `0x2eA6…` (an unrelated address) instead +of the owner `0xfFFE…`. The only correct verification path is to call the contract +itself (`isValidSignature`), which performs that wrapping internally. + +--- + +## 5. Why signatures used to fail (two reasons) + +### Reason A — ERC-6492 was not detected + +`EVMVerifier.verify_signature` called `Account.recover_message(hash, signature)` on the +full 800+ byte blob. `eth-account` either raised or returned a meaningless address. + +### Reason B — the owner does not sign the Aleph hash directly + +Kernel wraps the message with its own EIP-712 domain before asking the owner to sign. +The only correct way to verify this is to call the contract (`isValidSignature`), +which performs the wrapping internally. + +--- + +## 6. Example signature BEFORE deployment (ERC-6492) + +The wallet `0xa9F3…1635` is NOT yet on chain. The signature carries the full ERC-6492 +wrapper: + +``` +0x +# ABI-encoded (factory, calldata, innerSig): +000000000000000000000000d703aae79538628d27099b8c4f621be4ccd142d5 ← factory +0000000000000000000000000000000000000000000000000000000000000060 ← calldata offset +0000000000000000000000000000000000000000000000000000000000000260 ← innerSig offset +...452 bytes of factory calldata (createAccount + initialize)... +...86 bytes of inner signature (type + validator + r+s+v)... +6492649264926492649264926492649264926492649264926492649264926492 ← MAGIC (32 bytes) +``` + +**Validation requires:** simulate wallet deployment → call `isValidSignature`. + +--- + +## 7. Example signature AFTER deployment (ERC-1271) + +Once the wallet `0xa9F3…1635` has been deployed, Privy produces a signature without the +ERC-6492 wrapper. Only the 86 bytes of the inner signature are sent: + +``` +0x +01 ← type byte (root validator) +845adb2c711129d4f3966735ed98a9f09fc4ce57 ← validator plugin (20 bytes) +94f8df9bcc3e2fa2049519666e9977ff76f9c993 ← ECDSA r (partial, 32 bytes) +... +1c ← ECDSA v +``` + +Full hex example (86 bytes): + +``` +0x01845adb2c711129d4f3966735ed98a9f09fc4ce5794f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe3731c +``` + +**Validation:** call `isValidSignature(hash, sig)` directly on the deployed contract. + +--- + +## 8. Correct validation flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ Does the signature end with 0x6492…6492 (ERC-6492)? │ +└─────────────────────────────────────────────────────────┘ + │ Yes │ No + ▼ ▼ +┌─────────────────────────┐ ┌────────────────────────────┐ +│ ERC-6492 (counterfactual)│ │ Is len(sig) == 65 bytes? │ +│ Decode (factory, │ └────────────────────────────┘ +│ calldata, innerSig) │ │ Yes │ No +│ Call UniversalSig │ ▼ ▼ +│ Validator via eth_call │ ┌──────────────┐ ┌──────────────┐ +│ → isValidSignature? │ │ Plain ECDSA │ │ ERC-1271 │ +└─────────────────────────┘ │ ecrecover │ │ eth_call │ + ▼ │ == sender? │ │ isValidSig │ + valid/invalid └──────────────┘ └──────────────┘ +``` + +**Cost in RPC calls:** + +- Plain ECDSA: 0 calls (cheapest) +- ERC-1271 deployed: 1 `eth_call` +- ERC-6492 counterfactual: 1 `eth_call` (via `UniversalSigValidator`) + +--- + +## 9. ERC-1271 `isValidSignature` + +Selector: `0x1626ba7e` + +```python +from eth_abi.abi import encode + +VALID_SIG_MAGIC = bytes.fromhex("1626ba7e") +IS_VALID_SIG_SELECTOR = bytes.fromhex("1626ba7e") + +calldata = IS_VALID_SIG_SELECTOR + encode( + ["bytes32", "bytes"], [message_hash, signature] +) + +result = await w3.eth.call({"to": sender_address, "data": calldata}) +is_valid = result[:4] == VALID_SIG_MAGIC +``` + +--- + +## 10. ERC-6492 `UniversalSigValidator` + +ERC-6492 defines a `UniversalSigValidator` contract deployed at a deterministic address +on every major EVM chain. It validates counterfactual signatures in a single `eth_call`: + +```python +from eth_abi.abi import encode + +# UniversalSigValidator address (deterministic CREATE2) +# See: https://eips.ethereum.org/EIPS/eip-6492 +UNIVERSAL_VALIDATOR = "0x0000000000002fd5Aeb385D324B580FCa7c83823" + +calldata = encode( + ["address", "bytes32", "bytes"], + [sender_address, message_hash, full_erc6492_signature], +) + +# Call with selector isValidSigWithSideEffects(address,bytes32,bytes) = 0x8dca4bea +selector = bytes.fromhex("8dca4bea") +result = await w3.eth.call({ + "to": UNIVERSAL_VALIDATOR, + "data": selector + calldata, +}) +is_valid = bool(int.from_bytes(result, "big")) +``` + +--- + +## 11. Files changed in pyaleph + +| File | Change | +|---|---| +| `src/aleph/chains/evm.py` | Add ERC-6492 / ERC-1271 detection paths + RPC client | +| `src/aleph/chains/signature_verifier.py` | Pass `rpc_url` to `EVMVerifier` | +| `src/aleph/api_entrypoint.py` | Pass `config.ethereum.api_url.value` to `SignatureVerifier` | +| `src/aleph/jobs/process_pending_messages.py` | Same | +| `src/aleph/jobs/fetch_pending_messages.py` | Same | +| `tests/chains/test_evm.py` | Tests for ERC-1271 and ERC-6492 paths | From 6a66a2075eb2d439d08301d57df52138ea6c08db Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 13:48:11 +0200 Subject: [PATCH 5/9] feat: restrict ERC-1271/ERC-6492 validation to Ethereum mainnet --- docs/protocol/smart-wallet-signatures.md | 24 +++++++++++++++++++++++- src/aleph/chains/signature_verifier.py | 11 +++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/protocol/smart-wallet-signatures.md b/docs/protocol/smart-wallet-signatures.md index 26d1634d9..b2f9c889e 100644 --- a/docs/protocol/smart-wallet-signatures.md +++ b/docs/protocol/smart-wallet-signatures.md @@ -229,7 +229,29 @@ is_valid = bool(int.from_bytes(result, "big")) --- -## 11. Files changed in pyaleph +## 11. Current scope: Ethereum mainnet only + +Smart wallet verification (ERC-1271 and ERC-6492) is intentionally limited to +`Chain.ETH` for now. + +**Why:** smart wallets use `block.chainid` in their EIP-712 domain separator, so a +signature produced on (say) Base cannot be validated by calling `isValidSignature` +against an Ethereum mainnet RPC — the domains won't match and the call will return +invalid even when the signature is genuine. Until pyaleph has per-chain RPC URLs +wired through, other EVM chains keep their previous behavior: plain ECDSA only. + +| Chain | Verifier | Plain ECDSA | ERC-1271 | ERC-6492 | +|---|---|---|---|---| +| `ETH` | `EthereumVerifier(rpc_url=...)` | ✅ | ✅ | ✅ | +| `ETHERLINK` | `EthereumVerifier()` (no RPC) | ✅ | ❌ (skipped) | ❌ (skipped) | +| All other EVM chains (Base, Arbitrum, Optimism, …) | `EVMVerifier()` (no RPC) | ✅ | ❌ (skipped) | ❌ (skipped) | + +Plain EOA messages on every EVM chain continue to work exactly as before — the +scoping above affects only smart contract wallet signatures. + +--- + +## 12. Files changed in pyaleph | File | Change | |---|---| diff --git a/src/aleph/chains/signature_verifier.py b/src/aleph/chains/signature_verifier.py index 601d30c73..00ea4e25c 100644 --- a/src/aleph/chains/signature_verifier.py +++ b/src/aleph/chains/signature_verifier.py @@ -20,8 +20,15 @@ class SignatureVerifier: verifiers: Dict[Chain, Verifier] def __init__(self, rpc_url: Optional[str] = None): - evm = EVMVerifier(rpc_url=rpc_url) + # Smart wallet validation (ERC-1271 / ERC-6492) is chain-specific: the + # wallet's EIP-712 domain uses `block.chainid`, so verifying a signature + # from Base (or any other chain) against the Ethereum mainnet RPC would + # produce false negatives. Until per-chain RPCs are configured, only + # Ethereum mainnet gets the RPC-backed paths. Every other EVM chain + # keeps the previous behavior: plain ECDSA only. + evm = EVMVerifier() eth = EthereumVerifier(rpc_url=rpc_url) + etherlink = EthereumVerifier() self.verifiers = { Chain.ARBITRUM: evm, Chain.AVAX: AvalancheConnector(), @@ -34,7 +41,7 @@ def __init__(self, rpc_url: Optional[str] = None): Chain.DOT: SubstrateConnector(), Chain.ECLIPSE: SolanaConnector(), Chain.ETH: eth, - Chain.ETHERLINK: eth, + Chain.ETHERLINK: etherlink, Chain.FRAXTAL: evm, Chain.HYPE: evm, Chain.INK: evm, From 1a99187d33e1a883da360fa1a65c29ae21cd86a7 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 14:24:27 +0200 Subject: [PATCH 6/9] docs: add smart wallet signature verification flow and security analysis --- .../smart-wallet-signature-verification.md | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 docs/protocol/smart-wallet-signature-verification.md diff --git a/docs/protocol/smart-wallet-signature-verification.md b/docs/protocol/smart-wallet-signature-verification.md new file mode 100644 index 000000000..1fc3e58e2 --- /dev/null +++ b/docs/protocol/smart-wallet-signature-verification.md @@ -0,0 +1,210 @@ +# How Smart Wallet Signature Verification Works + +> Companion to [`smart-wallet-signatures.md`](./smart-wallet-signatures.md), which +> documents the signature *format*. This document explains the verification *flow* +> and the **security guarantees** it provides for each signer type. + +--- + +## 1. Three signer types, three verification paths + +Pyaleph's `EVMVerifier` handles three very different kinds of signers behind the same +`verify_signature` entry point: + +| Signer type | Signature shape | RPC calls | Authority | +|---|---|---|---| +| **EOA** (MetaMask, Ledger, …) | 65-byte ECDSA | 0 | Private key (eternal) | +| **Deployed smart wallet** (ERC-1271) | Any bytes the contract accepts | 2 (`get_code` + `isValidSignature`) | Current contract state | +| **Counterfactual smart wallet** (ERC-6492) | `abi.encode(factory, calldata, innerSig) + 0x6492…6492` | 1 (`UniversalSigValidator`) | Factory calldata (frozen at CREATE2 time) | + +Detection is upfront and cheap: if the signature ends with the ERC-6492 magic suffix, +route to the counterfactual path. Otherwise try plain ECDSA; if that fails and the +sender has deployed bytecode, route to ERC-1271. + +--- + +## 2. Counterfactual verification (before deployment) + +### Mechanical flow in pyaleph + +``` +1. Detect 0x6492…6492 suffix → present +2. Decode abi.encode(factory, calldata, innerSig) +3. eth_call → UniversalSigValidator.isValidSigWithSideEffects( + sender, messageHash, fullSignature + ) +4. Check the returned uint256 is non-zero +``` + +The `UniversalSigValidator` (canonical deployment at +`0x0000000000002fd5Aeb385D324B580FCa7c83823`) performs two operations atomically +inside the `eth_call`: + +1. **Simulates deployment**: `factory.call(factoryCalldata)` — this deploys the + wallet contract at some address, purely inside the simulation. No state changes + persist because we use `eth_call`, not `sendTransaction`. +2. **Calls `isValidSignature`**: routes the inner signature through the deployed + contract's verification logic. + +### What the simulation does (Kernel example) + +After the factory call, a Kernel wallet exists (inside the simulation) at some +CREATE2 address. Its `isValidSignature`: + +1. Parses the 86-byte inner sig → `(type=0x01, validator=0x845A…4cE57, ecdsa)` +2. Dispatches to the ECDSA validator plugin configured as **root validator** +3. The plugin wraps the Aleph hash in its own EIP-712 domain (with `block.chainid` + and wallet address) +4. Runs `ecrecover` on the wrapped hash with the 65-byte ECDSA +5. Compares the recovered address to the **owner baked into `factoryCalldata`** +6. Returns the ERC-1271 magic value `0x1626ba7e` on match + +### Security guarantees + +1. **The Aleph hash binds to the message.** + The hash is `keccak256(EIP-191(chain + sender + type + item_hash))`. Any + tampering with sender, item content, or chain invalidates the signature. + +2. **The sender address is cryptographically bound to the wallet's config.** + `sender = keccak256(0xff || factory || salt || keccak256(initcode))[12:]`. + The initcode embeds the owner (via `factoryCalldata`). Swapping the owner + changes the initcode → changes the CREATE2 address → no longer equals + `sender`. Finding a colliding `(factory, salt, initcode)` tuple is a 160-bit + preimage search — computationally infeasible. + +3. **Only the owner's private key could produce the inner ECDSA.** + The ECDSA validator recovers a key and compares it against the configured + owner. Without that private key, forging the inner sig would require + breaking secp256k1. + +### Attacks defeated + +| Attack | Why it fails | +|---|---| +| Submit ERC-6492 with a malicious factory | Simulated deployment lands at a different address → subsequent `isValidSignature` call hits empty bytecode → invalid | +| Swap the owner inside `factoryCalldata` | Changes the initcode → changes the CREATE2 address → no longer matches `sender` | +| Replay signature on a different Aleph message | Hash depends on `item_hash`, `sender`, `chain`, `type`; any change invalidates | +| Replay across chains | Kernel's EIP-712 domain includes `block.chainid` (also why pyaleph is scoped to ETH mainnet only until per-chain RPCs are wired) | + +### Residual trust assumptions + +- Anyone who obtains the owner's private key can sign as `sender` — same risk as + any EOA. +- Trust in the `UniversalSigValidator` deployment. The reference implementation is + documented in EIP-6492; worth verifying the bytecode at `0x0000…3823` on your + target chain before relying on it in production. + +--- + +## 3. Deployed-wallet verification (ERC-1271, the normal case) + +Once the smart wallet has been deployed (first on-chain transaction), clients stop +sending the ERC-6492 wrapper. They send only the inner signature (86 bytes for +Kernel) directly. + +### Mechanical flow in pyaleph + +``` +1. Detect 0x6492…6492 suffix → NOT present +2. Try plain ECDSA ecrecover → fails (sender is a contract, not an EOA) +3. eth_call → w3.eth.get_code(sender) → non-empty = deployed +4. eth_call → sender.isValidSignature(hash, innerSig) +5. Check return == 0x1626ba7e +``` + +No factory, no simulation, no counterfactual reasoning. We query the actual +on-chain contract at `sender`, and the contract itself is the authority on whether +the signature is valid. + +### What the contract does internally (Kernel example) + +Same as step 2→6 of the counterfactual flow above, but reading state from the +actual deployed contract instead of a simulated one: + +1. Parses 86-byte sig → `(type, validator, ecdsa)` +2. Routes to the **currently configured** root validator plugin +3. The plugin wraps the Aleph hash in EIP-712 +4. `ecrecover` on the wrapped hash +5. Compares recovered address to the **currently configured owner** +6. Returns `0x1626ba7e` if match + +### Crucial difference vs EOA / counterfactual + +Smart wallets have **mutable authorization**: + +- **Key rotation:** if the wallet rotates from Key-A to Key-B after signing, most + validators (including Kernel's ECDSA validator) will reject the old signature + on re-verification. Signatures effectively expire. +- **Multi-validator setups:** a wallet can later add session keys, passkeys, or + multisig plugins. Any of those authorized signers can also produce valid sigs + as `sender` — not just the original EOA. +- **Upgradeable logic:** if the contract is upgradeable, a malicious upgrade + could flip `isValidSignature` to always return true. Trust boundary = whoever + controls the upgrade keys. + +Contrast with EOAs: an ECDSA signature produced once is valid forever against the +same address, because the "address" is literally `keccak256(pubkey)[12:]` — there +is no state to mutate. + +### Security guarantees + +- ✅ The contract at `sender` **right now** approves this signature for this hash. +- ✅ Whoever holds currently-authorized keys signed this message. +- ✅ No address collision: the contract is queried directly, not reconstructed. +- ⚠️ "Owner at signing time" may differ from "owner at verification time". + +### Attacks defeated + +Same list as counterfactual, minus the factory-related ones (no factory involved). +Replay across chains is mitigated by ETH-mainnet-only scoping. + +--- + +## 4. Applied to the real example + +For Aleph message +[`6f699b25…1a26d`](https://api2.aleph.im/api/v0/messages/6f699b252db10e65e8651a77289a7789e2da77ce26c4f4ad247fbe9bd1e1a26d): + +``` +Aleph sender: 0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635 (Kernel smart wallet, counterfactual) +Owner EOA: 0xfFFEfCDE25e1d00474530f1A7b90D02CEda94fD7 (private key holder) +Factory: 0xd703aae79538628d27099b8c4f621be4ccd142d5 +Validator: 0x845ADb2C711129d4f3966735eD98a9F09fC4cE57 +``` + +The message arrives with the ERC-6492 wrapper because the wallet isn't deployed +yet. Pyaleph: + +1. Detects the `0x6492…6492` suffix → routes to `UniversalSigValidator`. +2. `eth_call` to `0x0000…3823` simulates deploying a Kernel at `0xa9F3…1635` + configured with owner `0xfFFE…4fD7`. +3. Calls `isValidSignature` on that simulated contract with the 86-byte inner + sig. +4. The ECDSA validator recovers an address from the EIP-712-wrapped hash and + compares it to `0xfFFE…4fD7`. +5. If it matches, returns `0x1626ba7e` → pyaleph accepts the message as signed by + `0xa9F3…1635`. + +At some later point, the user performs their first on-chain transaction and the +wallet is deployed. From that message onward, Privy sends only the 86-byte inner +sig (no wrapper). Pyaleph's `EVMVerifier` then routes through the ERC-1271 path +instead — same guarantees, fewer bytes, one fewer simulated step. + +--- + +## 5. What the verification does NOT guarantee + +Worth spelling out, so we don't over-promise: + +- **Liveness of the owner's key.** If the key is lost or stolen, the attacker can + sign as `sender`. This is identical to any EOA risk. +- **Correctness of contract logic.** We trust that the Kernel contract and its + plugins implement `isValidSignature` faithfully. A buggy or malicious wallet + implementation could accept arbitrary signatures. +- **Immutability over time for deployed wallets.** A signature that verified + today may not verify tomorrow if the wallet's config changes. Pyaleph's model + is "valid at time of verification", not "valid forever". +- **Cross-chain uniqueness.** A signature produced for a wallet on Base is not + validatable on Ethereum mainnet because the EIP-712 domain uses `block.chainid`. + This is why pyaleph currently only enables smart-wallet verification on + `Chain.ETH`. From 2184bdfa46b7ca5b63299c39b90df5e4be53c629 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 14:30:01 +0200 Subject: [PATCH 7/9] docs: add byte-level ERC-1271 signature schema alongside ERC-6492 --- docs/protocol/smart-wallet-signatures.md | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/docs/protocol/smart-wallet-signatures.md b/docs/protocol/smart-wallet-signatures.md index b2f9c889e..8e01d4fe6 100644 --- a/docs/protocol/smart-wallet-signatures.md +++ b/docs/protocol/smart-wallet-signatures.md @@ -66,6 +66,67 @@ FULL SIGNATURE (hex) └── MAGIC SUFFIX (32 bytes): 0x6492649264926492649264926492649264926492649264926492649264926492 ``` +### Full schema of an ERC-1271 signature (same wallet, after deployment) + +Once the smart wallet has been deployed on chain, the ERC-6492 wrapper, the factory +calldata and the magic suffix are all dropped. The client sends only the inner +signature — the wallet itself is now queryable on chain, so there's nothing to +"teach" the verifier about how to reconstruct it. + +For the same Kernel wallet (`0xa9F3…1635`, same owner), the post-deployment +signature is just 86 bytes: + +``` +INNER SIGNATURE (86 bytes) — sent as-is, no wrapper, no suffix +│ +├── [0] type byte: 0x01 +│ └── Kernel signature type selector (routes to the root validator) +│ +├── [1:21] validator: 0x845ADb2C711129d4f3966735eD98a9F09fC4cE57 +│ └── Address of the ECDSA validator plugin baked into this wallet +│ +└── [21:86] ECDSA (65 bytes) — produced by the owner EOA's private key + ├── [21:53] r: 0x94f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae + ├── [53:85] s: 0x316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe373 + └── [85] v: 28 (0x1c) +``` + +Full hex (exact bytes sent over the wire): + +``` +0x01 ← type byte + 845adb2c711129d4f3966735ed98a9f09fc4ce57 ← validator (20 bytes) + 94f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae ← r (32 bytes) + 316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe373 ← s (32 bytes) + 1c ← v (1 byte) +``` + +Flattened: + +``` +0x01845adb2c711129d4f3966735ed98a9f09fc4ce5794f8df9bcc3e2fa2049519666e9977ff76f9c99322db6a1f1117f3955411b2ae316b72e49bd1743a5dee905ea4f27c4e7912479995f6b99eb56c44349dabe3731c +``` + +**Size comparison:** + +| Signature type | Size | Overhead vs bare ECDSA | +|---|---|---| +| Bare ECDSA (plain EOA) | 65 bytes | — | +| ERC-1271 (deployed Kernel) | 86 bytes | +21 bytes (type + validator) | +| ERC-6492 (counterfactual Kernel) | ~800 bytes | +~735 bytes (factory, calldata, wrapper, magic) | + +**What's NOT in the ERC-1271 payload** (but is in ERC-6492): + +- No factory address — the wallet is already on chain, no need to redeploy. +- No `initData` / owner EOA — stored inside the deployed contract, queried directly + by `isValidSignature`. +- No `0x6492…6492` magic suffix — the verifier doesn't need to know "please simulate + a deployment"; it just calls the live contract. + +The inner signature byte-layout is identical to the one inside the ERC-6492 +wrapper. The client literally reuses the same `innerSig` bytes; it just stops +wrapping them once there's deployed code to talk to. + --- ## 4. The message being signed in Aleph From 479b1052c3ded680a35eb1854599d640bbdfd12c Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 15:34:00 +0200 Subject: [PATCH 8/9] fix: use EIP-6492 contract-creation pattern instead of bogus validator 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. --- .../smart-wallet-signature-verification.md | 26 ++++++----- docs/protocol/smart-wallet-signatures.md | 35 ++++++++------- .../assets/erc6492_validator_bytecode.hex | 1 + src/aleph/chains/evm.py | 45 ++++++++++++++----- tests/chains/test_evm.py | 16 ++++--- 5 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 src/aleph/chains/assets/erc6492_validator_bytecode.hex diff --git a/docs/protocol/smart-wallet-signature-verification.md b/docs/protocol/smart-wallet-signature-verification.md index 1fc3e58e2..356c675e9 100644 --- a/docs/protocol/smart-wallet-signature-verification.md +++ b/docs/protocol/smart-wallet-signature-verification.md @@ -29,22 +29,27 @@ sender has deployed bytecode, route to ERC-1271. ``` 1. Detect 0x6492…6492 suffix → present -2. Decode abi.encode(factory, calldata, innerSig) -3. eth_call → UniversalSigValidator.isValidSigWithSideEffects( +2. Build deploy_data = ValidateSigOffchainBytecode + abi.encode( sender, messageHash, fullSignature ) -4. Check the returned uint256 is non-zero +3. eth_call({"data": deploy_data}) # no `to` field = contract creation +4. Check the single returned byte == 0x01 ``` -The `UniversalSigValidator` (canonical deployment at -`0x0000000000002fd5Aeb385D324B580FCa7c83823`) performs two operations atomically -inside the `eth_call`: +ERC-6492 uses the **contract-creation pattern** — there is no pre-deployed +validator contract on chain. The bytecode is sent as creation data; it runs as +a constructor and performs two operations atomically: 1. **Simulates deployment**: `factory.call(factoryCalldata)` — this deploys the wallet contract at some address, purely inside the simulation. No state changes persist because we use `eth_call`, not `sendTransaction`. 2. **Calls `isValidSignature`**: routes the inner signature through the deployed - contract's verification logic. + contract's verification logic, and returns a single byte (`0x01` valid / + `0x00` invalid) from the constructor's RETURN opcode. + +The canonical bytecode is the EIP-6492 reference implementation from +AmbireTech/signature-validator, shipped at +`src/aleph/chains/assets/erc6492_validator_bytecode.hex`. ### What the simulation does (Kernel example) @@ -90,9 +95,10 @@ CREATE2 address. Its `isValidSignature`: - Anyone who obtains the owner's private key can sign as `sender` — same risk as any EOA. -- Trust in the `UniversalSigValidator` deployment. The reference implementation is - documented in EIP-6492; worth verifying the bytecode at `0x0000…3823` on your - target chain before relying on it in production. +- Trust in the `ValidateSigOffchain` / `UniversalSigValidator` bytecode. The + reference implementation is documented in EIP-6492 and sourced from + AmbireTech/signature-validator. Worth verifying the packaged hex against the + upstream source before relying on it in production. --- diff --git a/docs/protocol/smart-wallet-signatures.md b/docs/protocol/smart-wallet-signatures.md index 8e01d4fe6..6574c771b 100644 --- a/docs/protocol/smart-wallet-signatures.md +++ b/docs/protocol/smart-wallet-signatures.md @@ -262,30 +262,35 @@ is_valid = result[:4] == VALID_SIG_MAGIC --- -## 10. ERC-6492 `UniversalSigValidator` +## 10. ERC-6492 validation via contract-creation bytecode -ERC-6492 defines a `UniversalSigValidator` contract deployed at a deterministic address -on every major EVM chain. It validates counterfactual signatures in a single `eth_call`: +ERC-6492 does **not** rely on a pre-deployed contract. Instead it uses a clever +`eth_call` trick: send the `ValidateSigOffchain` deployer bytecode as +contract-creation data (no `to` field). The bytecode runs as a constructor, deploys +a `UniversalSigValidator` inline, simulates the factory deployment, calls +`isValidSignature`, and returns 1 byte: `0x01` if valid, `0x00` if invalid — all +inside a single `eth_call`, with no persisted state changes. + +The canonical bytecode is the reference implementation from +[AmbireTech/signature-validator](https://github.com/AmbireTech/signature-validator) +(see also [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492)). Pyaleph ships it +as an asset file at `src/aleph/chains/assets/erc6492_validator_bytecode.hex`. ```python from eth_abi.abi import encode -# UniversalSigValidator address (deterministic CREATE2) -# See: https://eips.ethereum.org/EIPS/eip-6492 -UNIVERSAL_VALIDATOR = "0x0000000000002fd5Aeb385D324B580FCa7c83823" - -calldata = encode( +constructor_args = encode( ["address", "bytes32", "bytes"], [sender_address, message_hash, full_erc6492_signature], ) -# Call with selector isValidSigWithSideEffects(address,bytes32,bytes) = 0x8dca4bea -selector = bytes.fromhex("8dca4bea") -result = await w3.eth.call({ - "to": UNIVERSAL_VALIDATOR, - "data": selector + calldata, -}) -is_valid = bool(int.from_bytes(result, "big")) +deploy_data = UNIVERSAL_VALIDATOR_BYTECODE + constructor_args + +# No `to` field = contract creation; the bytecode runs as a constructor. +result = await w3.eth.call({"data": "0x" + deploy_data.hex()}) + +# Returns a single byte: 0x01 valid / 0x00 invalid +is_valid = result == b"\x01" ``` --- diff --git a/src/aleph/chains/assets/erc6492_validator_bytecode.hex b/src/aleph/chains/assets/erc6492_validator_bytecode.hex new file mode 100644 index 000000000..14084f559 --- /dev/null +++ b/src/aleph/chains/assets/erc6492_validator_bytecode.hex @@ -0,0 +1 @@ +0x608060405234801561000f575f5ffd5b506040516106fb3803806106fb83398101604081905261002e91610559565b5f61003a848484610045565b9050805f526001601ff35b5f5f846001600160a01b0316803b806020016040519081016040528181525f908060200190933c90507f649264926492649264926492649264926492649264926492649264926492649261009884610470565b036101f9575f606080858060200190518101906100b591906105ae565b865192955090935091505f03610174575f836001600160a01b0316836040516100de919061060f565b5f604051808303815f865af19150503d805f8114610117576040519150601f19603f3d011682016040523d82523d5f602084013e61011c565b606091505b50509050806101725760405162461bcd60e51b815260206004820152601e60248201527f5369676e617475726556616c696461746f723a206465706c6f796d656e74000060448201526064015b60405180910390fd5b505b604051630b135d3f60e11b808252906001600160a01b038a1690631626ba7e906101a4908b908690600401610625565b602060405180830381865afa1580156101bf573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906101e39190610661565b6001600160e01b03191614945050505050610469565b8051156102e3575f5f866001600160a01b0316631626ba7e60e01b8787604051602401610227929190610625565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b0319909416939093179092529051610265919061060f565b5f60405180830381855afa9150503d805f811461029d576040519150601f19603f3d011682016040523d82523d5f602084013e6102a2565b606091505b50915091508180156102b5575080516020145b156102e057630b135d3f60e11b6102cb82610688565b6001600160e01b031916149350505050610469565b50505b82516041146103475760405162461bcd60e51b815260206004820152603a60248201525f5160206106db5f395f51905f5260448201527f3a20696e76616c6964207369676e6174757265206c656e6774680000000000006064820152608401610169565b61034f610487565b50602083015160408085015185518693925f918591908110610373576103736106c6565b016020015160f81c9050601b811480159061039257508060ff16601c14155b156103f25760405162461bcd60e51b815260206004820152603b60248201525f5160206106db5f395f51905f5260448201527f3a20696e76616c6964207369676e617475726520762076616c756500000000006064820152608401610169565b604080515f8152602081018083528a905260ff83169181019190915260608101849052608081018390526001600160a01b038a169060019060a0016020604051602081039080840390855afa15801561044d573d5f5f3e3d5ffd5b505050602060405103516001600160a01b031614955050505050505b9392505050565b5f60208251101561047f575f5ffd5b508051015190565b60405180606001604052806003906020820280368337509192915050565b6001600160a01b03811681146104b9575f5ffd5b50565b634e487b7160e01b5f52604160045260245ffd5b5f82601f8301126104df575f5ffd5b81516001600160401b038111156104f8576104f86104bc565b604051601f8201601f19908116603f011681016001600160401b0381118282101715610526576105266104bc565b60405281815283820160200185101561053d575f5ffd5b8160208501602083015e5f918101602001919091529392505050565b5f5f5f6060848603121561056b575f5ffd5b8351610576816104a5565b6020850151604086015191945092506001600160401b03811115610598575f5ffd5b6105a4868287016104d0565b9150509250925092565b5f5f5f606084860312156105c0575f5ffd5b83516105cb816104a5565b60208501519093506001600160401b038111156105e6575f5ffd5b6105f2868287016104d0565b604086015190935090506001600160401b03811115610598575f5ffd5b5f82518060208501845e5f920191825250919050565b828152604060208201525f82518060408401528060208501606085015e5f606082850101526060601f19601f8301168401019150509392505050565b5f60208284031215610671575f5ffd5b81516001600160e01b031981168114610469575f5ffd5b805160208201516001600160e01b03198116919060048210156106bf576001600160e01b0319600483900360031b81901b82161692505b5050919050565b634e487b7160e01b5f52603260045260245ffdfe5369676e617475726556616c696461746f72237265636f7665725369676e6572 \ No newline at end of file diff --git a/src/aleph/chains/evm.py b/src/aleph/chains/evm.py index 34fe4ce29..3a6a3ba8c 100644 --- a/src/aleph/chains/evm.py +++ b/src/aleph/chains/evm.py @@ -1,10 +1,12 @@ import functools +import importlib.resources import logging from typing import Optional from eth_abi.abi import encode from eth_account import Account from eth_account.messages import _hash_eip191_message, encode_defunct +from eth_typing import HexStr from eth_utils.address import to_checksum_address from web3 import AsyncHTTPProvider, AsyncWeb3 @@ -21,11 +23,22 @@ ) ERC1271_MAGIC = bytes.fromhex("1626ba7e") IS_VALID_SIGNATURE_SELECTOR = bytes.fromhex("1626ba7e") -# isValidSigWithSideEffects(address,bytes32,bytes) -UNIVERSAL_VALIDATOR_SELECTOR = bytes.fromhex("8dca4bea") -# ERC-6492 UniversalSigValidator (deterministic CREATE2 address) -# See: https://eips.ethereum.org/EIPS/eip-6492 -UNIVERSAL_VALIDATOR_ADDRESS = "0x0000000000002fd5Aeb385D324B580FCa7c83823" + + +# ERC-6492 UniversalSigValidator deployer bytecode (reference impl from +# AmbireTech/signature-validator, EIP-6492). It is NOT a pre-deployed contract +# — it is sent as contract-creation data in eth_call. The bytecode receives +# (address signer, bytes32 hash, bytes signature) as constructor args, deploys +# the UniversalSigValidator on the fly, runs isValidSig, and returns a single +# byte: 0x01 for valid, 0x00 for invalid. +@functools.lru_cache(maxsize=1) +def _universal_validator_bytecode() -> bytes: + resource = ( + importlib.resources.files("aleph.chains.assets") + / "erc6492_validator_bytecode.hex" + ) + hex_str = resource.read_text(encoding="utf-8").strip() + return bytes.fromhex(hex_str.removeprefix("0x")) class EVMVerifier(Verifier): @@ -51,18 +64,26 @@ async def _verify_erc6492( message_hash: bytes, signature: bytes, ) -> bool: - """Validate an ERC-6492 counterfactual signature via UniversalSigValidator.""" + """Validate an ERC-6492 counterfactual signature. + + Uses the EIP-6492 off-chain verification pattern: send the + ValidateSigOffchain deployer bytecode as contract-creation data in + eth_call (no `to` field). The bytecode runs as a constructor, deploys + the UniversalSigValidator inline, simulates the factory deployment, + calls isValidSignature, and returns 1 byte: 0x01 valid / 0x00 invalid. + """ try: - calldata = UNIVERSAL_VALIDATOR_SELECTOR + encode( + constructor_args = encode( ["address", "bytes32", "bytes"], [sender, message_hash, signature], ) - result = await w3.eth.call( - {"to": UNIVERSAL_VALIDATOR_ADDRESS, "data": calldata} - ) - return bool(int.from_bytes(result, "big")) + deploy_data = _universal_validator_bytecode() + constructor_args + result = await w3.eth.call({"data": HexStr("0x" + deploy_data.hex())}) + return result == b"\x01" except Exception: - LOGGER.exception("Error calling UniversalSigValidator for %s", sender) + LOGGER.exception( + "Error running ERC-6492 validation bytecode for %s", sender + ) return False async def _verify_erc1271( diff --git a/tests/chains/test_evm.py b/tests/chains/test_evm.py index 8f0b7ab7f..1d917ad93 100644 --- a/tests/chains/test_evm.py +++ b/tests/chains/test_evm.py @@ -198,25 +198,29 @@ async def test_verify_erc6492_counterfactual_valid( verifier = EVMVerifier(rpc_url="http://localhost:8545") mock_w3 = AsyncMock() - mock_w3.eth.call = AsyncMock(return_value=(1).to_bytes(32, "big")) + # The validator bytecode returns 1 byte: 0x01 = valid + mock_w3.eth.call = AsyncMock(return_value=b"\x01") with patch.object(verifier, "_get_web3_client", return_value=mock_w3): result = await verifier.verify_signature(erc6492_message) assert result is True + # Confirm it's a contract-creation call: no `to` field, data is the + # validator bytecode + ABI-encoded (signer, hash, signature) call_args = mock_w3.eth.call.call_args[0][0] - assert call_args["to"].lower() == "0x0000000000002fd5aeb385d324b580fca7c83823" + assert "to" not in call_args + assert call_args["data"].startswith("0x608060") @pytest.mark.asyncio async def test_verify_erc6492_counterfactual_invalid( erc6492_message: BasePendingMessage, ): - """ERC-6492: UniversalSigValidator returns 0 → invalid.""" + """ERC-6492: validator bytecode returns 0x00 → invalid.""" verifier = EVMVerifier(rpc_url="http://localhost:8545") mock_w3 = AsyncMock() - mock_w3.eth.call = AsyncMock(return_value=(0).to_bytes(32, "big")) + mock_w3.eth.call = AsyncMock(return_value=b"\x00") with patch.object(verifier, "_get_web3_client", return_value=mock_w3): result = await verifier.verify_signature(erc6492_message) @@ -228,11 +232,11 @@ async def test_verify_erc6492_counterfactual_invalid( async def test_erc6492_detection_skips_ecdsa( erc6492_message: BasePendingMessage, ): - """ERC-6492 signatures skip ECDSA entirely and go straight to UniversalSigValidator.""" + """ERC-6492 sigs skip ECDSA entirely and go straight to bytecode validation.""" verifier = EVMVerifier(rpc_url="http://localhost:8545") mock_w3 = AsyncMock() - mock_w3.eth.call = AsyncMock(return_value=(1).to_bytes(32, "big")) + mock_w3.eth.call = AsyncMock(return_value=b"\x01") with patch.object(verifier, "_get_web3_client", return_value=mock_w3): with patch("aleph.chains.evm.Account.recover_message") as mock_recover: From 8b15066e0bac1a208b872a6f91b869a975c751cb Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Thu, 23 Apr 2026 15:42:24 +0200 Subject: [PATCH 9/9] test: add network integration test for ERC-6492 against mainnet RPC 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. --- pyproject.toml | 7 ++- tests/chains/test_evm_integration.py | 82 ++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/chains/test_evm_integration.py diff --git a/pyproject.toml b/pyproject.toml index 21f216da8..b13ef80ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,13 +218,16 @@ keep_full_version = true [tool.pytest.ini_options] minversion = "6.0" pythonpath = [ "src" ] -addopts = "-vv -m \"not ledger_hardware\"" +addopts = "-vv -m \"not ledger_hardware and not network\"" filterwarnings = [ "ignore:pkg_resources is deprecated as an API:UserWarning", ] norecursedirs = [ "*.egg", "dist", "build", ".tox", ".venv", "*/site-packages/*", ".claude", ".worktrees" ] testpaths = [ "tests/unit" ] -markers = { ledger_hardware = "marks tests as requiring ledger hardware" } +markers = [ + "ledger_hardware: marks tests as requiring ledger hardware", + "network: marks tests as requiring external network access (e.g. Ethereum mainnet RPC)", +] [tool.coverage.run] branch = true diff --git a/tests/chains/test_evm_integration.py b/tests/chains/test_evm_integration.py new file mode 100644 index 000000000..7f9feaaeb --- /dev/null +++ b/tests/chains/test_evm_integration.py @@ -0,0 +1,82 @@ +""" +Integration tests for EVMVerifier against a real Ethereum mainnet RPC. + +These tests are marked `network` and excluded from the default pytest run. +Enable them explicitly: + + hatch run testing:test tests/chains/test_evm_integration.py -m network -v + +Or override addopts: + + hatch run testing:test tests/chains/test_evm_integration.py -m network -v \ + --override-ini="addopts=" + +Uses a public mainnet RPC by default. Override with ALEPH_TEST_ETH_RPC if you +have your own node. +""" + +import os + +import pytest + +from aleph.chains.evm import EVMVerifier +from aleph.schemas.pending_messages import BasePendingMessage, parse_message + +ETH_MAINNET_RPC = os.environ.get( + "ALEPH_TEST_ETH_RPC", "https://ethereum-rpc.publicnode.com" +) + + +# Real Aleph message signed by a Privy/Kernel counterfactual smart wallet. +# Source: the production Aleph network, originally rejected before EIP-6492 +# support was added. +# item_hash: f4daf9c0dadd7aa89c37e62e24f90a032183ba3b829b2bd2cf87568a940fd0a8 +REAL_ERC6492_MESSAGE = { + "item_hash": "f4daf9c0dadd7aa89c37e62e24f90a032183ba3b829b2bd2cf87568a940fd0a8", + "type": "POST", + "chain": "ETH", + "sender": "0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635", + "time": 1776949817.862, + "item_type": "inline", + "item_content": ( + '{"type":"ALEPH-SSH",' + '"address":"0xa9F3Cd4E416c6e911DB3DcB5CA6CD77e9F861635",' + '"content":{"key":"test1","label":"test1"},' + '"time":1776949817.862}' + ), + "channel": "ALEPH-CLOUDSOLUTIONS", + "signature": "0x000000000000000000000000d703aae79538628d27099b8c4f621be4ccd142d50000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000001c4c5265d5d000000000000000000000000aac5d4240af87249b3f71bc8e4a2cae074a3e4190000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001243c3b752b01845ADb2C711129d4f3966735eD98a9F09fC4cE570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000014fFFEfCDE25e1d00474530f1A7b90D02CEda94fD7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005601845ADb2C711129d4f3966735eD98a9F09fC4cE57ad3840a219707e52978ad891b851ac7302c95785dd6e233f010c205018312c7b1232d3eb5e60be5a12e41d3be3a9635660eea241fc2ef92cc461abf00d44b4831b000000000000000000006492649264926492649264926492649264926492649264926492649264926492", # noqa: E501 +} + + +@pytest.fixture +def real_erc6492_message() -> BasePendingMessage: + return parse_message(REAL_ERC6492_MESSAGE) + + +@pytest.mark.network +@pytest.mark.asyncio +async def test_erc6492_validation_against_mainnet( + real_erc6492_message: BasePendingMessage, +): + """End-to-end: real ERC-6492 sig + real mainnet RPC + EIP-6492 bytecode. + + This test exercises the full happy path against a live Ethereum mainnet + node: + 1. Detects the 0x6492…6492 magic suffix. + 2. Builds the ValidateSigOffchain deploy_data (bytecode + ABI-encoded args). + 3. eth_call with no `to` field → the bytecode runs as a constructor, + deploys UniversalSigValidator inline, simulates the Kernel factory + deployment, and calls isValidSignature. + 4. Asserts the returned byte is 0x01 (valid). + + Verifies that the bogus-address bug is fixed and the EIP-6492 + contract-creation pattern works as specified. + """ + verifier = EVMVerifier(rpc_url=ETH_MAINNET_RPC) + result = await verifier.verify_signature(real_erc6492_message) + assert result is True, ( + "Expected the real ERC-6492 signature to validate against mainnet. " + "If this fails, either the RPC is down or the bytecode asset drifted " + "from the EIP-6492 reference implementation." + )