How pqc.market works
A specification of the hash-based identity, attestation and proof protocol behind every launch. Everything below is implemented in src/lib/pq and runs identically in the browser and on the server.
#Overview
Each user holds a post-quantum identity: an XMSS-style Merkle tree over 256 Winternitz one-time signature (WOTS) keys, deterministically derived from a Solana wallet signature. The tree root is the public key; a hash of the root is the user's pq1… address.
Every launch consumes one leaf. That leaf signs a digest of the coin's identity (mint, creator, name, ticker, image) and also owns the mint keypair. The signature is pinned into the coin's IPFS metadata, so anyone can check provenance with nothing but SHA-256, after any elliptic-curve break.
#Threat model
We assume an adversary who can eventually forge ed25519 signatures, either via a cryptographically relevant quantum computer (Shor's algorithm) or via an unexpected classical advance against elliptic-curve discrete log. On Solana this is total: an account address is its ed25519 public key, so unlike a never-spent Bitcoin address there is no hash shield.
| Asset | Pre-break | Post-break |
|---|---|---|
| SOL / tokens in ed25519 accounts | safe | exposed (needs on-chain PQ vault) |
| Coin provenance (who launched it) | wallet signature | WOTS attestation + anchor |
| Login / account ownership | wallet signature | WOTS challenge-response |
| PQ identity (hardened) | safe | safe, given passphrase entropy |
| PQ identity (wallet-only) | safe | re-derivable by the attacker |
#Primitives
Every hash in the protocol is SHA-256 with n = 32 bytes. Security reduces to second-preimage resistance of SHA-256; Grover's algorithm lowers this to roughly 2128 quantum work, which remains out of reach.
Tweakable hash
Following SPHINCS+, each call is domain-separated by a public seed and a 32-byte address ADRSthat pins it to exactly one position in the structure. This prevents multi-target attacks: an adversary cannot amortise a preimage search across many chains or users.
#WOTS one-time signatures
Winternitz parameter w = 16 trades signature size against hashing: each 4-bit digit of the message selects a position on a hash chain of length 15.
| Symbol | Value | Meaning |
|---|---|---|
| n | 32 | hash output, bytes |
| w | 16 | chain length + 1 |
| len₁ | 64 | ⌈8n / log₂ w⌉ message digits |
| len₂ | 3 | ⌊log₂(len₁(w−1)) / log₂ w⌋ + 1 checksum digits |
| len | 67 | chains per key |
Message encoding
The 32-byte digest is split into 64 nibbles d₀…d₆₃. A checksum over the "remaining distance" of every chain is appended as three more base-16 digits:
The checksum is what makes the scheme unforgeable: to change a message digit upward (which needs only extra hashing from a public σᵢ), a forger necessarily lowers C, and inverting a checksum chain requires a SHA-256 preimage.
Sign & verify
σᵢ = Fdᵢ(skᵢ) · verify: F15−dᵢ(σᵢ) ≟ pkᵢ
Signing costs Σdᵢ hashes, verification Σ(15−dᵢ); together exactly 67 × 15 = 1,005. Because σ reveals intermediate chain values, a key that signs two different messages leaks enough to forge a third. Leaf reuse is therefore refused at the database level (see leaf ledger).
#Merkle identities
The 67 chain ends of leaf i compress to one node, and 256 such leaves form a binary tree of height 8. The root, together with pk.seed, is the identity's 64-byte public key.
nh,j = SHA-256(pk.seed ‖ ADRS(2, h, j) ‖ nh−1,2j ‖ nh−1,2j+1)
A full signature is (leaf, σ, a₀…a₇): 4 + 67·32 + 8·32 = 2,404 bytes. Verification recomputes the leaf from σ and climbs 8 levels; it is valid iff the result equals the registered root.
#Key derivation
No new seed phrase. The wallet signs a fixed message; RFC 8032 ed25519 signatures are deterministic, so the same wallet always produces the same 64 bytes. An optional passphrase is stretched with scrypt and mixed in, contributing entropy the curve never sees.
pqc.market — Post-Quantum Identity Derivation v1 Signing this message derives your hash-based (WOTS/XMSS) keys locally in your browser. It is not a transaction and costs nothing. Never sign this exact message on any other site. Wallet: 7xKX…AsU Domain: pqc.market Version: 1
Tree generation is 256 × 1,072 ≈ 274k SHA-256 calls and completes in about half a second in a modern browser. Secrets live only in memory for the tab's lifetime.
#Registration
Registration binds the wallet and the root in both directions. One statement is signed twice: by the wallet (ed25519) and by leaf 0 (WOTS). The server verifies both before storing the public key; leaf 0 is burned as the genesis signature.
pqc.market — Register Post-Quantum Identity Wallet: 7xKX…AsU PQ address: pq1Hn3…W8k Root: 3f9a1c…e07b Public seed: b41d02…9c55 Tree height: 8 Scheme: WOTS(w=16,SHA-256)+Merkle
#On-chain anchor
A registration in our database is only as trustworthy as our database. The anchor writes the root to Solana in an SPL Memo signed by the wallet, producing a public timestamp from the era when ed25519 signatures were still unforgeable. After a break, any claim about who owns a root can be checked against the earliest anchor.
pqc.market:v1:anchor:pq1Hn3…W8k:3f9a1c…e07b
#Launch attestation
Leaf i signs a domain-separated digest over the coin's public fields, sorted by key. The same leaf deterministically owns the mint keypair, so the mint address itself is a commitment to the identity.
The create (+ optional dev buy) is a single v0 transaction compiled against pump.fun's address lookup table; without the ALT the ~40 accounts exceed Solana's 1,232-byte packet limit. Before broadcasting, the server checks the fee payer, the mint and that the pump program is invoked, so it can never be used to relay an arbitrary transaction.
Metadata extension
The pinned JSON carries the full attestation, so verification needs neither our API nor our database:
{
"name": "…", "symbol": "…", "image": "ipfs://…",
"website": "https://pqc.market/coin/<mint>",
"pqc": {
"version": 1,
"scheme": "WOTS(w=16,SHA-256)+Merkle(h=8)",
"pqAddress": "pq1…", "root": "…", "pubSeed": "…", "height": 8,
"messageHash": "…",
"signature": { "leaf": 4, "wots": "<4288 hex>", "auth": ["…" ×8] }
}
}Creator fees
pump.fun fixes a coin's fee recipient at mint time via the creator field of create_v2. Every pqc.market coin sets it to the platform treasury; the launching wallet is the signer and fee payer, and is recorded as the creator in the attestation.
#Signature schemes
Creators choose how a launch is signed. WOTS + Merkle is the identity's root of trust and burns one leaf per launch. The three NIST schemes are many-time keys derived from the same identity seed; each is certified once by a WOTS leaf signing a binding digest, after which it signs any number of launches.
| Scheme | Standard | Assumption | Signature | Public key |
|---|---|---|---|---|
| WOTS + Merkle | RFC 8391 style | SHA-256 second preimage | 2,404 B | 64 B |
| ML-DSA-65 (Dilithium) | FIPS 204 | Module-LWE / SIS lattices | 3,309 B | 1,952 B |
| SLH-DSA-SHA2-128s (SPHINCS+) | FIPS 205 | SHA-256 (stateless) | 7,856 B | 32 B |
| Falcon-512 (FN-DSA) | FIPS 206 draft | NTRU lattices | ≤ 666 B | 897 B |
certs = WOTS.sign(H("bind" ‖ pq-address ‖ s ‖ H(pks)), leaf)
d = H("launch" ‖ creator ‖ image ‖ mint ‖ name ‖ scheme ‖ symbol)
A scheme attestation carries its signature, the scheme public key and the WOTS certificate, so verification needs only the identity root: check the certificate against the root, then the signature against the certified key. The digest commits to the scheme id, so a signature can never be replayed under a different scheme. Implementations are@noble/post-quantum, audited and dependency-free.
#Proof of possession
A challenge-response login that never consults ed25519. The server issues a 32-byte nonce valid for five minutes; the client signs SHA-256("pqc.market/proof/v1\n" ‖ leaf ‖ nonce ‖ wallet) with its next unused leaf.
| Check | Failure |
|---|---|
| challenge exists, matches wallet, unused, unexpired | 404 / 409 / 410 |
| XMSS.verify(digest, σ, registered root) | verified: false |
| INSERT (identity, leaf) into leaf ledger | 409 leaf already used |
The leaf ledger's primary key (identity_id, leaf_index) makes one-time use a database invariant; the challenge is consumed even on failure, so a nonce cannot be retried.
#Security analysis
- Forgery
- Reduces to SHA-256 second-preimage resistance under the tweakable-hash model; ~2¹²⁸ quantum work.
- Key reuse
- Prevented server-side by the leaf ledger. A malicious server could accept reuse; clients should never sign twice with one leaf, and the UI always fetches the next free index.
- Wallet compromise
- Wallet-only identities are re-derivable by anyone holding the wallet key, including a post-break attacker. Hardened identities additionally require the passphrase.
- Server compromise
- Cannot forge attestations (no secrets held). Can censor or lie about registrations, which is why the on-chain anchor and IPFS copy exist.
- Capacity
- 256 signatures per identity. Rotation to a fresh tree signed by the old tree's next leaf is on the roadmap.
#Parameters
| Item | Value |
|---|---|
| Hash | SHA-256 |
| WOTS | w = 16, 67 chains, 2,144 B |
| Tree | height 8, 256 leaves |
| Signature | 2,404 B |
| Public key | root ‖ pk.seed, 64 B |
| Address | pq1 ‖ base58(SHA-256(dom ‖ pk.seed ‖ root)[0..20]) |
| Passphrase KDF | scrypt N = 2¹⁵, r = 8, p = 1 |
| Seed KDF | HKDF-SHA256 |
| Mint KDF | HKDF-SHA256(sk.seed, 'pqc.market/mint/v1', u32be(leaf)) |
#Verify it yourself
Every coin page has a Verify button that runs this in your browser. The equivalent in code:
import { launchDigest } from "@/lib/pq/messages";
import { verify } from "@/lib/pq/xmss";
const meta = await fetch(metadataUri).then((r) => r.json());
const d = launchDigest({
mint, creator, name: meta.name, symbol: meta.symbol,
image: meta.image, leaf: meta.pqc.signature.leaf,
});
const { valid, computedRoot } = verify(d, meta.pqc.signature, {
root: meta.pqc.root, pubSeed: meta.pqc.pubSeed, height: meta.pqc.height,
});