mirror of
https://github.com/lux-cpp/consensus.git
synced 2026-10-30 18:14:55 +00:00
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]>
139 lines
5.8 KiB
C++
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
|