mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
* precompile: pulsar threshold ML-DSA at LP-4200 0x012204 Adds the Pulsar precompile slot for threshold ML-DSA verification. Pulsar's wire format is byte-equal to FIPS 204 ML-DSA (Class N1 manifesto), so the verification op dispatches to luxfi/crypto/mldsa under a Pulsar-specific domain-separation context. - Address: 0x0000000000000000000000000000000000012204 (LP-4200 unified PQCrypto block; companion to ML-KEM 0x012201, ML-DSA 0x012202, SLH-DSA 0x012203). - Three modes: Pulsar-44 / Pulsar-65 / Pulsar-87 mirroring ML-DSA Categories 2 / 3 / 5. Production target is Pulsar-65. - Gas: per-mode base + 10/byte (matches ML-DSA precompile tier). - Context: lux-evm-precompile-pulsar-v1 to keep on-chain transcripts unambiguous vs. single-party ML-DSA at 0x012202. Tests: 5 passing (address pin, gas table, round-trip across 3 modes, tampered-signature rejection, invalid-mode rejection). GOWORK=off go test -count=1 ./pulsar/... is green. Imports github.com/luxfi/pulsar-m (pre-rename); will be retargeted to github.com/luxfi/pulsar when the cross-repo move lands. * precompile/LLM.md: mark Pulsar 0x012204 as IMPLEMENTED Pulsar precompile now lives in precompile/pulsar/ and dispatches to luxfi/crypto/mldsa under a Pulsar-specific context. Update the LP-4200 canonical note to reflect deployment status alongside the other three PQCrypto slots. * precompile/zk: rip classical pairing-based opcodes (PQ-only) Shor breaks Groth16/PLONK/fflonk/Halo2/KZG/IPA/Bulletproofs. The 0x0900 EVM precompile now exposes only the PQ-safe ops: hash-based nullifiers (0x21) and Merkle commitment inclusion (0x22). PQ STARK verification is dispatched via the Z-Chain envelope path (consensus/protocol/zchain/verify.go), not from this precompile. * precompile/kzg4844: retire BLS12-381 KZG blob-opening precompile KZG point-evaluation verifies via the BLS12-381 pairing e(C - [y]G1, G2) == e(W, [tau - z]G2). The discrete-log assumption that anchors BLS12-381's security is broken by Shor's algorithm, so EIP-4844-style blob commitments give zero soundness against a future quantum adversary. The Lux EVM is PQ-only. Strip the implementation (gokzg4844, accel GPU MSM, all CPU opcodes). The module still registers the address 0xB002 so the precompile registry numbering stays stable, but every call to the precompile returns the new contract.ErrClassicalForbiddenInPQ sentinel. The minimum 21000 gas is deducted on every call to prevent the retired address being used as a zero-cost probe. Blob availability on the Lux EVM is now provided by hash-based commitments (Blake3 / Poseidon2) and STARK-validated state roots. * precompile/zk: strip Go-level classical pairing SNARK verifiers The Run() path of the 0x0900 ZK precompile only exposes hash-based nullifier checks and Merkle commitment inclusion, both PQ-safe. The remaining Go-level methods on ZKVerifier (VerifyGroth16, VerifyPlonk, VerifyFflonk, VerifyKZG, VerifyRangeProof, VerifyRollupBatch and their helpers) all dispatched through a BN254 pairing check or a BLS12-381 KZG opening, both broken by Shor's algorithm. Strip those methods, their bn256 / kzg4844 / accel-MSM dependencies, and the rollup-batch types (RollupBatch, RollupConfig, StateTransition, KZGSetup, BN254P) that only existed to feed the pairing verifier. Update verifier_test.go to cover only the PQ-safe surface that remains: NewZKVerifier, RegisterVerifyingKey (Go-side bookkeeping), nullifier check/spend, confidential pool create/add-commitment, hash-based Merkle inclusion. PQ STARK verification continues to be handled by the P3Q backend on the Z-chain envelope path (consensus/protocol/zchain/verify.go); it was never wired through this Go struct. * precompile/babyjubjub: retire BN254-companion twisted Edwards curve Baby Jubjub is the ZK-friendly twisted Edwards curve aligned to BN254's scalar field (used by circom / iden3 / Polygon zkEVM / Semaphore / RLN / EdDSA-on-BN254). Its security reduces to discrete-log on a curve of BN254 scalar order; Shor breaks that in poly time. Every consumer of Baby Jubjub is a classical pairing-based SNARK gadget with no PQ analogue, so the Lux EVM retires the precompile rather than offer a quantum-broken primitive. Strip the gnark-crypto twisted-edwards implementation. The module still registers address 0x0500..0007 so the precompile registry numbering stays stable, but every call returns contract.ErrClassicalForbiddenInPQ with 21000 minimum gas deducted. * precompile/pedersen: retire BN254 Pedersen commitment precompile The implementation built homomorphic Pedersen commitments over BN254 G1 (C = v*G + r*H, plus a 32-slot vector commitment). Both binding and hiding reduce to the discrete-log assumption on BN254, which Shor's algorithm breaks. The Lux EVM is PQ-only and retires the precompile rather than offer a quantum-broken commitment. Strip the gnark-crypto BN254 implementation and the 34 deterministic generators. The module still registers address 0x0500..0006 so the precompile registry numbering stays stable, but every call returns contract.ErrClassicalForbiddenInPQ with the 21000 minimum gas deducted. PQ-safe commitment schemes on the Lux EVM use hash-based primitives (Blake3 0x3202, Poseidon2 0x3201) and STARK gadgets on Z-chain. * Revert "precompile/pedersen: retire BN254 Pedersen commitment precompile" This reverts commitcc0db71edf. * Revert "precompile/babyjubjub: retire BN254-companion twisted Edwards curve" This reverts commite666729d6b. * Revert "precompile/zk: strip Go-level classical pairing SNARK verifiers" This reverts commit92e5ac182b. * Revert "precompile/kzg4844: retire BLS12-381 KZG blob-opening precompile" This reverts commit5786c8f57e. * Revert "precompile/zk: rip classical pairing-based opcodes (PQ-only)" This reverts commit555a9b68ff. * precompile: profile gate + P3Q + classical-primitive integration Strict-PQ gate (contract.RefuseUnderStrictPQ) at the top of every classical pairing/DLOG precompile's Run() refuses the call when the chain's ChainConfig implements StrictPQReporter and reports true at the current timestamp. Non-Lux chains that integrate Lux precompiles without wiring StrictPQReporter see the classical primitives execute normally -- forward perfection on Lux, full compatibility elsewhere. Wired into kzg4844, zk (classical opcodes only -- Nullifier and Merkle Commitment stay open), babyjubjub, pedersen, pasta. P3Q strict-PQ STARK verifier at LP-4200 0x012205. Plonky3 fork with cSHAKE256 Merkle / Goldilocks field / FRI low-degree test. Verifier callback registered at node startup; default returns ErrVerifierNotRegistered. 8 tests pass. Gate semantics: RefuseUnderStrictPQ(nil) → nil (test-permissive) ChainConfig without StrictPQReporter → nil (non-Lux chains) StrictPQReporter false → nil (pre-strict-PQ activation timestamp) StrictPQReporter true → ErrClassicalForbiddenInPQ Composable, orthogonal, complete. One and only one way. * precompile: gate every classical Run() via contract.RefuseUnderStrictPQ Decomplect: profile-gating logic stays in one place (contract.RefuseUnderStrictPQ); each classical precompile calls it once at the top of Run() and otherwise stays in its lane. Quasar/P3Q/Pulsar/ML-KEM/ML-DSA/SLH-DSA/Corona/X-Wing/FHE precompiles are post-quantum and skip the gate. Wired into: bls12381 (7 EIP-2537 ops: G1/G2 add/mul/MSM + pairing) ed25519 (signature verify) cggmp21 (ECDSA threshold) frost (Schnorr threshold) ring (LSAG classical schemes; lattice scheme stays open) vrf (ECVRF EDWARDS25519) sr25519 (Schnorrkel/Ristretto255) x25519 (DH) curve25519 (raw Edwards25519 ops) hpke (classical KEMs; Kyber/X-Wing hybrid stays open) blake3 (classical hash) poseidon (BN254 algebraic hash) Already gated upstream: kzg4844, zk (classical opcodes), babyjubjub, pedersen, pasta. secp256r1 uses the non-stateful Run([]byte) interface (EIP-7212 standard). Gating that precompile requires hooking the geth EVM precompile overrider, not this package -- left for the LuxPrecompileOverrider follow-up. Tests pass across all twelve gated dirs plus contract/.
128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package pulsar
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"testing"
|
|
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestPulsarPrecompile_Address pins the canonical LP-4200 address.
|
|
func TestPulsarPrecompile_Address(t *testing.T) {
|
|
want := common.HexToAddress("0x0000000000000000000000000000000000012204")
|
|
require.Equal(t, want, ContractPulsarVerifyAddress)
|
|
}
|
|
|
|
// TestPulsarPrecompile_RequiredGas exercises the per-mode gas table.
|
|
func TestPulsarPrecompile_RequiredGas(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
mode uint8
|
|
want uint64
|
|
}{
|
|
{ModePulsar44, Pulsar44VerifyBaseGas},
|
|
{ModePulsar65, Pulsar65VerifyBaseGas},
|
|
{ModePulsar87, Pulsar87VerifyBaseGas},
|
|
} {
|
|
input := []byte{tc.mode}
|
|
got := PulsarVerifyPrecompile.RequiredGas(input)
|
|
// One-byte input → base + 1 * PerByteGas.
|
|
want := tc.want + 1*PulsarVerifyPerByteGas
|
|
require.Equal(t, want, got, "mode=0x%x", tc.mode)
|
|
}
|
|
}
|
|
|
|
// TestPulsarPrecompile_RoundTrip_VerifyAcceptsValid generates a real
|
|
// ML-DSA signature with the precompile context, hands the bytes to
|
|
// the Pulsar precompile, and asserts it accepts. This is the Class
|
|
// N1 manifesto test: a single-party FIPS 204 signature on the
|
|
// Pulsar context verifies through the Pulsar precompile because the
|
|
// verification operation IS FIPS 204 ML-DSA.Verify.
|
|
func TestPulsarPrecompile_RoundTrip_VerifyAcceptsValid(t *testing.T) {
|
|
for _, mode := range []mldsa.Mode{mldsa.MLDSA44, mldsa.MLDSA65, mldsa.MLDSA87} {
|
|
mode := mode
|
|
t.Run(modeString(mode), func(t *testing.T) {
|
|
priv, err := mldsa.GenerateKey(rand.Reader, mode)
|
|
require.NoError(t, err)
|
|
pub := priv.Public().(*mldsa.PublicKey)
|
|
pubBytes := pub.Bytes()
|
|
|
|
message := []byte("pulsar threshold sign roundtrip via precompile")
|
|
sig, err := priv.SignCtx(rand.Reader, message, precompileCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Build precompile input.
|
|
var modeByte uint8
|
|
switch mode {
|
|
case mldsa.MLDSA44:
|
|
modeByte = ModePulsar44
|
|
case mldsa.MLDSA65:
|
|
modeByte = ModePulsar65
|
|
case mldsa.MLDSA87:
|
|
modeByte = ModePulsar87
|
|
}
|
|
input := buildInput(modeByte, pubBytes, message, sig)
|
|
gas := PulsarVerifyPrecompile.RequiredGas(input) * 2 // give plenty
|
|
out, gasLeft, err := PulsarVerifyPrecompile.Run(nil, common.Address{}, ContractPulsarVerifyAddress, input, gas, true)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
require.Less(t, gasLeft, gas)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPulsarPrecompile_RejectsTamperedSignature confirms that flipping
|
|
// a byte in the signature causes verification to fail.
|
|
func TestPulsarPrecompile_RejectsTamperedSignature(t *testing.T) {
|
|
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
require.NoError(t, err)
|
|
pub := priv.Public().(*mldsa.PublicKey)
|
|
pubBytes := pub.Bytes()
|
|
message := []byte("tamper detection")
|
|
sig, err := priv.SignCtx(rand.Reader, message, precompileCtx)
|
|
require.NoError(t, err)
|
|
sig[0] ^= 0xff
|
|
input := buildInput(ModePulsar65, pubBytes, message, sig)
|
|
gas := PulsarVerifyPrecompile.RequiredGas(input) * 2
|
|
_, _, err = PulsarVerifyPrecompile.Run(nil, common.Address{}, ContractPulsarVerifyAddress, input, gas, true)
|
|
require.ErrorIs(t, err, ErrInvalidSignature)
|
|
}
|
|
|
|
// TestPulsarPrecompile_RejectsInvalidMode confirms that mode bytes
|
|
// outside {0x44, 0x65, 0x87} are rejected.
|
|
func TestPulsarPrecompile_RejectsInvalidMode(t *testing.T) {
|
|
input := []byte{0x99} // bogus mode
|
|
_, _, err := PulsarVerifyPrecompile.Run(nil, common.Address{}, ContractPulsarVerifyAddress, input, 1_000_000, true)
|
|
require.ErrorIs(t, err, ErrInvalidMode)
|
|
}
|
|
|
|
func buildInput(mode uint8, pub, msg, sig []byte) []byte {
|
|
out := make([]byte, 0, 1+len(pub)+32+len(msg)+len(sig))
|
|
out = append(out, mode)
|
|
out = append(out, pub...)
|
|
var mlen [32]byte
|
|
binary.BigEndian.PutUint64(mlen[24:], uint64(len(msg)))
|
|
out = append(out, mlen[:]...)
|
|
out = append(out, msg...)
|
|
out = append(out, sig...)
|
|
return out
|
|
}
|
|
|
|
func modeString(m mldsa.Mode) string {
|
|
switch m {
|
|
case mldsa.MLDSA44:
|
|
return "Pulsar-44"
|
|
case mldsa.MLDSA65:
|
|
return "Pulsar-65"
|
|
case mldsa.MLDSA87:
|
|
return "Pulsar-87"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|