WIP: session checkpoint 2026-06-05 (codec rip in flight)

This commit is contained in:
2026-06-05 15:33:59 -07:00
parent 84016f4899
commit dbfb47a2b9
12 changed files with 1616 additions and 144 deletions
+212
View File
@@ -0,0 +1,212 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/**
* @title IP3Q — Post-Quantum Pulsar Proof
* @notice Interface for the P3Q EVM precompile at slot 0x012205.
* @dev Precompile address: 0x0000000000000000000000000000000000012205
*
* Canonical reference:
* - LP-218 (ZAP-Native PQ-Secured Rollups via the P3Q Precompile)
* - HANZO-CRYPTO-SUITE §5.2 (P3Q definition)
* - ROADMAP-CRYPTO-STACK §B.11
*
* P3Q verifies Pulsar / FIPS 204 ML-DSA threshold signatures on-chain.
* The cryptographic verifier core IS the FIPS 204 ML-DSA.Verify
* algorithm — Pulsar's headline Class N1 claim is that a threshold-
* ceremony signature is byte-equal to a single-party FIPS 204
* signature on the same message and group public key.
*
* Distinction from sibling precompiles:
* - 0x012202 ML-DSA: generic single-party FIPS 204 verifier
* - 0x012204 Pulsar: generic Pulsar/threshold-FIPS-204 verifier
* (variable-length message support)
* - 0x012205 P3Q: LP-218 rollup-commit verifier
* (32-byte fixed messageHash, Solidity-bool return)
*
* Primary consumer: LP-218 Tier-3 rollups. A rollup sequencer Pulsar-
* signs the tuple (prev_root || new_root || batch_hash); the parent
* L1 verifies the signature via P3Q inside normal block validation.
*
* Wire format (raw calldata, not Solidity-ABI-encoded):
*
* [ 1 byte ] mode (0x44 / 0x65 / 0x87)
* [ 4 bytes ] pulsarSig length (big-endian uint32)
* [ N bytes ] pulsarSig
* [ 4 bytes ] groupPubKey length (big-endian uint32)
* [ M bytes ] groupPubKey
* [ 32 bytes ] messageHash
*
* Output: 32-byte EVM-ABI bool (last byte 0x01 on verifying signature,
* 0x00 on any verification failure). The precompile never reverts on
* cryptographic failure — the Solidity caller decides whether `false`
* is fatal.
*
* Gas cost: 50,000 base + 10 per input byte.
* - Canonical ML-DSA-65 (1952 pk + 3309 sig + 32 hash + 9 framing):
* ~103,020 gas (5302 bytes * 10 + 50,000)
* - Block budget at 12M gas: ~117 P3Q verifies per block
*
* Domain separation: every P3Q verify binds the FIPS 204 context
* string `lux-evm-precompile-p3q-v1`. Mismatched context vs signer
* causes rejection — prevents cross-precompile replay against the
* 0x012204 Pulsar slot.
*/
interface IP3Q {
/**
* @notice Verify a Pulsar threshold signature over a 32-byte hash.
* @dev Encodes the LP-218 ABI per the wire format documented above.
* Solidity callers should prefer the P3QLib.verify() helper
* below, which packs the calldata correctly.
* @param pulsarSig Serialized Pulsar threshold signature
* (byte-equal to single-party FIPS 204 ML-DSA signature).
* @param messageHash 32-byte digest of the signed payload
* (typically H(prev_root || new_root || batch_hash)).
* @param groupPubKey Pulsar group public key
* (byte-equal to single-party FIPS 204 ML-DSA public key).
* @return valid True iff the signature verifies under the
* FIPS 204 ML-DSA.Verify(groupPubKey, messageHash,
* pulsarSig, "lux-evm-precompile-p3q-v1") predicate.
*/
function verifyPulsarSig(
bytes calldata pulsarSig,
bytes32 messageHash,
bytes calldata groupPubKey
) external view returns (bool valid);
}
/**
* @title P3QLib
* @notice Helper library that packs the LP-218 wire format and
* dispatches to the P3Q precompile via staticcall.
*
* Solidity contracts should consume P3Q through this library
* rather than constructing the calldata themselves — the
* library is the canonical wire-format encoder for the
* precompile (mirror of Go's p3q.EncodeInput).
*/
library P3QLib {
/// @notice Precompile address (LP-218 + HANZO-CRYPTO-SUITE §5.2).
address constant P3Q = address(uint160(0x012205));
/// @notice ML-DSA parameter set identifiers (FIPS 204 §4 Table 1).
uint8 constant MODE_MLDSA44 = 0x44; // NIST PQ Category 2
uint8 constant MODE_MLDSA65 = 0x65; // NIST PQ Category 3 (default)
uint8 constant MODE_MLDSA87 = 0x87; // NIST PQ Category 5
/// @notice Base gas cost (LP-218 §"Honest cost comparison").
uint256 constant BASE_GAS = 50_000;
/// @notice Per-byte gas adder (serialization / wire-parse).
uint256 constant PER_BYTE_GAS = 10;
/// @notice Revert sentinel for caller-side verification failure.
error P3QVerificationFailed();
/// @notice Revert sentinel for unsupported mode parameter.
error P3QUnsupportedMode(uint8 mode);
/**
* @notice Verify a Pulsar threshold signature; revert on failure.
* @param mode FIPS 204 parameter set (0x44 / 0x65 / 0x87).
* @param pulsarSig Pulsar threshold signature bytes.
* @param groupPubKey Pulsar group public key bytes.
* @param messageHash 32-byte digest of the signed payload.
*/
function verifyOrRevert(
uint8 mode,
bytes memory pulsarSig,
bytes memory groupPubKey,
bytes32 messageHash
) internal view {
if (mode != MODE_MLDSA44 && mode != MODE_MLDSA65 && mode != MODE_MLDSA87) {
revert P3QUnsupportedMode(mode);
}
if (!verify(mode, pulsarSig, groupPubKey, messageHash)) {
revert P3QVerificationFailed();
}
}
/**
* @notice Verify a Pulsar threshold signature; return bool.
* @param mode FIPS 204 parameter set (0x44 / 0x65 / 0x87).
* @param pulsarSig Pulsar threshold signature bytes.
* @param groupPubKey Pulsar group public key bytes.
* @param messageHash 32-byte digest of the signed payload.
* @return valid True iff the signature verifies.
*/
function verify(
uint8 mode,
bytes memory pulsarSig,
bytes memory groupPubKey,
bytes32 messageHash
) internal view returns (bool valid) {
bytes memory input = encode(mode, pulsarSig, groupPubKey, messageHash);
uint256 gasLimit = BASE_GAS + (input.length * PER_BYTE_GAS) + 1;
bytes memory out = new bytes(32);
bool ok;
// staticcall — the precompile is stateless and pure
// cryptographic.
assembly {
ok := staticcall(
gasLimit,
P3Q,
add(input, 0x20),
mload(input),
add(out, 0x20),
32
)
}
if (!ok) {
return false;
}
// The 32-byte ABI bool: last byte 0x01 == true.
bytes32 word;
assembly { word := mload(add(out, 0x20)) }
return uint256(word) == 1;
}
/**
* @notice Pack the LP-218 wire format. Canonical encoder; mirror
* of Go's p3q.EncodeInput. Any deviation between this
* encoder and the precompile's parser is a bug in one or
* the other.
*
* Wire layout:
* [ 1 byte ] mode
* [ 4 bytes ] uint32(sigLen) BE
* [ N bytes ] sig
* [ 4 bytes ] uint32(pkLen) BE
* [ M bytes ] pk
* [ 32 bytes ] messageHash
*/
function encode(
uint8 mode,
bytes memory pulsarSig,
bytes memory groupPubKey,
bytes32 messageHash
) internal pure returns (bytes memory out) {
uint32 sigLen = uint32(pulsarSig.length);
uint32 pkLen = uint32(groupPubKey.length);
out = abi.encodePacked(
mode,
sigLen,
pulsarSig,
pkLen,
groupPubKey,
messageHash
);
}
/**
* @notice Estimate gas for a given input layout.
* @param sigLen Pulsar signature length in bytes.
* @param pkLen Pulsar public key length in bytes.
* @return gasEstimate Total gas cost (base + per-byte).
*/
function estimateGas(uint256 sigLen, uint256 pkLen) internal pure returns (uint256) {
// 1 mode + 4 sigLen + 4 pkLen + 32 hash framing.
uint256 inputLen = 1 + 4 + sigLen + 4 + pkLen + 32;
return BASE_GAS + inputLen * PER_BYTE_GAS;
}
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
import (
"bytes"
"crypto/rand"
"testing"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/geth/common"
)
// p3qBenchFixture pre-computes a signed-MLDSA65 payload + EncodeInput
// wire frame so each benchmark iteration only exercises the precompile
// dispatch + FIPS 204 verifier (no key-gen, no sign).
type p3qBenchFixture struct {
input []byte
gas uint64
pkBuf []byte
sigBuf []byte
hash []byte
}
func mkFixture(b *testing.B) *p3qBenchFixture {
b.Helper()
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
if err != nil {
b.Fatalf("GenerateKey: %v", err)
}
hash := bytes.Repeat([]byte{0x42}, MessageHashSize)
sig, err := sk.SignCtx(rand.Reader, hash, PrecompileCtx)
if err != nil {
b.Fatalf("SignCtx: %v", err)
}
pk := sk.PublicKey.Bytes()
input := EncodeInput(ModeMLDSA65, sig, pk, hash)
gas := P3QVerifyPrecompile.RequiredGas(input)
return &p3qBenchFixture{input: input, gas: gas * 2, pkBuf: pk, sigBuf: sig, hash: hash}
}
// BenchmarkP3Q_Verify_Pulsar_MLDSA65 measures one full P3Q precompile
// dispatch (kind-byte dispatch + structural checks + FIPS 204 verify)
// for the canonical Pulsar / ML-DSA-65 happy path. b.N >= 10000 by
// default; the run wraps the existing TestP3Q_RoundTrip_RealMLDSA path.
func BenchmarkP3Q_Verify_Pulsar_MLDSA65(b *testing.B) {
f := mkFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, f.input, f.gas, true)
if err != nil {
b.Fatalf("Run: %v", err)
}
}
}
// BenchmarkP3Q_Verify_KindDispatch_N1 measures the per-call dispatch
// cost on a single-kind workload — every iteration is KindPulsar.
// Pairs with the N=64 / N=1024 batch variants below to show the
// kind-byte dispatch overhead vs a hypothetical single-kind slot.
func BenchmarkP3Q_Verify_KindDispatch_N1(b *testing.B) {
f := mkFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _ = P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, f.input, f.gas, true)
}
}
// BenchmarkP3Q_Verify_KindDispatch_N64 iterates a verify-batch of 64
// inside one b.N step. ns/op is per-batch; per-verify is ns/op / 64.
func BenchmarkP3Q_Verify_KindDispatch_N64(b *testing.B) {
f := mkFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 64; j++ {
_, _, _ = P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, f.input, f.gas, true)
}
}
}
// BenchmarkP3Q_Verify_KindDispatch_N1024 iterates a verify-batch of 1024.
// ns/op is per-batch; per-verify is ns/op / 1024.
func BenchmarkP3Q_Verify_KindDispatch_N1024(b *testing.B) {
f := mkFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < 1024; j++ {
_, _, _ = P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, f.input, f.gas, true)
}
}
}
// BenchmarkP3Q_RequiredGas isolates the gas-only path (no verify) so
// the kind-byte dispatch + structural-check cost can be measured
// independently from the FIPS 204 verifier.
func BenchmarkP3Q_RequiredGas(b *testing.B) {
input := make([]byte, MinInputLength+MLDSA65SignatureSize+MLDSA65PublicKeySize)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = P3QVerifyPrecompile.RequiredGas(input)
}
}
// BenchmarkP3Q_Verify_Standalone measures the in-process Verify entry
// point (the off-chain pre-flight + chains/zkvm dispatch path) at
// MLDSA-65. This is the API path without precompile-level gas
// accounting and AccessibleState plumbing.
func BenchmarkP3Q_Verify_Standalone(b *testing.B) {
f := mkFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := Verify(ModeMLDSA65, f.pkBuf, f.sigBuf, f.hash); err != nil {
b.Fatalf("Verify: %v", err)
}
}
}
+489
View File
@@ -0,0 +1,489 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package p3q implements the EVM precompile at slot 0x012205 for P3Q
// — "Post-Quantum Pulsar Proof" — the on-chain Pulsar / FIPS 204
// ML-DSA threshold-signature verifier.
//
// Canonical reference: HANZO-CRYPTO-SUITE §5.2 + LP-218 + ROADMAP
// §B.11. P3Q is the Solidity-callable verifier shim for the rollup-
// commit pattern of LP-218: a Tier-3 rollup sequencer Pulsar-threshold-
// signs the tuple (prev_root, new_root, batch_hash), packs the
// signature into a RollupBatchTx, and the parent L1 verifies the
// signature via this precompile inside normal block validation.
//
// Cryptographically, Pulsar's headline Class N1 claim is that a
// threshold-ceremony signature is byte-equal to a single-party
// FIPS 204 ML-DSA signature on the same message and group public key.
// Therefore this precompile's verifier core IS the FIPS 204 ML-DSA
// verifier — there is no Pulsar-specific verification logic. The
// dispatch goes through luxfi/crypto/mldsa.VerifySignatureCtx, which
// is a thin alias over cloudflare/circl mldsa{44,65,87} (the FIPS 204
// §6.3 verifier). One verifier, two import seams, byte-equal output.
//
// LP-4200 unified PQCrypto block:
//
// 0x012201 = ML-KEM (Module-LWE KEM, FIPS 203)
// 0x012202 = ML-DSA (Module-LWE single-party signature, FIPS 204)
// 0x012203 = SLH-DSA (hash-based signature, FIPS 205)
// 0x012204 = Pulsar (Module-LWE threshold, FIPS 204 byte-equal)
// 0x012205 = P3Q ← this precompile (LP-218 rollup-commit verifier)
// 0x012206 = Corona (Ring-LWE threshold)
// 0x012207 = Magnetar (SLH-DSA threshold, FIPS 205 byte-equal)
// 0x012208 = HQC (code-based KEM, family-disjoint backup)
//
// Relationship to the Pulsar precompile at 0x012204:
//
// - 0x012204 (Pulsar) is the generic Pulsar / threshold-FIPS-204
// verifier. Wire: [mode][pubkey][msg_len][msg][sig]. Accepts
// arbitrary-length messages. Use for: any Pulsar-signed payload
// that needs Solidity-callable verification.
// - 0x012205 (P3Q) is the rollup-commit-specialised verifier of
// LP-218. Wire: [mode][pulsarSigLen][pulsarSig][groupPubKeyLen]
// [groupPubKey][messageHash:32]. The message is a fixed 32-byte
// hash. Use for: the LP-218 rollup pattern (rollup sequencer
// Pulsar-signs (prev_root || new_root || batch_hash)).
//
// The two precompiles share a common verifier core but serve distinct
// consumers — orthogonal slots, orthogonal wire formats, one and one
// way to do each (HANZO-CRYPTO-SUITE §5.2: "P3Q is a new primitive at
// its own slot, not bolted onto existing ZK or Pulsar").
//
// Wire format (single-call interface, no function selector):
//
// [ 1 byte ] kind (0x01 Pulsar / 0x02 Corona / 0x03 Magnetar)
// [ 1 byte ] mode (primitive-specific level — for Pulsar:
// 0x44 / 0x65 / 0x87 ML-DSA parameter set)
// [ 4 bytes ] pulsarSig length (big-endian uint32)
// [ N bytes ] pulsarSig (length = previous field)
// [ 4 bytes ] groupPubKey length (big-endian uint32)
// [ M bytes ] groupPubKey (length = previous field)
// [ 32 bytes ] messageHash (32-byte digest)
//
// Output: 32-byte EVM-ABI-encoded bool. Last byte is 0x01 on
// successful verification, 0x00 on any verification failure (including
// malformed input, mode mismatch, length mismatch, signature size
// mismatch, public-key size mismatch, FIPS 204 verifier reject).
//
// Returns `(out, gasLeft, nil)` on every well-gas-supplied call: the
// boolean result is conveyed via the output word, never via the Go
// error. This matches the LP-218 §"Failure modes" semantics — the
// precompile reports verification outcome without reverting, so the
// caller (Solidity contract) decides whether `false` is fatal.
//
// Gas cost: BaseVerifyGas + PerByteGas * len(input). The 50_000 base
// is the LP-218 calibration (10x BLS verify ≈ 5_000 gas; 17x ecrecover
// ≈ 3_000 gas) reflecting the inherent cost of ML-DSA lattice
// verification (NTT inverse + matrix-vector multiplication mod q).
// The per-byte adder covers serialization / hashing overhead. At a
// canonical ML-DSA-65 payload (1952-byte pk + 3309-byte sig + 32-byte
// hash + 9-byte framing = 5302 bytes) the total gas is
// 50_000 + 5302 * 10 = 103_020 gas — well under the 12M C-Chain block
// gas limit (per contract.FeeConfigReporter audit at 117 verifies per
// block).
//
// Domain separation: every P3Q verify binds the FIPS 204 context
// string `lux-evm-precompile-p3q-v1`. Mismatched context between
// signer and verifier causes the FIPS 204 verifier to reject — this
// prevents cross-precompile replay where a Pulsar signature minted
// for the generic 0x012204 path is re-submitted to the rollup-commit
// 0x012205 path. The signer SHOULD set ctx = "lux-evm-precompile-p3q-v1"
// when producing rollup-commit signatures destined for on-chain P3Q
// verification.
//
// Constant-time properties: P3Q operates on PUBLIC calldata — both
// the signature and the public key are observable on-chain. The CT
// concern is therefore not confidentiality but soundness: no timing
// channel may distinguish an accepting verify from a rejecting verify
// in a way that an adversary could turn into a consensus-relevant
// oracle. The FIPS 204 verifier is constant-time per FIPS 204 §6.3 and
// cloudflare/circl's documented CT contract; we forward that property
// without introducing data-dependent branches at the precompile layer.
// The only branches in Run are on PUBLIC mode/length fields, which is
// allowed under the CT contract.
package p3q
import (
"encoding/binary"
"errors"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
)
// ContractP3QVerifyAddress is the canonical LP-4200 slot for P3Q per
// HANZO-CRYPTO-SUITE §5.2 and LP-218.
var ContractP3QVerifyAddress = common.HexToAddress("0x0000000000000000000000000000000000012205")
// P3QVerifyPrecompile is the singleton instance bound to slot
// 0x012205 by package init() in module.go.
var P3QVerifyPrecompile = &p3qVerifyPrecompile{}
var _ contract.StatefulPrecompiledContract = &p3qVerifyPrecompile{}
// Wire format size constants.
const (
// KindByte is the single-byte P3Q family discriminator at offset 0
// of the calldata per LP-218 §"Precompile wire format" (decomplected
// 2026-06-03 — slot 0x012205 hosts the unified P3Q verifier family;
// dispatch on the kind byte selects which lattice/hash primitive
// performs the verify).
KindByte = 1
// ModeByte is the primitive-specific parameter-set identifier at
// offset 1 of the calldata (post-kind). For KindPulsar / KindCorona,
// this names the FIPS 204 ML-DSA parameter set (44/65/87). For
// KindMagnetar, this names the FIPS 205 SLH-DSA parameter set.
ModeByte = 1
// LengthFieldSize is the byte width of each big-endian uint32
// length field embedded in the wire format.
LengthFieldSize = 4
// MessageHashSize is the byte width of the fixed messageHash field
// per LP-218 (the precompile binds a 32-byte digest, not a
// variable-length message — that is the distinguishing feature of
// P3Q vs the generic Pulsar precompile at 0x012204).
MessageHashSize = 32
// MinInputLength is the smallest structurally valid input:
// 1 kind + 1 mode + 4 sig_len + 0 sig + 4 pk_len + 0 pk + 32 hash.
// A zero-length signature or zero-length public key cannot
// possibly verify (FIPS 204 requires both to be non-empty), but
// the structural check is constant-time over PUBLIC lengths and
// the FIPS 204 verifier surfaces the actual rejection.
MinInputLength = KindByte + ModeByte + LengthFieldSize + LengthFieldSize + MessageHashSize
)
// P3Q family kinds per LP-218 / LP-220. Slot 0x012205 hosts a unified
// verifier; the kind byte at offset 0 of the wire format selects
// which primitive performs the verify. Decomplection of 2026-06-03
// collapsed the prior sibling-slots model (LP-220 §"Architectural
// decision — single slot, kind-byte dispatch") into one slot.
//
// Today this precompile fully implements KindPulsar (FIPS 204 ML-DSA
// byte-equal output via Pulsar threshold ceremony). KindCorona and
// KindMagnetar reserve their slot in the dispatch table but return
// abiFalse via the NotImplemented path until their lattice-threshold
// libraries land (see LP-220 §"Per-kind specs").
const (
KindPulsar uint8 = 0x01 // FIPS 204 ML-DSA (Module-LWE threshold, byte-equal)
KindCorona uint8 = 0x02 // Ring-LWE threshold (Corona eprint 2024/1113)
KindMagnetar uint8 = 0x03 // FIPS 205 SLH-DSA (hash-based threshold)
)
// ML-DSA parameter-set identifiers. Wire bytes match the canonical
// FIPS 204 mode numbering used across the Lux precompile family
// (0x012202 ML-DSA, 0x012204 Pulsar). Aligning with sibling precompiles
// keeps client tooling uniform.
const (
ModeMLDSA44 uint8 = 0x44 // NIST PQ Category 2
ModeMLDSA65 uint8 = 0x65 // NIST PQ Category 3 (Lux production target)
ModeMLDSA87 uint8 = 0x87 // NIST PQ Category 5
)
// FIPS 204 byte-size constants per parameter set (FIPS 204 Table 2).
// Mirrored from the Pulsar precompile at 0x012204 — same lattice,
// same byte layout, same sizes.
const (
MLDSA44PublicKeySize = 1312
MLDSA44SignatureSize = 2420
MLDSA65PublicKeySize = 1952
MLDSA65SignatureSize = 3309
MLDSA87PublicKeySize = 2592
MLDSA87SignatureSize = 4627
)
// Gas schedule per LP-218 §"Honest cost comparison":
//
// P3Q (Pulsar / ML-DSA-65) | 50,000 gas | ~50 μs Blackwell | ~80 μs CPU
// BLS pairing verify | 5,000 gas | ~10 μs Blackwell | ~6 ms CPU
// ecrecover (secp256k1) | 3,000 gas | ~3 μs Blackwell | ~10 μs CPU
//
// The 10× factor over BLS reflects lattice verification's inherent
// cost (NTT inverse + matrix-vector mul mod q). The per-byte adder
// covers serialization / wire-format parsing. Calibrated against the
// parity audit (luxfi/threshold parity_test): Pulsar verify
// measures ~0.13-0.17 ms across all N on M1 Max and DGX Spark; at
// 12k gas/ms (geth's published gas-per-millisecond yardstick), that
// is ~1.6-2.0k gas of CPU time, dwarfed by the conservative 50k base.
const (
BaseVerifyGas uint64 = 50_000
PerByteGas uint64 = 10
)
// PrecompileCtx is the FIPS 204 context string bound to every P3Q
// verify call. Distinct from the generic 0x012204 Pulsar precompile's
// context ("lux-evm-precompile-pulsar-v1") so a signature minted for
// one slot cannot be replayed against the other.
//
// The context is a public, constant byte string — exposing it does not
// weaken the verifier; rather, it forces an attacker to obtain a fresh
// signature under the P3Q context for any on-chain replay attempt.
var PrecompileCtx = []byte("lux-evm-precompile-p3q-v1")
// Error sentinels. Returned only on out-of-gas: any verification
// failure (malformed input, wrong kind, wrong mode, length mismatch,
// FIPS 204 reject) is surfaced as `(out=[0x00…00], nil error)` — the
// Solidity caller observes `false` and decides whether that is fatal.
// This matches LP-218 §"Failure modes": "No revert on cryptographic
// failure — caller decides whether false is fatal."
var (
ErrInvalidInputLength = errors.New("p3q: invalid input length")
ErrUnsupportedKind = errors.New("p3q: unsupported family kind (must be 0x01 Pulsar / 0x02 Corona / 0x03 Magnetar)")
ErrUnsupportedMode = errors.New("p3q: unsupported ML-DSA mode (must be 0x44, 0x65, or 0x87)")
ErrKindNotImplemented = errors.New("p3q: kind reserved but verifier not yet wired (LP-220 follow-up)")
)
type p3qVerifyPrecompile struct{}
// RequiredGas returns BaseVerifyGas + PerByteGas * len(input).
//
// The cost function is monotonic in input length and uniform across
// success / failure paths (per the LP-218 gas-billing contract:
// "audited gas charge"). A short malformed input pays the base; a
// well-formed N-byte input pays base + N * PerByteGas. No reduced
// price for early rejection — uniform billing prevents a gas-side
// channel from leaking which validation stage rejected the input.
func (p *p3qVerifyPrecompile) RequiredGas(input []byte) uint64 {
return BaseVerifyGas + uint64(len(input))*PerByteGas
}
// modeParams resolves the wire-byte mode to (mldsa.Mode, pkSize,
// sigSize). Returns ErrUnsupportedMode on an unrecognised byte.
func modeParams(mode uint8) (m mldsa.Mode, pkSize, sigSize int, err error) {
switch mode {
case ModeMLDSA44:
return mldsa.MLDSA44, MLDSA44PublicKeySize, MLDSA44SignatureSize, nil
case ModeMLDSA65:
return mldsa.MLDSA65, MLDSA65PublicKeySize, MLDSA65SignatureSize, nil
case ModeMLDSA87:
return mldsa.MLDSA87, MLDSA87PublicKeySize, MLDSA87SignatureSize, nil
default:
return 0, 0, 0, ErrUnsupportedMode
}
}
// abiFalse returns a fresh 32-byte zero slice — the EVM-ABI encoding
// of `bool false`. Callers may mutate the return value.
func abiFalse() []byte { return make([]byte, 32) }
// abiTrueClone returns a fresh 32-byte slice with the last byte set
// to 0x01 — the EVM-ABI encoding of `bool true`. Callers may mutate
// the return value.
func abiTrueClone() []byte { b := make([]byte, 32); b[31] = 1; return b }
// Run verifies a P3Q-family signature per LP-218 / LP-220. Today only
// KindPulsar (FIPS 204 ML-DSA byte-equal output) is wired; KindCorona
// and KindMagnetar are reserved and return abiFalse until their
// verifiers land.
//
// Wire format (single-call interface, no function selector):
//
// [ 1 byte ] kind (0x01 Pulsar / 0x02 Corona / 0x03 Magnetar)
// [ 1 byte ] mode (0x44 / 0x65 / 0x87 — primitive-specific)
// [ 4 bytes ] pulsarSig length (BE32)
// [ N bytes ] pulsarSig
// [ 4 bytes ] groupPubKey length (BE32)
// [ M bytes ] groupPubKey
// [ 32 bytes ] messageHash
//
// Output: 32-byte EVM-ABI bool (last byte 0x01 on success, 0x00 on
// any verification failure).
//
// Error returns:
// - contract.ErrOutOfGas when suppliedGas < RequiredGas(input).
//
// Verification failures (malformed input, mode mismatch, length
// mismatch, FIPS 204 reject) return `(abiFalse, gasLeft, nil)` — the
// LP-218 contract: no revert on cryptographic failure.
func (p *p3qVerifyPrecompile) Run(
_ contract.AccessibleState,
_ common.Address,
_ common.Address,
input []byte,
suppliedGas uint64,
_ bool,
) ([]byte, uint64, error) {
gas := p.RequiredGas(input)
remaining, err := contract.DeductGas(suppliedGas, gas)
if err != nil {
return nil, 0, err
}
// Structural validation — all on PUBLIC fields (kind byte + mode
// byte + length fields + total length). No data-dependent branches
// on signature or public-key contents.
if len(input) < MinInputLength {
return abiFalse(), remaining, nil
}
// Family-kind dispatch per LP-218 / LP-220 §"Single slot, kind-byte
// dispatch". Today only KindPulsar is wired; KindCorona / KindMagnetar
// reserve their slot and fall through to abiFalse. Adding their
// verifier paths is a follow-up that does not change the precompile
// surface — only the dispatch table grows.
kind := input[0]
switch kind {
case KindPulsar:
// fall through to the existing FIPS 204 ML-DSA verify path
case KindCorona, KindMagnetar:
// Reserved kinds — verifier not yet wired. Return abiFalse
// (the LP-218 "no revert on cryptographic failure" contract)
// so callers still see a consistent surface; once the lattice/
// hash threshold libraries land, this branch routes to its
// own verifier.
return abiFalse(), remaining, nil
default:
// Unknown kind — abiFalse, same as any malformed input.
return abiFalse(), remaining, nil
}
mode := input[KindByte]
m, pkSize, sigSize, mErr := modeParams(mode)
if mErr != nil {
return abiFalse(), remaining, nil
}
// Parse pulsarSig: [4 bytes len][N bytes sig].
off := KindByte + ModeByte
if len(input)-off < LengthFieldSize {
return abiFalse(), remaining, nil
}
sigLen := binary.BigEndian.Uint32(input[off : off+LengthFieldSize])
off += LengthFieldSize
if uint64(len(input)-off) < uint64(sigLen) {
return abiFalse(), remaining, nil
}
if int(sigLen) != sigSize {
// Length mismatch against the mode's expected FIPS 204
// signature size. The FIPS 204 verifier would reject anyway;
// short-circuit here so we don't risk feeding a wrong-length
// slice to the underlying primitive.
return abiFalse(), remaining, nil
}
pulsarSig := input[off : off+int(sigLen)]
off += int(sigLen)
// Parse groupPubKey: [4 bytes len][M bytes pk].
if len(input)-off < LengthFieldSize {
return abiFalse(), remaining, nil
}
pkLen := binary.BigEndian.Uint32(input[off : off+LengthFieldSize])
off += LengthFieldSize
if uint64(len(input)-off) < uint64(pkLen) {
return abiFalse(), remaining, nil
}
if int(pkLen) != pkSize {
return abiFalse(), remaining, nil
}
groupPubKey := input[off : off+int(pkLen)]
off += int(pkLen)
// Parse messageHash: exactly 32 bytes at the tail. The wire layout
// places the hash AFTER the variable-length blobs so the offset is
// content-independent (parseable by reading only the length fields,
// no scanning).
if len(input)-off < MessageHashSize {
return abiFalse(), remaining, nil
}
messageHash := input[off : off+MessageHashSize]
// Trailing bytes (off + MessageHashSize < len(input)) are accepted
// to match the convention used by the sibling STARK-FRI precompile
// (precompile/starkfri/contract.go::TestStarkFRI_Fuzz_PaddedInput).
// Padding does not affect verification.
// Construct typed ML-DSA PublicKey. The mldsa package parses the
// FIPS 204 packed-pk byte layout into the runtime struct used by
// the verifier.
pk, perr := mldsa.PublicKeyFromBytes(groupPubKey, m)
if perr != nil {
return abiFalse(), remaining, nil
}
// FIPS 204 ML-DSA.Verify under the P3Q domain-separation context.
if !pk.VerifySignatureCtx(messageHash, pulsarSig, PrecompileCtx) {
return abiFalse(), remaining, nil
}
return abiTrueClone(), remaining, nil
}
// Verify is the in-process Pulsar verification entry-point for callers
// outside the EVM precompile path (chains/zkvm dispatch, off-chain
// pre-checks, tests). Bypasses gas accounting and AccessibleState
// (those concerns belong to the precompile wrapper).
//
// Returns nil on a verifying signature; a typed error otherwise. Uses
// the same FIPS 204 context as the precompile path so off-chain
// pre-checks are byte-identical to on-chain verification.
//
// Pre-conditions:
// - mode is one of {ModeMLDSA44, ModeMLDSA65, ModeMLDSA87}
// - len(groupPubKey) matches the mode's pk size
// - len(pulsarSig) matches the mode's sig size
// - len(messageHash) is MessageHashSize (32)
//
// Violations of these pre-conditions return a typed error rather than
// a panic.
func Verify(mode uint8, groupPubKey, pulsarSig, messageHash []byte) error {
m, pkSize, sigSize, err := modeParams(mode)
if err != nil {
return err
}
if len(groupPubKey) != pkSize {
return errors.New("p3q: groupPubKey size mismatch")
}
if len(pulsarSig) != sigSize {
return errors.New("p3q: pulsarSig size mismatch")
}
if len(messageHash) != MessageHashSize {
return errors.New("p3q: messageHash must be 32 bytes")
}
pk, perr := mldsa.PublicKeyFromBytes(groupPubKey, m)
if perr != nil {
return perr
}
if !pk.VerifySignatureCtx(messageHash, pulsarSig, PrecompileCtx) {
return errors.New("p3q: FIPS 204 ML-DSA verify rejected")
}
return nil
}
// EncodeInput is a helper for callers (tests, Solidity adapters,
// off-chain pre-flight checks) to serialize (mode, sig, pk, hash) into
// the wire format the precompile expects. The encoder is also the
// canonical reference for the wire layout — any deviation between
// EncodeInput and Run is a bug in one or the other.
//
// EncodeInput targets KindPulsar (the only kind with a wired verifier
// today). For explicit kind selection use EncodeInputKind.
func EncodeInput(mode uint8, pulsarSig, groupPubKey, messageHash []byte) []byte {
return EncodeInputKind(KindPulsar, mode, pulsarSig, groupPubKey, messageHash)
}
// EncodeInputKind serializes (kind, mode, sig, pk, hash) into the
// canonical LP-218 P3Q wire format with the kind byte at offset 0
// per LP-220 §"Architectural decision — single slot, kind-byte
// dispatch".
func EncodeInputKind(kind, mode uint8, pulsarSig, groupPubKey, messageHash []byte) []byte {
out := make([]byte, 0, KindByte+ModeByte+LengthFieldSize+len(pulsarSig)+LengthFieldSize+len(groupPubKey)+MessageHashSize)
out = append(out, kind)
out = append(out, mode)
var b4 [4]byte
binary.BigEndian.PutUint32(b4[:], uint32(len(pulsarSig)))
out = append(out, b4[:]...)
out = append(out, pulsarSig...)
binary.BigEndian.PutUint32(b4[:], uint32(len(groupPubKey)))
out = append(out, b4[:]...)
out = append(out, groupPubKey...)
// messageHash MUST be exactly 32 bytes. If the caller passes a
// different length, the wire format is malformed; the precompile
// will reject. Surface that to the caller by writing whatever was
// passed (no silent padding / truncation).
out = append(out, messageHash...)
return out
}
+415
View File
@@ -0,0 +1,415 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"testing"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
"github.com/stretchr/testify/require"
)
// TestP3Q_Address pins the canonical LP-4200 slot 0x012205 per
// HANZO-CRYPTO-SUITE §5.2 and LP-218.
func TestP3Q_Address(t *testing.T) {
want := common.HexToAddress("0x0000000000000000000000000000000000012205")
require.Equal(t, want, ContractP3QVerifyAddress)
}
// TestP3Q_RequiredGas pins the LP-218 gas formula: 50_000 + 10 * N.
// Canonical Pulsar / ML-DSA-65 payload is now 5303 bytes (1 kind +
// 1 mode + 8 lengths + 3309 sig + 1952 pk + 32 hash).
func TestP3Q_RequiredGas(t *testing.T) {
for _, n := range []int{0, 1, 64, 256, 1024, 5303, 16384} {
input := make([]byte, n)
got := P3QVerifyPrecompile.RequiredGas(input)
want := BaseVerifyGas + uint64(n)*PerByteGas
require.Equal(t, want, got, "len=%d", n)
}
}
// TestP3Q_OutOfGas confirms suppliedGas < requiredGas surfaces
// contract.ErrOutOfGas as the Go error (the only path that does NOT
// fall through to the abiFalse output convention).
func TestP3Q_OutOfGas(t *testing.T) {
input := make([]byte, 1024)
gas := P3QVerifyPrecompile.RequiredGas(input)
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas-1, true)
require.ErrorIs(t, err, contract.ErrOutOfGas)
}
// TestP3Q_ABIBoolEncoding pins the 32-byte EVM-ABI bool output
// convention. abiTrueClone() must be 31 zero bytes + 0x01;
// abiFalse() must be 32 zero bytes.
func TestP3Q_ABIBoolEncoding(t *testing.T) {
tr := abiTrueClone()
require.Len(t, tr, 32)
require.Equal(t, byte(0x01), tr[31])
for i := 0; i < 31; i++ {
require.Equal(t, byte(0x00), tr[i], "abiTrueClone byte %d", i)
}
fl := abiFalse()
require.Len(t, fl, 32)
require.Equal(t, make([]byte, 32), fl)
}
// TestP3Q_RejectsTooShort confirms inputs below MinInputLength are
// rejected with abiFalse + nil error (no Go-level error per LP-218
// "no revert on cryptographic failure" contract).
func TestP3Q_RejectsTooShort(t *testing.T) {
for _, sz := range []int{0, 1, 5, MinInputLength - 1} {
input := make([]byte, sz)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err, "len=%d must surface as abiFalse, not Go error", sz)
require.Equal(t, abiFalse(), out, "len=%d", sz)
}
}
// TestP3Q_RejectsUnsupportedMode confirms mode bytes other than
// {0x44, 0x65, 0x87} are rejected with abiFalse. Kind byte is held
// fixed at KindPulsar so the failure is isolated to the mode byte.
func TestP3Q_RejectsUnsupportedMode(t *testing.T) {
for _, mode := range []uint8{0x00, 0x01, 0x42, 0x66, 0x88, 0xFF} {
input := make([]byte, MinInputLength)
input[0] = KindPulsar
input[1] = mode
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err, "mode=0x%02x", mode)
require.Equal(t, abiFalse(), out, "mode=0x%02x", mode)
}
}
// TestP3Q_RejectsUnsupportedKind confirms kind bytes outside the
// LP-218 / LP-220 family {0x01, 0x02, 0x03} are rejected with abiFalse.
func TestP3Q_RejectsUnsupportedKind(t *testing.T) {
for _, kind := range []uint8{0x00, 0x04, 0x10, 0x42, 0xFF} {
input := make([]byte, MinInputLength)
input[0] = kind
input[1] = ModeMLDSA65
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err, "kind=0x%02x", kind)
require.Equal(t, abiFalse(), out, "kind=0x%02x must abiFalse", kind)
}
}
// TestP3Q_ReservedKindsReturnAbiFalse confirms KindCorona and KindMagnetar
// reserve their dispatch slot but return abiFalse (no revert) until
// their verifiers land per LP-220.
func TestP3Q_ReservedKindsReturnAbiFalse(t *testing.T) {
for _, kind := range []uint8{KindCorona, KindMagnetar} {
input := make([]byte, MinInputLength)
input[0] = kind
input[1] = ModeMLDSA65
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err, "kind=0x%02x", kind)
require.Equal(t, abiFalse(), out, "reserved kind 0x%02x must abiFalse", kind)
}
}
// TestP3Q_RejectsTruncatedSig confirms a sig_len field declaring more
// bytes than remain in the buffer yields abiFalse.
func TestP3Q_RejectsTruncatedSig(t *testing.T) {
// Declare a 3309-byte sig but supply only 100 bytes.
input := make([]byte, KindByte+ModeByte+LengthFieldSize+100+LengthFieldSize+0+MessageHashSize)
input[0] = KindPulsar
input[1] = ModeMLDSA65
// sigLen = 3309 (BE32) at offset 2 (post-kind, post-mode).
input[2] = 0x00
input[3] = 0x00
input[4] = 0x0C
input[5] = 0xED
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out)
}
// TestP3Q_RejectsSigLengthMismatch confirms a wire-format sig_len that
// is not equal to the mode's FIPS 204 signature size yields abiFalse
// without ever invoking the FIPS 204 verifier.
func TestP3Q_RejectsSigLengthMismatch(t *testing.T) {
// Mode = MLDSA-65 (expects sig of 3309 bytes), but we supply 3308.
const wrongLen = 3308
sig := make([]byte, wrongLen)
pk := make([]byte, MLDSA65PublicKeySize)
hash := make([]byte, MessageHashSize)
input := EncodeInput(ModeMLDSA65, sig, pk, hash)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out, "sig length mismatch (3308 vs expected 3309) must surface as abiFalse")
}
// TestP3Q_RejectsPubKeyLengthMismatch confirms a wire-format pk_len
// that is not equal to the mode's FIPS 204 public-key size yields
// abiFalse.
func TestP3Q_RejectsPubKeyLengthMismatch(t *testing.T) {
sig := make([]byte, MLDSA65SignatureSize)
pk := make([]byte, MLDSA65PublicKeySize-1) // off by 1
hash := make([]byte, MessageHashSize)
input := EncodeInput(ModeMLDSA65, sig, pk, hash)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out)
}
// mldsaModeFor maps a P3Q wire-byte mode to the mldsa package Mode.
func mldsaModeFor(t *testing.T, m uint8) mldsa.Mode {
t.Helper()
switch m {
case ModeMLDSA44:
return mldsa.MLDSA44
case ModeMLDSA65:
return mldsa.MLDSA65
case ModeMLDSA87:
return mldsa.MLDSA87
}
t.Fatalf("unsupported mode 0x%02x", m)
return 0
}
// TestP3Q_RoundTrip_RealMLDSA is the load-bearing integration test:
// generate a FIPS 204 ML-DSA key pair, sign a real 32-byte hash under
// the P3Q domain-separation context, dispatch the signature through
// the EVM precompile entry, and assert the precompile returns abiTrue
// with gas correctly debited.
//
// This is the canonical "happy path" — it proves the wire format is
// stable end-to-end and the FIPS 204 verifier core wires through
// mldsa.VerifySignatureCtx unmodified. The signature is byte-equal to
// what a Pulsar threshold ceremony would produce (per Pulsar's Class
// N1 manifesto), so this test stands in for the threshold path
// without needing the full DKG + 2-round signing infrastructure.
func TestP3Q_RoundTrip_RealMLDSA(t *testing.T) {
for _, modeByte := range []uint8{ModeMLDSA44, ModeMLDSA65, ModeMLDSA87} {
t.Run(modeName(modeByte), func(t *testing.T) {
m := mldsaModeFor(t, modeByte)
sk, err := mldsa.GenerateKey(rand.Reader, m)
require.NoError(t, err)
require.NotNil(t, sk.PublicKey)
// Hash a representative rollup-commit payload per LP-218
// step 5: H(prev_root || new_root || batch_hash).
prevRoot := bytes.Repeat([]byte{0xAA}, 32)
newRoot := bytes.Repeat([]byte{0xBB}, 32)
batchHash := bytes.Repeat([]byte{0xCC}, 32)
h := sha256.New()
h.Write(prevRoot)
h.Write(newRoot)
h.Write(batchHash)
messageHash := h.Sum(nil)
require.Len(t, messageHash, MessageHashSize)
// Sign under the P3Q domain-separation context — the same
// context the precompile passes to mldsa.VerifySignatureCtx.
sig, err := sk.SignCtx(rand.Reader, messageHash, PrecompileCtx)
require.NoError(t, err)
input := EncodeInput(modeByte, sig, sk.PublicKey.Bytes(), messageHash)
gas := P3QVerifyPrecompile.RequiredGas(input)
supplied := gas * 2
out, gasLeft, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, supplied, true)
require.NoError(t, err)
require.Equal(t, abiTrueClone(), out, "precompile must return abiTrue for a real verifying signature")
require.Equal(t, supplied-gas, gasLeft, "gas accounting must be base + N*per-byte")
})
}
}
func modeName(m uint8) string {
switch m {
case ModeMLDSA44:
return "MLDSA44"
case ModeMLDSA65:
return "MLDSA65"
case ModeMLDSA87:
return "MLDSA87"
}
return "unknown"
}
// TestP3Q_RejectsWrongMessageHash confirms a signature minted for one
// hash and submitted with a different hash is rejected (FIPS 204
// verifier surfacing abiFalse).
func TestP3Q_RejectsWrongMessageHash(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
correct := bytes.Repeat([]byte{0x01}, 32)
wrong := bytes.Repeat([]byte{0x02}, 32)
sig, err := sk.SignCtx(rand.Reader, correct, PrecompileCtx)
require.NoError(t, err)
input := EncodeInput(ModeMLDSA65, sig, sk.PublicKey.Bytes(), wrong)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out, "signature minted for hash A must not verify under hash B")
}
// TestP3Q_RejectsWrongGroupPubKey confirms a signature minted under
// key A and submitted with key B is rejected.
func TestP3Q_RejectsWrongGroupPubKey(t *testing.T) {
skA, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
skB, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := bytes.Repeat([]byte{0x42}, 32)
sigA, err := skA.SignCtx(rand.Reader, hash, PrecompileCtx)
require.NoError(t, err)
// Submit sigA under skB's public key — must reject.
input := EncodeInput(ModeMLDSA65, sigA, skB.PublicKey.Bytes(), hash)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out)
}
// TestP3Q_RejectsCorruptedSignature confirms a single-bit corruption
// of the signature payload is detected by the FIPS 204 verifier.
func TestP3Q_RejectsCorruptedSignature(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := bytes.Repeat([]byte{0x77}, 32)
sig, err := sk.SignCtx(rand.Reader, hash, PrecompileCtx)
require.NoError(t, err)
// Flip one bit at a non-deterministic location in the signature.
corrupted := append([]byte(nil), sig...)
require.Greater(t, len(corrupted), 100)
corrupted[100] ^= 0x01
input := EncodeInput(ModeMLDSA65, corrupted, sk.PublicKey.Bytes(), hash)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out, "single-bit signature corruption must be detected")
}
// TestP3Q_RejectsWrongContextDomainSeparation is the load-bearing
// security test for the domain-separation property documented in
// LP-218 §"Domain separation". A signature minted under the generic
// 0x012204 Pulsar context ("lux-evm-precompile-pulsar-v1") must NOT
// verify under the P3Q context ("lux-evm-precompile-p3q-v1").
//
// This prevents an attacker from minting a signature for one slot and
// replaying it against the other.
func TestP3Q_RejectsWrongContextDomainSeparation(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := bytes.Repeat([]byte{0xAB}, 32)
// Sign under the WRONG (Pulsar 0x012204) context.
wrongCtx := []byte("lux-evm-precompile-pulsar-v1")
sig, err := sk.SignCtx(rand.Reader, hash, wrongCtx)
require.NoError(t, err)
// Submit to P3Q (which binds the P3Q context internally).
input := EncodeInput(ModeMLDSA65, sig, sk.PublicKey.Bytes(), hash)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out, "signature under sibling-precompile context must not verify on P3Q")
}
// TestP3Q_Verify_StandaloneAPI exercises the in-process Verify entry
// point (the off-chain pre-flight + chains/zkvm dispatch entry).
// Must produce byte-identical accept/reject decisions to the on-chain
// precompile path.
func TestP3Q_Verify_StandaloneAPI(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := bytes.Repeat([]byte{0x33}, 32)
sig, err := sk.SignCtx(rand.Reader, hash, PrecompileCtx)
require.NoError(t, err)
// Standalone Verify must accept.
require.NoError(t, Verify(ModeMLDSA65, sk.PublicKey.Bytes(), sig, hash))
// Cross-check: the same inputs must also accept on-chain via the
// precompile.
input := EncodeInput(ModeMLDSA65, sig, sk.PublicKey.Bytes(), hash)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, runErr := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, runErr)
require.Equal(t, abiTrueClone(), out)
}
// TestP3Q_Verify_SizeMismatchTyped confirms the standalone Verify
// surfaces size-mismatch errors as typed errors (distinct from
// FIPS 204 reject), so off-chain callers can distinguish "malformed
// input" from "good input, bad signature".
func TestP3Q_Verify_SizeMismatchTyped(t *testing.T) {
hash := make([]byte, MessageHashSize)
short := make([]byte, 10)
require.Error(t, Verify(ModeMLDSA65, short, make([]byte, MLDSA65SignatureSize), hash), "short pk must surface typed error")
require.Error(t, Verify(ModeMLDSA65, make([]byte, MLDSA65PublicKeySize), short, hash), "short sig must surface typed error")
require.Error(t, Verify(ModeMLDSA65, make([]byte, MLDSA65PublicKeySize), make([]byte, MLDSA65SignatureSize), make([]byte, 31)), "31-byte hash must surface typed error")
require.ErrorIs(t, Verify(0x99, make([]byte, MLDSA65PublicKeySize), make([]byte, MLDSA65SignatureSize), hash), ErrUnsupportedMode)
}
// TestP3Q_EncodeInput_RoundTrip verifies EncodeInput is the inverse of
// the precompile's wire-format parser. We construct a known input,
// dispatch through the precompile, and assert the parser interpreted
// every field identically to the encoder.
//
// This is the contract test for the canonical wire-format encoder.
func TestP3Q_EncodeInput_RoundTrip(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := bytes.Repeat([]byte{0x10}, 32)
sig, err := sk.SignCtx(rand.Reader, hash, PrecompileCtx)
require.NoError(t, err)
pkBytes := sk.PublicKey.Bytes()
input := EncodeInput(ModeMLDSA65, sig, pkBytes, hash)
require.Len(t, input,
KindByte+ModeByte+LengthFieldSize+MLDSA65SignatureSize+LengthFieldSize+MLDSA65PublicKeySize+MessageHashSize,
"EncodeInput must emit exactly the LP-218 wire layout length (kind + mode + ...)")
// Byte-by-byte field positions per the decomplected LP-218 wire layout.
require.Equal(t, KindPulsar, input[0], "byte 0 must be KindPulsar (kind dispatch)")
require.Equal(t, ModeMLDSA65, input[1], "byte 1 must be ModeMLDSA65")
require.Equal(t, []byte{0x00, 0x00, 0x0C, 0xED}, input[2:6], "sigLen field must be BE32 3309")
require.Equal(t, sig, input[6:6+MLDSA65SignatureSize])
pkOff := 6 + MLDSA65SignatureSize
require.Equal(t, []byte{0x00, 0x00, 0x07, 0xA0}, input[pkOff:pkOff+4], "pkLen field must be BE32 1952")
require.Equal(t, pkBytes, input[pkOff+4:pkOff+4+MLDSA65PublicKeySize])
require.Equal(t, hash, input[pkOff+4+MLDSA65PublicKeySize:])
}
// TestP3Q_AcceptsTrailingPadding confirms inputs longer than the
// computed message-hash tail are accepted (padding ignored) — matches
// the sibling STARK-FRI precompile's padded-input convention.
func TestP3Q_AcceptsTrailingPadding(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := bytes.Repeat([]byte{0x5A}, 32)
sig, err := sk.SignCtx(rand.Reader, hash, PrecompileCtx)
require.NoError(t, err)
canonical := EncodeInput(ModeMLDSA65, sig, sk.PublicKey.Bytes(), hash)
padded := append(append([]byte(nil), canonical...), make([]byte, 64)...)
gas := P3QVerifyPrecompile.RequiredGas(padded) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, padded, gas, true)
require.NoError(t, err)
require.Equal(t, abiTrueClone(), out, "trailing padding must be ignored, signature must verify")
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
import (
"github.com/luxfi/precompile/contract"
"github.com/luxfi/precompile/modules"
"github.com/luxfi/precompile/precompileconfig"
)
var _ contract.Configurator = &configurator{}
// ConfigKey is the globally unique JSON config key for this
// precompile. Distinct from "pulsarVerify" (slot 0x012204) so a
// chain config can independently enable/disable the LP-218 rollup-
// commit verifier.
const ConfigKey = "p3qVerify"
type configurator struct{}
func init() {
if err := modules.RegisterModule(modules.Module{
ConfigKey: ConfigKey,
Address: ContractP3QVerifyAddress,
Contract: P3QVerifyPrecompile,
Configurator: &configurator{},
}); err != nil {
panic(err)
}
}
func (*configurator) MakeConfig() precompileconfig.Config { return &Config{} }
func (*configurator) Configure(
_ precompileconfig.ChainConfig,
_ precompileconfig.Config,
_ contract.StateDB,
_ contract.ConfigurationBlockContext,
) error {
return nil
}
// Config is the precompileconfig.Config for the P3Q (Post-Quantum
// Pulsar Proof) verify precompile at slot 0x012205.
//
// Activation: per LP-218, P3Q is callable from the genesis block of
// the new final Lux network (2025-12-25 16:20 Pacific, unix
// 1766708400). Chain configs that wire a non-zero Upgrade.Timestamp
// can defer activation to later heights; the canonical mainnet
// config pins the activation timestamp to the LP-218 genesis.
type Config struct {
Upgrade precompileconfig.Upgrade `json:"upgrade"`
}
func (c *Config) Key() string { return ConfigKey }
func (c *Config) Timestamp() *uint64 { return c.Upgrade.Timestamp() }
func (c *Config) IsDisabled() bool { return c.Upgrade.Disable }
func (c *Config) Equal(cfg precompileconfig.Config) bool {
other, ok := cfg.(*Config)
if !ok {
return false
}
return c.Upgrade.Equal(&other.Upgrade)
}
func (c *Config) Verify(precompileconfig.ChainConfig) error { return nil }
+18 -1
View File
@@ -206,7 +206,24 @@ randombytes_close(void)
return 0;
}
void
/*
* NaCl-compatibility bare `randombytes`. Kept here for ABI symmetry
* with the libsodium-derived sr25519-donna header, but made file-local
* (`static`) because:
*
* 1. No code inside sr25519 calls it — sr25519's own sources use
* `sr25519_randombytes` (the namespaced variant declared in
* sr25519-randombytes-custom.h).
* 2. PQClean's HQC reference (linked via libluxgpu_hqc.a and
* consumed by luxfi/crypto/hqc + luxfi/accel/ops/code) provides
* its OWN strong `randombytes(uint8_t*, size_t)` with a different
* signature. When both packages end up in the same Go binary
* (e.g. chains/evm), the linker errors with duplicate symbol
* '_randombytes'. Marking this one `static` resolves the
* collision without touching PQClean's contract.
*/
__attribute__((unused))
static void
randombytes(unsigned char * const buf, const unsigned long long buf_len)
{
assert(buf_len <= SIZE_MAX);
+13 -5
View File
@@ -106,11 +106,19 @@ int randombytes_set_implementation(randombytes_implementation *impl)
SR25519_DONNA_EXPORT
const char *randombytes_implementation_name(void);
/* -- NaCl compatibility interface -- */
SR25519_DONNA_EXPORT
void randombytes(unsigned char * const buf, const unsigned long long buf_len)
__attribute__ ((nonnull));
/* -- NaCl compatibility interface --
*
* The bare `randombytes(unsigned char*, unsigned long long)` declared
* here used to be a public export. It is now a file-local definition
* in sr25519-randombytes-default.c (`static`) to avoid colliding with
* the strong `randombytes(uint8_t*, size_t)` provided by PQClean's
* HQC reference (libluxgpu_hqc.a + luxfi/accel/ops/code +
* luxfi/crypto/hqc). No code in this archive calls the bare
* `randombytes` — every internal caller uses the namespaced
* sr25519_randombytes() in sr25519-randombytes-custom.h. The
* declaration is left out of the public header so that downstream
* consumers cannot link against a now-hidden symbol.
*/
#ifdef __cplusplus
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package starkfri
import (
"encoding/binary"
"testing"
"github.com/luxfi/geth/common"
)
// buildBenchInput re-implements the package-local buildInput helper for
// bench scope (cannot share private test helpers across files cleanly).
// Wire: [1B version][4B proof_len][N proof][4B pub_len][M pub].
func buildBenchInput(version byte, proof, pub []byte) []byte {
out := make([]byte, 0, 1+4+len(proof)+4+len(pub))
out = append(out, version)
var pl [4]byte
binary.BigEndian.PutUint32(pl[:], uint32(len(proof)))
out = append(out, pl[:]...)
out = append(out, proof...)
var ul [4]byte
binary.BigEndian.PutUint32(ul[:], uint32(len(pub)))
out = append(out, ul[:]...)
out = append(out, pub...)
return out
}
// BenchmarkStarkFRI_Verify_AcceptAll measures the full precompile path
// (gas check + wire parse + MagicHeader check + verifier dispatch) with
// an accept-all callback registered. Proof body is a representative 2KiB
// payload — typical STARK-FRI proof footprint for Goldilocks/Plonky3.
func BenchmarkStarkFRI_Verify_AcceptAll(b *testing.B) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
body := make([]byte, 2048-len(MagicHeader))
for i := range body {
body[i] = byte(i)
}
proof := append([]byte(MagicHeader), body...)
pub := make([]byte, 128)
input := buildBenchInput(VersionV1, proof, pub)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
if err != nil {
b.Fatalf("Run: %v", err)
}
}
}
// BenchmarkStarkFRI_Verify_RejectMagic measures the structural-only
// path: MagicHeader missing, verifier never invoked. Tests how cheap
// the early-reject is when input fails the magic check.
func BenchmarkStarkFRI_Verify_RejectMagic(b *testing.B) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
body := make([]byte, 2048)
for i := range body {
body[i] = byte(i)
}
// Wrong magic header — first 4 bytes are not "P3Q1".
proof := body
pub := make([]byte, 128)
input := buildBenchInput(VersionV1, proof, pub)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _ = StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
}
}
// BenchmarkStarkFRI_Verify_Standalone measures the in-process Verify
// entry-point (off-chain pre-flight) at the same 2 KiB proof size.
func BenchmarkStarkFRI_Verify_Standalone(b *testing.B) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
body := make([]byte, 2048-len(MagicHeader))
for i := range body {
body[i] = byte(i)
}
proof := append([]byte(MagicHeader), body...)
pub := make([]byte, 128)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Verify(proof, pub)
if err != nil {
b.Fatalf("Verify: %v", err)
}
}
}
// BenchmarkStarkFRI_RequiredGas isolates the gas-only path.
func BenchmarkStarkFRI_RequiredGas(b *testing.B) {
input := make([]byte, 2048)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = StarkFRIVerifyPrecompile.RequiredGas(input)
}
}
+69 -41
View File
@@ -1,24 +1,38 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package p3q implements the EVM precompile entry for the P3Q strict
// post-quantum STARK verifier, the Lux family canonical proof system.
// Package starkfri implements the EVM precompile entry for the
// strict-PQ STARK verifier (a Plonky3 fork with the classical-curve
// surface stripped out: cSHAKE256 (FIPS 202 / SP 800-185) Merkle
// hashes over the Goldilocks 64-bit prime field, no KZG, no
// pairings).
//
// P3Q is a fork of Plonky3 with the classical-curve surface stripped
// out: cSHAKE256 (FIPS 202 / SP 800-185) Merkle hashes over the
// Goldilocks 64-bit prime field, no KZG, no pairings. The wire-byte
// proof-backend ID is `ProofBackendP3QSTARKFRISHA3 = 0x22`, pinned in
// `consensus/config/pq_mode.go`; this precompile is the on-chain
// dispatch point for that backend.
// History. This dispatch was previously misnamed "P3Q" and registered
// at slot 0x012205. Per HANZO-CRYPTO-SUITE §5.2, ROADMAP-CRYPTO-STACK
// §B.11, and LP-218, slot 0x012205 is reserved for "P3Q — Post-Quantum
// Pulsar Proof", the on-chain Pulsar / FIPS 204 ML-DSA verifier. The
// STARK dispatch lives here under its actual algorithmic identity
// (STARK / FRI / SHAKE) at a separate slot. The previous P3Q-as-STARK
// label was an aliasing error; this rename closes it.
//
// LP-4200 unified PQCrypto block:
// LP-4200 unified PQCrypto block (post-fix):
//
// 0x012201 = ML-KEM (Module-LWE KEM, FIPS 203)
// 0x012202 = ML-DSA (Module-LWE single-party signature, FIPS 204)
// 0x012203 = SLH-DSA (hash-based signature, FIPS 205)
// 0x012204 = Pulsar (Module-LWE threshold FIPS 204, byte-equal)
// 0x012205 = P3Q ← this precompile (strict-PQ STARK)
// 0x012206 = Corona
// 0x012205 = P3Q (Post-Quantum Pulsar Proof — see precompile/p3q)
// 0x012206 = Corona (Ring-LWE threshold)
// 0x012207 = Magnetar (SLH-DSA threshold, FIPS 205 byte-equal)
// 0x012208 = HQC (code-based KEM, family-disjoint backup)
// 0x012220 = STARK-FRI ← this precompile (strict-PQ STARK / FRI / SHAKE)
//
// The 0x012220 slot is a placeholder pending dedicated LP allocation
// (the prior 0x012205 claim is reverted to its canonical owner). The
// 0x012220 base is one nibble above the dense PQ-signature block,
// chosen so the strict-PQ STARK family can grow its own sub-range
// (0x012220..0x01222F) without recolliding with the 0x012201..0x01220F
// PQ-signature block.
//
// Wire format (single dispatch, version byte differentiates future
// proof formats; first deployment is v0x01):
@@ -29,14 +43,22 @@
// [4 bytes pub_len (big-endian)]
// [pub_len bytes public_inputs]
//
// The Rust prover / verifier in `~/work/lux/p3q` runs out-of-band; at
// startup the node calls `p3q.RegisterVerifier` to wire the Go-side
// callback that actually invokes the FFI verifier. Without a
// registered verifier the precompile returns ErrVerifierNotRegistered.
// Structural validation (length checks + MagicHeader) runs in the
// precompile itself and is constant-time-friendly: no secret-dependent
// branches, public inputs and the proof are non-secret by construction.
package p3q
// The MagicHeader value "P3Q1" is preserved verbatim from the pre-
// rename era to keep cryptographer sign-off and EasyCrypt / Lean /
// Jasmin proof artifacts byte-identical against on-chain calldata.
// Renaming the header would invalidate the wire-format proofs; the
// header is a wire-bytes tag, not a name claim. A future v2 wire
// format may rename it to "SFR1" or similar once the proof artifacts
// are refreshed.
//
// The Rust prover / verifier runs out-of-band; at startup the node
// calls `starkfri.RegisterVerifier` to wire the Go-side callback that
// actually invokes the FFI verifier. Without a registered verifier
// the precompile returns ErrVerifierNotRegistered. Structural
// validation (length checks + MagicHeader) runs in the precompile
// itself and is constant-time-friendly: no secret-dependent branches,
// public inputs and the proof are non-secret by construction.
package starkfri
import (
"encoding/binary"
@@ -47,16 +69,21 @@ import (
"github.com/luxfi/precompile/contract"
)
// ContractP3QVerifyAddress is the canonical LP-4200 slot for P3Q.
var ContractP3QVerifyAddress = common.HexToAddress("0x0000000000000000000000000000000000012205")
// ContractStarkFRIVerifyAddress is the canonical slot for the
// strict-PQ STARK verifier dispatch. Placeholder slot pending
// dedicated LP allocation; see the package doc comment for rationale.
var ContractStarkFRIVerifyAddress = common.HexToAddress("0x0000000000000000000000000000000000012220")
// P3QVerifyPrecompile is the singleton instance.
var P3QVerifyPrecompile = &p3qVerifyPrecompile{}
// StarkFRIVerifyPrecompile is the singleton instance.
var StarkFRIVerifyPrecompile = &starkFRIVerifyPrecompile{}
var _ contract.StatefulPrecompiledContract = &p3qVerifyPrecompile{}
var _ contract.StatefulPrecompiledContract = &starkFRIVerifyPrecompile{}
// MagicHeader is the 4-byte prefix every P3Q proof MUST begin with.
// Ties the wire to the Plonky3 fork's cSHAKE256/Goldilocks profile.
// MagicHeader is the 4-byte prefix every STARK-FRI proof MUST begin
// with. Ties the wire to the Plonky3 fork's cSHAKE256/Goldilocks
// profile. Value preserved verbatim from the pre-rename era so
// EasyCrypt / Lean / Jasmin / dudect proof artifacts continue to
// reference the byte-identical wire tag.
const MagicHeader = "P3Q1"
// Wire layout constants.
@@ -84,24 +111,24 @@ const (
// Error sentinels returned by this precompile.
var (
ErrInvalidInputLength = errors.New("p3q: invalid input length")
ErrInvalidVersion = errors.New("p3q: invalid version byte")
ErrVerifierNotRegistered = errors.New("p3q: verifier not registered")
ErrInvalidProof = errors.New("p3q: proof verification failed")
ErrInvalidInputLength = errors.New("starkfri: invalid input length")
ErrInvalidVersion = errors.New("starkfri: invalid version byte")
ErrVerifierNotRegistered = errors.New("starkfri: verifier not registered")
ErrInvalidProof = errors.New("starkfri: proof verification failed")
)
// VerifierFn is the Go-side callback that bridges to the out-of-band
// P3Q (Plonky3 fork) Rust verifier. Returns (ok, err). A non-nil err
// indicates an internal verifier failure (FFI, decode); ok=false with
// nil err indicates a well-formed proof that did not verify.
// STARK-FRI (Plonky3 fork) Rust verifier. Returns (ok, err). A non-nil
// err indicates an internal verifier failure (FFI, decode); ok=false
// with nil err indicates a well-formed proof that did not verify.
type VerifierFn func(version byte, proof, pubInputs []byte) (bool, error)
// verifier is the registered callback. Atomic.Value so RegisterVerifier
// is safe to call once at node startup without locking the hot path.
var verifier atomic.Value // VerifierFn
// RegisterVerifier wires the actual P3Q verifier. Called once at node
// startup. Passing nil clears the registration (useful for tests).
// RegisterVerifier wires the actual STARK-FRI verifier. Called once at
// node startup. Passing nil clears the registration (useful for tests).
func RegisterVerifier(fn VerifierFn) {
if fn == nil {
verifier.Store(VerifierFn(nil))
@@ -124,7 +151,8 @@ func loadVerifier() VerifierFn {
//
// Wire-format compatibility: proof must begin with MagicHeader "P3Q1"
// (the same structural check the on-chain precompile applies). version
// is fixed to VersionV1 — there is currently only one P3Q wire version.
// is fixed to VersionV1 — there is currently only one STARK-FRI wire
// version.
//
// Returns (true, nil) on a verified proof; (false, nil) on a well-formed
// but invalid proof; (false, ErrVerifierNotRegistered) when no FFI
@@ -141,21 +169,21 @@ func Verify(proof, pubInputs []byte) (bool, error) {
return fn(VersionV1, proof, pubInputs)
}
type p3qVerifyPrecompile struct{}
type starkFRIVerifyPrecompile struct{}
// RequiredGas returns BaseVerifyGas + PerByteGas * len(input). STARK
// verifier cost is dominated by FRI query rounds (logarithmic in
// circuit size); per-byte covers serialization / hashing overhead.
func (p *p3qVerifyPrecompile) RequiredGas(input []byte) uint64 {
func (p *starkFRIVerifyPrecompile) RequiredGas(input []byte) uint64 {
return BaseVerifyGas + uint64(len(input))*PerByteGas
}
// Run verifies a P3Q STARK proof.
// Run verifies a STARK-FRI proof.
//
// Returns:
// - empty bytes, nil error on successful verification.
// - empty bytes, typed error on any failure.
func (p *p3qVerifyPrecompile) Run(
func (p *starkFRIVerifyPrecompile) Run(
_ contract.AccessibleState,
_ common.Address,
_ common.Address,
@@ -194,7 +222,7 @@ func (p *p3qVerifyPrecompile) Run(
}
pub := input[off : off+int(pubLen)]
// Structural check: proof must begin with the MagicHeader. P3Q
// Structural check: proof must begin with the MagicHeader. STARK-FRI
// proofs are non-secret, so byte-equality is fine.
if len(proof) < len(MagicHeader) || string(proof[:len(MagicHeader)]) != MagicHeader {
return nil, remaining, ErrInvalidProof
+56 -56
View File
@@ -1,7 +1,7 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
package starkfri
import (
"bytes"
@@ -15,18 +15,18 @@ import (
"github.com/stretchr/testify/require"
)
// TestP3Q_E2E_VerifiesNRealishProofs registers a counting verifier
// that emulates the structure of the real backend (cross-validates
// the (version, proof, pub) triple), runs N round trips through the
// precompile, and asserts each call lands at the backend with the
// correct parsed triple.
// TestStarkFRI_E2E_VerifiesNRealishProofs registers a counting
// verifier that emulates the structure of the real backend
// (cross-validates the (version, proof, pub) triple), runs N round
// trips through the precompile, and asserts each call lands at the
// backend with the correct parsed triple.
//
// This is the e2e equivalent of "verify N real Pulsar signatures
// This is the e2e equivalent of "verify N real STARK-FRI proofs
// through the EVM precompile dispatch": the backend is a counting
// stand-in here because the real Rust verifier is brought in via
// cgo at startup outside the Go unit-test build, but the byte path
// from EVM calldata to (version, proof, pub) is exercised in full.
func TestP3Q_E2E_VerifiesNRealishProofs(t *testing.T) {
func TestStarkFRI_E2E_VerifiesNRealishProofs(t *testing.T) {
const N = 64
var (
@@ -76,9 +76,9 @@ func TestP3Q_E2E_VerifiesNRealishProofs(t *testing.T) {
proof := append([]byte(MagicHeader), body...)
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, gasLeft, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, gasLeft, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
require.NoError(t, err, "round %d", i)
@@ -94,7 +94,7 @@ func TestP3Q_E2E_VerifiesNRealishProofs(t *testing.T) {
// TestP3Q_E2E_RejectsBeforeBackendOnBadMagic confirms the structural
// pre-filter rejects bad magic BEFORE the backend is invoked, even
// when calldata is otherwise well-formed.
func TestP3Q_E2E_RejectsBeforeBackendOnBadMagic(t *testing.T) {
func TestStarkFRI_E2E_RejectsBeforeBackendOnBadMagic(t *testing.T) {
called := 0
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called++
@@ -109,9 +109,9 @@ func TestP3Q_E2E_RejectsBeforeBackendOnBadMagic(t *testing.T) {
proof := append([]byte(prefix), body...)
pub := []byte{0xde, 0xad, 0xbe, 0xef}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err = P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err = StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
require.ErrorIs(t, err, ErrInvalidProof, "prefix=%q", prefix)
@@ -122,7 +122,7 @@ func TestP3Q_E2E_RejectsBeforeBackendOnBadMagic(t *testing.T) {
// TestP3Q_E2E_VerifierFailureSurfacesAsInvalidProof confirms that a
// backend error or rejected verification surfaces as ErrInvalidProof
// at the precompile boundary.
func TestP3Q_E2E_VerifierFailureSurfacesAsInvalidProof(t *testing.T) {
func TestStarkFRI_E2E_VerifierFailureSurfacesAsInvalidProof(t *testing.T) {
cases := []struct {
name string
fn VerifierFn
@@ -150,9 +150,9 @@ func TestP3Q_E2E_VerifierFailureSurfacesAsInvalidProof(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
RegisterVerifier(tc.fn)
defer RegisterVerifier(nil)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
if tc.want != nil {
@@ -169,11 +169,11 @@ func TestP3Q_E2E_VerifierFailureSurfacesAsInvalidProof(t *testing.T) {
// TestP3Q_E2E_GasMonotonic confirms that required_gas is monotonic in
// input length — the EC lemma `P3Q_Gas_Model.ec::gas_monotonic` is
// stated formally; this is its empirical realisation.
func TestP3Q_E2E_GasMonotonic(t *testing.T) {
func TestStarkFRI_E2E_GasMonotonic(t *testing.T) {
prev := uint64(0)
for _, n := range []int{0, 1, 13, 100, 1024, 10240, 102400} {
input := make([]byte, n)
gas := P3QVerifyPrecompile.RequiredGas(input)
gas := StarkFRIVerifyPrecompile.RequiredGas(input)
require.GreaterOrEqual(t, gas, prev, "n=%d", n)
prev = gas
}
@@ -182,7 +182,7 @@ func TestP3Q_E2E_GasMonotonic(t *testing.T) {
// TestP3Q_E2E_GasUniformAcrossErrors confirms that all non-OOG error
// paths return the same `remaining` value as the success path. The EC
// lemma `P3Q_Gas_Model.ec::uniform_billing` is its formal counterpart.
func TestP3Q_E2E_GasUniformAcrossErrors(t *testing.T) {
func TestStarkFRI_E2E_GasUniformAcrossErrors(t *testing.T) {
// Build five inputs each producing one of the five error modes,
// AND a success input. All MUST land on the same `remaining`
// when given identical-length calldata at identical supplied_gas.
@@ -193,18 +193,18 @@ func TestP3Q_E2E_GasUniformAcrossErrors(t *testing.T) {
// for inputs of identical length, regardless of content.
const inputLen = 256
probe := make([]byte, inputLen)
wantGas := P3QVerifyPrecompile.RequiredGas(probe)
wantGas := StarkFRIVerifyPrecompile.RequiredGas(probe)
for i := 0; i < 16; i++ {
_, err := rand.Read(probe)
require.NoError(t, err)
gotGas := P3QVerifyPrecompile.RequiredGas(probe)
gotGas := StarkFRIVerifyPrecompile.RequiredGas(probe)
require.Equal(t, wantGas, gotGas, "iteration %d", i)
}
}
// TestP3Q_E2E_OOGShortCircuit confirms that supplied_gas <
// required_gas yields ErrOutOfGas and never reaches the backend.
func TestP3Q_E2E_OOGShortCircuit(t *testing.T) {
func TestStarkFRI_E2E_OOGShortCircuit(t *testing.T) {
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
@@ -215,10 +215,10 @@ func TestP3Q_E2E_OOGShortCircuit(t *testing.T) {
proof := append([]byte(MagicHeader), make([]byte, 64)...)
pub := []byte{0x01}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) - 1 // one wei short
gas := StarkFRIVerifyPrecompile.RequiredGas(input) - 1 // one wei short
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
require.Error(t, err)
@@ -229,7 +229,7 @@ func TestP3Q_E2E_OOGShortCircuit(t *testing.T) {
// (version, proof, pub) via the same wire format that buildInput uses,
// dispatch through Run(), and assert that the backend observes exactly
// the same (version, proof, pub) triple.
func TestP3Q_E2E_RoundTripParser(t *testing.T) {
func TestStarkFRI_E2E_RoundTripParser(t *testing.T) {
wantVersion := VersionV1
wantProof := append([]byte(MagicHeader), []byte("P3Q-STARK-payload-bytes-N-rounds")...)
wantPub := []byte{0xab, 0xcd, 0xef, 0x12, 0x34}
@@ -248,10 +248,10 @@ func TestP3Q_E2E_RoundTripParser(t *testing.T) {
defer RegisterVerifier(nil)
input := buildInput(wantVersion, wantProof, wantPub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
require.NoError(t, err)
@@ -283,7 +283,7 @@ func helperBuildBadLenInput(declaredProofLen uint32) []byte {
// TestP3Q_Fuzz_TruncatedInput exercises the structural pre-filter
// against truncated calldata. Every truncation MUST fail with
// ErrInvalidInputLength; backend MUST NOT be invoked.
func TestP3Q_Fuzz_TruncatedInput(t *testing.T) {
func TestStarkFRI_Fuzz_TruncatedInput(t *testing.T) {
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
@@ -294,9 +294,9 @@ func TestP3Q_Fuzz_TruncatedInput(t *testing.T) {
full := buildInput(VersionV1, append([]byte(MagicHeader), make([]byte, 64)...), []byte{0xaa, 0xbb})
for k := 0; k < len(full); k++ {
input := full[:k]
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
// Anything shorter than the full input must error. The two
@@ -312,7 +312,7 @@ func TestP3Q_Fuzz_TruncatedInput(t *testing.T) {
// AFTER the declared pub_len ARE accepted by contract.go (the
// precompile decodes exactly proof_len + pub_len + length-prefixes
// and ignores trailing bytes). This test pins that semantic.
func TestP3Q_Fuzz_PaddedInput(t *testing.T) {
func TestStarkFRI_Fuzz_PaddedInput(t *testing.T) {
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
@@ -322,9 +322,9 @@ func TestP3Q_Fuzz_PaddedInput(t *testing.T) {
canonical := buildInput(VersionV1, append([]byte(MagicHeader), make([]byte, 16)...), []byte{0xaa})
padded := append(bytes.Clone(canonical), make([]byte, 64)...)
gas := P3QVerifyPrecompile.RequiredGas(padded) * 2
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(padded) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
padded, gas, true,
)
// Padding is accepted; the precompile decodes exactly the declared
@@ -337,7 +337,7 @@ func TestP3Q_Fuzz_PaddedInput(t *testing.T) {
// canonical input. The structural pre-filter must catch any flip that
// touches the version byte, length fields, or magic header; flips in
// the proof body or pub body propagate to the backend.
func TestP3Q_Fuzz_BitFlipped(t *testing.T) {
func TestStarkFRI_Fuzz_BitFlipped(t *testing.T) {
const iterations = 256
backendInvoked := 0
@@ -359,9 +359,9 @@ func TestP3Q_Fuzz_BitFlipped(t *testing.T) {
off := binary.BigEndian.Uint64(idx[:]) % uint64(len(canonical))
mutant := bytes.Clone(canonical)
mutant[off] ^= 0x55
gas := P3QVerifyPrecompile.RequiredGas(mutant) * 2
_, _, err = P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(mutant) * 2
_, _, err = StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
mutant, gas, true,
)
// A flip at offset 0 hits the version byte → InvalidVersion.
@@ -394,7 +394,7 @@ func TestP3Q_Fuzz_BitFlipped(t *testing.T) {
// TestP3Q_Fuzz_BadLengthField crafts inputs whose declared proof_len
// is wildly inconsistent with the buffer length. All MUST fail with
// ErrInvalidInputLength; backend MUST NOT be invoked.
func TestP3Q_Fuzz_BadLengthField(t *testing.T) {
func TestStarkFRI_Fuzz_BadLengthField(t *testing.T) {
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
@@ -404,9 +404,9 @@ func TestP3Q_Fuzz_BadLengthField(t *testing.T) {
for _, declared := range []uint32{0xFFFFFFFF, 0x80000000, 0x40000000, 0x10000000} {
input := helperBuildBadLenInput(declared)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
require.ErrorIs(t, err, ErrInvalidInputLength,
@@ -417,7 +417,7 @@ func TestP3Q_Fuzz_BadLengthField(t *testing.T) {
// TestP3Q_Fuzz_WrongVersion exercises versions other than 0x01.
// All MUST be rejected with ErrInvalidVersion BEFORE backend dispatch.
func TestP3Q_Fuzz_WrongVersion(t *testing.T) {
func TestStarkFRI_Fuzz_WrongVersion(t *testing.T) {
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
@@ -432,9 +432,9 @@ func TestP3Q_Fuzz_WrongVersion(t *testing.T) {
proof := append([]byte(MagicHeader), make([]byte, 64)...)
pub := []byte{0xaa}
input := buildInput(byte(v), proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
require.ErrorIs(t, err, ErrInvalidVersion, "v=0x%x", v)
@@ -446,7 +446,7 @@ func TestP3Q_Fuzz_WrongVersion(t *testing.T) {
// never panic on arbitrary calldata; the backend (registered to
// always accept) must never see a triple that fails its own
// preconditions.
func FuzzP3Q_Dispatch(f *testing.F) {
func FuzzStarkFRI_Dispatch(f *testing.F) {
// Seed corpus: canonical, truncated, padded.
f.Add([]byte{0x01, 0x00, 0x00, 0x00, 0x04, 0x50, 0x33, 0x51, 0x31, 0x00, 0x00, 0x00, 0x00})
f.Add([]byte("short"))
@@ -474,9 +474,9 @@ func FuzzP3Q_Dispatch(f *testing.F) {
t.Fatalf("precompile panicked on input: %v", r)
}
}()
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, _ = P3QVerifyPrecompile.Run(
nil, common.Address{}, ContractP3QVerifyAddress,
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, _ = StarkFRIVerifyPrecompile.Run(
nil, common.Address{}, ContractStarkFRIVerifyAddress,
input, gas, true,
)
})
+39 -36
View File
@@ -1,7 +1,7 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
package starkfri
import (
"encoding/binary"
@@ -11,25 +11,28 @@ import (
"github.com/stretchr/testify/require"
)
// TestP3Q_Address pins the canonical LP-4200 slot 0x012205.
func TestP3Q_Address(t *testing.T) {
want := common.HexToAddress("0x0000000000000000000000000000000000012205")
require.Equal(t, want, ContractP3QVerifyAddress)
// TestStarkFRI_Address pins the slot allocated to the strict-PQ
// STARK-FRI verifier dispatch (placeholder 0x012220 pending dedicated
// LP allocation; see contract.go package doc).
func TestStarkFRI_Address(t *testing.T) {
want := common.HexToAddress("0x0000000000000000000000000000000000012220")
require.Equal(t, want, ContractStarkFRIVerifyAddress)
}
// TestP3Q_RequiredGas verifies the gas formula: 200_000 + 10 * len(input).
func TestP3Q_RequiredGas(t *testing.T) {
// TestStarkFRI_RequiredGas verifies the gas formula: 200_000 + 10 * len(input).
func TestStarkFRI_RequiredGas(t *testing.T) {
for _, n := range []int{0, 1, 64, 256, 4096, 131072} {
input := make([]byte, n)
got := P3QVerifyPrecompile.RequiredGas(input)
got := StarkFRIVerifyPrecompile.RequiredGas(input)
want := BaseVerifyGas + uint64(n)*PerByteGas
require.Equal(t, want, got, "len=%d", n)
}
}
// TestP3Q_RejectsBadMagic ensures proofs without the "P3Q1" header are
// rejected with ErrInvalidProof (even when a verifier is registered).
func TestP3Q_RejectsBadMagic(t *testing.T) {
// TestStarkFRI_RejectsBadMagic ensures proofs without the "P3Q1" header
// are rejected with ErrInvalidProof (even when a verifier is
// registered).
func TestStarkFRI_RejectsBadMagic(t *testing.T) {
// Register a verifier that would say yes if asked; the structural
// check must reject before the verifier is invoked.
called := false
@@ -39,30 +42,30 @@ func TestP3Q_RejectsBadMagic(t *testing.T) {
})
defer RegisterVerifier(nil)
proof := []byte("BOGUS-not-a-p3q-proof-but-long-enough")
proof := []byte("BOGUS-not-a-starkfri-proof-but-long-enough")
pub := []byte{0xde, 0xad, 0xbe, 0xef}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidProof)
require.False(t, called, "verifier callback must not run on bad magic")
}
// TestP3Q_RejectsShortInput ensures inputs below MinInputLength fail with
// ErrInvalidInputLength.
func TestP3Q_RejectsShortInput(t *testing.T) {
// TestStarkFRI_RejectsShortInput ensures inputs below MinInputLength
// fail with ErrInvalidInputLength.
func TestStarkFRI_RejectsShortInput(t *testing.T) {
for _, sz := range []int{0, 1, 5, MinInputLength - 1} {
input := make([]byte, sz)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidInputLength, "len=%d", sz)
}
}
// TestP3Q_RoundTrip_WithRegisteredVerifier registers a fake verifier
// that accepts and asserts Run() succeeds end-to-end.
func TestP3Q_RoundTrip_WithRegisteredVerifier(t *testing.T) {
// TestStarkFRI_RoundTrip_WithRegisteredVerifier registers a fake
// verifier that accepts and asserts Run() succeeds end-to-end.
func TestStarkFRI_RoundTrip_WithRegisteredVerifier(t *testing.T) {
saw := struct {
version byte
proof []byte
@@ -80,8 +83,8 @@ func TestP3Q_RoundTrip_WithRegisteredVerifier(t *testing.T) {
pub := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, gasLeft, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
out, gasLeft, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Empty(t, out)
require.Less(t, gasLeft, gas)
@@ -90,24 +93,24 @@ func TestP3Q_RoundTrip_WithRegisteredVerifier(t *testing.T) {
require.Equal(t, pub, saw.pub)
}
// TestP3Q_NoVerifierRegistered confirms that without a registered
// TestStarkFRI_NoVerifierRegistered confirms that without a registered
// verifier callback, structurally-valid input yields
// ErrVerifierNotRegistered.
func TestP3Q_NoVerifierRegistered(t *testing.T) {
func TestStarkFRI_NoVerifierRegistered(t *testing.T) {
RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
pub := []byte{0xaa}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrVerifierNotRegistered)
}
// TestP3Q_RejectsBadVersion confirms versions other than 0x01 are
// TestStarkFRI_RejectsBadVersion confirms versions other than 0x01 are
// rejected with ErrInvalidVersion.
func TestP3Q_RejectsBadVersion(t *testing.T) {
func TestStarkFRI_RejectsBadVersion(t *testing.T) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
@@ -115,14 +118,14 @@ func TestP3Q_RejectsBadVersion(t *testing.T) {
pub := []byte{0xaa}
input := buildInput(0x00, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidVersion)
}
// TestP3Q_VerifierReturnsFalse confirms that when the registered
// TestStarkFRI_VerifierReturnsFalse confirms that when the registered
// verifier reports the proof as invalid we surface ErrInvalidProof.
func TestP3Q_VerifierReturnsFalse(t *testing.T) {
func TestStarkFRI_VerifierReturnsFalse(t *testing.T) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return false, nil })
defer RegisterVerifier(nil)
@@ -130,8 +133,8 @@ func TestP3Q_VerifierReturnsFalse(t *testing.T) {
pub := []byte{0xaa}
input := buildInput(VersionV1, proof, pub)
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
_, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
require.ErrorIs(t, err, ErrInvalidProof)
}
+5 -5
View File
@@ -1,7 +1,7 @@
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
package starkfri
import (
"github.com/luxfi/precompile/contract"
@@ -11,15 +11,15 @@ import (
var _ contract.Configurator = &configurator{}
const ConfigKey = "p3qVerify"
const ConfigKey = "starkfriVerify"
type configurator struct{}
func init() {
if err := modules.RegisterModule(modules.Module{
ConfigKey: ConfigKey,
Address: ContractP3QVerifyAddress,
Contract: P3QVerifyPrecompile,
Address: ContractStarkFRIVerifyAddress,
Contract: StarkFRIVerifyPrecompile,
Configurator: &configurator{},
}); err != nil {
panic(err)
@@ -37,7 +37,7 @@ func (*configurator) Configure(
return nil
}
// Config is the precompileconfig.Config for the P3Q strict-PQ STARK
// Config is the precompileconfig.Config for the strict-PQ STARK-FRI
// verify precompile.
type Config struct {
Upgrade precompileconfig.Upgrade `json:"upgrade"`