mirror of
https://github.com/lux-apps/papers.git
synced 2026-10-30 18:15:21 +00:00
LP-strict-e2e-pq specifies LUX_STRICT_E2E_PQ, the locked profile under which every byte that gates a state transition on Lux is bound to a FIPS 203 / 204 / 205 primitive or a SHA3-family transcript. The profile is a single ChainSecurityProfile (luxfi/[email protected].6 config/security_profile.go:244) with 11 zero-value-invalid enum/flag fields and a SHA3-384 self-hash (ComputeHash, security_profile.go:835); the golden production digest is 93efd103…323bfcd0. Sections (sections/01..15): - 01 intro: motivation, 14-surface coverage, what closes vs. classical - 02 profile: ChainSecurityProfile struct + golden vector - 03 tx-auth-envelope: 17-field TupleHash256 LUX_TX_AUTH_V1, EIP-7932 cmp - 04 pq-permit: 11-field LUX_PQ_PERMIT_V1, PQAuth precompile 0x012202 - 05 zchain-rollup: 6 ops, 7-root aggregator, VerifyAuthPassed - 06 qchain-finality: 23-axis ComputeRoundDigest, QBlock wire - 07 pulsar-m: Module-LWE threshold, 43-of-64, identifiable abort, no BLS fallback - 08 p3q: 10 crates, STARK/FRI/SHA3, no KZG/BN254/Groth16, expanded-pubkey - 09 p2p-pq: ML-KEM-768/1024 + ML-DSA-65, 5 cSHAKE customizations - 10 cpx-coverage: TypedNodeID/SchemeGate, mldsafx, ClassicalCompatRegistry - 11 bridge-pq: BridgeProfile, refused primitives, post_quantum_end_to_end label - 12 evaluation: 8.1M→130k gas, sig-size economics, latency budget, batching - 13 security-considerations: 14 PQ surfaces, threat model, profile-drift defence - 14 related-work: NIST MPTC, FIPS, Avalanche/Cosmos/Ethereum PQ posts, ETHDILITHIUM - 15 references: NIST FIPS 202/203/204/205 + SP 800-90A/185 + HIP/LP/ZIP series INDEX.md updated: 121 papers, 120 PDFs compiled (this paper awaiting first compile). Companion proofs in luxfi/proofs/strict-e2e-pq/.
229 lines
10 KiB
TeX
229 lines
10 KiB
TeX
\section{TxAuthEnvelope: a 17-field signed transaction}
|
|
\label{sec:tx-auth-envelope}
|
|
|
|
\subsection{What it replaces}
|
|
|
|
Every classical Ethereum-family transaction lineage --- legacy raw RLP,
|
|
EIP-2930 access list, EIP-1559 dynamic fee, EIP-4844 blob --- shares the same
|
|
authorisation primitive: ECDSA over keccak256 of an RLP-encoded payload. The
|
|
payload format changes across tx types but the authorisation primitive is
|
|
fixed. Both are classical: ECDSA falls to Shor, keccak is fine but the
|
|
domain-separation gaps in RLP encoding admit cross-version replays
|
|
\cite{eip2718,eip7932}.
|
|
|
|
A strict-PQ chain needs a transaction-authorisation primitive that (1) signs
|
|
under ML-DSA-65 or another FIPS-204 scheme, (2) binds every security-relevant
|
|
field via TupleHash256 so a single byte flip on any field breaks the
|
|
signature, (3) carries the profile identity on the wire so cross-profile
|
|
replays are impossible, and (4) survives lazy-deserialiser bugs by refusing to
|
|
admit a transaction whose decoded fields disagree with its decoded digest.
|
|
|
|
\texttt{TxAuthEnvelope} (\texttt{luxfi/[email protected]},
|
|
\texttt{protocol/auth/tx\_envelope.go:108}) is that primitive.
|
|
|
|
\subsection{The 17 fields}
|
|
|
|
\begin{table}[H]
|
|
\centering
|
|
\caption{TxAuthEnvelope field list. Source:
|
|
\texttt{protocol/auth/tx\_envelope.go:108}.}
|
|
\label{tab:tx-auth-fields}
|
|
\begin{tabular}{llp{7cm}}
|
|
\toprule
|
|
\# & Field & Role \\
|
|
\midrule
|
|
1 & \texttt{Version} & envelope version (uint16). $v=1$ on strict-PQ. \\
|
|
2 & \texttt{ProfileID} & profile under which signed. \\
|
|
3 & \texttt{ChainID} & destination chain id. \\
|
|
4 & \texttt{NetworkID} & destination network id (mainnet / testnet). \\
|
|
5 & \texttt{AccountID} & 48-byte signer account identifier. \\
|
|
6 & \texttt{Nonce} & uint64 monotonic per-account nonce. \\
|
|
7 & \texttt{ExpiryHeight} & uint64 chain height after which envelope is invalid. \\
|
|
8 & \texttt{WalletSchemeID} & signature scheme (matches \texttt{profile.WalletSchemeID}). \\
|
|
9 & \texttt{HashSuiteID} & hash suite (matches \texttt{profile.HashSuiteID}). \\
|
|
10 & \texttt{FeePayer} & 48-byte account paying gas. \\
|
|
11 & \texttt{GasLimit} & uint64 gas allowance. \\
|
|
12 & \texttt{MaxFee} & 32-byte EIP-1559-style fee cap. \\
|
|
13 & \texttt{CallRoot} & 32-byte hash of the call payload. \\
|
|
14 & \texttt{AccessListRoot} & 32-byte hash of the access list. \\
|
|
15 & \texttt{ZIdentityRoot} & 32-byte Z-Chain identity root binding. \\
|
|
16 & \texttt{AccountStateRoot} & 32-byte expected account state root at submit. \\
|
|
17 & \texttt{PublicKeyRef} & 32-byte SHA3-256 of the signer's full ML-DSA pubkey. \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
The 17 fields are the security-relevant inputs to the signing transcript. The
|
|
sole non-transcript field is \texttt{Signature}, which carries the
|
|
3309-byte ML-DSA-65 signature itself (variable-length under other schemes).
|
|
|
|
The choice of 17 fields is the smallest set that closes \emph{every} replay
|
|
class the protocol team enumerated in HIP-0085 §3:
|
|
|
|
\begin{itemize}[leftmargin=2em,nosep]
|
|
\item \textbf{Cross-chain replay} is closed by \texttt{ChainID} and
|
|
\texttt{NetworkID}.
|
|
\item \textbf{Cross-profile replay} is closed by \texttt{ProfileID}.
|
|
\item \textbf{Cross-version replay} is closed by \texttt{Version}.
|
|
\item \textbf{Cross-account replay} is closed by \texttt{AccountID} and
|
|
\texttt{PublicKeyRef}.
|
|
\item \textbf{Cross-fee-payer replay} is closed by \texttt{FeePayer},
|
|
\texttt{GasLimit}, \texttt{MaxFee}.
|
|
\item \textbf{Stale state replay} is closed by \texttt{ExpiryHeight},
|
|
\texttt{AccountStateRoot}, and \texttt{ZIdentityRoot}.
|
|
\item \textbf{Cross-permission-set replay} is closed by
|
|
\texttt{AccessListRoot}.
|
|
\item \textbf{Same-account same-state different-call replay} is closed by
|
|
\texttt{CallRoot}.
|
|
\item \textbf{Cross-scheme replay} is closed by \texttt{WalletSchemeID}
|
|
and \texttt{HashSuiteID}.
|
|
\end{itemize}
|
|
|
|
Dropping any field would re-open a replay class. The 17-field minimum is
|
|
audit-driven.
|
|
|
|
\subsection{SigningDigest}
|
|
|
|
\texttt{SigningDigest} (\texttt{tx\_envelope.go:148}) is what the wallet signs:
|
|
|
|
\begin{lstlisting}[language=Go,caption={\texttt{SigningDigest} construction.
|
|
Source: \texttt{protocol/auth/tx\_envelope.go:148}. Hash family is cSHAKE256
|
|
under \texttt{LUX\_TX\_AUTH\_V1}, independent of \texttt{HashSuiteID} which
|
|
is bound as data into the transcript.}]
|
|
func (e *TxAuthEnvelope) SigningDigest() [48]byte {
|
|
parts := [][]byte{
|
|
[]byte(txAuthProtocolTag), // "Lux/TxAuthEnvelope/v1"
|
|
u16BE(e.Version),
|
|
{byte(e.ProfileID)},
|
|
u32BE(e.ChainID),
|
|
u32BE(e.NetworkID),
|
|
e.AccountID[:],
|
|
u64BE(e.Nonce),
|
|
u64BE(e.ExpiryHeight),
|
|
{byte(e.WalletSchemeID)},
|
|
{byte(e.HashSuiteID)},
|
|
e.FeePayer[:],
|
|
u64BE(e.GasLimit),
|
|
e.MaxFee[:],
|
|
e.CallRoot[:],
|
|
e.AccessListRoot[:],
|
|
e.ZIdentityRoot[:],
|
|
e.AccountStateRoot[:],
|
|
e.PublicKeyRef[:],
|
|
}
|
|
return tupleHash48(parts, txAuthSigningCustomization)
|
|
}
|
|
\end{lstlisting}
|
|
|
|
The hash family is cSHAKE256, output truncated to 48 bytes; the customization
|
|
string is \texttt{LUX\_TX\_AUTH\_V1}. \texttt{HashSuiteID} is bound \emph{as
|
|
data} into the transcript --- it is not the hash family of the transcript
|
|
itself. This means a profile change to \texttt{HashSuiteID} bumps the
|
|
customization (\texttt{LUX\_TX\_AUTH\_V1} $\to$ \texttt{V2}) at the same time
|
|
it changes the data byte. Both changes are observable in the digest.
|
|
|
|
\subsection{Verification gate}
|
|
|
|
\texttt{VerifyTxAuthEnvelope} (\texttt{protocol/auth/tx\_envelope.go}) is the
|
|
profile-gated verifier. The order matters; we list it explicitly because
|
|
the order is the security-relevant claim:
|
|
|
|
\begin{enumerate}[leftmargin=2em,nosep]
|
|
\item \textbf{Structural.} Envelope non-nil, profile non-nil,
|
|
\texttt{Version} supported, every field bytes-of-the-right-width.
|
|
\item \textbf{Profile/id match.} \texttt{envelope.ProfileID} equals
|
|
\texttt{profile.ProfileID}.
|
|
\item \textbf{Profile/scheme match.} \texttt{envelope.WalletSchemeID}
|
|
equals \texttt{profile.WalletSchemeID}.
|
|
\item \textbf{Profile/suite match.} \texttt{envelope.HashSuiteID} equals
|
|
\texttt{profile.HashSuiteID}.
|
|
\item \textbf{Profile typed-auth gate.}
|
|
\texttt{profile.RequireTypedTxAuth} is \texttt{true}; if false this
|
|
code path is not reached at all.
|
|
\item \textbf{Z-Chain binding.} \texttt{ZIdentityRoot} matches the
|
|
current Z-Chain identity root for this epoch (look-up against
|
|
\texttt{ZRegistryRoots.IdentityRoot}, Section~\ref{sec:zchain-rollup}).
|
|
\item \textbf{Account state binding.} \texttt{AccountStateRoot} matches
|
|
the current account state root.
|
|
\item \textbf{Account / pubkey binding.} The resolved pubkey hashes to
|
|
\texttt{PublicKeyRef}, and the pubkey is on the active key list for
|
|
\texttt{AccountID} as of \texttt{ZIdentityRoot}.
|
|
\item \textbf{Expiry.} Current chain height $\le$ \texttt{ExpiryHeight}.
|
|
\item \textbf{Nonce.} \texttt{envelope.Nonce} equals
|
|
\texttt{account.Nonce} (no fast-forward, no replay).
|
|
\item \textbf{Signature.} ML-DSA-65 verify of \texttt{Signature} over
|
|
\texttt{SigningDigest()} under the resolved pubkey, FIPS 204 §5.2 ctx
|
|
set to a per-profile constant.
|
|
\end{enumerate}
|
|
|
|
Steps 2--5 reject before the expensive signature verification runs. Step 6--8
|
|
reject before a stale-state replay can land. Step 11 is the only step that
|
|
runs the lattice signature primitive, which is the only PQ-expensive step.
|
|
|
|
\subsection{Comparison to EIP-7932}
|
|
|
|
EIP-7932 \cite{eip7932} proposes a signature registry for EVM transactions
|
|
that lets new schemes plug in at the transaction-type byte. The EIP is
|
|
structurally compatible with this paper's goals --- it would allow a chain to
|
|
admit ML-DSA-65 transactions alongside ECDSA transactions --- but it falls
|
|
short in two ways:
|
|
|
|
\begin{itemize}[leftmargin=2em,nosep]
|
|
\item EIP-7932 does not standardise the \emph{transcript}. Each scheme
|
|
declares its own signing digest. Two schemes sharing inputs can produce
|
|
different digests because the scheme-byte appears in the wrapper but
|
|
not in the inner transcript. TupleHash256 with a single customization
|
|
closes that gap.
|
|
\item EIP-7932 does not standardise the profile binding. A transaction
|
|
signed under scheme A on chain X can be replayed as a scheme-A
|
|
transaction on chain Y if both chains admit scheme A in their
|
|
registry. \texttt{ProfileID} closes that gap.
|
|
\end{itemize}
|
|
|
|
Bullets aside, the two designs converge: a strict-PQ chain that wants to
|
|
expose a 7932-style registry to other chains can declare exactly one entry
|
|
(\texttt{TxAuthEnvelope}) and exactly one scheme (ML-DSA-65), and the
|
|
externally-observable behaviour is equivalent. The Lux production canonical
|
|
takes the simpler path: no in-chain registry, a single mandatory envelope.
|
|
|
|
\subsection{Wire codec}
|
|
|
|
\texttt{TxAuthEnvelope} serialises as a deterministic big-endian layout
|
|
(\texttt{tx\_envelope.go:178}):
|
|
|
|
\begin{table}[H]
|
|
\centering
|
|
\caption{TxAuthEnvelope wire layout, fixed-width except \texttt{signature}.}
|
|
\begin{tabular}{lll}
|
|
\toprule
|
|
Offset & Size & Field \\
|
|
\midrule
|
|
$0$ & $2$ & \texttt{version} (uint16 BE) \\
|
|
$2$ & $1$ & \texttt{profile\_id} (uint8) \\
|
|
$3$ & $4$ & \texttt{chain\_id} (uint32 BE) \\
|
|
$7$ & $4$ & \texttt{network\_id} (uint32 BE) \\
|
|
$11$ & $48$ & \texttt{account\_id} \\
|
|
$59$ & $8$ & \texttt{nonce} (uint64 BE) \\
|
|
$67$ & $8$ & \texttt{expiry\_height} (uint64 BE) \\
|
|
$75$ & $1$ & \texttt{wallet\_scheme\_id} (uint8) \\
|
|
$76$ & $1$ & \texttt{hash\_suite\_id} (uint8) \\
|
|
$77$ & $48$ & \texttt{fee\_payer} \\
|
|
$125$ & $8$ & \texttt{gas\_limit} (uint64 BE) \\
|
|
$133$ & $32$ & \texttt{max\_fee} \\
|
|
$165$ & $32$ & \texttt{call\_root} \\
|
|
$197$ & $32$ & \texttt{access\_list\_root} \\
|
|
$229$ & $32$ & \texttt{z\_identity\_root} \\
|
|
$261$ & $32$ & \texttt{account\_state\_root} \\
|
|
$293$ & $32$ & \texttt{public\_key\_ref} \\
|
|
$325$ & $4$ & \texttt{sig\_length} (uint32 BE) \\
|
|
$329$ & $\le$ 3309 & \texttt{signature} (ML-DSA-65) \\
|
|
\bottomrule
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
The 329-byte fixed header followed by a length-prefixed signature is the
|
|
entire wire format. There is no nested encoding, no varints, no RLP
|
|
ambiguity, no field-ordering choice. A naive decoder that consumes the bytes
|
|
in order produces the canonical envelope; \emph{any} ambiguity in decoding is
|
|
a fatal decoder bug, not a protocol question.
|