// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /// @dev The subset of AerePQAggregateVerifier this module depends on. The verifier checks, via ONE /// SP1 proof through the deployed gateway, that a registered t-of-n post-quantum committee /// signed a message digest, at a gas cost independent of the committee size n. interface IAerePQAggregateVerifier { function committeeExists(uint256 committeeId) external view returns (bool); function tryVerifyAggregate( uint256 committeeId, bytes32 messageDigest, bytes calldata publicValues, bytes calldata proofBytes ) external view returns (bool); } /// @dev Canonical ERC-4337 v0.7 packed user operation. Declared locally so this module is /// self-contained and portable to ANY EVM chain and account stack (this repo pins an older /// OpenZeppelin under the top-level import path; the canonical v5.6 layout is reproduced here /// byte-for-byte). Matches OpenZeppelin interfaces/draft-IERC4337.sol PackedUserOperation and /// AereEntryPointV2.PackedUserOperation. [VERIFY] confirm field-for-field against the account /// stack you deploy under (Safe7579 / Kernel / Nexus all consume this exact struct). struct PackedUserOperation { address sender; uint256 nonce; bytes initCode; bytes callData; bytes32 accountGasLimits; uint256 preVerificationGas; bytes32 gasFees; bytes paymasterAndData; bytes signature; } /** * @title AerePQAggregateModule, a portable ERC-7579 validator module for t-of-n POST-QUANTUM * committee authorization via ONE succinct SP1 proof. * * @notice An ERC-7579 validator module (module type 1) that lets a Safe / Kernel / Nexus style * modular smart account, on ANY EVM chain, use "a t-of-n post-quantum committee approved * this" as its validation rule. The account delegates validation here; this module wraps * AerePQAggregateVerifier, so validating a userOp is a SINGLE SP1 proof check whose gas is * INDEPENDENT of the committee size n. The post-quantum committee legs are ML-DSA / Falcon * signatures proven off-chain in the SP1 zkVM (that guest circuit is [MEASURE] and NOT part * of this repository, see docs/AERE-PQ-AGGREGATE.md); on-chain the account checks one small * proof. * * @dev ERC-7579 CONFORMANCE. Implements the type-1 validator surface from OpenZeppelin * interfaces/draft-IERC7579.sol (IERC7579Validator): * - onInstall(bytes) / onUninstall(bytes) / isModuleType(uint256) [IERC7579Module] * - validateUserOp(PackedUserOperation, bytes32) returns (uint256) [ERC-4337 return packing] * - isValidSignatureWithSender(address, bytes32, bytes) returns (bytes4) [ERC-1271] * Singleton design: per-account state is keyed by msg.sender, exactly like * AereSessionKeyValidator, so configuration is owner-only by construction (only the account * itself, via its EntryPoint / self-call / root-owner gated install and execute paths, is * ever msg.sender here). * * [VERIFY] The account is expected to pass, in userOp.signature and in the ERC-1271 * `signature`, ONLY this module's payload (the ABI encoding of the SP1 public values and * proof). ERC-7579 accounts select the validator by an address prefix carried in the * userOp NONCE key (Safe7579 / Kernel / Nexus) or, in some stacks, stripped from the * signature before the module is called. This module assumes the routing prefix has already * been removed by the account, matching the OZ AccountERC7579 dispatcher. Confirm the exact * unwrapping against the specific account implementation you deploy under. * * @dev SCOPE. Account / authorization layer only. This does NOT make Aere Network consensus * post-quantum (blocks stay classical ECDSA QBFT). It holds no funds and has no admin. What * is post-quantum is the AUTHORITY that approves the account's userOps. */ contract AerePQAggregateModule { // ERC-7579 / ERC-4337 return conventions. uint256 internal constant VALIDATION_SUCCESS = 0; uint256 internal constant SIG_VALIDATION_FAILED = 1; uint256 internal constant MODULE_TYPE_VALIDATOR = 1; bytes4 internal constant EIP1271_MAGIC = 0x1626ba7e; bytes4 internal constant EIP1271_FAIL = 0xffffffff; /// @notice The aggregation verifier this module validates against (immutable; set at deploy). IAerePQAggregateVerifier public immutable VERIFIER; /// @notice account => the committeeId that authorizes this account's userOps and ERC-1271 /// signatures. Zero means the module is not configured for that account. mapping(address => uint256) public committeeOf; event ModuleConfigured(address indexed account, uint256 indexed committeeId); event ModuleDeconfigured(address indexed account, uint256 indexed committeeId); error BadVerifier(); error AlreadyConfigured(); error NotConfigured(); error UnknownCommittee(uint256 committeeId); constructor(address verifier) { if (verifier == address(0)) revert BadVerifier(); VERIFIER = IAerePQAggregateVerifier(verifier); } // ========================================================================== // ERC-7579 module base // ========================================================================== /// @notice ERC-7579 module-type check: this is a type-1 validator module. function isModuleType(uint256 moduleTypeId) external pure returns (bool) { return moduleTypeId == MODULE_TYPE_VALIDATOR; } /// @notice ERC-7579 accountId-style identifier for this module. function name() external pure returns (string memory) { return "aere.pq-aggregate-validator.1.0.0"; } /// @notice Install on the calling account. initData layout: abi.encode(uint256 committeeId). /// The committee must already be registered in the verifier. function onInstall(bytes calldata data) external { address account = msg.sender; if (committeeOf[account] != 0) revert AlreadyConfigured(); uint256 committeeId = abi.decode(data, (uint256)); if (committeeId == 0 || !VERIFIER.committeeExists(committeeId)) revert UnknownCommittee(committeeId); committeeOf[account] = committeeId; emit ModuleConfigured(account, committeeId); } /// @notice Uninstall on the calling account, clearing its committee binding. function onUninstall(bytes calldata) external { address account = msg.sender; uint256 committeeId = committeeOf[account]; if (committeeId == 0) revert NotConfigured(); delete committeeOf[account]; emit ModuleDeconfigured(account, committeeId); } /// @notice Rebind the calling account to a different registered committee (for example after a /// committee rotation). Owner-only by the singleton msg.sender convention. function setCommittee(uint256 committeeId) external { address account = msg.sender; if (committeeOf[account] == 0) revert NotConfigured(); if (committeeId == 0 || !VERIFIER.committeeExists(committeeId)) revert UnknownCommittee(committeeId); committeeOf[account] = committeeId; emit ModuleConfigured(account, committeeId); } // ========================================================================== // ERC-7579 validation // ========================================================================== /** * @notice Validate a userOp: succeed iff its signature carries a valid aggregate proof that the * account's committee signed `userOpHash`. Called by the account (msg.sender == account). * Returns the ERC-4337 code (0 = success, 1 = failure) and does NOT revert on a bad or * malformed signature, per the ERC-7579 validator SHOULD. * * The `userOpHash` IS the message digest the committee signs, so the aggregate proof is * bound to this exact operation (and, through the account nonce inside the userOpHash, is * non-replayable). userOp.signature MUST be abi.encode(bytes publicValues, bytes proofBytes). */ function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external view returns (uint256) { uint256 committeeId = committeeOf[msg.sender]; if (committeeId == 0) return SIG_VALIDATION_FAILED; return _validate(committeeId, userOpHash, userOp.signature) ? VALIDATION_SUCCESS : SIG_VALIDATION_FAILED; } /** * @notice ERC-1271 validation: return the magic value iff `signature` carries a valid aggregate * proof that the account's committee signed `hash`. Called by the account * (msg.sender == account). Never reverts on a bad or malformed signature. * @param sender the address that sent the ERC-1271 request to the account (unused here; the * authority is the committee, not the forwarding sender). [VERIFY] some accounts * scope by `sender`; this module intentionally does not. */ function isValidSignatureWithSender(address sender, bytes32 hash, bytes calldata signature) external view returns (bytes4) { sender; // explicitly unused; committee is the sole authority uint256 committeeId = committeeOf[msg.sender]; if (committeeId == 0) return EIP1271_FAIL; return _validate(committeeId, hash, signature) ? EIP1271_MAGIC : EIP1271_FAIL; } // ========================================================================== // Internal // ========================================================================== /// @dev Parse `signature` as (bytes publicValues, bytes proofBytes) and ask the verifier whether /// the committee signed `digest`. Never reverts: a malformed signature blob is caught and /// treated as a validation failure (the ERC-7579 SHOULD-not-revert rule). The verifier's /// tryVerifyAggregate is itself fail-closed and gas-constant in the committee size n. function _validate(uint256 committeeId, bytes32 digest, bytes calldata signature) internal view returns (bool) { try this.decodeSignature(signature) returns (bytes memory publicValues, bytes memory proofBytes) { return VERIFIER.tryVerifyAggregate(committeeId, digest, publicValues, proofBytes); } catch { return false; } } /// @notice External pure helper so a malformed signature is caught by the internal try/catch /// instead of reverting validateUserOp. Signature layout: abi.encode(bytes publicValues, /// bytes proofBytes). function decodeSignature(bytes calldata signature) external pure returns (bytes memory publicValues, bytes memory proofBytes) { (publicValues, proofBytes) = abi.decode(signature, (bytes, bytes)); } /// @notice Encode a module signature from its parts (off-chain / test helper). function encodeSignature(bytes calldata publicValues, bytes calldata proofBytes) external pure returns (bytes memory) { return abi.encode(publicValues, proofBytes); } }