Draft. No backwards compatibility. No flags. No replay.
Activated at the genesis of the new final Lux network: **2025-12-25
16:20 Pacific (unix 1766708400)**. Predates every rollup block. The
pre-Quasar Edition Lux network (2020–2025) had no P3Q precompile and is
a separate network out of scope.
A ZAP-native PQ rollup is a Tier-3 chain (per LP-204) whose sequencer
collects N transactions as ZAP frames, applies them against the rollup
state, computes a new state root over the ZAP-buffer Merkle structure
defined in LP-203, and signs the tuple `(prev_root, new_root,
batch_hash, sequencer_id)` with a Pulsar threshold signature (LP-073).
The sequencer submits `RollupBatchTx{prev_root, new_root, batch_hash,
pulsar_sig}` as a ZAP frame to Z-Chain (the parent L1 Tier-1 hosting
the rollup-VM at Tier-3 in the LP-204 hierarchy).
Z-Chain validators verify the PQ signature via the **P3Q precompile
at EVM slot 0x012205**. P3Q is the on-chain verifier family defined in
HANZO-CRYPTO-SUITE §5.2 — a single precompile that takes a kind byte
(0x01 Pulsar, 0x02 Corona, 0x03 Magnetar), a threshold signature, a
message hash, and a group public key, and returns true iff the
signature validates under the named PQ family. One slot, one ABI,
three primitive kinds dispatched by leading byte (LP-220 specifies the
Corona and Magnetar uses of this same slot). The verification is part
of normal Z-Chain block validation; no second consensus is needed. The
Z-Chain block then finalizes via the parent L1's QuasarCert (LP-202
tier degradation, named per LP-217: PQ-off → PQ-fast → PQ-strict →
PQ-heavy; was "BLS preliminary → Pulsar → Aurora → Polaris" in LP-017).
The rollup state inherits the parent L1's full cryptographic finality
because the rollup commit IS a Z-Chain transaction and Z-Chain IS the
parent L1's rollup substrate (LP-063 + LP-204). No rollup-side
validator set exists. No fraud-proof bond is required for the
cryptographic path — the Pulsar signature is sufficient for the
authenticity of the state transition; an optimistic-rollup challenge
window is only needed if a fraud-proof system is the integrity model
of the execution itself.
┌───────────────────────────────────────────────────────────────────┐
│ Rollup sequencer (Tier-3 — no validator set, single or threshold) │
└────────────────────────────────┬──────────────────────────────────┘
│
1. Collect N txs (each ZAP frame, no re-encode)
│
2. Apply tx batch → compute new state root (LP-203 GPU Merkle)
│
3. Pulsar threshold-sign (prev_root, new_root, batch_hash, seq_id)
│
4. Submit RollupBatchTx as ZAP frame to Z-Chain
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ Z-Chain validators (parent L1 Tier-1 — full QuasarCert validators)│
└────────────────────────────────┬──────────────────────────────────┘
│
5. P3Q(0x012205) verify PQ sig (kind ∈ {Pulsar/Corona/Magnetar})
over H(prev_root‖new_root‖batch_hash)
│
6. Z-Chain block finalizes via parent L1 QuasarCert
→ rollup state inherits parent L1 finality
Stage by stage:
End-to-end commit latency from sequencer batch close to parent L1
finality at PQ-strict: ~10–50 ms. End-to-end at PQ-off (BLS only):
~1–5 ms.
// EVM precompile at slot 0x012205 — unified PQ verifier family.
// One slot, one dispatch, one ABI shape across all primitive kinds.
//
// kind:
// 0x01 Pulsar (Module-LWE, FIPS-204 ML-DSA-65, LP-073)
// 0x02 Corona (Module-LWE threshold, LP-220 §"Corona kind")
// 0x03 Magnetar (hash-based, FIPS-205 SLH-DSA-192f threshold,
// LP-181 + LP-220 §"Magnetar kind")
//
// sig and groupPubKey are primitive-specific. Lengths are
// verified per-kind against the underlying scheme's canonical sizes.
function verify(
uint8 kind, // 0x01 / 0x02 / 0x03
bytes calldata sig, // primitive-specific sig bytes
bytes32 messageHash, // H(prev_root || new_root || batch_hash)
bytes calldata groupPubKey // primitive-specific group pubkey
) external view returns (bool);
Properties:
sig is a valid threshold signature on messageHash under groupPubKey for the
PQ family identified by kind.
Each P3QVerifier impl preserves the constant-time property of its
underlying primitive.
P3QVerifier.GasCost()method registered for each kind (see LP-220 §"Architectural decision
— common verifier framework"):
kind | Primitive | Gas |0x01 | Pulsar (ML-DSA-65 threshold) | 50,000 |0x02 | Corona (M-LWE threshold) | 5,000,000 |0x03 | Magnetar (SLH-DSA-192f threshold) | 30,000 |Pulsar's 50k figure is the 10× factor over BLS pairing verify
(~5,000 gas) that reflects the inherent cost of lattice verification
— ML-DSA verify requires NTT inverse + matrix-vector multiplication
mod q. Corona's 5M reflects Module-LWE's larger sig (33 KB) and
CPU-class verify cost ~1.6 ms; Magnetar's 30k reflects ~1.92 ms CPU
verify amortized cleanly over a memory-bandwidth-bound WOTS+ walk.
See LP-220 §"Gas cost calibration" for the cross-kind ladder.
0x012205 per HANZO-CRYPTO-SUITE §5.2 and Lux globalguidance ("P3Q is a new primitive at its own slot, not bolted onto
existing ZK or Pulsar"). One slot covers the entire P3Q family;
adding a fourth PQ kind is one new P3QVerifier impl and one new
kind byte, not a new precompile slot.
false on unknown kind, malformed sig for the named kind (wrong length, invalid coefficients,
incorrect group-pubkey size), or signature-verify failure. No revert
on cryptographic failure — caller decides whether false is fatal.
Calls with no leading kind byte (legacy 3-argument shape) are
rejected: P3Q activation predates any live rollup using this
precompile, so there is no backwards-compat layer.
~/work/lux/precompile/p3q/contract.go | EVM precompile implementation registered at slot 0x012205 |~/work/lux/precompile/p3q/module.go | init() registers p3qVerify config key + slot 0x012205 |~/work/lux/precompile/p3q/contract_test.go | 18 tests: address pin, gas formula, OOG, structural rejection, real FIPS 204 round-trip across MLDSA44/65/87, wrong-hash / wrong-pk / corrupted-sig rejection, domain-separation replay defense, EncodeInput round-trip |~/work/lux/precompile/p3q/IP3Q.sol | IP3Q interface + P3QLib helper library (verify, verifyOrRevert, encode, estimateGas) for Solidity callers |github.com/luxfi/crypto/mldsa (mldsa.VerifySignatureCtx) | Constant-time FIPS 204 ML-DSA verifier — P3Q dispatches to this; Pulsar threshold-signature output is byte-equal so the same verifier accepts both |~/work/lux/threshold/protocols/pulsar (re-exports ~/work/lux/pulsar/ref/go/pkg/pulsar) | Threshold-sign producer side; consensus consumers use pulsar.VerifyCtx for the same FIPS 204 verification but the precompile uses mldsa.VerifySignatureCtx directly to avoid an unnecessary module hop |~/work/lux/accel/cuda/p3q.cu | Blackwell sm_120 batched verify path (LP-203 GPU verify dispatch) |P3Q's verifier core IS the FIPS-204 ML-DSA verifier — Pulsar signatures
serialize byte-identically to FIPS-204 ML-DSA signatures per LP-073
wire format. Any conformant FIPS-204 verifier suffices. The precompile
exists to give EVM-side callers a stable ABI and an audited gas charge.
Single-call interface; no Solidity function selector. The wire bytes
begin with a 1-byte kind discriminator that selects which
P3QVerifier impl handles the call:
[ 1 byte ] kind — 0x01 Pulsar | 0x02 Corona | 0x03 Magnetar
[ 1 byte ] mode — primitive-specific (Pulsar: 0x44/0x65/0x87
for ML-DSA-44/-65/-87; Corona: 0x01 reserved;
Magnetar: 0x01 reserved)
[ 4 bytes ] sigLen — big-endian uint32
[ N bytes ] sig — N = sigLen, must equal the kind+mode's
canonical sig size
[ 4 bytes ] groupPubKeyLen — big-endian uint32
[ M bytes ] groupPubKey — M = groupPubKeyLen, must equal the kind+mode's
canonical pk size
[ 32 bytes ] messageHash — fixed; same across kinds
Output: 32-byte EVM-ABI bool. Last byte 0x01 on a verifying signature,
0x00 on any verification failure (unknown kind, malformed input,
mode mismatch, length mismatch, primitive verify reject). The precompile
NEVER reverts on cryptographic failure — the Solidity caller decides
whether false is fatal.
The Solidity-side encoder is P3QLib.encode(kind, mode, sig, groupPubKey, messageHash)
in IP3Q.sol. The Go-side encoder is p3q.EncodeInput(kind, mode, sig, groupPubKey, messageHash).
Both produce byte-identical output for the same inputs across all kinds.
Every P3Q verify binds the FIPS 204 context string
lux-evm-precompile-p3q-v1. Mismatched context vs signer causes
rejection — prevents cross-precompile replay where a Pulsar signature
minted for the generic 0x012204 Pulsar slot (context
lux-evm-precompile-pulsar-v1) is re-submitted to the rollup-commit
0x012205 P3Q slot. Signers producing rollup-commit signatures MUST
set ctx = "lux-evm-precompile-p3q-v1" (see pulsar.SignCtx /
mldsa.SignCtx in the producer paths).
The unit test TestP3Q_RejectsWrongContextDomainSeparation in
~/work/lux/precompile/p3q/contract_test.go pins this: a signature
under the Pulsar-slot context is rejected by P3Q.
Gas is charged per kind by P3QVerifier.GasCost() (LP-220 §"Architectural
decision — common verifier framework"). The per-byte adder is 10 gas/byte
of input across all kinds. Base charge by kind:
kind | Base gas | CPU verify | Blackwell sm_120 |0x01 Pulsar (ML-DSA-65 threshold) | 50,000 | ~80 µs | ~50 µs |0x02 Corona (M-LWE threshold) | 5,000,000 | ~1.6 ms | ~15 ms |0x03 Magnetar (SLH-DSA-192f threshold) | 30,000 | ~1.92 ms | ~50 ms |LP-218 §"Honest cost comparison" sets the Pulsar base at 50,000 gas;
LP-220 sets Corona at 5M and Magnetar at 30k. See LP-220 §"Gas cost
calibration" for the cross-kind ladder and the validator-hardware
assumption behind it.
Measured Pulsar CPU verify time on M1 Max / DGX Spark (Pulsar parity
audit 2026-06-03, harness at lux/threshold/protocols/parity/parity_test.go):
At 12k gas/ms (geth's published gas-per-millisecond yardstick), that
is ~1.6-2.0k gas of CPU time, well within the conservative 50k Pulsar
base.
Canonical Pulsar-kind input length is 5303 bytes (1 kind + 1 mode +
4 sigLen + 3309 sig + 4 pkLen + 1952 pk + 32 hash). Total gas at this
length: 50000 + 5303 * 10 = 103,030 gas. At a 12M C-Chain block gas
limit, that is ~117 Pulsar-kind P3Q verifies per block before the
rest of the block budget is exhausted. Corona-kind verifies cost ~5M
gas each (2 per 12M block); Magnetar-kind verifies cost ~30k each
(~400 per 12M block, bounded by CPU verify time in practice).
The 0x012205 slot was previously occupied by a Plonky3-fork STARK / FRI
verifier dispatch that had been misnamed "P3Q" — its package p3q
contained the line "P3Q strict-PQ STARK verifier" and the magic header
"P3Q1" still threads through its proof artifacts (EasyCrypt, Lean,
Jasmin, dudect). Per HANZO-CRYPTO-SUITE §5.2 and ROADMAP-CRYPTO-STACK
§B.11, P3Q canonically means "Post-Quantum Pulsar Proof" — the on-chain
Pulsar verifier. The STARK / FRI dispatch was therefore moved to its
own precompile slot:
~/work/lux/precompile/p3q/ → ~/work/lux/precompile/starkfri/0x012205 → 0x012220 (placeholder pending dedicated LP allocation)p3q → starkfriP3QVerifyPrecompile → StarkFRIVerifyPrecompile, ContractP3QVerifyAddress → ContractStarkFRIVerifyAddress
Lean / Jasmin / dudect proof artifacts remain byte-identical
against the on-chain calldata. The magic is a wire-format tag, not
a name claim; renaming would invalidate the proof artifacts.
The geth EVM wiring (lux/geth/core/vm/lux_precompiles.go), chain
import shims (lux/chains/evm/main.go, lux/evm/precompile/registry/registry.go),
and dudect CT harness (lux/crypto/p3q/ct/dudect/verify_ct.go) were
updated in lockstep to the renamed identifiers.
Rollup transaction envelopes are ZAP frames in the **0xE8..0xEF
rollup schema range** per LP-300 master schema registry. (The original
draft of this LP claimed 0xE0..0xEF, which collided with LP-211
cross-shard atomic at 0xE2..0xE7; per the canonical Option-B map in
LP-300, the consensus-extension band 0xE0..0xEF is partitioned as
0xE0..0xE1 LP-208 DAG mempool, 0xE2..0xE7 LP-211 cross-shard,
0xE8..0xEF LP-218 rollup-VM.)
0xE8 | RollupBatchTx | 0x40 | Sequencer commit of a batch of N txs to Z-Chain |0xE9 | RollupChallengeTx | 0x41 | Fraud-proof challenge for optimistic rollups (omitted by ZK rollups) |0xEA | RollupExitTx | 0x42 | User exit from rollup to parent L1 with state-membership proof |0xEB | RollupGenesisTx | 0x43 | Rollup-creation tx — submitted once at rollup deploy time |0xEC..0xEF | reserved | — | Reserved for future rollup-VM envelopes (cross-rollup atomic commit, sequencer rotation, etc.) |LP-300 master schema registry is normative; this table is informative.
Kind bytes 0x40..0x43 live in a separate namespace per LP-200
§"Frame structure" and do not need a master registry today.
Fixed section (offset table):
0x40 |0x01 |sha256(concat(tx_i bytes)) over the N tx ZAP frames |Variable-length tail: Pulsar sig, group pubkey, optional ZK proof,
packed tx frames. The tx frames are the exact same ZAP buffers the
sequencer received from clients — no re-encode, no re-frame.
Submitted by any peer to challenge an optimistic rollup batch during
its challenge window. Fields:
0x41 |0x01 |sha256 of the challenged RollupBatchTx |Tail: fraud witness — typically a single tx ZAP frame plus a state
witness showing the sequencer applied the tx incorrectly. The Z-Chain
re-executes the tx witness; if the sequencer's claimed new root
disagrees, the batch is rejected and the sequencer is slashed. Omitted
entirely from ZK rollups (no challenge window).
A user exits a rollup back to the parent L1 by proving inclusion of a
withdrawal record in a finalized batch.
0x42 |0x01 |sha256 of the RollupBatchTx the exit is anchored to |Tail: Merkle proof from NewRoot of BatchID down to the withdrawal
leaf. Z-Chain verifies the proof against the recorded root.
Submitted once at rollup-deploy time. Establishes the rollup's
parameters on Z-Chain.
0x43 |0x01 |0x00 optimistic, 0x01 ZK |CertPolicy.Mode required on the parent L1 (see §"Inherited cert mode" below) |Tail: initial root, sequencer Pulsar group public key, rollup config
(execution engine, fee config, etc.).
The rollup-VM execution stack reuses the LP-200 ZAP buffer guarantee:
no re-encoding at any stage. The sequencer's hot path:
NIC RAM ─GPUDirect RDMA─▶ GPU memory (LP-203 UMA)
│
▼
Mempool (ZAP frames)
│
▼
Sequencer batch close
│
▼
State diff + Merkle root (GPU kernel)
│
▼
Pulsar threshold sign (GPU kernel)
│
▼
RollupBatchTx (ZAP frame, same buffer)
│
▼
QUIC submit to Z-Chain (LP-201)
Every stage operates on the same UMA-backed ZAP buffer. No Marshal,
no Unmarshal, no codec, no copy. This is the operational dividend
of the LP-200 stack guarantee — it applies at Tier-3 identically to
how it applies at Tier-1.
The sequencer may be:
centralization risk on the sequencing/ordering function (NOT on
state integrity — that is still parent-L1-secured).
(Pulsar threshold round per batch), better censorship resistance.
a separate Pulsar DKG. Future LP-219 specs the rotation protocol.
Both modes share the same envelopes and the same P3Q precompile
verification path. The difference is whether the integrity of the
state transition is established by (a) sequencer Pulsar-signed
attestation plus optimistic-challenge window, or (b) sequencer
Pulsar-signed attestation plus STARK / Groth16 proof of correct
execution. The Pulsar-sig + P3Q path is identical; the integrity
witness differs.
Rollup deployer picks mode at genesis (RollupMode field in
RollupGenesisTx). No mode-switching after deploy.
Per LP-204 "Tier-3 rollups have NO own validator set", a rollup's
security model IS the parent L1's security model. Concrete
consequences:
Rollups DO NOT add a new validator set. There is no rollup-side stake,
no rollup-side leader election, no rollup-side consensus quorum. The
sequencer is a sequencer, not a validator — its job is ordering and
batching, not finality. Finality comes from the parent L1.
Each rollup pins one field of the LP-217 CertPolicy struct at
genesis: CertPolicy.Mode (historically named CertModeFloor;
both names refer to the same value). This is the minimum cert mode
the parent L1 must be operating at for the rollup batch to be
considered final.
The other three CertPolicy fields (Variant, TimeoutMs,
Fallback) are inherited from the parent L1 by reference. Tier-3
rollups are the only tier where a partial-CertPolicy declaration
is valid (see LP-204 §"Tier-3 — L3 rollups on Z-Chain"). A rollup
does not own TimeoutMs because the parent L1 owns the cert
timeline; the rollup observes the parent L1's tier-degradation
behavior.
(Mode, Variant) on parent L1 | Wire name | Required parent-L1 legs |(PQ-off, hybrid) | PQ-off | BLS |(PQ-fast, hybrid) | PQ-fast | BLS + Pulsar |(PQ-strict, hybrid) | PQ-strict | BLS + Pulsar + Corona |(PQ-heavy, hybrid) | PQ-heavy | BLS + Pulsar + Corona + Magnetar |(PQ-fast, strict) | strict-PQ-fast | Pulsar |(PQ-strict, strict) | strict-PQ-strict | Pulsar + Corona |(PQ-heavy, strict) | strict-PQ-heavy | Pulsar + Corona + Magnetar |A rollup cannot ENFORCE a stronger CertPolicy.Mode than its parent
L1 operates at. If parent L1 is at PQ-fast, the rollup cannot
synthesize PQ-strict finality — it would have to wait for legs the
parent L1 never produces. A rollup that requires PQ-strict MUST
deploy on a parent L1 operating at PQ-strict or higher. Genesis
validation refuses such a configuration before any rollup batch is
processed.
The rollup MAY pin its Mode lower than the parent L1's Mode and
finalize earlier in the LP-202 tier-degradation timeline. A rollup
with CertPolicy.Mode = PQ-fast running on a PQ-strict parent L1
finalizes at the parent L1's PQ-fast preliminary tier (~5 ms), not
the PQ-strict final tier (~15 ms).
Two rollups deployed on the same Z-Chain instance can commit
atomically: both RollupBatchTxes land in the same Z-Chain block and
share the same QuasarCert. Either both finalize or neither does. This
is automatic — no additional protocol is needed.
Cross-L1 atomicity (rollup-on-Lux ↔ rollup-on-Hanzo-L1) requires the
bridgevm path defined in LP-204 §"Tier-1 ↔ Tier-1 bridge" and is out
of scope for this LP. A future LP-219 will specify rollup-to-rollup
cross-L1 atomic composition.
RollupBatchTx |Throughput scales horizontally in M (rollups per Z-Chain). Z-Chain's
verify capacity is the bottleneck only at M >> 100 — at M=100, Z-Chain
verifies 100 P3Q sigs per block (~5 ms total verify, well within
block budget).
P3Q at slot 0x012205 is a PQ verifier family, not a single
primitive. The 1-byte kind discriminator at the head of calldata
selects the underlying P3QVerifier impl: 0x01 Pulsar
(Module-LWE / FIPS-204 ML-DSA), 0x02 Corona (Module-LWE threshold),
0x03 Magnetar (FIPS-205 SLH-DSA threshold). LP-220 specifies the
Corona and Magnetar uses of this slot; the framework type
P3QVerifier and its registration model live in LP-220
§"Architectural decision — common verifier framework".
Rationale: one slot is one audit surface, one ABI shape, one dispatch
path, one operator-known address — the canonical "one and only one
way to do everything" applied to PQ-family verification. Adding a
fourth PQ family (a code-based threshold sig, say) is one new
P3QVerifier impl and one new kind byte, not a new precompile
slot. The earlier LP-218 design ("Pulsar-only verifier; LP-220 to
add separate precompiles") proliferated audit surfaces and ABI shapes
for one family — decomplected on 2026-06-03 into a single slot with
kind-byte dispatch.
The protocol allows CertPolicy.Mode = PQ-off (BLS only). This is the
right choice for ephemeral or non-financial rollups: gaming session
state, social-graph mutations, real-time bidding, telemetry.
For value-bearing state — money, securities, identity, custody —
the recommended floor is PQ-strict (BLS + Corona + Pulsar). This
matches the floor recommended by LP-217 for "custody, treasury,
regulated securities, store-of-value". A rollup pinning a lower floor
for value-bearing state is making a posture choice; the protocol does
not prevent it but also does not endorse it.
This LP does not introduce a protocol-level requirement that rollups
hit PQ-strict. The parent L1's cert mode is the security ceiling;
the rollup pins its floor anywhere up to that ceiling. Codifying a
"must be PQ-strict for tokens > $X" rule belongs in operator-policy,
not in LP-218.
Every RollupBatchTx MUST carry a valid Pulsar threshold signature
verified by P3Q. There is no path where a rollup batch skips P3Q
verification.
The ZK proof field (ZKProofOff) is optional. If present, Z-Chain
verifies it inline (Groth16 verify ≈ 200k gas, STARK verify ≈ 500k gas
on a generic verifier). If absent, the batch is optimistic and subject
to challenge.
Rationale: the Pulsar signature is the authenticity witness (the
sequencer attests to the batch). The ZK proof is the correctness
witness (the batch was applied correctly). Authenticity is always
required; correctness can be optimistic.
Specified by LP-204 and reaffirmed here. A rollup is a sequencer plus
a state machine, not a chain with its own consensus. Anyone who wants
their own validator set deploys an L1, not a rollup.
ecrecover (secp256k1) | 3,000 | ~3 μs | ~10 μs | Classical, broken by Shor's |P3Q is more expensive than BLS verify because PQ verification is
inherently heavier — lattice operations dominate elliptic-curve
operations on every CPU and most GPUs. The 10× factor is honest.
P3Q is dramatically cheaper than re-running consensus to attest to the
rollup batch. The rollup pays 50k gas per batch instead of running an
entire validator set per batch. This is the economic case for the
pattern.
activates: 2025-12-25T16:20:00-08:00
activates-unix: 1766708400
The P3Q precompile is callable at slot 0x012205 from the genesis
block of the new final Lux network. Rollup deployments via
RollupGenesisTx are accepted by Z-Chain from height 0.
timeline the rollup inherits)
"no own validator set")
CertModeFloor selection)Proof", EVM precompile at slot 0x012205)
Z-Chain — two or more L3 rollups commit atomically via a single
Z-Chain QuasarCert; reuses the LP-211 cross-shard 2PC schema
family at the rollup tier.
0x02) + Magnetar (kind 0x03) uses of the same P3Q slot 0x012205 — Corona (Module-LWE) and FIPS-205 SLH-DSA
threshold rollup-batch verifiers dispatched through the kind byte;
complete the single-slot PQ family that supports PQ-strict and
PQ-heavy LP-217 modes. (Earlier draft of LP-220 allocated separate
slots 0x012206 / 0x012207; decomplected 2026-06-03 into the
unified slot.)
to the generic EVM verifier for ZK rollups; closes the ~500k-gas
STARK-verify cost gap)
via the bridgevm path defined in LP-204 §"Tier-1 ↔ Tier-1 bridge";
not yet drafted, distinct from LP-219 which is single-Z-Chain.