WIP: session checkpoint 2026-06-05 (codec rip in flight)

This commit is contained in:
2026-06-05 15:33:51 -07:00
parent 70c294d771
commit ce78badda9
2 changed files with 276 additions and 1 deletions
+275
View File
@@ -0,0 +1,275 @@
# Bridge Mesh Deployment
White-label deployment guide for the multi-chain bridge surface. Every brand
that runs an L1 (or L2) on top of the canonical platform inherits this
architecture verbatim — there is one mesh topology, one canonical contract
set, one off-chain orchestrator pattern.
## Architecture
### On-chain surface (per chain)
Each EVM chain in the mesh deploys two contracts and one synthetic token
family:
| Contract / Family | Source | Role |
|---|---|---|
| `BasketRegistry` | `@luxfi/contracts/bridge/v4/BasketRegistry.sol` | Per-basket membership table (USD/BTC/ETH/SOL/TON/XRP/DOT/native). |
| `BridgeV4` | `@luxfi/contracts/bridge/v4/BridgeV4.sol` | Canonical post-quantum-default bridge entrypoint. Holds `MINTER_ROLE` on every sToken on this chain. |
| `sLUX`, `sBTC`, `sETH`, `sUSDC` | `@luxfi/contracts/bridge/v4/BridgedSyntheticToken.sol` | Mint-by-bridge ERC20 wrappers — the unit-of-account inside the DeFi stack on this chain. |
The sToken contracts implement `IBridgedToken` (mint/burn) so `BridgeV4` can
use them as destination assets. Each sToken optionally points at an
underlying source-asset contract (e.g. `BridgedBTC` for `sBTC`) — when the
underlying is set, holders can `wrap(amount)` and `unwrap(amount)` 1:1
without involving the bridge.
### Off-chain surface
The mesh has **no on-chain peer-chain registry** — `BridgeV4` itself does
not store the topology. Cross-chain coupling lives in the **off-chain MPC
broadcaster daemon** (`@luxbridge/server` in this repo). The daemon:
1. Observes `RedeemRequested` events on every chain.
2. Computes the destination chain by reading `dstChain` from the event.
3. Composes a Warp 2.0 envelope and signs it via the threshold-MPC cluster
(P3Q strict profile by default — Pulsar threshold sig × Prism commitment
cut, verified by the P3Q precompile at `0x012205` on the dst chain).
4. Calls `BridgeV4.claim(envelope, proof)` on the destination chain.
Adding a new chain to the mesh is a pair of operations:
1. Run the canonical deploy on the new chain (see Step-by-step below).
2. Update the off-chain topology config so the daemon starts observing it.
### Why no on-chain peer registry?
Three reasons:
- **Envelope-carried topology** — every Warp 2.0 envelope already carries
its own `srcChain` field. The verifier (P3Q precompile) doesn't need a
peer-registry to check authenticity.
- **No registry-bloat tax** — a registry would mean n*(n-1) on-chain
writes per mesh expansion, with the daemon having to coordinate them.
Off-chain topology is a single JSON drop.
- **No dual-source-of-truth** — the daemon is the authority on which
chains it brokers between. Putting that on-chain would just duplicate
daemon config and create drift.
## Step-by-step: deploy one chain
Pre-requisites: the chain has `WLUX` + `BridgedBTC` + `BridgedETH` +
`BridgedUSDC` deployed (canonical token addresses captured in the brand
deployment manifest under `~/work/lux/standard/deployments/`). Without these
underlyings, sToken wrap/unwrap is disabled (but bridge mint/burn still
works — see "Pure-bridge mode" below).
```bash
cd ~/work/lux/standard
# 1) Read the brand manifest for this (brand, env) to source token addrs.
BRAND_MANIFEST=deployments/brand-l1-<env>/<brand>.json
WLUX=$(jq -r .contracts.WLUX $BRAND_MANIFEST)
BBTC=$(jq -r .contracts.BridgedBTC $BRAND_MANIFEST)
BETH=$(jq -r .contracts.BridgedETH $BRAND_MANIFEST)
BUSDC=$(jq -r .contracts.BridgedUSDC $BRAND_MANIFEST)
RPC=$(jq -r .rpc $BRAND_MANIFEST)
# 2) Deploy BridgeV4 + BasketRegistry on the chain.
LUX_PRIVATE_KEY=$(cat ~/.lux/keys/main-deployer.pk) \
BRAND=<brand> BRIDGE_ENV=<env> \
forge script contracts/script/DeployBridgeV4.s.sol \
--rpc-url $RPC --broadcast -vvv
# Capture BridgeV4 + BasketRegistry addresses from the logs and write them
# into the brand manifest:
# .contracts.BridgeV4 = "0x…"
# .contracts.BasketRegistry = "0x…"
# 3) Deploy sLUX / sBTC / sETH / sUSDC and register with BridgeV4 + Basket.
LUX_PRIVATE_KEY=$(cat ~/.lux/keys/main-deployer.pk) \
BRIDGE_V4=$BRIDGE_V4 BASKET_REGISTRY=$BASKET_REGISTRY \
WLUX=$WLUX BRIDGED_BTC=$BBTC BRIDGED_ETH=$BETH BRIDGED_USDC=$BUSDC \
forge script contracts/script/DeploySTokens.s.sol \
--rpc-url $RPC --broadcast -vvv
# Append .contracts.sTokens = { sLUX, sBTC, sETH, sUSDC } to manifest.
```
The two scripts are idempotent w.r.t. on-chain side effects — re-running
will deploy fresh instances at fresh addresses. Always run once and capture
the addresses into the manifest.
### Pure-bridge mode (no wrap/unwrap)
If a chain has no `Bridged{ETH,BTC,USDC}` deployed yet, pass empty
`address(0)` for the missing underlyings to `DeploySTokens`:
```bash
BRIDGED_BTC=0x0000000000000000000000000000000000000000 \
BRIDGED_ETH=0x0000000000000000000000000000000000000000 \
BRIDGED_USDC=0x0000000000000000000000000000000000000000 \
forge script ... DeploySTokens.s.sol
```
The sToken is still bridge-mintable and bridge-burnable. Only the user-facing
`wrap` / `unwrap` paths revert until the underlying is set later (which
requires re-deploying the sToken, since `underlying` is `immutable`).
## Add a chain to the off-chain mesh
After both contracts are deployed, register the chain with the daemon's
topology config. The daemon reads a JSON manifest at startup:
```json
{
"chains": [
{
"id": <evm-chain-id>,
"rpc": "<rpc-url>",
"bridgeV4": "0x…",
"basketRegistry": "0x…",
"sTokens": {
"sLUX": "0x…",
"sBTC": "0x…",
"sETH": "0x…",
"sUSDC": "0x…"
},
"active": true
}
]
}
```
Drop the new chain entry into the daemon config and restart. The daemon
starts observing `RedeemRequested` events on the new chain immediately;
all other chains start accepting `claim()` calls signed with the new
chain's `srcChain` id.
## Cross-chain redeem flow (proof-of-life)
The canonical round-trip test is:
```
Chain A: Chain B:
user holds sETH on A (no balance yet)
│
│ user calls BridgeV4(A).redeem(sETH_A, 1e18, B, recipient)
▼
sETH(A).burn(user, 1e18)
emit RedeemRequested(hash=…)
│
│ daemon observes event
│ daemon composes envelope:
│ srcChain=A, dstAsset=sETH_B, amount=1e18, recipient=recipient
│ daemon signs via threshold MPC (Pulsar × Prism)
▼
daemon calls BridgeV4(B).claim(envelope, proof)
sETH(B).mint(recipient, 1e18 - fee)
sETH(B).mint(feeReceiver, fee)
emit Claimed(claimId=…)
```
The reverse direction is the same flow with A and B swapped. No on-chain
state on A needs to change other than the burn and the event.
## Operational invariants
Three invariants the daemon (and external auditors) MUST check
continuously:
1. **Per-asset conservation**: for every basket asset `X`, the sum of
`sX.totalSupply()` across all chains EQUALS the sum of locked underlying
in canonical custody on every source chain (e.g. real BTC in the BTC
custody multisig). Violation = bridge insolvency.
2. **Per-chain conservation**: for every chain `c`, the sum of mints on
`sX(c)` minus the sum of burns on `sX(c)` EQUALS the net inflow recorded
by the daemon for `(c, X)`. Violation = unauthorized mint or burn.
3. **Claim-id monotonicity**: `BridgeV4.usedClaims[claimId]` is set on
every successful claim and never cleared. Replay attempts revert with
`V4_ClaimAlreadyUsed`. Auditors should grep events for any
`Claimed(claimId)` that already appears in a previous block — should
be zero.
## Role rotation post-deploy
The deployer key holds `DEFAULT_ADMIN_ROLE`, `GOVERNANCE_ROLE`, and
`OPERATOR_ROLE` on `BridgeV4` immediately after deploy. Rotate to:
| Role | Holder |
|---|---|
| `DEFAULT_ADMIN_ROLE` | Brand governance Safe (3-of-N multisig). |
| `GOVERNANCE_ROLE` | Brand governance Safe (3-of-N multisig). |
| `OPERATOR_ROLE` | Brand operations Safe (2-of-N multisig). |
| `MPC_ROLE` | The MPC cluster's threshold address (also used in classical-compat tail). |
Same pattern for `BasketRegistry.DEFAULT_ADMIN_ROLE` → governance Safe.
Same pattern for each sToken's `DEFAULT_ADMIN_ROLE` → governance Safe and
`MINTER_ROLE` → `BridgeV4` (the deploy script already grants this when
admin == deployer).
After rotation, the deployer key has NO authority over the bridge. All
parameter changes (fee bps, fee receiver, classical-compat window,
Z-Chain bridge address) go through the governance Safe.
## Per-chain registry
The canonical address registry per (brand, env) is the deployment manifest
file under `~/work/lux/standard/deployments/brand-l1-<env>/<brand>.json`.
Each manifest has the shape:
```json
{
"brand": "<brand>",
"env": "<env>",
"chainId": <evm-chain-id>,
"rpc": "<rpc-url>",
"deployer": "0x…",
"contracts": {
"WLUX": "0x…",
"BridgedETH": "0x…",
"BridgedBTC": "0x…",
"BridgedUSDC": "0x…",
"sLUX": "0x…",
"BasketRegistry": "0x…",
"BridgeV4": "0x…",
"sTokens": {
"sLUX": "0x…",
"sBTC": "0x…",
"sETH": "0x…",
"sUSDC": "0x…"
}
}
}
```
The off-chain daemon's topology config is generated from these manifests
via:
```bash
jq -s '{chains: map({id: .chainId, rpc: .rpc, bridgeV4: .contracts.BridgeV4,
basketRegistry: .contracts.BasketRegistry,
sTokens: .contracts.sTokens, active: true})}' \
~/work/lux/standard/deployments/brand-l1-*/*.json \
> config/topology.json
```
## Adding a new sToken (asset family)
The current set is sLUX / sBTC / sETH / sUSDC. To add (e.g.) sSOL:
1. Extend `BasketRegistry.BasketClass` enum (`SOL` already exists).
2. Add `BridgedSOL` to the standard token set (mint pattern matching
`BridgedBTC` — admin-mintable LRC20B descendant).
3. Add `sSOL` deploy step to `DeploySTokens.s.sol` (one
`new BridgedSyntheticToken(...)` line + one
`registry.addAssetToBasket(BasketClass.SOL, ...)` line).
4. Re-deploy `DeploySTokens` on every chain in the mesh — each chain
gets its own sSOL instance with its own deterministic address.
5. Update daemon topology JSON to include `sSOL` in every chain's
`sTokens` block.
That's it — no protocol-level change required. The bridge mesh is built
to absorb new asset families with a single per-chain deploy.
+1 -1
View File
@@ -38,7 +38,7 @@ type Config struct {
// bridged collateral mints). Independent of NetworkID — the primary
// network ID is shared across all Lux-derived L1s (1=mainnet,
// 2=fuji, 3=local, 1337=development), while the EVM chain ID here
// is per-subnet, per-env (see CLAUDE.md "Network ID vs EVM Chain
// is per-chain, per-env (see CLAUDE.md "Network ID vs EVM Chain
// ID — NEVER CONFLATE").
Network NetworkConfig `yaml:"network"`