PQ_BATCH
Lightweight post-quantum batch primitive. Conditions commit only SHA256(falcon_pubkey). In a spending tx, exactly one input per commit group is the anchor: its witness reveals the canonical PQ pubkey + a PQ signature over the per-input ladder sighash. Every other input gated by the same hash carries an empty witness and short-circuits via a tx-local cache the anchor populates after its first verify. No coordinator, no priming round, no output-set binding — the simple "key-sharing pool" complement to QABIO.
| Field | Data Type | Size | Side | Description |
|---|---|---|---|---|
| pubkey_hash | HASH256 | 32 B | Conditions | SHA256(canonical_falcon_pubkey) — the only thing committed at fund time |
| pubkey (anchor only) |
PUBKEY | 32 / 33 / 897 / 1,793 / 1,952 B | Witness | Canonical PQ pubkey (or Schnorr/ECDSA carve-out). Scheme derived from pubkey size: 32-33 → SCHNORR (rejected by PQ_BATCH — classical sigs handled by SIG with merkle_pub_key gating), 897 → FALCON-512, 1,793 → FALCON-1024, 1,952 → Dilithium3. Any other size → UNSATISFIED. SPHINCS+ is not supported — its ~13 KB sig makes the amortisation benefit marginal. |
| signature (anchor only) |
SIGNATURE | scheme-dependent | Witness | PQ signature over the per-input ladder sighash (FetchLadderSighash(SIGHASH_DEFAULT)). FALCON-512 sigs are variable up to ~666 B; OQS_SIG_verify validates the encoded length internally. |
The merged conditions+witness block has 1 field for non-anchor inputs (just HASH256) or 3 fields for the anchor input (HASH256, PUBKEY, SIGNATURE). Audit #3 fix E-020/E-021: the evaluator pins these per-slot types exactly — the cardinality check alone was bypassable (witness=[SIG, SIG] gave a 3-field merged block with no PUBKEY, falling through to the non-anchor cache lookup with up to ~99 KB of attacker bytes silently embedded). PQ_BATCH_WITNESS is declared nullptr in BlockTypeInfo precisely because the merged shape is what's pinned.
[HASH256] (non-anchor) or 3 fields [HASH256, PUBKEY, SIGNATURE] in that exact order. Anything else → ERROR
uint256 commit_key for the cache.
ctx.pq_batch_cache_mutex (when provided), look up commit_key; cached & true → SATISFIED; absent or false → UNSATISFIED (anchor must precede this input in script-check order).
SHA256(PUBKEY) and memcmp against the committed HASH256. Mismatch → UNSATISFIED (failures are not cached, so a later input with a different commit can still anchor).
pubkey.size(): 32 or 33 → SCHNORR; 897 → FALCON512; 1793 → FALCON1024; 1952 → DILITHIUM3; anything else → UNSATISFIED. Then FetchLadderSighash(ctx, SIGHASH_DEFAULT) → 32-byte message; failure → ERROR
IsPQScheme(scheme): VerifyPQSignature(scheme, sig, sighash, pubkey) → verified flag. Schnorr/ECDSA carve-out: verified stays false — PQ_BATCH's value-add is PQ-specific (use SIG with merkle_pub_key for classical paths). On verified: take the mutex and write (*ctx.pq_batch_cache)[commit_key] = true so subsequent non-anchor inputs (and concurrent workers) see the verification immediately. v0.13 fix.
verified → SATISFIED; else → UNSATISFIED
Script verification runs in input order (0, 1, 2, ...). The anchor input must be at the lowest-index PQ_BATCH input per commit group — non-anchor inputs at lower indices return UNSATISFIED because the cache entry doesn't exist yet. This is acceptable: the cost model of "one anchor input per commit group per tx" still delivers ~10× amortisation for N=100. Signers must respect this ordering.
| Condition | Result |
|---|---|
Merged block field count outside {1, 3}, or per-slot types don't match the strict [HASH256] / [HASH256, PUBKEY, SIGNATURE] shape | ERROR |
| Anchor input: SHA256(pubkey) ≠ pubkey_hash | UNSATISFIED |
| Anchor input: PQ signature invalid | UNSATISFIED |
| Non-anchor input: cache empty (anchor not yet seen) | UNSATISFIED |
| Anchor input: PQ sig valid — populates cache | SATISFIED |
| Non-anchor input: cache reads true for commit | SATISFIED |
{
"type": "PQ_BATCH",
"inverted": false,
"fields": [
{ "type": "HASH256", "hex": "a1b2c3...32 bytes (SHA256(falcon_pubkey))" }
]
}{
"type": "PQ_BATCH",
"fields": [
{ "type": "PUBKEY", "hex": "...897 bytes (FALCON-512)" },
{ "type": "SIGNATURE", "hex": "...666 bytes (FALCON-512 sig)" }
]
}{
"type": "PQ_BATCH",
"fields": []
}For the typical "drain N UTXOs to one sink" shape (FALCON-512). Per doc/ladder-script/PQ_BATCH_SPEC.md: anchor input ~392 vB (FALCON-512 witness, SegWit-discounted), non-anchor input ~14 vB (just the MLSC proof path).
| N | Total vsize | Per-input vsize | vs N × SIG(FALCON-512) at ~400 vB |
|---|---|---|---|
| 1 | ~392 vB | 392 vB | parity |
| 10 | ~518 vB | ~52 vB | ~8× cheaper |
| 100 | ~1,778 vB | ~17.8 vB | ~22× cheaper |
| 500 | ~7,378 vB | ~15 vB | ~27× cheaper |
| 1,000 | ~14,378 vB | ~14 vB | ~28× cheaper |
Asymptote: ~14 vB per non-anchor input. The anchor input pays the FALCON witness cost once (~392 vB after the SegWit 4× discount on 666 B sig + 897 B pubkey + scaffolding); every cache-read input pays only the PQ_BATCH micro-header + the rung's MLSC proof scaffolding (~14 vB). At N=100 the per-input mean is 17.8 vB; the BIP cites this figure (§Q11). Both numbers measure pure witness cost; the per-input tx-input baseline (32 B prevout + 4 B vout + 4 B sequence + 1 B scriptsig-length = 41 vB) sits on top, identical to every other input type.