mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
WIP: session checkpoint 2026-06-05 (codec rip in flight)
This commit is contained in:
+212
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user