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.
258 lines
13 KiB
Solidity
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 {}
|
|
}
|