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.
196 lines
9.9 KiB
Solidity
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]));
|
|
}
|
|
}
|