Files
zeekayandHanzo Dev 440958b215 converge the C++ consensus: absorb the quasar witness, drop the "2"
This is not version 2 of anything. It is the same 1.x Lux consensus written in
C++, and the suffix said otherwise in 182 places. Rename the whole surface —
namespace lux::consensus2 -> lux::consensus, include/lux/consensus2/ ->
include/lux/consensus/, CONSENSUS2_* -> CONSENSUS_* — so the name states what
the thing is rather than which attempt it was.

Absorb the half of luxcpp/consensus that was real. That repo held two unrelated
things under one tagline. The witness surface is genuine and unique: it is the
only C ABI in either tree (the gate has none), it aggregates threshold shares on
Go's (u32_be index || 96-byte G2) record form where the gate's aggregate_sigs
takes a flat concatenation with no index, and lux-private/multichain-cpp already
links it by name and fails closed without it. It arrives verbatim — headers,
bodies, both tests, all five Go fixtures — with the tests renamed onto the
one-list-one-loop convention. Their fixture fallback is __FILE__/../../testdata,
so moving tests/ to test/ makes the corpus resolve at this repo's root with no
argument.

Quasar is a SIBLING of the gate, not a layer of it. No gate source calls
lux::quasar, so linking them would fabricate a dependency neither has; they share
only the from-source blst and the NUL DST. The namespace stays lux::quasar and
the library keeps the name luxconsensus_quasar — that pair is a published
contract, not an implementation detail.

The other half of that repo does not come across. Its lux::consensus header is a
count-only toy with no cryptography, an XOR fold where a hash belongs, and a
k==1 config — the exact shape toy_stub_killer_test exists to reject. It is also
a hard blocker: its `enum class Decision` and wave.hpp's collide outright once
the rename lands ("multiple definition of 'enum class lux::consensus::Decision'").
Its chain.cpp has never compiled in any commit, its tests and examples were never
added to any build, and its MLX paths sit behind a HAS_MLX that nothing defines.
Nothing is lost: that tree is preserved whole at luxcpp/consensus-legacy.

The oracle's relative replace paths are re-aimed at this repo's home under
luxcpp, four levels up rather than three. The short form is worse than broken
there: ~/work/luxcpp/crypto exists and is the C++ crypto, so a stale path
resolves to the wrong repo instead of to nothing.

13/13 tests pass: the 11 gate tests plus both witness tests.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 09:00:38 -07:00

139 lines
5.8 KiB
C++

