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.
495 lines
25 KiB
Solidity
495 lines
25 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
/**
|
|
* @title AereThresholdPQCRegistry, an on-chain t-of-n POST-QUANTUM threshold
|
|
* authorization primitive over AERE's LIVE PQC precompiles (chain 2800)
|
|
*
|
|
* @notice A committee of n post-quantum signers, each with a registered NIST PQC
|
|
* public key, authorizes a message only when >= t DISTINCT committee members
|
|
* each PQC-sign the SAME domain-separated challenge, with every signature
|
|
* verified in full, on-chain, by the live PQC precompile for the committee's
|
|
* scheme. This gives genuine post-quantum t-of-n authority: every leg is
|
|
* quantum-resistant, no single member can authorize alone, and no member can
|
|
* be double-counted. This contract holds NO funds; it is verification only.
|
|
*
|
|
* @dev THE HONEST BOUNDARY, read this before trusting or describing anything.
|
|
*
|
|
* 1. THIS IS A THRESHOLD MULTISIG OF INDEPENDENT PQC SIGNATURES, NOT A SINGLE
|
|
* AGGREGATE THRESHOLD-PQC SIGNATURE. A valid authorization here is t distinct
|
|
* PQC signatures, each verified separately by the precompile. It is NOT one
|
|
* short signature proving ">= t of n signed" (as a threshold-BLS or a true
|
|
* threshold-lattice signature would be). A single-aggregate threshold-PQC
|
|
* signature over ML-DSA / Falcon is an OPEN RESEARCH problem: lattice /
|
|
* hash-based threshold signatures are immature, unstandardized, and have no
|
|
* audited production library or matching on-chain verifier. See
|
|
* docs/THRESHOLD-PQC-2026-07-12.md for the full research summary. Do NOT
|
|
* describe this as a single aggregate threshold-PQC signature, and do NOT
|
|
* describe it as post-quantum consensus (AERE consensus is classical ECDSA
|
|
* QBFT). What IS true and new: each of the t authorizing legs is a real NIST
|
|
* PQC signature verified on-chain, so the t-of-n authority is quantum-durable.
|
|
*
|
|
* 2. WHY THIS IS STRICTLY STRONGER THAN THE ECDSA THRESHOLD REGISTRY ON-CHAIN.
|
|
* In AereThresholdRegistry (threshold ECDSA), an on-chain t-of-n signature is
|
|
* byte-identical to a single-key `ecrecover`, so the chain literally cannot
|
|
* tell that t members participated: "t-of-n" lives entirely off-chain. HERE,
|
|
* by contrast, the chain verifies t SEPARATE PQC signatures against t DISTINCT
|
|
* registered public keys, so the t-of-n participation is proven ON-CHAIN. The
|
|
* trade-off is cost: t independent precompile verifications instead of one
|
|
* aggregate check, which bounds the practical (scheme, t) ceiling (see below).
|
|
*
|
|
* 3. LIVE PRECOMPILES (chain 2800), single-signer verifiers this contract calls:
|
|
* scheme 1 = Falcon-512 at 0x...0AE1 (cheapest verify: 40,000 gas)
|
|
* scheme 2 = Falcon-1024 at 0x...0AE2 (75,000 gas)
|
|
* scheme 3 = ML-DSA-44 at 0x...0AE3 (55,000 gas, FIPS 204)
|
|
* scheme 4 = SLH-DSA-128s at 0x...0AE4 (350,000 gas, FIPS 205 SPHINCS+)
|
|
* Gas values are the marginal precompile verify-op costs measured on the
|
|
* precompile fork (research/aip-draft-pqc-precompiles.md).
|
|
*
|
|
* 4. EIP-7825 GAS-CEILING HONESTY. AERE keeps Fusaka's per-transaction cap of
|
|
* 2^24 = 16,777,216 gas. authorize() performs t independent precompile
|
|
* verifications plus per-leg calldata (pubkey + signature) and bookkeeping, so
|
|
* the committee's t is bounded. Using the CHEAPEST path (Falcon-512), the
|
|
* per-leg cost is dominated by the 40,000-gas precompile verify plus ~1.6 KB
|
|
* of pubkey+signature calldata and the distinct-member bookkeeping. The
|
|
* practical (scheme, t) ceilings are documented in
|
|
* docs/THRESHOLD-PQC-2026-07-12.md; Falcon-512 admits the largest committee
|
|
* quorum, SLH-DSA-128s the smallest (its 7,856-byte signature makes calldata,
|
|
* not the verify, the dominant per-leg cost).
|
|
*
|
|
* @dev PRECOMPILE INPUT ENVELOPE (identical to AerePQCAttestation, which is proven
|
|
* bit-for-bit against the LIVE mainnet precompile via eth_call):
|
|
* 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 challenge itself, so no signer can sign a
|
|
* message other than the one this contract binds. Precompile output is a raw
|
|
* 32-byte word, 0x..01 valid else 0x..00; an empty/zero return (a stale/pre-fork
|
|
* address) is treated as INVALID, so a mis-targeted call can never authorize.
|
|
*
|
|
* @dev REPLAY PROTECTION. Each committee has a strictly increasing `authNonce`. Every
|
|
* member signs the SAME challenge, which binds
|
|
* {domain, chainId, this contract, committeeId, authNonce, messageHash}. On a
|
|
* successful authorization the nonce increments, so the exact same set of t
|
|
* signatures can never be replayed (they verified only for the old nonce) and no
|
|
* signature made for a different committee, message, contract, or chain verifies.
|
|
*/
|
|
contract AereThresholdPQCRegistry {
|
|
// ==========================================================================
|
|
// Scheme + precompile constants
|
|
// ==========================================================================
|
|
|
|
uint8 public constant SCHEME_FALCON512 = 1;
|
|
uint8 public constant SCHEME_FALCON1024 = 2;
|
|
uint8 public constant SCHEME_MLDSA44 = 3;
|
|
uint8 public constant SCHEME_SLHDSA128S = 4;
|
|
|
|
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);
|
|
|
|
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
|
|
|
|
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 per-authorization challenge each member PQC-signs.
|
|
bytes32 public constant AUTH_DOMAIN = keccak256("AereThresholdPQCRegistry.v1.authorize");
|
|
|
|
// ==========================================================================
|
|
// Types
|
|
// ==========================================================================
|
|
|
|
struct Committee {
|
|
bool exists;
|
|
uint8 scheme; // one of SCHEME_* (whole committee shares one scheme)
|
|
uint8 threshold; // t: distinct PQC signatures required to authorize
|
|
uint8 size; // n: number of members
|
|
uint64 authNonce; // strictly increasing; consumed on each successful authorize
|
|
address registrant; // who registered (informational; not an authority)
|
|
bytes32 membersHash; // keccak256(abi.encode(pubKeys)) integrity commitment
|
|
}
|
|
|
|
/// @notice One authorizing leg: the member's committee index and their PQC signature
|
|
/// over the current authorization challenge, in the scheme's envelope.
|
|
struct PqcSig {
|
|
uint8 memberIndex; // 0..n-1, must be a distinct committee member
|
|
bytes signature; // scheme envelope (see contract NatSpec)
|
|
}
|
|
|
|
/// @notice A recorded authorization, keyed by (committeeId, nonce).
|
|
struct Authorization {
|
|
bool exists;
|
|
bytes32 messageHash; // the caller-supplied 32-byte payload
|
|
bytes32 challenge; // the exact 32 bytes every leg PQC-signed
|
|
uint8 quorum; // number of distinct PQC signers (== threshold at record time)
|
|
uint64 blockNumber;
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Storage
|
|
// ==========================================================================
|
|
|
|
uint256 public committeeCount;
|
|
mapping(uint256 => Committee) private _committees;
|
|
// committeeId => ordered member PQC public keys (index is the memberIndex)
|
|
mapping(uint256 => bytes[]) private _pubKeys;
|
|
// keccak256(committeeId, nonce) => recorded authorization
|
|
mapping(bytes32 => Authorization) private _auths;
|
|
/// @notice Total number of authorizations recorded across all committees.
|
|
uint256 public authorizationCount;
|
|
|
|
// ==========================================================================
|
|
// Events
|
|
// ==========================================================================
|
|
|
|
event CommitteeRegistered(
|
|
uint256 indexed committeeId, address indexed registrant, uint8 indexed scheme, uint8 threshold, uint8 size
|
|
);
|
|
event Authorized(
|
|
uint256 indexed committeeId,
|
|
uint64 indexed nonce,
|
|
bytes32 indexed messageHash,
|
|
bytes32 challenge,
|
|
uint8 quorum,
|
|
uint256 blockNumber
|
|
);
|
|
|
|
// ==========================================================================
|
|
// Errors
|
|
// ==========================================================================
|
|
|
|
error InvalidScheme(uint8 scheme);
|
|
error InvalidPubKey(uint8 memberIndex);
|
|
error DuplicatePubKey(uint8 memberIndex);
|
|
error BadCommitteeParams();
|
|
error UnknownCommittee(uint256 committeeId);
|
|
error MemberOutOfRange(uint8 memberIndex);
|
|
error DuplicateMember(uint8 memberIndex);
|
|
error LegVerificationFailed(uint8 memberIndex);
|
|
error BelowThreshold(uint256 valid, uint8 required);
|
|
|
|
// ==========================================================================
|
|
// Registration
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Register a post-quantum t-of-n committee.
|
|
* @param scheme one of 1=Falcon-512, 2=Falcon-1024, 3=ML-DSA-44, 4=SLH-DSA-128s.
|
|
* All members use the same scheme.
|
|
* @param threshold t, with 1 <= t <= n. A valid authorization needs >= t distinct
|
|
* members to each PQC-sign the challenge.
|
|
* @param pubKeys ordered member PQC public keys (n = length, 1 <= n <= 255). Each is
|
|
* validated against the scheme's length (and Falcon header) so a
|
|
* malformed key cannot be stored. The memberIndex is the array index.
|
|
* @return committeeId the id of the new committee.
|
|
*
|
|
* @dev Permissionless: the registrant gains no privilege. Authority is ONLY the PQC
|
|
* keys registered here; there is no admin, no owner override, no fund custody.
|
|
* A `membersHash` commitment is stored so any relying party can pin the exact
|
|
* key set a committee was created with.
|
|
*/
|
|
function registerCommittee(uint8 scheme, uint8 threshold, bytes[] calldata pubKeys)
|
|
external
|
|
returns (uint256 committeeId)
|
|
{
|
|
uint256 n = pubKeys.length;
|
|
if (n < 1 || n > 255) revert BadCommitteeParams();
|
|
if (threshold < 1 || threshold > n) revert BadCommitteeParams();
|
|
|
|
// Committee keys MUST be distinct: authorization counts distinct signers by member INDEX, so a
|
|
// repeated key would let one keyholder occupy multiple "distinct member" slots and reach the
|
|
// threshold alone (same class as the AereThresholdAccount 2026-07-15 finding). Reject any dup
|
|
// key here, at registration, by hashing once and comparing pairwise (n <= 255, one-time cost).
|
|
bytes32[] memory keyHashes = new bytes32[](n);
|
|
for (uint256 i = 0; i < n; i++) {
|
|
_validatePubKey(scheme, pubKeys[i], uint8(i)); // also reverts InvalidScheme for a bad scheme
|
|
bytes32 kh = keccak256(pubKeys[i]);
|
|
for (uint256 j = 0; j < i; j++) {
|
|
if (keyHashes[j] == kh) revert DuplicatePubKey(uint8(i));
|
|
}
|
|
keyHashes[i] = kh;
|
|
}
|
|
|
|
committeeId = ++committeeCount;
|
|
Committee storage c = _committees[committeeId];
|
|
c.exists = true;
|
|
c.scheme = scheme;
|
|
c.threshold = threshold;
|
|
c.size = uint8(n);
|
|
c.authNonce = 0;
|
|
c.registrant = msg.sender;
|
|
c.membersHash = keccak256(abi.encode(pubKeys));
|
|
|
|
bytes[] storage store = _pubKeys[committeeId];
|
|
for (uint256 i = 0; i < n; i++) {
|
|
store.push(pubKeys[i]);
|
|
}
|
|
|
|
emit CommitteeRegistered(committeeId, msg.sender, scheme, threshold, uint8(n));
|
|
}
|
|
|
|
function _validatePubKey(uint8 scheme, bytes calldata pubKey, uint8 memberIndex) internal pure {
|
|
if (scheme == SCHEME_FALCON512) {
|
|
if (pubKey.length != FALCON512_PK_LEN || uint8(pubKey[0]) != FALCON512_PK_HEADER) {
|
|
revert InvalidPubKey(memberIndex);
|
|
}
|
|
} else if (scheme == SCHEME_FALCON1024) {
|
|
if (pubKey.length != FALCON1024_PK_LEN || uint8(pubKey[0]) != FALCON1024_PK_HEADER) {
|
|
revert InvalidPubKey(memberIndex);
|
|
}
|
|
} else if (scheme == SCHEME_MLDSA44) {
|
|
if (pubKey.length != MLDSA44_PK_LEN) revert InvalidPubKey(memberIndex);
|
|
} else if (scheme == SCHEME_SLHDSA128S) {
|
|
if (pubKey.length != SLHDSA128S_PK_LEN) revert InvalidPubKey(memberIndex);
|
|
} else {
|
|
revert InvalidScheme(scheme);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Threshold authorization
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Authorize `messageHash` under a committee iff >= t DISTINCT committee
|
|
* members each supply a valid PQC signature over the committee's current
|
|
* authorization challenge, verified on-chain by the scheme's live precompile.
|
|
*
|
|
* challenge = keccak256(abi.encode(
|
|
* AUTH_DOMAIN, block.chainid, address(this), committeeId, authNonce, messageHash))
|
|
*
|
|
* Semantics (strict): EVERY provided leg must reference a distinct, in-range
|
|
* member and must verify; a leg that is out of range reverts MemberOutOfRange,
|
|
* a repeated member reverts DuplicateMember (so no member is ever double
|
|
* counted), and a leg whose PQC signature does not verify reverts
|
|
* LegVerificationFailed. After all legs are checked, the count of distinct
|
|
* valid signers must be >= t or the call reverts BelowThreshold. On success
|
|
* the authorization is recorded and the committee's authNonce increments, so
|
|
* the same set of signatures can never be replayed.
|
|
*
|
|
* @param committeeId the target committee.
|
|
* @param messageHash the 32-byte payload being authorized (caller binds its own
|
|
* domain separation into this value for its use case).
|
|
* @param sigs the authorizing legs (memberIndex + PQC signature envelope).
|
|
* @return nonce the authNonce this authorization consumed.
|
|
*/
|
|
function authorize(uint256 committeeId, bytes32 messageHash, PqcSig[] calldata sigs)
|
|
external
|
|
returns (uint64 nonce)
|
|
{
|
|
Committee storage c = _committees[committeeId];
|
|
if (!c.exists) revert UnknownCommittee(committeeId);
|
|
|
|
uint8 scheme = c.scheme;
|
|
uint8 size = c.size;
|
|
nonce = c.authNonce;
|
|
bytes32 challenge = authChallenge(committeeId, nonce, messageHash);
|
|
|
|
// Distinct-member bitmap over indices 0..size-1 (size <= 255 fits one word).
|
|
uint256 seen = 0;
|
|
uint256 valid = 0;
|
|
bytes[] storage keys = _pubKeys[committeeId];
|
|
|
|
for (uint256 i = 0; i < sigs.length; i++) {
|
|
uint8 idx = sigs[i].memberIndex;
|
|
if (idx >= size) revert MemberOutOfRange(idx);
|
|
uint256 bit = uint256(1) << idx;
|
|
if (seen & bit != 0) revert DuplicateMember(idx);
|
|
seen |= bit;
|
|
|
|
if (!_verify(scheme, keys[idx], challenge, sigs[i].signature)) {
|
|
revert LegVerificationFailed(idx);
|
|
}
|
|
valid += 1;
|
|
}
|
|
|
|
if (valid < c.threshold) revert BelowThreshold(valid, c.threshold);
|
|
|
|
// Effects: consume the nonce and record before emitting.
|
|
c.authNonce = nonce + 1;
|
|
|
|
bytes32 id = authorizationId(committeeId, nonce);
|
|
_auths[id] = Authorization({
|
|
exists: true,
|
|
messageHash: messageHash,
|
|
challenge: challenge,
|
|
quorum: uint8(valid),
|
|
blockNumber: uint64(block.number)
|
|
});
|
|
authorizationCount += 1;
|
|
|
|
emit Authorized(committeeId, nonce, messageHash, challenge, uint8(valid), block.number);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Views / challenge helpers
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice The exact 32-byte challenge every member must PQC-sign to authorize
|
|
* `messageHash` under `committeeId` at a given `nonce`. Off-chain signers
|
|
* query this (with the committee's current authNonce) to know what to sign.
|
|
*/
|
|
function authChallenge(uint256 committeeId, uint64 nonce, bytes32 messageHash) public view returns (bytes32) {
|
|
return keccak256(abi.encode(AUTH_DOMAIN, block.chainid, address(this), committeeId, nonce, messageHash));
|
|
}
|
|
|
|
/// @notice Deterministic id for the authorization recorded by `committeeId` at `nonce`.
|
|
function authorizationId(uint256 committeeId, uint64 nonce) public pure returns (bytes32) {
|
|
return keccak256(abi.encode(committeeId, nonce));
|
|
}
|
|
|
|
/**
|
|
* @notice Pure preview of whether a set of legs WOULD authorize at a given nonce,
|
|
* without recording. Returns (would, validCount). Never reverts on a bad leg:
|
|
* out-of-range and duplicate members are simply not counted, so this is a safe
|
|
* off-chain quorum check. Note the on-chain authorize() is STRICT (it reverts
|
|
* on any bad leg); this preview is intentionally lenient for UX.
|
|
*/
|
|
function previewAuthorize(uint256 committeeId, uint64 nonce, bytes32 messageHash, PqcSig[] calldata sigs)
|
|
external
|
|
view
|
|
returns (bool would, uint256 validCount)
|
|
{
|
|
Committee storage c = _committees[committeeId];
|
|
if (!c.exists) return (false, 0);
|
|
bytes32 challenge = authChallenge(committeeId, nonce, messageHash);
|
|
bytes[] storage keys = _pubKeys[committeeId];
|
|
uint256 seen = 0;
|
|
for (uint256 i = 0; i < sigs.length; i++) {
|
|
uint8 idx = sigs[i].memberIndex;
|
|
if (idx >= c.size) continue;
|
|
uint256 bit = uint256(1) << idx;
|
|
if (seen & bit != 0) continue;
|
|
seen |= bit;
|
|
if (_verify(c.scheme, keys[idx], challenge, sigs[i].signature)) validCount += 1;
|
|
}
|
|
would = validCount >= c.threshold;
|
|
}
|
|
|
|
function getCommittee(uint256 committeeId)
|
|
external
|
|
view
|
|
returns (
|
|
bool exists,
|
|
uint8 scheme,
|
|
uint8 threshold,
|
|
uint8 size,
|
|
uint64 authNonce,
|
|
address registrant,
|
|
bytes32 membersHash
|
|
)
|
|
{
|
|
Committee storage c = _committees[committeeId];
|
|
return (c.exists, c.scheme, c.threshold, c.size, c.authNonce, c.registrant, c.membersHash);
|
|
}
|
|
|
|
function getMemberPubKey(uint256 committeeId, uint8 memberIndex) external view returns (bytes memory) {
|
|
Committee storage c = _committees[committeeId];
|
|
if (!c.exists) revert UnknownCommittee(committeeId);
|
|
if (memberIndex >= c.size) revert MemberOutOfRange(memberIndex);
|
|
return _pubKeys[committeeId][memberIndex];
|
|
}
|
|
|
|
function nonceOf(uint256 committeeId) external view returns (uint64) {
|
|
Committee storage c = _committees[committeeId];
|
|
if (!c.exists) revert UnknownCommittee(committeeId);
|
|
return c.authNonce;
|
|
}
|
|
|
|
function getAuthorization(uint256 committeeId, uint64 nonce)
|
|
external
|
|
view
|
|
returns (bool exists, bytes32 messageHash, bytes32 challenge, uint8 quorum, uint64 blockNumber)
|
|
{
|
|
Authorization storage a = _auths[authorizationId(committeeId, nonce)];
|
|
return (a.exists, a.messageHash, a.challenge, a.quorum, a.blockNumber);
|
|
}
|
|
|
|
/// @notice True iff the authorization at (committeeId, nonce) exists and committed to `messageHash`.
|
|
function isAuthorized(uint256 committeeId, uint64 nonce, bytes32 messageHash) external view returns (bool) {
|
|
Authorization storage a = _auths[authorizationId(committeeId, 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);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Internal PQC verifier
|
|
// ==========================================================================
|
|
//
|
|
// Identical envelope to AerePQCAttestation, which is proven bit-for-bit against the
|
|
// LIVE mainnet precompile via eth_call (scripts/pqc/verify-live-precompile.js). We
|
|
// inline it here so a committee authorization is one contract with no external call.
|
|
|
|
function _verify(uint8 scheme, bytes memory pubKey, bytes32 message, bytes memory signature)
|
|
internal
|
|
view
|
|
returns (bool)
|
|
{
|
|
(address precompile, bytes memory input, bool wellFormed) = _buildInput(scheme, pubKey, message, signature);
|
|
if (!wellFormed) return false; // a wrong-length signature is invalid, not a revert
|
|
(bool ok, bytes memory ret) = precompile.staticcall(input);
|
|
return ok && ret.length >= 32 && ret[31] == 0x01;
|
|
}
|
|
|
|
/// @dev Assemble (precompile, input) for a scheme. `wellFormed` is false when the
|
|
/// signature length cannot form a valid input (treated as an invalid leg, so a
|
|
/// malformed signature never reverts authorize()); a well-formed-but-wrong
|
|
/// signature yields an input the precompile rejects with 0.
|
|
function _buildInput(uint8 scheme, bytes memory pubKey, bytes32 message, bytes memory signature)
|
|
internal
|
|
pure
|
|
returns (address precompile, bytes memory input, bool wellFormed)
|
|
{
|
|
if (scheme == SCHEME_FALCON512 || scheme == SCHEME_FALCON1024) {
|
|
if (signature.length <= FALCON_NONCE_LEN) return (address(0), "", false);
|
|
uint256 sigLen = signature.length - FALCON_NONCE_LEN; // esig length
|
|
if (sigLen > type(uint16).max) return (address(0), "", false);
|
|
|
|
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;
|
|
wellFormed = true;
|
|
} else if (scheme == SCHEME_MLDSA44) {
|
|
if (signature.length != MLDSA44_SIG_LEN) return (address(0), "", false);
|
|
input = abi.encodePacked(pubKey, signature, message); // pk(1312) || sig(2420) || message(32)
|
|
precompile = PRECOMPILE_MLDSA44;
|
|
wellFormed = true;
|
|
} else if (scheme == SCHEME_SLHDSA128S) {
|
|
if (signature.length != SLHDSA128S_SIG_LEN) return (address(0), "", false);
|
|
input = abi.encodePacked(pubKey, signature, message); // pk(32) || sig(7856) || message(32)
|
|
precompile = PRECOMPILE_SLHDSA128S;
|
|
wellFormed = true;
|
|
} else {
|
|
return (address(0), "", false);
|
|
}
|
|
}
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|