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.
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 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);
|
|
}
|
|
}
|