// Copyright (c) 2026 Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause-Eco
//
// Quasar witness BLS verification — C++ surface.
//
// The Go reference is `protocol/quasar/quasar.go` (signer.VerifyThresholdSignatureBytes
// and AggregateThresholdSignatures). The cryptographic kernel is the
// IRTF BLS12-381 signature primitive with the "BLS_SIG" ciphersuite
// (NUL DST). Pubkeys live on G1 (48-byte Zcash compressed), signatures
// on G2 (96-byte Zcash compressed), hash-to-G2 via SSWU per RFC 9380.
//
// This is the hot path for finalised-round verification — every
// finalised block walks through `WitnessVerifier::verify` exactly once
// (twice if there's a re-verify on cert receipt). The C ABI in
// `quasar.h` is the only external contract; this header is the
// in-process surface for C++ consumers (tests, benchmarks).
//
// Backend selection (`Backend::Cpu` today, `Backend::Gpu` reserved):
// the verify path stays CPU-only for now — pairings are a tight
// inner loop where the blst optimised assembly dominates anything
// we can write in CUDA without batching, and per-round verify is
// inherently latency-bound. A batched verifier that amortises
// final_exp across many certs can swap in behind the same enum
// without changing the call sites.
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
#include "quasar.h"
namespace lux::quasar {
constexpr std::size_t kPubKeyLen = LUX_QUASAR_PK_LEN; // 48
constexpr std::size_t kSignatureLen = LUX_QUASAR_SIG_LEN; // 96
constexpr std::size_t kShareLen = 4 + kSignatureLen; // 100 (u32 index || G2)
enum class Backend : std::uint8_t {
Cpu = 0, // blst CPU — the only path today
Gpu = 1, // reserved — future batched pairing oracle
};
enum class Status : std::int32_t {
Ok = LUX_QUASAR_OK,
ErrInvalid = LUX_QUASAR_ERR_INVALID,
ErrSignature = LUX_QUASAR_ERR_SIG,
ErrVerify = LUX_QUASAR_ERR_VERIFY,
};
// WitnessVerifier — one verifier instance per chain / per validator
// set. Holds the bound group public key and pre-parsed affine form to
// keep `verify()` allocation-free on the success path. Mutation of the
// group key is not supported on a live verifier; a validator-set
// rotation constructs a fresh `WitnessVerifier` (mirrors Go's
// `NewVerifier`).
class WitnessVerifier {
public:
// Construct with the 48-byte compressed group public key. Throws
// std::invalid_argument if the bytes do not deserialise to a valid
// G1 point or fail the subgroup check.
explicit WitnessVerifier(std::span<const std::uint8_t, kPubKeyLen> group_key,
Backend backend = Backend::Cpu);
WitnessVerifier(const WitnessVerifier&) = delete;
WitnessVerifier& operator=(const WitnessVerifier&) = delete;
WitnessVerifier(WitnessVerifier&&) noexcept;
WitnessVerifier& operator=(WitnessVerifier&&) noexcept;
~WitnessVerifier();
// Verify `sig` over `msg`. Returns Status::Ok on a verifying
// signature, Status::ErrSignature if `sig` is not a valid G2
// curve point, Status::ErrVerify if the pairing rejects.
[[nodiscard]] Status verify(std::span<const std::uint8_t> msg,
std::span<const std::uint8_t, kSignatureLen> sig) const noexcept;
// Convenience overload for non-fixed-extent signature spans.
[[nodiscard]] Status verify(std::span<const std::uint8_t> msg,
std::span<const std::uint8_t> sig) const noexcept;
[[nodiscard]] Backend backend() const noexcept { return backend_; }
private:
struct Impl;
Impl* impl_ {nullptr}; // owning, allocated in ctor (pImpl to keep blst out of header)
Backend backend_ {Backend::Cpu};
};
// WitnessAggregator — combines threshold-signature shares into a
// single aggregated G2 signature. Holds the bound group public key for
// future Lagrange-weighted aggregation; today the aggregation is the
// n-of-n branch the Go aggregator takes when every Lagrange
// coefficient reduces to 1 (i.e. the caller has pre-applied weights).
class WitnessAggregator {
public:
explicit WitnessAggregator(std::span<const std::uint8_t, kPubKeyLen> group_key,
Backend backend = Backend::Cpu);
WitnessAggregator(const WitnessAggregator&) = delete;
WitnessAggregator& operator=(const WitnessAggregator&) = delete;
WitnessAggregator(WitnessAggregator&&) noexcept;
WitnessAggregator& operator=(WitnessAggregator&&) noexcept;
~WitnessAggregator();
// Aggregate `n` shares laid out as (u32 index || 96-byte sig) records.
// Writes the 96-byte aggregated G2 signature to `out_sig`. Returns
// Status::Ok on success; Status::ErrSignature if any share fails
// subgroup check.
[[nodiscard]] Status aggregate(std::span<const std::uint8_t> shares_packed,
std::size_t n,
std::span<std::uint8_t, kSignatureLen> out_sig) const noexcept;
[[nodiscard]] Backend backend() const noexcept { return backend_; }
private:
struct Impl;
Impl* impl_ {nullptr};
Backend backend_ {Backend::Cpu};
};
// Free-function fast path — for one-shot verifies that don't want to
// build a WitnessVerifier. Construct + verify + destruct. Same
// semantics as the C entry point `lux_quasar_witness_verify`.
[[nodiscard]] Status
verify_once(std::span<const std::uint8_t, kPubKeyLen> group_key,
std::span<const std::uint8_t> msg,
std::span<const std::uint8_t, kSignatureLen> sig) noexcept;
// DST string — exposed so test code can confirm parity with the Go
// side without re-encoding the string literal in every test.
constexpr const char* kDstSignature =
"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";
constexpr std::size_t kDstSignatureLen = 43; // strlen, must match Go's len()
} // namespace lux::quasar