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

81 lines
3.5 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AerePQCVerifier — on-chain post-quantum signature verification
* @notice Verifies a WOTS+ (Winternitz One-Time Signature) — a HASH-BASED
* signature that is secure against quantum computers (its security rests
* only on the pre-image resistance of keccak256, which Shor's algorithm
* does not break, unlike ECDSA). This lets AERE contracts natively
* verify a post-quantum signature TODAY, without touching consensus.
*
* Scheme (n=32, w=16): the 256-bit message hash is split into 64 base-16
* digits plus a 3-digit checksum (67 chains total). Each signature
* element is a keccak hash-chain value; verification completes each
* chain to its end and checks that the hash of all chain-ends equals the
* committed public key. The checksum makes forgery infeasible: raising
* any message digit lowers the checksum, which would require inverting
* keccak.
*
* WOTS+ is ONE-TIME (each key signs one message); it is the building
* block of the stateful many-time schemes XMSS / SPHINCS+. A public
* AereXmssVerifier (WOTS+ leaf + Merkle authentication path to a long-
* term root) is the natural extension.
*/
contract AerePQCVerifier {
uint256 public constant W = 16; // Winternitz parameter
uint256 public constant LEN1 = 64; // message digits (256 bits / 4)
uint256 public constant LEN2 = 3; // checksum digits
uint256 public constant LEN = 67; // total chains
/// @notice Derive the 67 base-16 digits (message + checksum) from a hash.
function digits(bytes32 m) public pure returns (uint8[67] memory d) {
uint256 csum = 0;
for (uint256 i = 0; i < 32; i++) {
uint8 b = uint8(m[i]);
uint8 hi = b >> 4;
uint8 lo = b & 0x0f;
d[2 * i] = hi;
d[2 * i + 1] = lo;
csum += (15 - uint256(hi)) + (15 - uint256(lo));
}
// checksum (max 64*15 = 960 < 16^3) as 3 base-16 digits, MSB first
d[64] = uint8((csum >> 8) & 0x0f);
d[65] = uint8((csum >> 4) & 0x0f);
d[66] = uint8(csum & 0x0f);
}
function _chain(bytes32 x, uint256 count) internal pure returns (bytes32) {
for (uint256 i = 0; i < count; i++) {
x = keccak256(abi.encodePacked(x));
}
return x;
}
/**
* @notice Verify a WOTS+ signature.
* @param msgHash keccak256 of the message being signed
* @param sig the 67 hash-chain signature elements
* @param compressedPK keccak256 of the 67 public-key chain-ends
* @return ok true iff the signature is valid for compressedPK
*/
function verify(bytes32 msgHash, bytes32[67] calldata sig, bytes32 compressedPK) public pure returns (bool ok) {
uint8[67] memory d = digits(msgHash);
bytes32[67] memory ends;
for (uint256 i = 0; i < LEN; i++) {
// complete each chain from the signature to the public-key end
ends[i] = _chain(sig[i], 15 - uint256(d[i]));
}
return keccak256(abi.encodePacked(ends)) == compressedPK;
}
/// @notice Compute a WOTS+ public key from a full private key (test/helper).
function derivePublicKey(bytes32[67] calldata sk) external pure returns (bytes32 compressedPK) {
bytes32[67] memory ends;
for (uint256 i = 0; i < LEN; i++) {
ends[i] = _chain(sk[i], 15);
}
return keccak256(abi.encodePacked(ends));
}
}