Lux Proposals
← All proposals
LP-0221Draft

Status

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.

Abstract

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:

| Slot | LP | Verifier surface |
|---|---|---|
| 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.

Motivation

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:

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).

Specification — slot, ABI, wire format

Slot


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.

Module config key


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.

Wire format

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.

Version field

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.

Gas schedule


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.

Return contract

| Outcome | Return |
|---|---|
| Verified | (nil, gasLeft, nil) |
| Insufficient gas | (nil, 0, contract.ErrOutOfGas) |
| Input shorter than MinInputLength, or internal length fields exceed remaining bytes | (nil, gasLeft, ErrInvalidInputLength) |
| version != 0x01 | (nil, gasLeft, ErrInvalidVersion) |
| proof[0:4] != "P3Q1" | (nil, gasLeft, ErrInvalidProof) |
| No verifier registered via RegisterVerifier | (nil, gasLeft, ErrVerifierNotRegistered) |
| Backend returned (false, nil) | (nil, gasLeft, ErrInvalidProof) |
| Backend returned (_, 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.

Verifier reference implementation

The reference implementation is single-source-of-truth at

~/work/lux/precompile/starkfri/:

| Artifact | Path | Purpose |
|---|---|---|
| Go precompile body | ~/work/lux/precompile/starkfri/contract.go (244 LOC) | StarkFRIVerifyPrecompile struct; Run() does parse, structural pre-filter, dispatch |
| Go module registration | ~/work/lux/precompile/starkfri/module.go (57 LOC) | init() registers starkfriVerify config key + slot 0x012220 |
| Backend dispatch | RegisterVerifier(fn VerifierFn) | Wires a Go callback to the out-of-band Rust verifier; atomic load on hot path |
| In-process verify | Verify(proof, pubInputs []byte) (bool, error) | Off-EVM entry point for chains/zkvm STARK dispatch and tests |
| Go unit 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 |
| Go integration + fuzz tests | ~/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.

Cryptographic properties

Drawn from ~/work/lux/precompile/starkfri/CRYPTOGRAPHER-SIGN-OFF.md

(internal cryptographer review, verdict APPROVED WITH GATES).

Soundness

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:

PQ assumption

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.

Domain separation

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.

What is NOT claimed

Constant-time properties

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.

Why CT matters for a public proof

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.

Backend CT discharge

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:

Slot history — the 0x012205 → 0x012220 relocation

The 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.

Pre-rename state

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).

Why the rename

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.

What changed (2026-06-03)

| Surface | Pre-rename | Post-rename |
|---|---|---|
| Source path | ~/work/lux/precompile/p3q/ | ~/work/lux/precompile/starkfri/ |
| Slot | 0x012205 | 0x012220 |
| Go package | package p3q | package starkfri |
| Address symbol | ContractP3QVerifyAddress | ContractStarkFRIVerifyAddress |
| Precompile singleton | P3QVerifyPrecompile | StarkFRIVerifyPrecompile |
| Config key | p3qVerify | starkfriVerify |
| Wire-bytes magic header | "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.

Why the magic header stayed "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.

Lockstep updates (cross-repo)

The rename touched four downstream surfaces in the same atomic landing:

LP-221 references these cross-repo updates only for the slot history;

they are not normative to LP-221's verifier contract.

LP-0120 historical row

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:

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.

Composition

With LP-218 (P3Q at 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:

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.

With LP-0120 (PQ precompile band)

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.

With LP-300 (master schema registry)

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.

With LP-203 (GPU-native verify)

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.

With LP-217 (cert profile modes)

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.

Cross-references

LP corpus

External papers

Internal artifacts

Activation marker


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.