mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
H1 — shielded-tx ProofVerifier was classical-only (groth16/plonk) and fail-closed only via all-zero dummy VKs; real bn254 VKs would re-enable a CRQC-forgeable shield/unshield mint/steal path. Add ZConfig.StrictPQ and a single gate refuseClassicalUnderStrictPQ(proofType) called at the top of VerifyTransactionProof AND per-tx in the GPU batch path (which verifies groth16 inline, bypassing VerifyTransactionProof). Under strict-PQ only "stark" is accepted — wired to precompile/starkfri (cSHAKE256/Goldilocks FRI, fail-closed until the prover binds), public inputs bound to the tx's nullifiers+commitments. loadVerifyingKeys now ERRORS on a non-dummy bn254 VK under strict-PQ (explicit fail-closed, not implicit dummy-keys). The remaining prover work (p3q_prove C ABI + shielded/ML-DSA AIR) is tracked as a code comment; shielded value is effectively disabled on strict-PQ chains until it lands — the correct no-classical-fallback posture. H2 — Groth16/PLONK precompiles (0x80/0x81) were registered unconditionally and Run([]byte) has no AccessibleState to call RefuseUnderStrictPQ. RegisterZKPrecompiles(registry, strictPQ) now omits Groth16/PLONK on strict-PQ chains (registering STARK 0x82 + fail-closed Halo2/Nova stubs only) so 0x80/0x81 fail closed by absence; non-strict chains keep them as an optional building block (kept, not deleted). CrossChainZKVerifier gains StrictPQ that refuses routing Groth16/PLONK while still routing STARK. H4 — verifyPLONK computed a self-cancelling pairing, discarded the result, ignored public inputs, and returned valid for ANY >=544-byte blob (total bypass). Now fails closed (errPLONKVerifierIncomplete) after structural parsing — NEVER universal-accepts. Groth16/bn254/PLONK remain present-but-gated; STARK/FRI (P3Q) is the only accepted proof path on strict-PQ chains. Tests: strict-PQ refuses groth16/plonk/bulletproofs (direct + GPU-batch); real-VK-load errors; STARK fails-closed unbound and accepts bound with nullifier+commitment-bound public inputs; non-strict path unchanged; strict registry omits classical + keeps STARK; cross-chain strict refuses classical; PLONK never universal-accepts. H-02 regression updated to the new wired-STARK fail-closed behavior.
155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package precompiles
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// CrossChainZKVerifierAddr is deployed on every EVM chain to route
|
|
// ZK verification requests to Z-Chain via Warp messaging.
|
|
const CrossChainZKVerifierAddr = 0x0F20
|
|
|
|
// Verifier type identifiers used in cross-chain routing.
|
|
const (
|
|
VerifierTypeGroth16 = 0x01
|
|
VerifierTypePLONK = 0x02
|
|
VerifierTypeSTARK = 0x03
|
|
VerifierTypeHalo2 = 0x04
|
|
VerifierTypeNova = 0x05
|
|
)
|
|
|
|
// CrossChainZKVerifier routes ZK verification from any EVM chain to Z-Chain.
|
|
//
|
|
// Input format:
|
|
//
|
|
// verifier_type (1 byte)
|
|
// proof_data (remaining bytes — forwarded to Z-Chain verifier)
|
|
//
|
|
// The routing works via Warp messaging:
|
|
// 1. Caller on C-Chain (or any subnet EVM) invokes this precompile
|
|
// 2. Precompile constructs a Warp message addressed to Z-Chain
|
|
// 3. Z-Chain receives the message and invokes the appropriate verifier
|
|
// 4. Result is returned via Warp response
|
|
//
|
|
// Gas: base 100K (includes Warp relay overhead).
|
|
//
|
|
// StrictPQ, when set, makes this router refuse the classical
|
|
// (quantum-breakable) verifier types Groth16 and PLONK: on a strict-PQ
|
|
// chain only the post-quantum STARK/FRI path (VerifierTypeSTARK) may be
|
|
// routed. This mirrors the registration-time gate in
|
|
// RegisterZKPrecompiles — a strict-PQ chain wires this router with
|
|
// StrictPQ=true so it cannot relay a classical proof to Z-Chain.
|
|
type CrossChainZKVerifier struct {
|
|
ZChainID ids.ID
|
|
StrictPQ bool
|
|
}
|
|
|
|
func (v *CrossChainZKVerifier) RequiredGas(input []byte) uint64 {
|
|
if len(input) < 1 {
|
|
return 100_000
|
|
}
|
|
|
|
// Base relay cost + verifier-specific cost
|
|
base := uint64(100_000)
|
|
switch input[0] {
|
|
case VerifierTypeGroth16:
|
|
return base + groth16Gas
|
|
case VerifierTypePLONK:
|
|
return base + plonkGas
|
|
case VerifierTypeSTARK:
|
|
return base + starkGas
|
|
case VerifierTypeHalo2:
|
|
return base + halo2Gas
|
|
case VerifierTypeNova:
|
|
return base + novaGas
|
|
default:
|
|
return base
|
|
}
|
|
}
|
|
|
|
func (v *CrossChainZKVerifier) Run(input []byte) ([]byte, error) {
|
|
if len(input) < 2 {
|
|
return resultInvalid, errInputTooShort
|
|
}
|
|
|
|
verifierType := input[0]
|
|
proofData := input[1:]
|
|
|
|
// Strict-PQ chains refuse the classical pairing-based paths: only the
|
|
// quantum-safe STARK/FRI rollup path may be routed.
|
|
if v.StrictPQ && (verifierType == VerifierTypeGroth16 || verifierType == VerifierTypePLONK) {
|
|
return resultInvalid, errClassicalForbiddenStrictPQ
|
|
}
|
|
|
|
// Validate verifier type
|
|
switch verifierType {
|
|
case VerifierTypeGroth16, VerifierTypePLONK, VerifierTypeSTARK:
|
|
// supported — route to Z-Chain. STARK is the strict-PQ
|
|
// (quantum-safe) rollup-proof path; Groth16/PLONK are the
|
|
// classical pairing-based paths kept for non-strict-PQ chains.
|
|
case VerifierTypeHalo2, VerifierTypeNova:
|
|
return resultInvalid, errNotImplemented
|
|
default:
|
|
return resultInvalid, fmt.Errorf("unknown verifier type: 0x%02x", verifierType)
|
|
}
|
|
|
|
// Construct Warp message payload:
|
|
// target_precompile_addr(1) | proof_data
|
|
var targetAddr byte
|
|
switch verifierType {
|
|
case VerifierTypeGroth16:
|
|
targetAddr = Groth16VerifierAddr
|
|
case VerifierTypePLONK:
|
|
targetAddr = PLONKVerifierAddr
|
|
case VerifierTypeSTARK:
|
|
targetAddr = STARKVerifierAddr
|
|
}
|
|
|
|
msg := encodeWarpPayload(v.ZChainID, targetAddr, proofData)
|
|
|
|
// In production, this payload is submitted as a Warp unsigned message
|
|
// to the Z-Chain, which executes the precompile and returns the result
|
|
// via a Warp response. The EVM integration layer handles the async
|
|
// Warp send/receive. Here we return the encoded message for the
|
|
// runtime to dispatch.
|
|
return msg, nil
|
|
}
|
|
|
|
// encodeWarpPayload encodes a cross-chain ZK verification request.
|
|
//
|
|
// Format:
|
|
//
|
|
// z_chain_id (32 bytes)
|
|
// target_addr (1 byte)
|
|
// payload_len (4 bytes, big-endian)
|
|
// payload (payload_len bytes)
|
|
func encodeWarpPayload(zChainID ids.ID, targetAddr byte, payload []byte) []byte {
|
|
buf := make([]byte, 32+1+4+len(payload))
|
|
copy(buf[0:32], zChainID[:])
|
|
buf[32] = targetAddr
|
|
binary.BigEndian.PutUint32(buf[33:37], uint32(len(payload)))
|
|
copy(buf[37:], payload)
|
|
return buf
|
|
}
|
|
|
|
// DecodeWarpPayload decodes a cross-chain ZK verification request.
|
|
func DecodeWarpPayload(data []byte) (zChainID ids.ID, targetAddr byte, payload []byte, err error) {
|
|
if len(data) < 37 {
|
|
return ids.ID{}, 0, nil, errors.New("warp payload too short")
|
|
}
|
|
copy(zChainID[:], data[0:32])
|
|
targetAddr = data[32]
|
|
payloadLen := binary.BigEndian.Uint32(data[33:37])
|
|
if uint32(len(data)) < 37+payloadLen {
|
|
return ids.ID{}, 0, nil, errors.New("warp payload truncated")
|
|
}
|
|
payload = data[37 : 37+payloadLen]
|
|
return zChainID, targetAddr, payload, nil
|
|
}
|