aere-contracts/contracts/pqc/AerePQCPrecompiles.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

196 lines
9.9 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AerePQCPrecompiles - clean view wrappers for AERE's two newest PQC precompiles
*
* @notice A dependency-free Solidity library that any contract can `using`/import to call the
* two native precompiles added to the AERE Besu PQC fork:
*
* - ML-KEM-768 (FIPS 203) deterministic encapsulation, at 0x0AE6.
* - Falcon HashToPoint (SHAKE256 rejection sampler), at 0x0AE7.
*
* Every function is `internal view` and fail-closed: it validates the exact wire
* lengths, performs one `staticcall`, checks the returned length, and returns
* `ok == false` (never reverts) on any malformed input or unexpected output. Because
* the functions are `internal`, the library is inlined into the caller - there is no
* separate deployment and no external call surface to trust.
*
* @dev HONEST SCOPE - TESTNET ONLY (as of 2026-07-18). These two precompiles are live on the
* AERE TESTNET ONLY; they are NOT activated on mainnet chain 2800, which has exactly the
* FIVE precompiles 0x0AE1..0x0AE5 (activated block 9,189,161). On a chain WITHOUT these
* precompiles a staticcall to 0x0AE6 / 0x0AE7 hits an empty account and returns success
* with EMPTY data, so the length check fails and every wrapper here returns `ok == false`.
* A contract using this library therefore behaves safely (deny, never a false positive)
* on mainnet today, and starts succeeding automatically once the precompiles are
* activated per docs/PQC-PRECOMPILES-MAINNET-ACTIVATION-PLAN-2026-07-18.md (founder +
* external-audit gated). Nothing here changes AERE consensus, which remains classical
* secp256k1 ECDSA QBFT.
*
* @dev These wrappers deliberately do NOT hold state and make NO cryptographic claim of their
* own: they are a thin, exact restatement of each precompile's byte contract. The
* precompiles themselves are KAT/ACVP-validated (ML-KEM-768: 25/25 NIST ACVP; Falcon
* HashToPoint: 12/12 vs an independent FIPS 202 SHAKE256 oracle). A standalone contract
* form of the ML-KEM-768 wrapper also exists at AereMLKEM768.sol; this library is the
* reusable, importable surface covering BOTH precompiles.
*/
library AerePQCPrecompiles {
// ---- Precompile addresses (TESTNET ONLY; not on mainnet 2800 yet) ----------------------
/// @dev ML-KEM-768 (FIPS 203) deterministic-Encaps precompile.
address internal constant MLKEM768 = address(0x0AE6);
/// @dev Falcon HashToPoint (SHAKE256 rejection sampler) precompile.
address internal constant FALCON_HASHTOPOINT = address(0x0AE7);
// ---- ML-KEM-768 field sizes (FIPS 203, bytes) ------------------------------------------
/// @dev Encapsulation key (public key) length.
uint256 internal constant MLKEM768_EK_LEN = 1184;
/// @dev Encapsulation randomness ("coins") length.
uint256 internal constant MLKEM768_M_LEN = 32;
/// @dev Ciphertext length.
uint256 internal constant MLKEM768_CT_LEN = 1088;
/// @dev Shared-secret length.
uint256 internal constant MLKEM768_SS_LEN = 32;
// ---- Falcon HashToPoint sizes ----------------------------------------------------------
/// @dev Falcon salt / nonce length (bytes).
uint256 internal constant FALCON_NONCE_LEN = 40;
/// @dev Minimum Falcon logn accepted (n = 2^logn). Falcon-512 uses 9, Falcon-1024 uses 10.
uint8 internal constant FALCON_MIN_LOGN = 9;
/// @dev Maximum Falcon logn accepted.
uint8 internal constant FALCON_MAX_LOGN = 10;
/// @dev Falcon ring modulus q; every returned coefficient is in [0, q).
uint256 internal constant FALCON_Q = 12289;
// ========================================================================================
// ML-KEM-768 @ 0x0AE6
// ========================================================================================
/**
* @notice Deterministically encapsulate to `ek` using the 32-byte coins `m`, via the native
* ML-KEM-768 precompile. Reproduces FIPS 203 `ML-KEM.Encaps(ek, m)`: the same
* (ct, ss) every node computes for the same (ek, m).
*
* @dev Wire contract of 0x0AE6:
* input = ek(1184) || m(32) (1216 bytes, exact)
* output = ct(1088) || ss(32) (1120 bytes); EMPTY on malformed input.
* Fail-closed: returns (false, "", "") on any wrong-length input or output (which
* includes a chain where the precompile is not activated).
*
* @param ek 1184-byte ML-KEM-768 encapsulation key (public key).
* @param m 32-byte encapsulation randomness (coins).
* @return ok true iff the precompile returned a well-formed 1120-byte (ct, ss).
* @return ct 1088-byte ciphertext (empty when ok is false).
* @return ss 32-byte shared secret (empty when ok is false).
*/
function mlkem768Encapsulate(bytes memory ek, bytes memory m)
internal
view
returns (bool ok, bytes memory ct, bytes memory ss)
{
if (ek.length != MLKEM768_EK_LEN || m.length != MLKEM768_M_LEN) {
return (false, "", "");
}
bytes memory input = bytes.concat(ek, m);
(bool success, bytes memory out) = MLKEM768.staticcall(input);
if (!success || out.length != MLKEM768_CT_LEN + MLKEM768_SS_LEN) {
return (false, "", "");
}
ct = new bytes(MLKEM768_CT_LEN);
ss = new bytes(MLKEM768_SS_LEN);
for (uint256 i = 0; i < MLKEM768_CT_LEN; i++) {
ct[i] = out[i];
}
for (uint256 i = 0; i < MLKEM768_SS_LEN; i++) {
ss[i] = out[MLKEM768_CT_LEN + i];
}
return (true, ct, ss);
}
/**
* @notice Recompute only the 32-byte shared secret for (ek, m). Convenience for a relying
* contract that already holds the ciphertext.
* @return ok true iff the precompile returned a well-formed transcript.
* @return ss 32-byte shared secret (empty when ok is false).
*/
function mlkem768SharedSecret(bytes memory ek, bytes memory m)
internal
view
returns (bool ok, bytes memory ss)
{
bytes memory ct;
(ok, ct, ss) = mlkem768Encapsulate(ek, m);
}
/**
* @notice Verify a claimed ML-KEM-768 transcript: does Encaps(ek, m) reproduce exactly the
* claimed ciphertext and shared secret?
* @return valid true iff the recomputed (ct, ss) equal the claimed values byte-for-byte.
*/
function mlkem768VerifyTranscript(
bytes memory ek,
bytes memory m,
bytes memory claimedCt,
bytes memory claimedSs
) internal view returns (bool valid) {
(bool ok, bytes memory ct, bytes memory ss) = mlkem768Encapsulate(ek, m);
if (!ok) return false;
return keccak256(ct) == keccak256(claimedCt) && keccak256(ss) == keccak256(claimedSs);
}
// ========================================================================================
// Falcon HashToPoint @ 0x0AE7
// ========================================================================================
/**
* @notice HashToPoint(nonce || message) -> the Falcon challenge polynomial c, via the native
* precompile. Matches the reference `hash_to_point_vartime`: absorb `nonce||message`
* into SHAKE256, squeeze 16-bit big-endian samples, keep `w mod q` whenever
* `w < 5q = 61445`, until `n = 2^logn` coefficients are collected.
*
* @dev Wire contract of 0x0AE7:
* input = logn(1) || nonce(40) || message(rest)
* output = 2n bytes = n big-endian uint16 coefficients in [0, q), q=12289; EMPTY on malformed.
* The raw 2n-byte output is returned as-is; use {challengeCoeff} to read a coefficient,
* or decode each pair as `(challenge[2i] << 8) | challenge[2i+1]`. Fail-closed: returns
* (false, "") on a bad logn, a wrong-length nonce, or an unexpected output length
* (which includes a chain where the precompile is not activated).
*
* @param logn Falcon log-degree: 9 for Falcon-512 (n=512) or 10 for Falcon-1024 (n=1024).
* @param nonce 40-byte Falcon salt r.
* @param message the message bytes hashed together with the nonce.
* @return ok true iff the precompile returned the expected 2n bytes.
* @return challenge the 2n-byte challenge polynomial (n big-endian uint16 coeffs); empty when ok is false.
*/
function falconHashToPoint(uint8 logn, bytes memory nonce, bytes memory message)
internal
view
returns (bool ok, bytes memory challenge)
{
if (logn < FALCON_MIN_LOGN || logn > FALCON_MAX_LOGN) return (false, "");
if (nonce.length != FALCON_NONCE_LEN) return (false, "");
uint256 n = uint256(1) << logn;
uint256 outLen = 2 * n;
bytes memory input = bytes.concat(bytes1(logn), nonce, message);
(bool success, bytes memory out) = FALCON_HASHTOPOINT.staticcall(input);
if (!success || out.length != outLen) return (false, "");
return (true, out);
}
/**
* @notice Read coefficient `i` of a HashToPoint challenge polynomial produced by
* {falconHashToPoint}. Coefficient `i` is the big-endian uint16 at bytes [2i, 2i+1].
* @dev Reverts if `i` is out of range for the given challenge buffer.
* @param challenge the 2n-byte challenge polynomial.
* @param i coefficient index (0-based).
* @return coeff the i-th coefficient, in [0, q).
*/
function challengeCoeff(bytes memory challenge, uint256 i) internal pure returns (uint256 coeff) {
uint256 hi = 2 * i;
require(hi + 1 < challenge.length, "AerePQCPrecompiles: coeff index OOB");
return (uint256(uint8(challenge[hi])) << 8) | uint256(uint8(challenge[hi + 1]));
}
}