Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
226 lines
11 KiB
Solidity
226 lines
11 KiB
Solidity
// 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);
|
|
}
|
|
}
|