luxd serves chain APIs under one segment from constants.ChainAliasPrefix (constants v1.6.4, node v1.36.181): /v1/bc/<alias>/... -> /v1/chain/<alias>/... /ext/bc/<alias>/... -> /v1/chain/<alias>/... Observations keep their original addresses: a dated note or a recorded status code is a fact about when it was written, not an instruction to follow.
13 KiB
DEPLOYMENT-RUNBOOK — luxfi/teleport
Operator-facing runbook for deploying and operating a Teleport bridge instance. Covers initial deployment, MPC ceremony bootstrap, relay validator onboarding, custody key rotation, and profile transition (classical-compat → strict-PQ).
Audience: SRE / operator. Assumes familiarity with Hardhat, multi-sig
wallets, and the Lux production stack (lux-k8s, mpcd, the
threshold dispatcher).
1. Prerequisites
| Tool | Version |
|---|---|
| Node.js | >= 18 |
| pnpm | >= 8 |
| Hardhat | ^2.28.2 (pinned in contracts/package.json) |
| Solidity | 0.8.20 |
| MongoDB | 6.x (api + mpc persistence) |
mpcd |
from ~/work/lux/mpc — must be running before MPC service starts |
Build:
cd ~/work/lux/teleport
pnpm install -r # all workspaces
cd contracts && pnpm build
cd ../api && pnpm install && pnpm tsc
cd ../mpc && pnpm install && pnpm build
Run tests (must all pass before any deployment):
cd ~/work/lux/teleport/contracts && pnpm test
cd ~/work/lux/teleport/mpc && pnpm test
Expected: 60 contract tests + 31 MPC tests = 91 green.
2. Initial deployment
2.1 Solidity contracts
Per-chain deployment order:
-
Deploy
LBTC/LETH/LUXD(bridgeable token contracts) on the destination chain. Each is anERC20Bderivative. -
Deploy
Warp2VerifierStubwith admin = governance multi-sig and flags =0x07(require all three lanes) for high-value assets, or0x01(Beam-only) for low-value / migration.cd contracts pnpm hardhat run scripts/deploy-warp2-stub.ts --network luxFor PRODUCTION (Lux mainnet), do NOT use the stub — wire in the precompile-dispatcher verifier (LP-2517 / LP-2311 / LP-2312). The stub is for testnet / migration only and emits
Registered(messageHash, true)for any admin-pre-registered hash. -
Deploy
BridgeV2with constructor:new BridgeV2( admin: GOVERNANCE_MULTISIG, verifier: WARP2_VERIFIER_ADDR, feeRecipient: FEE_TREASURY, feeRate: 5e15, // 0.5% minPolicyFlags: 0x07 // require all three lanes );The constructor seeds
activeProfile = PROFILE_CLASSICAL_COMPAT(the default). Strict-PQ is opt-in via a follow-upsetActiveProfilecall (§4 below). -
Grant
BRIDGE_ROLEtoBridgeV2on each token:token.grantRole(BRIDGE_ROLE, bridgeV2Addr); -
Set token allowlist on
BridgeV2:bridgeV2.setTokenAllowed(LBTC_ADDR, true); bridgeV2.setTokenAllowed(LETH_ADDR, true); bridgeV2.setTokenAllowed(LUXD_ADDR, true); -
Configure per-asset policy flags (optional, default is global
minPolicyFlags):bridgeV2.setAssetPolicyFlags(LBTC_ADDR, 0x07); // require all three bridgeV2.setAssetPolicyFlags(LETH_ADDR, 0x07); bridgeV2.setAssetPolicyFlags(LUXD_ADDR, 0x01); // Beam-only OK
2.2 API service
Per-validator deployment (each Lux validator runs its own API + MPC service for redundancy):
-
Configure
api/settings.json:{ "RPC": { "96369": "http://luxd-headless.lux-mainnet.svc:9630/v1/chain/C/rpc", "1": "https://eth-rpc.lux.network", "8453": "https://base-rpc.lux.network" }, "Bridges": { "lux": "0x...BridgeV2_lux", "eth": "0x...BridgeV2_eth", "base": "0x...BridgeV2_base" }, "AllowedTokens": { ... }, "NetNames": { "96369": "lux", "1": "eth", "8453": "base" }, "DB": "<mongodb_password>", "SigningManagers": { "0": "http://mpc.lux.network:7100" }, "KeyStore": { "0": "lux_keystore.json" }, "MaxRetries": 3, "RetryDelayMs": 2000 } -
Set environment:
# Strict-PQ deployment (recommended for Lux ↔ Lux) export LUX_PROFILE=quasar-strict-pq export MPCD_URL=http://127.0.0.1:7300 # mpcd loopback # Classical-compat deployment (Lux ↔ Ethereum mainnet, see LEGACY-CLASSICAL.md) export LUX_PROFILE=classical-compat -
Start:
cd api node dist/bridge.jsHealth:
curl http://localhost:6000/health.
2.3 MPC service
The MPC service is a client of mpcd (which runs in M-Chain). It does
NOT own keys.
-
Ensure
mpcdis running and reachable atMPCD_URL. From the~/work/lux/mpcrepo:./mpcd --threshold-listen 127.0.0.1:7300 --keystore /var/lib/mpcd/keys -
Configure
mpc/settings.json(same shape asapi/settings.jsonwith an additionalMPCPeerssection). -
Set environment (must match the API service):
export LUX_PROFILE=quasar-strict-pq export MPC_SCHEME=double-lattice # default under strict-PQ export MPCD_URL=http://127.0.0.1:7300 -
Start:
cd mpc node dist/teleporter.jsHealth:
curl http://localhost:6000/health. Should log:>> Threshold signer scheme=double-lattice policyId=DOUBLE-LATTICE-PULSAR+CORONA >> Teleporter MPC Running on port 6000
If LUX_PROFILE is unset, the service refuses to start (fail-
closed). The error message includes remediation text.
3. MPC ceremony bootstrap
For each external asset (BTC, ETH, …), an MPC keygen ceremony must run to establish the custody key. This is per-asset, one-time at deployment.
The ceremony itself runs in mpcd (M-Chain), not in Teleport. From
Teleport's perspective, the steps are:
-
Operator initiates the ceremony via
mpcdadmin RPC:curl -X POST http://127.0.0.1:7300 \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc":"2.0", "method":"cggmp21.keygen", "params":{"threshold":4,"participants":7}, "id":1 }'Response:
{ "publicKey": "0x...", "shares": [...] }. Each share is distributed to a custodian shareholder via the M-Chain identity layer (ML-KEM-768 sealed envelopes). -
Public key is published on-chain via a one-shot tx (per-asset
setCustodyPublicKeyon the M-Chain ThresholdVM contract; out of scope ofBridgeV2). -
Teleport configuration: nothing to do. The bridge endpoint sees the custody public key indirectly via the Warp 2.0 envelope — the envelope's authorisation is what triggers M-Chain to release.
For intra-Lux signing (the threshold-signer bus used by the MPC service), the ceremony is similarly:
# Pulsar keygen for the bridge's own envelope-signing key
curl -X POST $MPCD_URL -d '{
"jsonrpc":"2.0",
"method":"pulsar.keygen",
"params":{"threshold":5,"participants":7},
"id":1
}'
# Corona keygen (for double-lattice composition)
curl -X POST $MPCD_URL -d '{
"jsonrpc":"2.0",
"method":"corona.keygen",
"params":{"threshold":5,"participants":7},
"id":2
}'
After both keygens complete, the MPC service can sign with
MPC_SCHEME=double-lattice.
4. Strict-PQ profile transition
Once the bridge has been operating under classical-compat for the warm-up period and the PQ verifier precompile is available, transition to strict-PQ:
4.1 Off-chain prep
-
Deploy a PQ verifier contract implementing
IVerifier:contract MLDSAVerifier is IVerifier, IPolicyId { function verify(bytes calldata proof, bytes32 publicInputHash) external returns (bool) { /* ML-DSA-65 precompile call */ } function policyId() external pure returns (bytes32) { return PQProfileIDs.POLICY_MLDSA65_FIPS204; } } -
Verify the contract on Etherscan / Lux explorer.
4.2 On-chain transition
-
DEFAULT_ADMIN_ROLE(multi-sig + timelock) extends the allow-list:bytes32 ch = MLDSAVerifier.deployedBytecodeHash(); // off-chain bridgeV2.setAllowedVerifierCodeHash(ch, true); -
ADMIN_ROLEflips the profile:bridgeV2.setActiveProfile( PROFILE_QUASAR_STRICT_PQ, MLDSAVerifier_addr );This call reverts if (a)
MLDSAVerifier_addr == address(0), (b)policyId() != POLICY_MLDSA65_FIPS204 && != POLICY_STARK_FRI_SHA3_PQ, or (c)extcodehash(MLDSAVerifier_addr)is not in the allow-list. -
(Optional) Install P3Q verifier for
claimWithProof:bridgeV2.setP3QVerifier(P3Q_VERIFIER_ADDR);Same install-time checks:
policyId == POLICY_STARK_FRI_SHA3_PQANDextcodehashin allow-list.
4.3 Off-chain transition
In the same maintenance window as the on-chain flip:
-
Update each validator's API + MPC service env:
export LUX_PROFILE=quasar-strict-pq # MPC_SCHEME defaults to double-lattice; explicit MPC_SCHEME=pulsar # or =corona is also accepted; classical schemes throw. -
Restart the services. Health check should log:
>> Threshold signer scheme=double-lattice policyId=DOUBLE-LATTICE-PULSAR+CORONA -
Submit a test burn/mint round-trip with a small amount to confirm the new wire format.
If anything fails, rollback by setting
LUX_PROFILE=classical-compat on both sides and calling
setActiveProfile(PROFILE_CLASSICAL_COMPAT, ...) (any verifier is
admissible under classical-compat).
5. Custody key rotation (LSS)
M-Chain ThresholdVM (LP-7330) refreshes per-asset MPC key shares on a schedule via Live Secret Sharing (LSS). The public key stays the same; only the shares rotate.
From Teleport's perspective: nothing to do. The bridge endpoint verifies envelopes signed under the (stable) public key. LSS is transparent to the bridge.
Verification steps for an operator during a rotation:
- Pre-rotation: capture a "before" envelope. Check that
verifier.verify(env)returns true on the destination chain. - Trigger rotation via M-Chain admin RPC:
curl -X POST $MPCD_URL -d '{"jsonrpc":"2.0","method":"cggmp21.reshare",...}' - Post-rotation: capture an "after" envelope. Check that
verifier.verify(env)still returns true. - Cross-check: the public key on the M-Chain ThresholdVM contract should be unchanged.
If the public key changes during a rotation, the rotation MUST be treated as a key change (re-publish on M-Chain, re-deploy any verifiers that bake in the public key) — this is NOT a normal LSS rotation.
6. Relay / oracle validator onboarding
To add a new Lux validator to the Teleport signing set:
-
Validator generates an ML-DSA-65 identity key (FIPS 204) and a Pulsar share locally, both via
mpcd's keygen flow. -
Existing validator quorum runs a reshare ceremony to include the new validator's share:
curl -X POST $MPCD_URL -d '{ "jsonrpc":"2.0", "method":"pulsar.reshare", "params":{ "oldThreshold":4, "oldParticipants":5, "newThreshold":5, "newParticipants":7 }, "id":1 }' -
New validator deploys API + MPC service per §2.
-
Governance updates the on-chain Warp 2.0 verifier's validator registry to include the new ML-DSA-65 public key. This is out of scope of
BridgeV2(lives in the Warp 2.0 verifier dispatcher contract). -
Smoke test with a low-value burn → claim round-trip from a non-production chain.
7. Pause / emergency stop
If an issue is detected:
// PAUSER_ROLE can pause; subsequent burn() and claim() revert with Pausable.Paused
bridgeV2.pause();
While paused, the v1 Bridge.sol may still be operational (separate
contract, separate role set). To stop v1 too:
bridge.pause();
To resume:
bridgeV2.unpause();
bridge.unpause();
8. Monitoring
Recommended metrics (Prometheus / Grafana):
| Metric | Source | Alert threshold |
|---|---|---|
teleport_api_signature_requests_total |
api/ Express middleware |
none — pure liveness |
teleport_api_signature_failures_total{reason} |
api/ |
> 1% sustained → page |
teleport_mpc_sign_latency_seconds |
mpc/ signWithMPC |
p99 > 60s → page |
teleport_mpc_active_processes |
mpc/ activeProcesses.size |
> 50 → page (backpressure) |
bridge_v2_BridgeBurnedV2_count |
on-chain via Lux Graph Engine | none — pure liveness |
bridge_v2_BridgeMintedV2_count |
on-chain | should track BridgeBurnedV2 modulo finality lag |
bridge_v2_VerifierCodeHashAllowed_count |
on-chain | > 0 in a non-deployment window → page (governance action) |
bridge_v2_ProfileUpdated_count |
on-chain | any spike → page (profile flip) |
Dashboards: see https://grafana.lux.network/d/teleport.
9. Reference
SUBMISSION.md— cover sheetSPEC.md— bridge state machineWARP2_BRIDGE.md— Warp 2.0 architecture + Phase 1-5 migrationLEGACY-CLASSICAL.md— classical-compat opt-inTRUSTED-COMPUTING-BASE.md— TCB inventory