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 Z-Chain block that
carries a ZK-rollup batch. The pre-Quasar Edition Lux network
(2020–2025) had no STARK-FRI precompile and is a separate network out
of scope.
LP-221 defines a dedicated EVM precompile, slot 0x012220, for
verifying strict-PQ STARK proofs. The verifier is a Plonky3 fork with
the classical-curve surface stripped out: FRI low-degree testing
over the Goldilocks 64-bit prime field, cSHAKE256 (FIPS 202 /
SP 800-185) Merkle commitments, no KZG, no pairings, no elliptic-curve
arithmetic. The reference implementation lives at
~/work/lux/precompile/starkfri/ and registers under config key
starkfriVerify.
This LP is the dedicated allocation that LP-300 §"Post-quantum
precompile registry" had recorded as `0x012220 — Placeholder for
displaced Plonky3-fork STARK — pending dedicated LP allocation`. The
placeholder is now resolved. LP-300 is the normative slot pin; LP-221
specifies the wire format, gas schedule, soundness assumptions, and
constant-time properties.
LP-221 is orthogonal to LP-218. LP-218 defines P3Q at slot
0x012205 as the unified PQ signature verifier family
(Pulsar / Corona / Magnetar — kind-byte dispatched, signatures and
threshold signatures only). LP-221 is the SNARK proof verifier
(arbitrary R1CS / Plonkish circuits compiled to STARK). The two slots
serve disjoint use cases and never overlap:
0x012205 | LP-218 | PQ threshold signatures (Pulsar / Corona / Magnetar) |0x012220 | LP-221 (this LP) | STARK proofs (Plonky3-fork FRI / cSHAKE256 / Goldilocks) |Consumers: ZK-rollup variants under the LP-218 rollup pattern carry a
ZKProofOff field in RollupBatchTx (schema 0xE8) pointing at a
STARK proof. Z-Chain validators verify that proof by calling
precompile 0x012220 with the proof bytes and the public-input vector
(prev_root, new_root, batch_hash). The PQ threshold signature on the
batch is verified separately at 0x012205 (LP-218). Both checks must
pass; they verify different statements.
The Quasar / Lux primary network needs an on-chain SNARK proof
verifier for two reasons:
1. ZK-rollup batch verification. A Tier-3 ZK rollup (per LP-204)
commits to Z-Chain by submitting `RollupBatchTx{prev_root, new_root,
batch_hash, pulsar_sig, zk_proof}`. The Pulsar signature
authenticates the *who* (sequencer or threshold-group identity); the
STARK proof authenticates the *what* (correct execution of the batch
under the rollup-VM's transition relation). Without an on-chain
SNARK verifier, Z-Chain can only run optimistic rollups with
fraud-proof challenge windows. With LP-221, Z-Chain accepts
ZK-rollup batches at the cost of one precompile call per batch.
2. PQ-safe SNARK substrate. Most production SNARK verifiers
(Groth16, PLONK over BN254 / BLS12-381) are pairing-based and break
under a sufficiently large quantum adversary because pairing
verification reduces to discrete log on a pairing-friendly elliptic
curve. STARK proofs over a small prime field with collision-
resistant hash commitments have **no algebraic structure for
Shor**; their post-quantum security reduces to collision resistance
of the underlying hash (here, cSHAKE256, modeled as a random
oracle). LP-221 therefore complements LP-218 (signatures) and
LP-072 (KEM) as the strict-PQ SNARK leg of the Quasar cryptographic
suite. There is no classical-curve assumption anywhere in the LP-221
verifier.
The choices of FRI / cSHAKE256 / Goldilocks are deliberate:
Oracle Proofs of Proximity") is the canonical low-degree test for
STARKs. Soundness reduces to a Reed-Solomon decoding bound; no
trusted setup.
output function. Domain-separated Merkle commitments use cSHAKE256
rather than a Merkle-tree hash baked from a non-standard sponge. The
standardization gates conformance and audit.
p = 2^64 - 2^32 + 1) is a 64-bit prime field withfast modular arithmetic on AArch64 / x86_64, native register width,
and a smooth multiplicative subgroup of order 2^32. Plonky3 and
RISC Zero converged on Goldilocks for the same reasons.
Combined: a verifier with ~30 KB proof size (measured for typical
Plonky3-fork batches), ~5–10 ms CPU verify on a modern x86_64 core
(per LP-0120 historical row), and PQ security under the random-oracle
model for cSHAKE256. Throughput on Blackwell sm_120 is projected at
~50 μs per verify (LP-203 GPU verify dispatch; not yet measured at LP-
221 publish time).
0x0000000000000000000000000000000000012220
Per ~/work/lux/precompile/starkfri/contract.go:75:
var ContractStarkFRIVerifyAddress = common.HexToAddress(
"0x0000000000000000000000000000000000012220")
LP-300 master registry is normative; this LP carries the slot
informatively for self-containment.
starkfriVerify
Per ~/work/lux/precompile/starkfri/module.go:14:
const ConfigKey = "starkfriVerify"
This is the unique key under which the upgrade config is keyed in the
node's chain configuration JSON.
Single-call interface; no Solidity function selector. The wire bytes
are:
[ 1 byte ] version — 0x01 (VersionV1)
[ 4 bytes ] proof_len — big-endian uint32
[ proof_len bytes ] proof — must begin with MagicHeader "P3Q1"
[ 4 bytes ] pub_len — big-endian uint32
[ pub_len bytes ] public_inputs
MinInputLength = 1 + 4 + 4 + 4 = 13 bytes (1 version + 4 proof_len +
4 magic header bytes inside the proof + 4 pub_len). Shorter calls are
rejected with ErrInvalidInputLength.
MagicHeader. The 4-byte tag "P3Q1" is preserved verbatim from
the pre-rename era so EasyCrypt / Lean / Jasmin / dudect proof
artifacts remain byte-identical against the on-chain calldata. The
header is a wire-bytes tag, not a name claim; renaming would
invalidate the proof artifacts. A future v2 wire format may rename it
to "SFR1" once the proof artifacts are refreshed and the artifact
budget tracker (scripts/checks/ec-admits.sh) is updated in lockstep.
This is documented at contract.go:46-52.
Only VersionV1 = 0x01 is accepted today. Versions 0x00 or
>= 0x02 are rejected with ErrInvalidVersion. The version byte
exists so a future LP can graft a v2 wire format (e.g., post-Plonky3-
upstream-rotation) without re-allocating the slot.
RequiredGas(input) = BaseVerifyGas + len(input) * PerByteGas
= 200_000 + len(input) * 10
Per contract.go:107-110:
const (
BaseVerifyGas uint64 = 200_000
PerByteGas uint64 = 10
)
The 200k base reflects FRI verify cost being **logarithmic in circuit
size**: on-chain we only see the serialized proof, so we charge a flat
base plus per-byte for bandwidth and decode. At a typical 30 KB proof,
total gas is 200_000 + 30_000 * 10 = 500_000 gas per verify —
~24 verifies per 12 M C-Chain block.
Gas is charged upfront before any structural validation or backend
dispatch (contract.go:194-198). The oog_dominates EasyCrypt lemma
(proofs/easycrypt/P3Q_Gas_Model.ec) pins this: out-of-gas wins
against every other failure mode.
(nil, gasLeft, nil) |(nil, 0, contract.ErrOutOfGas) |MinInputLength, or internal length fields exceed remaining bytes | (nil, gasLeft, ErrInvalidInputLength) |version != 0x01 | (nil, gasLeft, ErrInvalidVersion) |proof[0:4] != "P3Q1" | (nil, gasLeft, ErrInvalidProof) |RegisterVerifier | (nil, gasLeft, ErrVerifierNotRegistered) |(false, nil) | (nil, gasLeft, ErrInvalidProof) |(_, err) for err != nil | (nil, gasLeft, err) — backend internal failure surfaces verbatim |Per contract.go:113-118, error sentinels are exported package
symbols so Solidity callers (via revert-data decoding) and Go callers
can pattern-match on them.
The reference implementation is single-source-of-truth at
~/work/lux/precompile/starkfri/:
~/work/lux/precompile/starkfri/contract.go (244 LOC) | StarkFRIVerifyPrecompile struct; Run() does parse, structural pre-filter, dispatch |~/work/lux/precompile/starkfri/module.go (57 LOC) | init() registers starkfriVerify config key + slot 0x012220 |RegisterVerifier(fn VerifierFn) | Wires a Go callback to the out-of-band Rust verifier; atomic load on hot path |Verify(proof, pubInputs []byte) (bool, error) | Off-EVM entry point for chains/zkvm STARK dispatch and tests |~/work/lux/precompile/starkfri/contract_test.go (156 LOC) | 8 unit tests: address pin, gas formula, bad-magic structural rejection, short-input rejection, registered-verifier round trip, missing-verifier surfaces typed error, wrong-version rejection, backend-rejection surfaces typed error |~/work/lux/precompile/starkfri/contract_e2e_test.go (~440 LOC) | 14 tests: 64 round-trip dispatches, structural rejection before backend on bad magic, backend-failure surfacing, gas monotonicity, gas uniformity across errors, OOG short-circuit, round-trip parser; 5 Go fuzz targets (truncated, padded, bit-flipped, bad-length-field, wrong-version) + native FuzzStarkFRI_Dispatch |Test count: 22 tests pass under go test -race -count=1 -v ./...
at HEAD on main of github.com/luxfi/precompile. Measured 2026-06-04
on darwin/arm64; total wallclock 1.376s including race detector.
The Go layer is intentionally thin: ~70 LOC of Run() body is
structural parsing and pre-filter; the heavy lifting (FRI verify,
cSHAKE256 Merkle openings, Goldilocks arithmetic) happens in the
audited Rust verifier crate at ~/work/lux/p3q/crates/p3q-verifier,
reached through RegisterVerifier at node startup. The cgo bridge is
out of scope of LP-221 (it is a node-build detail) and is gated by an
external audit per the sign-off's GATE-4.
Drawn from ~/work/lux/precompile/starkfri/CRYPTOGRAPHER-SIGN-OFF.md
(internal cryptographer review, verdict APPROVED WITH GATES).
The on-chain precompile's soundness is **load-bearing on the Rust
verifier crate's soundness**. The Go layer is a structural pre-filter
+ atomic dispatch; the EasyCrypt theorem
accept_iff_backend_accept (file P3Q_Verifier.ec) pins the bridge:
the precompile accepts (proof, pub) if and only if the registered
backend verifier accepts (version=0x01, proof, pub). The backend's
soundness is the standard STARK / FRI soundness bound:
cSHAKE256, FRI is a knowledge-sound interactive oracle proof
(Ben-Sasson, Bentov, Horesh, Riabzev 2018; "Scalable, transparent,
and post-quantum secure computational integrity"). Knowledge
soundness extracts a witness from any prover that convinces the
verifier with probability above the soundness error.
audited backend crate (specifically: FRI folding factor, query
count, blowup factor), the soundness error is ≤ 2^{-100} in the
random-oracle model. The exact parameter set is documented in
~/work/lux/p3q/crates/p3q-verifier; LP-221 pins the verifier
conformance, not the prover parameters.
accepts with probability 1 (no statistical-zero-knowledge slack at
the verify side; the prover is a separate concern).
transform with cSHAKE256 as the random oracle compiles it to a
non-interactive argument. The transcript hashing domain-separates
rounds; cSHAKE256's customization string carries the domain tag.
The single cryptographic assumption is **collision resistance of
cSHAKE256**, modeled as a random oracle in the Fiat-Shamir reduction.
There is no discrete-log, no pairing, no factoring, no LWE, no SVP
assumption. cSHAKE256 inherits the SHA-3 / Keccak-f[1600] design and
is standardized in FIPS 202 and SP 800-185. The conjectured security
strength is 256 bits classical / 128 bits quantum (Grover); this is
adequate for ≤ 2^{-100} soundness error at the parameter choices the
backend uses.
The on-chain wire format carries a fixed leading MagicHeader = "P3Q1"
that ties the precompile to the strict-PQ Plonky3 fork. Within the
proof bytes, the Plonky3-fork verifier applies its own domain
separation via cSHAKE256 customization strings on each Fiat-Shamir
round; that domain separation is internal to the backend crate and is
out of scope of LP-221's wire-format contract.
carry a zero-knowledge property at the LP-221 boundary. Public
inputs (prev_root, new_root, batch_hash) are explicitly
public. A future LP can graft a ZK-extension by activating Plonky3's
hiding commitment mode and bumping the version byte.
setup of any kind).
not recurse over itself on-chain; recursion happens off-chain in the
prover and surfaces to the EVM as a single STARK proof. A future
recursive variant would land at a separate version byte (and likely
separate magic header) under the same slot.
Three layers, ordered by decreasing strength:
1. Jasmin reference implementation at
~/work/lux/precompile/starkfri/jasmin/verify.jazz (14.2 KB).
Statically constant-time-checkable via jasminc -checkCT verify.jazz.
The CT annotation is
#[ct = "public * public * public * public -> public"] per the
jasmin README. The Jasmin file covers the structural pre-dispatch
gate only (length checks, version byte, magic-header equality via
XOR-accumulator ct_check_magic); the FRI verify inner loop is
delegated to the Rust verifier crate.
2. Go EVM precompile at contract.go. The Go body is shaped to
be CT-friendly: no secret-dependent branches; Run() conditions
on public calldata lengths and the public version byte
only; the magic-header comparison is a byte-string equality on
public wire bytes. The Go layer is NOT formally CT-verified
(Go lacks a libjade-style CT checker) but inherits the same byte-
layout discipline as the Jasmin reference. **LP-221 does not claim
the Go layer itself is constant-time as a primitive**; it claims
only the byte-flow shape matches the CT-checkable Jasmin reference.
3. Empirical CT via dudect. The harness lives at
~/work/lux/crypto/p3q/ct/dudect/ (per the sign-off; relocation to
~/work/lux/crypto/starkfri/ct/dudect/ is pending the cross-tree
rename). Builds clean on arm64 + x86_64; the submission-grade
10^9-sample run is gated under GATE-3 of the sign-off and has not
yet been published. The harness's README explicitly documents that
per-push smoke runs (~10k samples) are a **WEAK assertion, not a
measurement** — LP-221 makes no production-grade CT claim until
GATE-3 closes.
STARK proofs and their public inputs are non-secret by construction;
side-channel leakage of "the proof verifies" vs "the proof doesn't
verify" is not a confidentiality violation. However, CT is required
for soundness: a timing oracle over magic-mismatch vs
length-mismatch vs wrong-version lets an adversarial caller probe
the chain's structural pre-filter and craft calldata that exploits
the boundary between gas billing and structural rejection. The Jasmin
reference and the Go shape both ensure that pre-filter outcomes are
not data-dependent on values an adversary can manipulate per call.
The cgo handoff to the Rust verifier crate is the external CT
obligation. The EasyCrypt theory's lemmas/P3Q_CT.ec reduces the
precompile-level CT obligation to a backend axiom backend_ct. That
axiom is discharged by:
#[ct = ...] annotation (statically checkable with jasminc -checkCT), and
#![forbid(unsafe_code)], panic-free by contract), pending the external review under GATE-4.
0x012205 → 0x012220 relocationThe slot history is documented in LP-218 §"Slot history: STARK-FRI
rename" (lines 272–296 of ~/work/lux/lps/LP-218-zap-p3q-pq-rollup.md)
and recorded normatively in LP-300 §"Post-quantum precompile
registry" (line 292 of ~/work/lux/lps/LP-300-schema-registry.md).
LP-221 records the same history for self-containment.
The Plonky3-fork STARK / FRI verifier was originally registered at
slot 0x012205 under the package name p3q. The package contained
the line *"P3Q strict-PQ STARK verifier"* and the magic header "P3Q1"
threaded through every proof artifact (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 unified
PQ threshold-signature verifier dispatch (Pulsar / Corona / Magnetar).
The STARK / FRI dispatch occupying slot 0x012205 under that name was
an aliasing error: STARK proof verification and PQ signature
verification are two different primitives serving disjoint use cases.
~/work/lux/precompile/p3q/ | ~/work/lux/precompile/starkfri/ |0x012205 | 0x012220 |package p3q | package starkfri |ContractP3QVerifyAddress | ContractStarkFRIVerifyAddress |P3QVerifyPrecompile | StarkFRIVerifyPrecompile |p3qVerify | starkfriVerify |"P3Q1" | "P3Q1" (preserved verbatim) |0x012205 is now LP-218's unified PQ signature verifier
(P3Q-Pulsar / P3Q-Corona / P3Q-Magnetar — kind-byte dispatched).
0x012220 is LP-221's STARK-FRI verifier. The two slots are
orthogonal.
"P3Q1"The 4-byte tag is a wire-format byte string, not a name claim.
Renaming it would invalidate the byte-identical proof artifacts
(EasyCrypt theories, Lean theorems, Jasmin source, dudect harness)
that reference the header verbatim. A future v2 wire format MAY rename
the magic to "SFR1" (STARK-FRI Rev 1) once the proof artifacts and
the artifact-budget tracker are refreshed in lockstep. Today, breaking
proof-artifact byte equality for a cosmetic rename has no upside.
The rename touched four downstream surfaces in the same atomic landing:
lux/geth/core/vm/lux_precompiles.go — EVM wiring (registry entry updated to starkfri.StarkFRIVerifyPrecompile at 0x012220)
lux/chains/evm/main.go — chain import shimlux/evm/precompile/registry/registry.go — precompile registrylux/crypto/p3q/ct/dudect/verify_ct.go — dudect harness (cross-tree rename to lux/crypto/starkfri/ct/dudect/ pending; see CT §3 above)
LP-221 references these cross-repo updates only for the slot history;
they are not normative to LP-221's verifier contract.
LP-0120 §"PQ precompile slot registry" originally recorded 0x012205
as the slot for *"Plonky3 PQ-only STARK"*. That row is now superseded
by LP-300 master registry, which records:
STARK + LP-218 rollup-batch verifier wrapper`
— pending dedicated LP allocation`
LP-221 closes the 0x012220 placeholder. LP-300's row 292 should be
updated in its next amendment to read:
| 0x012220 | LP-221 | STARK-FRI | Plonky3-fork FRI / cSHAKE256 /
Goldilocks STARK verifier; magic header "P3Q1" preserved verbatim
for proof-artifact stability | Draft | 2026-06-04 |
LP-300 is normative; LP-221 is the dedicated LP that LP-300's
placeholder row was waiting on.
0x012205)LP-218 defines 0x012205 as a single precompile that takes a leading
kind byte (0x01 Pulsar, 0x02 Corona, 0x03 Magnetar) and verifies a PQ
threshold signature. LP-221 defines 0x012220 as a single
precompile that verifies a STARK proof. The two are composed at
the rollup layer:
RollupBatchTx (schema 0xE8) carries both a pulsar_sig field and a ZKProofOff field.
authenticates the sequencer's commit, and the STARK proof
authenticates correct execution of the batch under the rollup-VM's
transition relation.
ZKProofOff = 0 and the integrity model is fraud-proof challenges
(LP-218 schema 0xE9 RollupChallengeTx).
Z-Chain block validation under the ZK-rollup variant calls **both
precompiles**: 0x012205 for the signature, 0x012220 for the proof.
Both must accept. No nested call. Gas budget is the sum: Pulsar verify
~50k + STARK verify ~500k = ~550k per ZK-rollup batch under typical
parameters.
LP-0120 owns the 0x012201..0x012208 PQ-signature / KEM band. LP-221
sits one nibble above at 0x012220, **chosen so the strict-PQ STARK
family can grow its own sub-range (0x012220..0x01222F) without
recolliding with the PQ-signature block**. Future SNARK-family
allocations (recursive STARK, hash-based zkVM variants, etc.) land
inside 0x012220..0x01222F per LP-300 master registry amendments.
LP-300 is normative for the slot pin. LP-221 carries the slot
informatively. Any future amendment to LP-221's wire format,
gas schedule, or version byte must be registered in LP-300 first.
LP-203 specifies the Blackwell sm_120 batched-verify dispatch path.
A STARK batch verify kernel for LP-221 is projected at
~/work/lux/accel/cuda/starkfri.cu (not yet implemented at LP-221
publish time). The projected per-verify cost on Blackwell sm_120 is
~50 μs amortized across a batch of ~64 proofs; the gas schedule above
is sized to the CPU-class verify cost (5–10 ms), so GPU acceleration
is a node-operator latency improvement, not a gas reduction. LP-221
does NOT claim measured GPU throughput; that lands when the kernel
ships.
LP-221 is orthogonal to the QuasarCert ladder (PQ-off, PQ-fast,
PQ-strict, PQ-heavy). The STARK verifier is invoked inside EVM
execution, not inside consensus cert formation. Cert profile mode
affects how LP-221's precompile call is finalized (which signature
families validate the containing block) but does not affect the
precompile's verify cost or correctness.
allocation at 0x012205, defines the rollup pattern that consumes
LP-221 in the ZK variant.
is closed by this LP.
0x012205 STARK claim now vacated to 0x012220.
not precompile semantics.
*Scalable, transparent, and post-quantum secure computational
integrity.* ePrint 2018/046.
*Fast Reed-Solomon Interactive Oracle Proofs of Proximity.* ICALP
2018.
Extendable-Output Functions.* August 2015.
and ParallelHash.* December 2016.
github.com/Plonky3/Plonky3 upstream; LP-221'sreference backend is a fork with classical-curve and KZG surfaces
stripped out.
~/work/lux/precompile/starkfri/contract.go — Go precompile body~/work/lux/precompile/starkfri/module.go — module registration~/work/lux/precompile/starkfri/contract_test.go — unit tests~/work/lux/precompile/starkfri/contract_e2e_test.go — integration+ fuzz tests
~/work/lux/precompile/starkfri/CRYPTOGRAPHER-SIGN-OFF.md —internal cryptographer review (APPROVED WITH GATES)
~/work/lux/precompile/starkfri/proofs/easycrypt/ — EasyCrypt theories (4 files, admit budget pinned at 0 / 0)
~/work/lux/precompile/starkfri/jasmin/verify.jazz — Jasminconstant-time reference
~/work/lux/precompile/starkfri/scripts/check-high-assurance.sh —combined CI gate (jasmin + EC admits + EC compile)
LP-221 Activation
─────────────────
Activates: 2025-12-25T16:20:00-08:00
Activates-Unix: 1766708400
Status: Draft
Slot: 0x0000000000000000000000000000000000012220
Config key: starkfriVerify
Wire version: 0x01 (VersionV1)
Magic header: "P3Q1" (preserved verbatim from pre-rename era)
Gas: BaseVerifyGas (200_000) + len(input) * 10
Reference: ~/work/lux/precompile/starkfri/
Tests passing: 22 / 22 under `go test -race -count=1 -v ./...`
(measured 2026-06-04 on darwin/arm64, 1.376s)
Master pin: LP-300 §"Post-quantum precompile registry" row 292
(placeholder closed by this LP)
Per LP-300 / LP-217 convention, LP-221 activates at the genesis of the
new final Lux network. The pre-Quasar Edition Lux network (2020–2025)
had no STARK-FRI precompile; there is nothing to migrate, no
backwards-compat layer, no replay window.