// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AerePQCAttestation, quantum-durable on-chain attestations for AERE (chain 2800) * * @notice A permissionless registry that turns AERE's LIVE native post-quantum * verification precompiles into a usable primitive: bind a NIST PQC public * key to your address, then record attestations whose validity rests on a * real post-quantum signature verified in full, on-chain, by a native * precompile. No funds are ever held by this contract. * * HONEST SCOPE. This is application-layer PQC signature verification. It * does NOT change AERE consensus: blocks are still produced and signed by * Besu QBFT validators with ECDSA (secp256k1). What is new and live is the * set of native precompiles this contract calls, activated on mainnet 2800 * at block 9,189,161. Each precompile wraps the audited Bouncy Castle NIST * verifiers. This contract makes those precompiles USABLE end to end: it * is the difference between "PQC verification is callable" and * "quantum-durable attestations are recorded on-chain". * * LIVE PRECOMPILES (chain 2800): * scheme 1 = Falcon-512 at 0x00000000000000000000000000000000000000000000000000000000000000000AE1 * scheme 2 = Falcon-1024 at 0x...0AE2 * scheme 3 = ML-DSA-44 at 0x...0AE3 (FIPS 204, internal interface) * scheme 4 = SLH-DSA-128s at 0x...0AE4 (FIPS 205 SPHINCS+, internal interface) * * @dev PRECOMPILE INPUT ENCODING (exactly what the live precompiles parse): * * Falcon-512 / Falcon-1024 (0x0AE1 / 0x0AE2): * input = pk || sm * pk = (0x00+logn) || packed_h (897 bytes logn=9, 1793 bytes logn=10) * sm = sigLen(2, big-endian) || nonce(40) || message || esig * esig = (0x20+logn) || compressedSig, sigLen == esig.length * * ML-DSA-44 (0x0AE3): input = pk(1312) || sig(2420) || message * SLH-DSA-128s (0x0AE4): input = pk(32) || sig(7856) || message * * Output for every scheme: a 32-byte word, 0x..01 valid else 0x..00. * An empty/zero return is treated as INVALID (this is the pre-fork behaviour * of an address with no precompile, so a stale caller can never record). * * @dev SIGNATURE ENVELOPE handed to attest() / verifySignature(): * Falcon (1,2): signature = nonce(40) || esig (esig starts 0x29 / 0x2A) * ML-DSA-44 (3): signature = sig, exactly 2420 bytes * SLH-DSA-128s (4): signature = sig, exactly 7856 bytes * The contract supplies the 32-byte message itself, so the signer can never * choose a different message than the one this contract binds and records. * * @dev POST-QUANTUM AUTHORSHIP. Authorship of an attestation is the PQC signature * alone. attest() may be relayed by any msg.sender: only the holder of the * registered private key can produce a valid signature over this contract's * per-key nonce challenge, so a relayer only pays gas and can never forge or * replay. Binding authorship to the ECDSA tx sender would defeat the purpose, * since secp256k1 is exactly what a quantum computer breaks. */ contract AerePQCAttestation { // ----- scheme identifiers ----- uint8 public constant SCHEME_FALCON512 = 1; uint8 public constant SCHEME_FALCON1024 = 2; uint8 public constant SCHEME_MLDSA44 = 3; uint8 public constant SCHEME_SLHDSA128S = 4; // ----- live precompile addresses on chain 2800 ----- address public constant PRECOMPILE_FALCON512 = address(0x0AE1); address public constant PRECOMPILE_FALCON1024 = address(0x0AE2); address public constant PRECOMPILE_MLDSA44 = address(0x0AE3); address public constant PRECOMPILE_SLHDSA128S = address(0x0AE4); // ----- public key sizes / headers ----- uint256 internal constant FALCON512_PK_LEN = 897; uint256 internal constant FALCON1024_PK_LEN = 1793; uint256 internal constant MLDSA44_PK_LEN = 1312; uint256 internal constant SLHDSA128S_PK_LEN = 32; uint8 internal constant FALCON512_PK_HEADER = 0x09; // 0x00 | logn=9 uint8 internal constant FALCON1024_PK_HEADER = 0x0A; // 0x00 | logn=10 // ----- signature sizes (fixed-length schemes) ----- uint256 internal constant MLDSA44_SIG_LEN = 2420; uint256 internal constant SLHDSA128S_SIG_LEN = 7856; uint256 internal constant FALCON_NONCE_LEN = 40; // ----- domain separator for the attestation challenge ----- bytes32 public constant CHALLENGE_DOMAIN = keccak256("AerePQCAttestation.v1.challenge"); // ----- registered keys ----- struct Key { address owner; // address that registered the key (informational; not an authority) uint8 scheme; // one of SCHEME_* uint64 nonce; // strictly increasing per-key replay counter bytes pubKey; // the raw NIST public key for the scheme } Key[] private _keys; // keyId is the index into this array // ----- recorded attestations, keyed by keccak256(keyId, nonce) ----- struct Attestation { bytes32 messageHash; // the caller-supplied 32-byte payload commitment bytes32 challenge; // the exact 32 bytes the PQC signature verified over uint256 keyId; uint64 nonce; // the per-key nonce this attestation consumed uint64 blockNumber; // block the attestation was recorded at bool exists; } mapping(bytes32 => Attestation) private _attestations; /// @notice Total number of attestations recorded across all keys. uint256 public attestationCount; // ----- events ----- event KeyRegistered(uint256 indexed keyId, address indexed owner, uint8 indexed scheme, uint256 pubKeyLen); event Attested( uint256 indexed keyId, uint8 indexed scheme, uint64 indexed nonce, bytes32 messageHash, bytes32 challenge, uint256 blockNumber ); error InvalidScheme(uint8 scheme); error InvalidPubKey(); error UnknownKey(uint256 keyId); error InvalidSignatureLength(); error PQCVerificationFailed(); // ========================================================================== // Registration // ========================================================================== /** * @notice Register a PQC public key and bind it to msg.sender. Multiple keys * per address are allowed; each call returns a fresh keyId. Registration * validates the key length (and, for Falcon, the header byte) against the * scheme so a malformed key cannot be stored. * @param scheme one of 1=Falcon-512, 2=Falcon-1024, 3=ML-DSA-44, 4=SLH-DSA-128s. * @param pubKey the raw NIST public key for the scheme (see contract NatSpec). * @return keyId the identifier assigned to this key. */ function registerKey(uint8 scheme, bytes calldata pubKey) external returns (uint256 keyId) { _validatePubKey(scheme, pubKey); keyId = _keys.length; _keys.push(Key({owner: msg.sender, scheme: scheme, nonce: 0, pubKey: pubKey})); emit KeyRegistered(keyId, msg.sender, scheme, pubKey.length); } function _validatePubKey(uint8 scheme, bytes calldata pubKey) internal pure { if (scheme == SCHEME_FALCON512) { if (pubKey.length != FALCON512_PK_LEN || uint8(pubKey[0]) != FALCON512_PK_HEADER) revert InvalidPubKey(); } else if (scheme == SCHEME_FALCON1024) { if (pubKey.length != FALCON1024_PK_LEN || uint8(pubKey[0]) != FALCON1024_PK_HEADER) revert InvalidPubKey(); } else if (scheme == SCHEME_MLDSA44) { if (pubKey.length != MLDSA44_PK_LEN) revert InvalidPubKey(); } else if (scheme == SCHEME_SLHDSA128S) { if (pubKey.length != SLHDSA128S_PK_LEN) revert InvalidPubKey(); } else { revert InvalidScheme(scheme); } } // ========================================================================== // Attestation // ========================================================================== /** * @notice Record a quantum-durable attestation. Verifies, on-chain via the live * precompile for the key's scheme, that `signature` is a valid PQC * signature by the registered public key over this contract's challenge: * * challenge = keccak256(abi.encode( * CHALLENGE_DOMAIN, block.chainid, address(this), keyId, nonce, messageHash)) * * where `nonce` is the key's current per-key nonce. On success the * attestation is recorded and the nonce is incremented, so the same * (messageHash, signature) can never be replayed and no signature made * for a different key, message, contract or chain verifies. * * Reverts with PQCVerificationFailed on any invalid signature; nothing * is recorded and the nonce is unchanged. * @param keyId the registered key to attest under. * @param messageHash the 32-byte payload commitment being attested. * @param signature the PQC signature envelope for the key's scheme. */ function attest(uint256 keyId, bytes32 messageHash, bytes calldata signature) external { if (keyId >= _keys.length) revert UnknownKey(keyId); Key storage k = _keys[keyId]; uint64 nonce = k.nonce; bytes32 challenge = attestChallenge(keyId, nonce, messageHash); if (!_verify(k.scheme, k.pubKey, challenge, signature)) revert PQCVerificationFailed(); // Effects: consume the nonce and record before emitting. k.nonce = nonce + 1; bytes32 id = attestationId(keyId, nonce); _attestations[id] = Attestation({ messageHash: messageHash, challenge: challenge, keyId: keyId, nonce: nonce, blockNumber: uint64(block.number), exists: true }); attestationCount += 1; emit Attested(keyId, k.scheme, nonce, messageHash, challenge, block.number); } // ========================================================================== // Verification (views) // ========================================================================== /** * @notice Verify a PQC signature over a raw 32-byte message on-chain, for any * scheme, using the live precompile. Unbound utility: `messageHash` is * used directly as the signed message (no contract/nonce binding), so * anyone can check any NIST PQC signature via eth_call for free. * @return true iff the live precompile accepts the signature. */ function verifySignature(uint8 scheme, bytes calldata pubKey, bytes32 messageHash, bytes calldata signature) external view returns (bool) { return _verify(scheme, pubKey, messageHash, signature); } /** * @notice The exact 32-byte challenge that a signature for `keyId` must sign at a * given nonce. Off-chain signers query this to know what to sign. */ function attestChallenge(uint256 keyId, uint64 nonce, bytes32 messageHash) public view returns (bytes32) { return keccak256(abi.encode(CHALLENGE_DOMAIN, block.chainid, address(this), keyId, nonce, messageHash)); } /// @notice Deterministic id for the attestation recorded by `keyId` at `nonce`. function attestationId(uint256 keyId, uint64 nonce) public pure returns (bytes32) { return keccak256(abi.encode(keyId, nonce)); } // ========================================================================== // Internal verifier // ========================================================================== /** * @dev Build the exact precompile input for `scheme` with `message` as the signed * 32-byte message, staticcall the live precompile, and return true iff it * answers 0x..01. Any empty/short/zero return is invalid. */ function _verify(uint8 scheme, bytes memory pubKey, bytes32 message, bytes memory signature) internal view returns (bool) { (address precompile, bytes memory input) = _buildInput(scheme, pubKey, message, signature); (bool ok, bytes memory ret) = precompile.staticcall(input); return ok && ret.length >= 32 && ret[31] == 0x01; } /// @dev Assemble (precompile, input) for a scheme. Reverts only on a length that /// cannot form a valid input; a well-formed-but-wrong signature returns an /// input the precompile will reject with 0. function _buildInput(uint8 scheme, bytes memory pubKey, bytes32 message, bytes memory signature) internal pure returns (address precompile, bytes memory input) { if (scheme == SCHEME_FALCON512 || scheme == SCHEME_FALCON1024) { // signature = nonce(40) || esig if (signature.length <= FALCON_NONCE_LEN) revert InvalidSignatureLength(); uint256 sigLen = signature.length - FALCON_NONCE_LEN; // esig length if (sigLen > type(uint16).max) revert InvalidSignatureLength(); bytes memory nonce = _slice(signature, 0, FALCON_NONCE_LEN); bytes memory esig = _slice(signature, FALCON_NONCE_LEN, sigLen); // sm = sigLen(2, big-endian) || nonce(40) || message(32) || esig bytes memory sm = abi.encodePacked(uint16(sigLen), nonce, message, esig); input = abi.encodePacked(pubKey, sm); precompile = scheme == SCHEME_FALCON512 ? PRECOMPILE_FALCON512 : PRECOMPILE_FALCON1024; } else if (scheme == SCHEME_MLDSA44) { if (signature.length != MLDSA44_SIG_LEN) revert InvalidSignatureLength(); input = abi.encodePacked(pubKey, signature, message); // pk(1312) || sig(2420) || message(32) precompile = PRECOMPILE_MLDSA44; } else if (scheme == SCHEME_SLHDSA128S) { if (signature.length != SLHDSA128S_SIG_LEN) revert InvalidSignatureLength(); input = abi.encodePacked(pubKey, signature, message); // pk(32) || sig(7856) || message(32) precompile = PRECOMPILE_SLHDSA128S; } else { revert InvalidScheme(scheme); } } function _slice(bytes memory data, uint256 start, uint256 len) internal pure returns (bytes memory out) { out = new bytes(len); for (uint256 i = 0; i < len; i++) { out[i] = data[start + i]; } } // ========================================================================== // Views // ========================================================================== /// @notice Number of registered keys (keyIds run 0..keyCount-1). function keyCount() external view returns (uint256) { return _keys.length; } /// @notice Read a registered key. function getKey(uint256 keyId) external view returns (address owner, uint8 scheme, uint64 nonce, bytes memory pubKey) { if (keyId >= _keys.length) revert UnknownKey(keyId); Key storage k = _keys[keyId]; return (k.owner, k.scheme, k.nonce, k.pubKey); } /// @notice The current (next unused) nonce for a key. function nonceOf(uint256 keyId) external view returns (uint64) { if (keyId >= _keys.length) revert UnknownKey(keyId); return _keys[keyId].nonce; } /// @notice Read a recorded attestation by (keyId, nonce). function getAttestation(uint256 keyId, uint64 nonce) external view returns (bool exists, bytes32 messageHash, bytes32 challenge, uint256 blockNumber) { Attestation storage a = _attestations[attestationId(keyId, nonce)]; return (a.exists, a.messageHash, a.challenge, a.blockNumber); } /// @notice True iff the attestation at (keyId, nonce) exists and committed to `messageHash`. function isValidAttestation(uint256 keyId, uint64 nonce, bytes32 messageHash) external view returns (bool) { Attestation storage a = _attestations[attestationId(keyId, nonce)]; return a.exists && a.messageHash == messageHash; } /// @notice The live precompile address for a scheme (reverts on an unknown scheme). function precompileFor(uint8 scheme) external pure returns (address) { if (scheme == SCHEME_FALCON512) return PRECOMPILE_FALCON512; if (scheme == SCHEME_FALCON1024) return PRECOMPILE_FALCON1024; if (scheme == SCHEME_MLDSA44) return PRECOMPILE_MLDSA44; if (scheme == SCHEME_SLHDSA128S) return PRECOMPILE_SLHDSA128S; revert InvalidScheme(scheme); } }