aere-contracts/contracts/pqc/AerePQCAccount.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

258 lines
13 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AerePQCAccount, a post-quantum-secured ERC-4337 smart account (roadmap #270)
* @notice An ERC-4337 v0.7 smart account whose sole owner is a NIST Falcon-512
* public key (897 bytes, the 0x09-headed NIST encoding). Every
* authorization, whether a bundled UserOperation or an EIP-1271
* off-chain signature check, is decided by the LIVE, spec-complete
* on-chain AereFalcon512Verifier (chain 2800: 0x4E8e9682...D8fFC): full
* SHAKE256 HashToPoint, 14-bit public-key decode, compressed-signature
* (comp_decode) decode, negacyclic NTT multiply mod q=12289 in
* Z_q[x]/(x^512+1), centering, and l2-norm bound check. There is no
* classical ECDSA fallback: the account is controlled by, and only by, a
* quantum-resistant lattice key.
*
* This turns AERE's on-chain PQC verifier suite into a usable wallet
* primitive: a user can hold an account whose spending authority is a
* Falcon-512 key, verified by real lattice cryptography on chain.
*
* @dev SIGNATURE ENVELOPE. Wherever this account consumes a Falcon signature
* (validateUserOp, isValidSignature, executeWithFalcon), the `signature`
* bytes are `abi.encode(bytes nonce, bytes compSig)` where `nonce` is the
* 40-byte Falcon salt and `compSig` is the compressed s2 encoding
* (comp_encode output, the esig body WITHOUT the 0x29 header byte). The
* signed message is DOMAIN-SEPARATED: a fund-moving authorization
* (validateUserOp, executeWithFalcon) signs `abi.encodePacked(domain, hash)`
* (64 bytes, domain = USEROP_DOMAIN / EXEC_DOMAIN), while the EIP-1271
* isValidSignature surface signs the raw 32 bytes `abi.encodePacked(hash)`.
* The two message shapes can never collide, so a signature obtained through
* a dapp "login" (isValidSignature over an attacker-chosen hash) can NOT be
* replayed as a spend. For a UserOperation the hash is the ERC-4337
* userOpHash, which already binds sender + chainid.
*
* @dev GAS / EIP-7825. A Falcon-512 verify is roughly 10.5M gas. A full
* handleOps → validateUserOp path therefore runs as a single ordinary
* transaction that must stay under the Fusaka EIP-7825 per-transaction
* cap of 2^24 = 16,777,216 gas; the deploy/proof script measures the
* real number. `isValidSignature` is a `view`, so integrators can verify
* a Falcon signature for free via eth_call regardless of the cap. A
* native SHAKE/keccak precompile would cut the per-auth cost sharply
* (tracked as a separate roadmap item); today PQC auth is heavy but real.
*
* No owner, no admin, no upgrade beyond the one-shot factory initializer.
*/
interface IFalcon512Verifier {
/// @notice Full on-chain Falcon-512 verification. Returns true iff
/// (pk, message, nonce, compSig) is a valid Falcon-512 signature.
function verify(
bytes memory pk,
bytes memory message,
bytes memory nonce,
bytes memory compSig
) external view returns (bool);
}
contract AerePQCAccount {
/// @notice The 897-byte NIST Falcon-512 public key that owns this account.
bytes public falconPubKey;
/// @notice The LIVE AereFalcon512Verifier this account delegates verification to.
IFalcon512Verifier public falcon;
/// @notice ERC-4337 EntryPoint allowed to call validateUserOp + executeFromEntryPoint.
address public entryPoint;
/// @notice Factory that deployed this account (gates the one-shot initializer).
address public immutable factory;
/// @notice Monotonic replay counter for the direct Falcon-authorized path.
uint256 public execNonce;
/// @notice EIP-1271 magic value returned by isValidSignature on success.
bytes4 internal constant EIP1271_MAGIC = 0x1626ba7e;
/// @notice ERC-4337 signature-validation-failed sentinel (validationData != 0).
uint256 internal constant SIG_VALIDATION_FAILED = 1;
/// @notice NIST Falcon-512 public key: 897 bytes, first byte 0x09 (0x00 | logn=9).
uint256 internal constant FALCON512_PK_LEN = 897;
uint8 internal constant FALCON512_PK_HEADER = 0x09;
/// @notice Signing-domain separators (adversarial-review HIGH, 2026-07-12).
/// The message handed to the Falcon verifier for a fund-moving
/// authorization (validateUserOp / executeWithFalcon) is
/// `abi.encodePacked(domain, hash)`, i.e. 64 bytes, which can NEVER
/// collide with the raw 32-byte message that isValidSignature (EIP-1271)
/// verifies. Without this separation, a Falcon signature phished through
/// an EIP-1271 "login" over a crafted 32-byte hash (set equal to an
/// execute-challenge or a userOpHash, both computable from public state
/// since execNonce is public) could be replayed as a spend authorization
/// and drain the account.
bytes32 internal constant USEROP_DOMAIN = keccak256("AerePQCAccount.validateUserOp.v1");
bytes32 internal constant EXEC_DOMAIN = keccak256("AerePQCAccount.executeWithFalcon.v1");
/// @notice Generic call struct (used by executeBatch).
struct Call {
address target;
uint256 value;
bytes data;
}
/// @notice ERC-4337 v0.7 PackedUserOperation shape (matches AereEntryPointV2).
struct PackedUserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
bytes32 accountGasLimits;
uint256 preVerificationGas;
bytes32 gasFees;
bytes paymasterAndData;
bytes signature;
}
event Initialized(address indexed entryPoint, address indexed falcon, uint256 pubKeyLen);
event Executed(uint256 indexed nonce, address indexed target, uint256 value, bytes data);
event ExecutedBatch(uint256 indexed nonce, uint256 count);
error Unauthorized();
error AlreadyInitialized();
error NotInitialized();
error InvalidEntryPoint();
error InvalidFalconVerifier();
error InvalidFalconPubKey();
error InvalidSignature();
error CallFailed(bytes returnData);
constructor() {
factory = msg.sender;
}
/// @notice One-shot initializer, callable only by the deploying factory.
/// @param _entryPoint the ERC-4337 EntryPoint the account will trust.
/// @param _falcon the live AereFalcon512Verifier.
/// @param _falconPubKey the 897-byte NIST Falcon-512 public key that owns this account.
function initialize(address _entryPoint, address _falcon, bytes calldata _falconPubKey) external {
if (msg.sender != factory) revert Unauthorized();
if (entryPoint != address(0)) revert AlreadyInitialized();
if (_entryPoint == address(0)) revert InvalidEntryPoint();
if (_falcon == address(0)) revert InvalidFalconVerifier();
if (_falconPubKey.length != FALCON512_PK_LEN || uint8(_falconPubKey[0]) != FALCON512_PK_HEADER) {
revert InvalidFalconPubKey();
}
entryPoint = _entryPoint;
falcon = IFalcon512Verifier(_falcon);
falconPubKey = _falconPubKey;
emit Initialized(_entryPoint, _falcon, _falconPubKey.length);
}
// ─── ERC-4337 path ────────────────────────────────────────────────────────
/// @notice Called by the EntryPoint to validate a user operation.
/// @dev Returns 0 on success and SIG_VALIDATION_FAILED (1) on a bad Falcon
/// signature, per ERC-4337 (does not revert on bad signature).
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData) {
if (msg.sender != entryPoint) revert Unauthorized();
// Post-quantum authorization: the userOpHash must carry a valid Falcon-512
// signature under this account's owner key, verified fully on chain. The
// signed message is domain-separated (USEROP_DOMAIN) so an EIP-1271 login
// signature over a raw hash can never be replayed here.
if (!_verifyFalconDomained(USEROP_DOMAIN, userOpHash, userOp.signature)) {
return SIG_VALIDATION_FAILED;
}
// Pay the EntryPoint any prefund it needs (EntryPoint enforces the gas budget).
if (missingAccountFunds > 0) {
(bool sent, ) = payable(entryPoint).call{value: missingAccountFunds}("");
(sent); // ignore — EntryPoint reverts if underfunded
}
return 0;
}
/// @notice Called by the EntryPoint after validation to dispatch the user's call.
function executeFromEntryPoint(address target, uint256 value, bytes calldata data) external {
if (msg.sender != entryPoint) revert Unauthorized();
(bool ok, bytes memory ret) = target.call{value: value}(data);
if (!ok) revert CallFailed(ret);
}
function executeBatchFromEntryPoint(Call[] calldata calls) external {
if (msg.sender != entryPoint) revert Unauthorized();
for (uint256 i = 0; i < calls.length; i++) {
(bool ok, bytes memory ret) = calls[i].target.call{value: calls[i].value}(calls[i].data);
if (!ok) revert CallFailed(ret);
}
}
// ─── Direct Falcon-authorized path (self-relay, no bundler required) ───────
/// @notice Hash the account owner's Falcon key must sign for an executeWithFalcon() call.
function getExecuteChallenge(
address target, uint256 value, bytes calldata data, uint256 expectedNonce
) public view returns (bytes32) {
return keccak256(abi.encode(address(this), block.chainid, expectedNonce, target, value, data));
}
/// @notice Execute a single call, authorized by a Falcon-512 signature over the
/// current execute-challenge. Lets the account be driven directly (e.g.
/// by a Foundation relayer) without routing through a 4337 bundler.
function executeWithFalcon(
bytes calldata signature,
address target,
uint256 value,
bytes calldata data
) external returns (bytes memory result) {
if (entryPoint == address(0)) revert NotInitialized();
bytes32 challenge = getExecuteChallenge(target, value, data, execNonce);
// Domain-separated (EXEC_DOMAIN): a signature phished via the raw-hash
// isValidSignature surface cannot authorize a spend here.
if (!_verifyFalconDomained(EXEC_DOMAIN, challenge, signature)) revert InvalidSignature();
uint256 n = execNonce;
execNonce = n + 1;
(bool ok, bytes memory ret) = target.call{value: value}(data);
if (!ok) revert CallFailed(ret);
emit Executed(n, target, value, data);
return ret;
}
// ─── EIP-1271 ─────────────────────────────────────────────────────────────
/// @notice EIP-1271: returns the magic value iff `signature` is a valid
/// Falcon-512 signature over `hash` under this account's owner key.
/// A `view`, so integrators (Permit2, OpenSea, Snapshot, dapp logins)
/// can verify a post-quantum signature for free via eth_call.
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
return _verifyFalcon(hash, signature) ? EIP1271_MAGIC : bytes4(0);
}
// ─── Falcon verification (internal) ───────────────────────────────────────
/// @dev Decode the (nonce, compSig) envelope and run the full on-chain
/// Falcon-512 verification of `hash` (as raw 32 bytes) via the live verifier.
function _verifyFalcon(bytes32 hash, bytes calldata signature) internal view returns (bool) {
(bytes memory nonce, bytes memory compSig) = abi.decode(signature, (bytes, bytes));
return falcon.verify(falconPubKey, abi.encodePacked(hash), nonce, compSig);
}
/// @dev Domain-separated Falcon verify for the fund-moving paths. The signed
/// message is `abi.encodePacked(domain, hash)` (64 bytes), which can never
/// equal the raw 32-byte message that isValidSignature verifies, so a
/// signature obtained through the EIP-1271 login surface cannot be replayed
/// as a validateUserOp or executeWithFalcon spend authorization.
function _verifyFalconDomained(bytes32 domain, bytes32 hash, bytes calldata signature) internal view returns (bool) {
(bytes memory nonce, bytes memory compSig) = abi.decode(signature, (bytes, bytes));
return falcon.verify(falconPubKey, abi.encodePacked(domain, hash), nonce, compSig);
}
receive() external payable {}
}