Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Has been cancelled
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Has been cancelled
contracts-ci / PQC known-answer tests (NIST vectors) (push) Has been cancelled
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
The published line and the local line had no common ancestor: the public one carried the redaction pass, the local one carried three weeks of corrections that never shipped. This commit ports the local work onto the public line, keeps every public redaction, and extends the same discretion to seven client mentions that were still named in published comments. Carried: LICENSE year and LICENSING.md; the measured burn figures replacing the deflation claim (the vault holds ~0.137 AERE of 2.8 billion, and burn is a share of validator coinbase revenue, which is zero today); 'audited' removed from next to Bouncy Castle; citation paths rewritten to published form with CITATIONS-UNRESOLVED.md remeasured 2026-08-11; VERIFY-POLICY.md; slashing and ownership comments brought down to what the code does; the AerePyth repair; the shutter test helper the tests cite; runnable package.json entries; the CI file split into a GitHub/Gitea twin pair with a real measured test-run status; and the .gitignore hardening written after a compiled artifact leaked a local path in a sibling repository. A false '2-of-3 multisig' description of the owner account is corrected to what the chain measures: an externally owned account. The self-audit findings catalog stays unpublished pending an explicit decision.
356 lines
17 KiB
Solidity
356 lines
17 KiB
Solidity
// 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 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);
|
|
}
|
|
}
|