mirror of
https://github.com/lux-apps/papers.git
synced 2026-10-30 18:15:21 +00:00
Bylines, paper authorship, correspondence addresses and the founder blurb under each post named an individual. They name the organisation that published the work instead: the brand on posts and pages, the research byline on papers, and research@ in place of a personal address. Both are what these repositories already used elsewhere — nothing new was coined. Co-authors are untouched. Where a paper named someone else alongside, they stay named; only the one byline changed. Bibliography entries, running heads, PDF metadata and affiliations move with it, so a citation still resolves to whoever the cited paper names. Sample and fixture data uses the placeholder name these tests already use elsewhere, so no assertion changed meaning.
315 lines
16 KiB
TeX
315 lines
16 KiB
TeX
\documentclass[11pt,a4paper]{article}
|
|
\usepackage{../../shared/luxcover}
|
|
\usepackage[utf8]{inputenc}
|
|
\usepackage[T1]{fontenc}
|
|
\usepackage{amsmath,amssymb}
|
|
\usepackage{graphicx}
|
|
\usepackage{hyperref}
|
|
\usepackage{booktabs}
|
|
\usepackage{enumitem}
|
|
\usepackage{xcolor}
|
|
\usepackage{listings}
|
|
\input{../../shared/lstlang}
|
|
|
|
\hypersetup{colorlinks=true,linkcolor=black,citecolor=blue,urlcolor=blue}
|
|
|
|
\lstset{
|
|
basicstyle=\ttfamily\scriptsize,
|
|
breaklines=true,
|
|
frame=single,
|
|
numbers=none,
|
|
keywordstyle=\color{blue!70!black}\bfseries,
|
|
commentstyle=\color{green!50!black}\itshape,
|
|
stringstyle=\color{red!60!black},
|
|
showstringspaces=false,
|
|
}
|
|
|
|
\title{Lux MPTC Serialization Specification}
|
|
\author{
|
|
Lux Research\\
|
|
\textit{Lux Industries, Inc.}\\
|
|
\texttt{[email protected]}
|
|
}
|
|
\date{2026-03-03}
|
|
|
|
\begin{document}
|
|
|
|
\luxcoverpage
|
|
|
|
\section*{Abstract}
|
|
|
|
\noindent We present Lux, a hybrid post-quantum consensus architecture for DAG-native chains. Lux separates fast operational finality from durable post-quantum finality. Validators emit Photons over a Nebula DAG substrate; classical BLS aggregates form Beams for low-latency confirmation, while ML-DSA attestations and Pulsar lattice-threshold Pulses provide post-quantum finality over Nebula roots. Prism binds all certificate lanes to the same transcript, and Quasar reaches Horizon finality when the bound lanes verify.
|
|
|
|
\textbf{This document.} Canonical wire formats for every Lux MPTC artifact: Pulsar shares, Pulsar signatures, Pulsar transcripts, Lens shares, Lens signatures, LSS snapshots, Horizon certificates, and Warp 2.0 envelopes. Where the wire format depends on length-prefix discipline, the discipline is normative and specified at byte resolution. The single source of truth is the canonical Go (\texttt{github.com/luxfi/pulsar}, etc.); this document specifies the format such that a C++ / WGSL / CUDA implementer can produce byte-equal output without reading the Go.
|
|
|
|
\section{Encoding primitives}
|
|
|
|
\subsection{Length-prefixed bytes}
|
|
|
|
A length-prefixed byte slice is encoded as:
|
|
|
|
\begin{lstlisting}
|
|
[uint64 LE length] [length * byte payload]
|
|
\end{lstlisting}
|
|
|
|
Lengths are 8-byte little-endian unsigned integers. The Pulsar wire format uses this primitive throughout; \texttt{appendLenPrefixed} in \texttt{warp/pulsar/pulsar.go} is the canonical encoder.
|
|
|
|
\subsection{TupleHash256 framing}
|
|
|
|
TupleHash256 (NIST SP 800-185 \cite{nistsp800185}) takes a tuple of byte strings $(\tau_1, \ldots, \tau_k)$ and a personalization string $S$. It produces an unambiguous hash by encoding the lengths and the personalization into the absorption.
|
|
|
|
The Lux usage:
|
|
|
|
\begin{lstlisting}
|
|
H_T(S, tau_1, ..., tau_k) := TupleHash256(input=(tau_1, ..., tau_k),
|
|
customization=S,
|
|
output_length=32 bytes)
|
|
\end{lstlisting}
|
|
|
|
A Pulsar transcript hash uses $S = \text{``PULSAR-TRANSCRIPT-v1''}$ (Section~\ref{sec:pulsar-transcript}); a Quasar transcript uses $S = \text{``QUASAR-TRANSCRIPT-v1''}$. The Warp ZAP envelope does \emph{not} use TupleHash256: its digest is a keccak256 over the canonical message bytes (Section~\ref{sec:warp-envelope}), a deliberate choice so the digest matches the on-chain \texttt{keccak256} opcode byte-for-byte.
|
|
|
|
\section{Pulsar share}
|
|
|
|
A Pulsar share is one validator's piece of the threshold key. Wire format:
|
|
|
|
\begin{lstlisting}
|
|
PulsarShare := struct {
|
|
Index uint64 // 1-based party index
|
|
KeyEraID uint64 // group-key lineage
|
|
Generation uint64 // LSS resharing version
|
|
GroupKeyHash [32]byte // hash of (A, b_tilde) public key bytes
|
|
ShareBytes []byte // s_i in NTT-Mont form, len-prefixed
|
|
Seeds [][]byte // pairwise PRF seeds, len-prefixed each
|
|
MACKeys [][]byte // pairwise MAC keys, len-prefixed each
|
|
HashSuiteID string // e.g., "Pulsar-SHA3"
|
|
Lambda []byte // Lagrange coefficient for this share, len-prefixed
|
|
}
|
|
\end{lstlisting}
|
|
|
|
The \texttt{ShareBytes} encoding mirrors Lattigo \texttt{v7}'s \texttt{Vector[Poly].WriteTo}: outer 8-byte vector length, then per-element 8-byte poly-levels-count followed by per-level (8-byte coeff-count + coeff-count $\times$ 8-byte uint64). See Section~\ref{sec:lattigo-vector}.
|
|
|
|
\section{Pulsar signature}
|
|
|
|
A Pulsar threshold signature is the output of $\mathsf{Pulsar.Combine}$ on $K$ partial signatures. Wire format:
|
|
|
|
\begin{lstlisting}
|
|
PulsarSignature := struct {
|
|
C Poly // challenge polynomial, len-prefixed Lattigo Poly
|
|
Z Vector // response vector, len-prefixed Lattigo Vector[Poly]
|
|
Delta Vector // hint correction, len-prefixed Lattigo Vector[Poly]
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Encoded byte stream:
|
|
|
|
\begin{lstlisting}
|
|
[len(C bytes)] [C bytes]
|
|
[len(Z bytes)] [Z bytes]
|
|
[len(Delta bytes)] [Delta bytes]
|
|
\end{lstlisting}
|
|
|
|
A signature is $\sim 33$ KB on Pulsar mainnet parameters. \texttt{warp/pulsar/SerializePulse} produces this format; \texttt{warp/pulsar/DeserializePulse} parses it with the four-layer guards (Section~\ref{sec:lattigo-vector}).
|
|
|
|
\subsection{Lattigo Poly / Vector wire format (normative)}
|
|
\label{sec:lattigo-vector}
|
|
|
|
A Lattigo \texttt{v7} \texttt{Poly} is encoded as:
|
|
|
|
\begin{lstlisting}
|
|
Poly := [
|
|
uint64 LE levels // matrix outer length (typically 1 for ring-q)
|
|
per level:
|
|
uint64 LE coeff_count
|
|
coeff_count * uint64 LE // the coefficient slice
|
|
]
|
|
\end{lstlisting}
|
|
|
|
A \texttt{Vector[Poly]} is encoded as:
|
|
|
|
\begin{lstlisting}
|
|
Vector[Poly] := [
|
|
uint64 LE n // number of polys in the vector
|
|
n * Poly // each poly encoded as above
|
|
]
|
|
\end{lstlisting}
|
|
|
|
\textbf{Hardening discipline.} Every length-prefix is bounded by $\mathsf{MaxLatticeUintSliceLen} = 4096$. Every nested slice's length is checked against the available bytes \emph{before} lattigo's \texttt{ReadFrom} is invoked. The frame walkers \texttt{validatePolyFrame} and \texttt{validateVectorPolyFrame} (\texttt{warp/pulsar/pulsar.go}) implement this discipline; a malformed input is rejected before lattigo can recurse.
|
|
|
|
\section{Pulsar transcript inputs}
|
|
\label{sec:pulsar-transcript}
|
|
|
|
A Pulsar transcript hash is computed over:
|
|
|
|
\begin{lstlisting}
|
|
TranscriptInputs := struct {
|
|
ChainID, NetworkID, GroupID [32]byte
|
|
KeyEraID, OldGeneration, NewGeneration uint64
|
|
OldEpochID, NewEpochID uint64
|
|
OldSetHash, NewSetHash [32]byte
|
|
ThresholdOld, ThresholdNew uint64
|
|
GroupPublicKeyHash [32]byte
|
|
NebulaRoot [32]byte
|
|
HashSuiteID string
|
|
ImplementationVersion string
|
|
Variant string // "refresh" or "reshare"
|
|
PrimaryNetworkID []byte // empty for L1; set for L2
|
|
PrimaryNebulaRoot [32]byte // zero for L1; bound for L2
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Each field is length-prefix-encoded as part of the TupleHash256 input tuple. The order is canonical (per \texttt{pulsar/reshare/transcript.go}); two distinct field tuples yield distinct hashes by collision resistance of TupleHash256 with the \texttt{PULSAR-TRANSCRIPT-v1} personalization.
|
|
|
|
\section{Pulsar activation message}
|
|
|
|
\begin{lstlisting}
|
|
ActivationMsg := "QUASAR-PULSAR-ACTIVATE-v1"
|
|
|| TranscriptHash(T)
|
|
|| ReshareTranscript.Hash(R)
|
|
\end{lstlisting}
|
|
|
|
Where $\mathcal{T}$ is the TranscriptInputs above and $\mathcal{R}$ encodes (commit digests, complaint hashes, disqualified senders, qualified quorum) per \texttt{pulsar/reshare/activation.go}.
|
|
|
|
\section{Lens share and signature}
|
|
|
|
Lens uses curve-side serialization:
|
|
|
|
\begin{lstlisting}
|
|
LensShare := struct {
|
|
Index uint64
|
|
KeyEraID, Generation uint64
|
|
GroupKeyHash [32]byte
|
|
ShareScalar [32]byte // big-endian scalar for Ed25519 / Ristretto255 / secp256k1
|
|
HashSuiteID string
|
|
Lambda [32]byte // Lagrange coefficient
|
|
}
|
|
|
|
LensSignature := struct {
|
|
R [32]byte // commitment point (compressed)
|
|
s [32]byte // response scalar
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Signature size is 64 bytes, regardless of group choice (Ed25519, secp256k1, Ristretto255). Curve-specific encoding follows the standard for each group.
|
|
|
|
\section{LSS snapshot}
|
|
|
|
\begin{lstlisting}
|
|
LSSSnapshot := struct {
|
|
Eta uint64 // KeyEraID
|
|
Generation uint64
|
|
RollbackFrom uint64
|
|
ValidatorSet [][20]byte // node IDs
|
|
Threshold uint64
|
|
State []byte // encrypted share state, len-prefixed
|
|
Timestamp int64
|
|
GroupKeyHash [32]byte
|
|
HashSuiteID string
|
|
}
|
|
\end{lstlisting}
|
|
|
|
The \texttt{State} field is encrypted under a chain-configured snapshot key (typically a chain-internal KMS reference). Decryption is gated by snapshot-manager authority; the on-disk snapshot does not reveal share material to disk-level adversaries.
|
|
|
|
\section{Horizon certificate}
|
|
|
|
\begin{lstlisting}
|
|
HorizonCertificate := struct {
|
|
Beam bls.AggregateCertificate // 96-byte BLS aggregate
|
|
MLDSA mldsa.CertificateSet // signer bitmap + per-validator sigs
|
|
// (or Z-Chain Groth16 rollup)
|
|
Pulsar pulsar.Certificate // one Pulse, ~33 KB
|
|
NebulaRoot [32]byte
|
|
KeyEraID uint64
|
|
GroupID [32]byte
|
|
Generation uint64
|
|
HashSuiteID string
|
|
Epoch uint64
|
|
Round uint64
|
|
ValidatorSetHash [32]byte
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Each component is length-prefix-encoded. The wire format is the same in the Quasar consensus path and in the Warp 2.0 destination's lift-into-HorizonCertificate path; one canonical shape, two embedding contexts.
|
|
|
|
\section{Warp 2.0 envelope}
|
|
\label{sec:warp-envelope}
|
|
|
|
The Warp 2.0 envelope ships over the \emph{ZAP} wire (Lux Warp Zap Protocol), a total-order canonical TLV encoding --- \emph{not} RLP. The canonical Go is \texttt{github.com/luxfi/warp} v1.22.0 (\texttt{codec.go}, \texttt{zap.go}, \texttt{envelope.go}, \texttt{message.go}, \texttt{signature.go}). The envelope is a flat byte stream:
|
|
|
|
\begin{lstlisting}
|
|
magic ("LWZP" || 0x01) // 5 bytes: 0x4C 0x57 0x5A 0x50 0x01
|
|
kind (0x02) // 1 byte: kindEnvelope
|
|
Message // canonical message bytes (see below)
|
|
Beam // BLS aggregate lane (BitSetSignature)
|
|
PulseSig // u32-len || bytes (Pulsar Pulse; u32(0) if absent)
|
|
MLDSACertSet // u32-len || bytes (ML-DSA cert set; u32(0) if absent)
|
|
\end{lstlisting}
|
|
|
|
\paragraph{Message (the signed subject).} The c14n encoding is the digest preimage. It folds the unsigned body together with the source-chain Pulsar lineage:
|
|
|
|
\begin{lstlisting}
|
|
zapKindMessage u8 // 0x01 (NOT the 0x02 envelope kind)
|
|
NetworkID u32 big-endian
|
|
SourceChainID [32]byte raw // no length prefix
|
|
SourceNebulaRoot [32]byte raw // no length prefix
|
|
SourceKeyEraID u64 big-endian
|
|
SourceGeneration u64 big-endian
|
|
HashSuiteID u32-len || utf8 // big-endian length prefix
|
|
Payload u32-len || bytes // big-endian length prefix
|
|
\end{lstlisting}
|
|
|
|
\paragraph{Beam (the BLS lane).}
|
|
|
|
\begin{lstlisting}
|
|
Signers u32-len || trim-canonical bitset bytes // no trailing zero byte
|
|
Signature [96]byte raw // BLS aggregate
|
|
\end{lstlisting}
|
|
|
|
\paragraph{Canonicality rules (the ZAP profile).} (1) integers are fixed-width \textbf{big-endian} (\texttt{u8}/\texttt{u16}/\texttt{u32}/\texttt{u64}), no varints; (2) every variable-length field is a \texttt{u32} big-endian length prefix followed by exactly that many bytes; (3) fixed-width arrays are raw; (4) every field is always present (an absent optional lane is the \texttt{u32(0)} empty frame); (5) booleans are exactly \texttt{0x00}/\texttt{0x01}; (6) the \texttt{Signers} bitset is trim-canonical (no trailing zero byte) --- trimmed on encode, rejected on decode; (7) decode rejects trailing bytes; (8) no pointers, padding, flags, or maps; (9) the stream begins with the 5-byte magic. A given struct value has exactly one encoding, and decode rejects anything else.
|
|
|
|
\paragraph{Digest $D$.} Every lane signs the same digest:
|
|
\[
|
|
D = \mathrm{keccak256}(\text{``LUX-WARP-ZAP-CORE-v1''} \,\|\, \mathrm{c14n}(\mathsf{Message})),
|
|
\]
|
|
where $\mathrm{keccak256}$ is Ethereum's legacy Keccak (padding \texttt{0x01}), \emph{not} NIST SHA3-256 (padding \texttt{0x06}). The lane signing-bytes are the digest prefixed by a per-lane tag: Beam over $\text{``LUX-WARP-ZAP-BEAM-v1''}\,\|\,D$; Pulse over $\text{``LUX-WARP-ZAP-PULSE-v1''}\,\|\,D$; ML-DSA cert set over $\text{``LUX-WARP-ZAP-MLDSA-v1''}\,\|\,D$. The four tags are frozen.
|
|
|
|
\paragraph{Why ZAP and not RLP.} The encoding is the signing domain, so it must be malleability-free: ZAP's total-order canonical form (one struct value $\to$ one byte string, decode rejects all non-canonical encodings) is what makes signing over it safe. The wire is also self-segregating from legacy Warp 1.x RLP: a legacy RLP decoder fed ZAP bytes rejects (the leading \texttt{'L'} = \texttt{0x4c} is below RLP's \texttt{0xc0} list floor), and the ZAP magic check rejects legacy RLP bytes (lead \texttt{0xc0}--\texttt{0xff}) and the bare legacy \texttt{0x02}. The big-endian length-prefix discipline is shared with the Horizon certificate codec (\texttt{warp/pulsar/pulsar.go}).
|
|
|
|
\paragraph{Wire bounds.} \texttt{MaxMessageSize = 256\,KiB}; \texttt{MaxEnvelopeSize}${} = 4 \times {}$\texttt{MaxMessageSize}, conservatively bounded for the optional Pulse and ML-DSA cert-set bytes alongside the message and Beam.
|
|
|
|
\section{Cross-implementation byte-equality}
|
|
|
|
The Go canonical produces byte-equal output across runs for every artifact in this spec. KAT vectors gate every release:
|
|
|
|
\begin{itemize}[leftmargin=2em,nosep]
|
|
\item \texttt{pulsar/cmd/corona\_oracle\_v2/}: emits 16 KAT entries covering every Pulsar wire object.
|
|
\item \texttt{pulsar/scripts/regen-kats.sh}: deterministic regeneration; \texttt{--verify} confirms byte-equality.
|
|
\item \texttt{warp/pulsar/testdata/}: representative Warp 2.0 envelope KAT.
|
|
\end{itemize}
|
|
|
|
A C++ / WGSL / CUDA implementer SHOULD produce byte-equal output against these KATs. Where this spec and the canonical disagree, the canonical is the tiebreaker; a discrepancy is a spec bug, not an implementation bug.
|
|
|
|
\section{Endianness, alignment, padding}
|
|
|
|
\begin{itemize}[leftmargin=2em,nosep]
|
|
\item Endianness: the lattice-backed Pulsar artifacts (shares, signatures, Lattigo \texttt{Poly} / \texttt{Vector} frames) are \textbf{little-endian}, consistent with Lattigo v7's wire format. The Warp ZAP envelope (Section~\ref{sec:warp-envelope}) and the Horizon certificate are \textbf{big-endian}: all fixed-width integers and all \texttt{u32} length prefixes are big-endian. Implementers MUST track which convention applies to each artifact.
|
|
\item Alignment: no implicit alignment; lengths and payloads are byte-packed.
|
|
\item Padding: no padding. A trailing-byte difference between two encodings of the same logical content is a wire-format error.
|
|
\end{itemize}
|
|
|
|
\section{Versioning}
|
|
|
|
Every wire format that may evolve carries an explicit version byte or version field:
|
|
|
|
\begin{itemize}[leftmargin=2em,nosep]
|
|
\item Warp ZAP envelope: the 5-byte magic \texttt{"LWZP"}$\,\|\,$\texttt{0x01} carries the format-version byte \texttt{0x01}, followed by the kind byte \texttt{0x02} (\texttt{kindEnvelope}); the message c14n stream independently leads with \texttt{0x01} (\texttt{zapKindMessage}). Legacy Warp 1.x RLP had no magic. There is no parallel envelope-version dispatch and no \texttt{0x03} reserved.
|
|
\item Pulsar transcript: \texttt{ImplementationVersion} string field (e.g., \texttt{"pulsar-v1.0"}).
|
|
\item Hash suite: \texttt{HashSuiteID} string field (e.g., \texttt{"Pulsar-SHA3"}).
|
|
\end{itemize}
|
|
|
|
A version mismatch returns an explicit error; there is no negotiation, no fallback, no implicit upgrade.
|
|
|
|
\bibliographystyle{plain}
|
|
\bibliography{../references}
|
|
|
|
\end{document}
|