Adds the Tier A bridge endpoint submission package:
- SUBMISSION.md cover sheet, headline claims, scope
- SPEC.md bridge state machine, custody MPC,
oracle attestation, relay protocol
- PROOF-CLAIMS.md P1-P6 proved, N1-N6 not proved
- TRUSTED-COMPUTING-BASE.md TCB inventory (HW/OS, primitives, quorums)
- PATENTS.md royalty-free patent grant
- CRYPTOGRAPHER-SIGN-OFF.md APPROVED WITH GATES, 5 gates listed
- DEPLOYMENT-RUNBOOK.md operator playbook (build / deploy / MPC
ceremony bootstrap / profile transition /
LSS rotation / pause / monitoring)
- LEGACY-CLASSICAL.md LUX_PROFILE=classical-compat opt-in docs
- CHANGELOG.md per-release changes (incl. Tier A pass)
Also strips the deprecated privacy-layer section from LLM.md (the
contracts it described were removed in commit 78e2dd7). The privacy
section is replaced with a brief pointer to git history and to the
Warp 2.0 envelope path which is the canonical surface today.
No code changes in this commit. The strict-PQ profile gate, six-
scheme threshold-signer bus, and EXTCODEHASH allow-list were all
landed in prior commits (b0ff75e, 5c64d64, bdfc56c, ddc5547). This
commit closes the documentation gap.
The cryptographer sign-off is APPROVED WITH GATES (5 gates listed,
all docs / runbook / interop deliverables, none algorithmic).
18 KiB
Cryptographer sign-off — luxfi/teleport (Tier A)
Independent review of the Teleport bridge endpoint surface at commit
3098d07(currentmainHEAD prior to this Tier A submission branch) plus the documentation / tests added undercryptographer/teleport-tier-a. Date of review: 2026-05-18. Reviewer: cryptographer agent (Lux Cryptography, internal review).
Summary
APPROVED WITH GATES for production use as the Lux cross-chain bridge endpoint, subject to the five disclosure / pre-publish gates in the "Gates" section below.
The repository's strict-PQ profile gate (StrictPQProfileGate.sol +
OnlyStrictPQ modifier from @luxfi/pq-profile-ids), the six-scheme
threshold-signer bus (mpc/src/signers/), and the EXTCODEHASH allow-
list (allowedVerifierCodeHashes) jointly close the install-time
attack surface that prior reviews flagged. The construction is
decomplected in the Hickey sense: policy enforcement lives in one
place (the gate), verification lives in another (the verifier), and
the two are composed at the entrypoint via a single canonical helper.
The residual gates are about documentation maturity (this Tier A
pass), KAT byte-level lock-in between the TypeScript and Go sides for
the ML-DSA-65 envelope path, runbook completeness for the LSS-rotation
ceremony, multi-sig setup for DEFAULT_ADMIN_ROLE vs ADMIN_ROLE,
and one minor doc drift — none are algorithmic or implementation
defects.
What was reviewed
- Solidity contracts (
contracts/contracts/):Bridge.sol— v1 EIP-712 oracle bridge, classical-compat path, retained for back-compat.BridgeV2.sol— v2 Warp 2.0 envelope bridge, PQ-default ready, EXTCODEHASH allow-list, dual admin role split.StrictPQProfileGate.sol— single-function refusal helper, re-exporting@luxfi/pq-profile-ids.Warp2VerifierStub.sol— dev / test stub, clearly labelled "NOT FOR PRODUCTION USE".ERC20B.sol,LBTC.sol,LETH.sol,LUXD.sol,IBridgeToken.sol— bridgeable tokens.- All interfaces in
contracts/contracts/interfaces/.
- Off-chain services:
api/src/bridge.ts— Express oracle service, ECDSA EIP-712 path.api/src/pq/pqSigner.ts— ML-DSA-65 envelope signing, cSHAKE256 digest, customization strings pinned to@luxfi/wallet.mpc/src/teleporter.ts— Express MPC service, threshold-signer dispatch.mpc/src/signers/— six implementations behind oneThresholdSignerinterface:ecdsa.ts(GG18 spawn, byte-identical to legacy)cggmp21.ts(JSON-RPC to mpcd)frost.ts(noble-curves in-process or daemon)pulsar.ts(JSON-RPC to mpcd, M-LWE threshold)corona.ts(JSON-RPC to mpcd, R-LWE threshold)doubleLattice.ts(composition: Pulsar ⊕ Corona)
mpc/src/signers/rpc.ts— JSON-RPC 2.0 client withMPCD_URL/THRESHOLD_DAEMON_URLenv resolution.mpc/src/signers/ThresholdSigner.ts— common interface,POLICY_IDmapping imported from@luxfi/pq-profile-ids-ts.mpc/src/signers/index.ts— registry +resolveSchemeFromEnvwith fail-closed semantics on unsetLUX_PROFILE.
- Tests:
contracts/test/Bridge.test.ts— 36 cases on v1.contracts/test/BridgeV2.test.ts— 14 cases on the strict-PQ gate- EXTCODEHASH allow-list + production artifact snapshot.
contracts/test/StrictPQProfileGate.test.ts— 7 cases on the canonical gate library.mpc/__tests__/signers.test.ts— 31 cases across the registry,LUX_PROFILEderivation, per-scheme suites.api/src/pq/pqSigner.test.ts— ML-DSA-65 envelope serializer + digest KAT against pinned@luxfi/walletconstants.
- External dependencies:
@luxfi/pq-profile-ids(Solidity + TS) — canonical profile / policy IDs.@luxfi/p3q-evm— audit-gated P3Q verifier stub (always rejects until post-audit bodies ship).@noble/curves@^2.2,@noble/hashes@^2.2— secp256k1, FROST, cSHAKE256.
- Documentation:
WARP2_BRIDGE.md— Warp 2.0 architecture + Phase 1-5 migration plan.LLM.md/AGENTS.md/CLAUDE.md(symlinks) — AI-context documentation.README.md— public-facing overview.
Findings
Section A — Construction
A1. Strict-PQ gate is decomplected
The gate is refuseUnderStrictPQ(bytes32 profile, IVerifier verifier),
defined once in @luxfi/pq-profile-ids/src/OnlyStrictPQ.sol, re-exported
from StrictPQProfileGate.sol. Both entrypoints (claim via the
onlyStrictPQ modifier, claimWithProof via an inline
_refuseUnderStrictPQ call) route through the same helper. The
canonical constants (PROFILE_*, POLICY_*) are imported from a
single npm package; the Go side reads the same constants from
@luxfi/pq-profile-ids-ts.
Decomplecting verdict: passes. No braiding of policy and
verification. The verifier calls verify(); the gate calls
policyId() + extcodehash. Independent concerns, independent
contracts, single canonical entry point.
A2. Allow-list, not deny-list
The gate refuses any policyId not in the explicit allow-list
{POLICY_STARK_FRI_SHA3_PQ, POLICY_MLDSA65_FIPS204}. A spoofed
verifier returning an unknown bytes32 from policyId() is refused —
Red HIGH #2 closed.
address(0) under strict-PQ is refused with
VerifierUnsetUnderStrictPQ — Red HIGH #5 closed. A "silent no-op"
slot is impossible under strict-PQ; the only way to clear a slot is to
flip to PROFILE_CLASSICAL_COMPAT first.
Allow-list verdict: passes. Tests in
contracts/test/StrictPQProfileGate.test.ts exercise the entire 2×6
profile × policy matrix.
A3. EXTCODEHASH defence in depth (Red round 5)
allowedVerifierCodeHashes is the load-bearing defence against a
"true-returning" verifier with allow-listed policyId. The role
split:
DEFAULT_ADMIN_ROLE(intended for multi-sig + timelock) can extend the allow-list viasetAllowedVerifierCodeHash.ADMIN_ROLE(intended for operations multi-sig) can swap among already-vetted code hashes viasetActiveProfileandsetP3QVerifier.
A compromised ADMIN_ROLE cannot install a new "true-returning"
verifier; it can only choose among allow-listed ones. This is the
correct role-split discipline.
Verdict: passes. Tests (i)-(n) in
contracts/test/BridgeV2.test.ts exercise the install-time refusal,
the DEFAULT_ADMIN_ROLE gating on the allow-list mutator, and the
classical-compat profile bypass (allow-list is profile-gated; classical
profiles don't need it).
A4. Off-chain profile coherence (Red HIGH #4)
resolveSchemeFromEnv in mpc/src/signers/index.ts is fail-closed
on unset LUX_PROFILE. The error message includes remediation text
("LUX_PROFILE must be set: quasar-strict-pq | classical-compat |
hybrid. There is no implicit default — the profile chooses the
on-the-wire signature scheme…"). This closes the previous attack
surface where a deployment missing STRICT_PQ would silently default
to ECDSA on a strict-PQ on-chain bridge.
The matrix:
LUX_PROFILE |
Default scheme | Admissible schemes |
|---|---|---|
quasar-strict-pq |
double-lattice |
{pulsar, corona, double-lattice} |
classical-compat |
ecdsa |
{ecdsa, cggmp21, frost} |
hybrid |
ecdsa (with WARN) |
all six |
| unset | THROW | THROW |
Symmetric refusal: classical schemes under strict-PQ throw; PQ schemes under classical-compat throw. This symmetry is the load-bearing property — a typo in env config cannot silently land in either direction.
Verdict: passes. 12 cases under "registry" + "LUX_PROFILE →
MPC_SCHEME derivation" in mpc/__tests__/signers.test.ts exercise
every cell of the matrix.
A5. Threshold-signer bus composition
The six signers all implement ThresholdSigner (one interface, six
files). double-lattice is composition over inheritance — it
owns a PulsarSigner and a CoronaSigner, not extending either.
The wire format is explicit:
sig = u32be(len(pulsar_sig)) || pulsar_sig || corona_sig
pk = u32be(len(pulsar_pk)) || pulsar_pk || corona_pk
The u32be length prefix is unambiguous; the verifier splits at the
length, verifies both lanes, returns AND. A break of M-LWE alone does
not forge if R-LWE holds.
Verdict: passes. The "double-lattice signer (pulsar ⊕ corona)"
suite in mpc/__tests__/signers.test.ts covers sign/verify round-trip,
forgery rejection, and wrong-key rejection.
Section B — Implementation
B1. ECDSA legacy path is byte-identical
The EcdsaSigner in mpc/src/signers/ecdsa.ts is a thin wrapper
around the legacy spawn(gg18_sign_client) flow. The output is the
same 65-byte r || s || v ethers ECDSA signature; the call shape
(args: signing-manager URL, keystore filename, msgHash hex) is
preserved. A deployment migrating from pre-2026 Teleport to the new
bus sees no wire change.
Verdict: passes.
B2. cSHAKE256 customization is pinned
api/src/pq/pqSigner.ts reproduces the cSHAKE256 customization
strings from @luxfi/wallet byte-for-byte:
CSHAKE_CUSTOMIZATION_TX = "LUX/WALLET/TX/V1"
CSHAKE_CUSTOMIZATION_ACCOUNT_ID = "LUX/WALLET/ACCOUNT_ID/V1"
ACCOUNT_ID_SIZE = 48
ACCOUNT_ID_LABEL = "LUX_ACCOUNT_V1"
WALLET_SCHEME_MLDSA65 = 0x42
The KAT in pqSigner.test.ts asserts equality with the @luxfi/wallet
exports. A drift on either side fails the KAT — the byte-level invariant
is locked in.
Verdict: passes.
B3. Envelope serializer is unambiguous
serializeEnvelope produces exactly 112 bytes:
u32be(burnChainID) || u32be(claimChainID) || u64be(nonce) ||
pad32(token) || pad32(recipient) || pad32(amount)
The check if (total !== 112) throw at the end of the function is the
defensive assertion; any future change to the layout would fire this
trip-wire.
Verdict: passes. Suggest adding an explicit cross-check test that constructs the same bytes from the Go side (via a pinned vector) and asserts byte-equality. See Gate GATE-2 below.
B4. Replay protection lives on-chain
usedMessages[bytes32] is the source of truth. The MongoDB
persistence in api/ and mpc/ is a cache; loss of MongoDB does NOT
enable replay (because the on-chain check fires before any storage
write). This is the correct ordering of concerns.
Verdict: passes.
B5. Token allowlist is non-bypassable
bridgeBurn reverts if !allowedTokens[r.token]. bridgeMint
(both claim and claimWithProof) reverts under the same predicate.
There is no "vault" path that bypasses the allowlist.
Verdict: passes.
B6. Fee bounds are enforced
MAX_FEE_RATE = 1e17 (10%); setFeeConfig reverts if _feeRate > MAX_FEE_RATE. There is no path to set a > 10% fee.
Verdict: passes.
B7. Constant-time considerations (off-chain)
The Solidity surface is not subject to a constant-time guarantee (EVM
gas leaks per opcode). The off-chain PQ primitives carry their own
constant-time evidence in luxfi/pulsar/CRYPTOGRAPHER-SIGN-OFF.md and
luxfi/corona/docs/mptc/. Teleport does not weaken these guarantees
— the JSON-RPC client to mpcd does not introduce a side channel.
The ECDSA path uses gg18_sign_client (spawn-based) — its constant-
time properties are inherited from the upstream multi-party-ecdsa
library. Teleport does not implement custom ECDSA arithmetic.
Verdict: passes (transitively; constant-time is upstream's responsibility).
B8. Hex / range validation at the boundary
bridge.ts and teleporter.ts validate every input (burnTxHash,
fromChainId, logIndex) at the express boundary with strict
regexes. The MPC service additionally re-parses the burn event from
chain — defence in depth. signWithMPC revalidates the hex msgHash
before invoking the signer.
Verdict: passes.
Section C — Documentation
C1. WARP2_BRIDGE.md is comprehensive
The pre-existing Warp 2.0 architecture doc covers the three lanes (Beam BLS, ML-DSA cert set, Pulsar Pulse), the threat model, the Phase 1-5 migration plan, the M-Chain custody integration, and the LP references.
Verdict: passes.
C2. SPEC / PROOF-CLAIMS / TCB are new in this pass
SPEC.md, PROOF-CLAIMS.md, TRUSTED-COMPUTING-BASE.md,
DEPLOYMENT-RUNBOOK.md, LEGACY-CLASSICAL.md are added in this Tier
A pass. They formalize what was previously only in WARP2_BRIDGE.md
and code comments. Cross-referenced from SUBMISSION.md.
Verdict: passes.
C3. LLM.md drift
The LLM.md / CLAUDE.md / AGENTS.md (all symlinks to the same
file) was last updated on 2026-01-01. It references the deprecated
privacy contracts (PrivateTeleport, ZNote, ZChainAMM,
BulletproofVerifier, PrivateBridge) which were removed in
commit 49522d3. The privacy layer description in the second half
of the file is stale and should be either removed or moved into an
ARCHIVE-PRIVACY-LAYER.md for historical reference.
Verdict: gate (see GATE-4 below).
Gates
The following gates are PRE-PUBLISH conditions. Each is a discrete deliverable; none are algorithmic.
GATE-1 — KAT cross-validation between TS and Go for ML-DSA-65 envelope
Status: OPEN. The TS-side pqSigner.ts reproduces the cSHAKE256
customization strings byte-for-byte from @luxfi/wallet. The Go side
in github.com/luxfi/sdk/wallet/account is the canonical source.
The test in pqSigner.test.ts asserts equality with the @luxfi/wallet
exports but does NOT execute a Go-side computation as a comparator.
Closure: add a fixture file (testdata/envelope_kat.json) with at
least 16 vectors generated from the Go side (luxfi/sdk test harness),
checked into the repo. The TS-side test reads the fixture and asserts
byte-equality. This locks the wire format in both directions: any
change on either side must update the fixture, which is visible in
PR review.
Pragmatic alternative: a CI step that runs the Go-side harness and diffs against the TS output. Acceptable as long as the fixture exists in a form that can be inspected statically.
GATE-2 — Two-multisig setup for DEFAULT_ADMIN_ROLE vs ADMIN_ROLE
Status: OPEN. The role split is encoded in the contract. The
deployment runbook (DEPLOYMENT-RUNBOOK.md §"Role assignments")
recommends specific multi-sig configurations:
DEFAULT_ADMIN_ROLE: 4-of-7 multi-sig with 48h timelockADMIN_ROLE: 3-of-5 multi-sig without timelockPAUSER_ROLE: on-call single-key with strict monitoring
Closure: actual production deployments MUST verify that the two roles are held by distinct multi-sig wallets, NOT by the same address. Re-verify after every Etherscan publish that the role assignments match the recommended split. Audit log entry per deployment.
GATE-3 — LSS rotation playbook integration test
Status: OPEN. DEPLOYMENT-RUNBOOK.md §5 describes the LSS
rotation flow. The integration test (an end-to-end ceremony driven by
mpcd test infra) lives in luxfi/mpc (the daemon's own repo), not
in teleport.
Closure: a teleport-side integration test that mocks mpcd and
asserts the bridge continues to mint correctly across a rotation
event. Acceptable to mock mpcd since the rotation is upstream's
responsibility — Teleport just needs to verify that envelopes signed
under the new shares (but same public key) still verify.
GATE-4 — LLM.md privacy-layer drift
Status: OPEN. LLM.md (and its three symlinks) describes the
removed privacy layer in detail. Either:
(a) Strip the privacy-layer section entirely from LLM.md. The
historical content can move to ARCHIVE-PRIVACY-LAYER.md for
git history reference.
(b) Add a deprecation banner at the top of the privacy-layer section
pointing at commit 49522d3 for the removal.
Closure: pick (a) or (b) and update LLM.md accordingly.
Recommended: (a), one-and-only-one-way discipline.
GATE-5 — Phase 3-5 verifier wire-in checklist
Status: OPEN — depends on upstream. Phases 3-5 of the Warp 2.0
migration (WARP2_BRIDGE.md §6) require the Lux EVM precompile suite:
- LP-2311 (BLS12-381 precompile) — already available (EIP-2537).
- LP-2517 (ML-DSA-65 precompile) — required for Phase 3 ML-DSA cert set verification.
- LP-2312 (Pulsar threshold precompile) — required for Phase 4 Pulse verification.
Closure: when LP-2517 and LP-2312 ship in luxfi/geth (or the
Lux EVM successor), the bridge MUST verify the wire format with a
real precompile dispatcher contract replacing Warp2VerifierStub.sol.
Add a per-precompile integration test that exercises the dispatcher
against a known-good envelope.
This gate is OPEN on Teleport because the closure depends on upstream
precompile availability. Track in WARP2_BRIDGE.md §6.
Recommendation
Tag the closure of this Tier A pass as v1.2.0 (minor bump — additive docs + tests, no wire change). The five gates above are pre-deployment / pre-publish conditions; v1.2.0 ships the documentation and tests, the gate closures happen per-deployment.
Future tags:
- v1.2.1 when GATE-1 (KAT cross-validation fixture) lands.
- v1.2.2 when GATE-3 (LSS rotation integration test) lands.
- v1.3.0 when Phase 3 (ML-DSA cert set on-chain) lands.
- v1.4.0 when Phase 4 (Pulsar Pulse on-chain) lands.
- v2.0.0 when Phase 5 (v1
Bridge.soldecommissioned) lands. Major bump because the wire format becomes Warp 2.0 only.
Conclusion
The bridge endpoint is in good shape. The strict-PQ profile gate
(P1, P5, P6) is the load-bearing defence, and it is implemented
correctly with a single canonical helper and an EXTCODEHASH allow-
list backstop. The off-chain profile resolver is fail-closed
(Red HIGH #4 closed). The six-scheme threshold-signer bus is
decomplected and composable. The off-chain envelope signer matches
the wallet KAT byte-for-byte. The role split between
DEFAULT_ADMIN_ROLE and ADMIN_ROLE is correctly encoded.
The gates listed are documentation / runbook / interop deliverables rather than algorithmic or implementation defects. None block deployment under the classical-compat profile today; GATE-5 is the only gate that blocks strict-PQ deployment, and it is upstream- dependent.
Approved with gates.
— Cryptographer agent, 2026-05-18