Files
bridge/docs/INTEGRATIONS.md
T
fd3bc75a29 bridge: sync latest — docs trim, tenant config, internal broadcast/substrate/txassembler fixes (#406)
Squash of the local main lineage onto the canonical origin/main. Net tree only
(62 files): stale docs pages removed, tenant config + tests updated, minor Go
fixes, refreshed opnet-watcher build. No third-party white-label branding in
code or history.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-07 12:39:25 -07:00

5.6 KiB

Integrations — Go-side operator guide

This document describes the Go-side daemon architecture of the Lux bridge for operators running validator / watcher / signer nodes. It is not an SDK reference. The only npm package consumers ever import is @luxfi/bridge — see that package's README for client usage.

The Go side is what an operator deploys to physically observe source chains, run the threshold-signature protocol, and submit relays to the destination chain. The two binaries built by goreleaser are:

Binary Path Role
bridge cmd/bridge API + frontend host; coordinates the watcher fleet and serves the relay queue.
opnet-watcher opnet-watcher/ Per-chain indexer + threshold cosigner. One instance per source chain, federated across operators.

1. Cross-chain MPC bridge model

The bridge connects 15+ heterogeneous chain families — EVM L1s, EVM L2 rollups, non-EVM L1s, Cosmos zones, UTXO chains, and a small number of niche substrates — into a uniform deposit / relay / mint pipeline.

Each source chain is observed by a per-chain plugin that emits a single canonical event type:

type DepositEvent struct {
    SrcChainID  uint64
    Nonce       uint64
    Recipient   [20]byte    // destination address on the relay chain
    Amount      *big.Int
    TxID        string      // source-chain tx identifier
    BlockHeight uint64
}

Operators never write chain-specific glue at the relay or signer layer. A new chain only has to satisfy ChainPlugin (see opnet-watcher/plugins/plugin.go):

type ChainPlugin interface {
    Name() string
    ChainID() uint64
    PollDeposits(ctx context.Context, fromSlot uint64) ([]DepositEvent, uint64, error)
    QueryBacking(ctx context.Context) (*big.Int, error)
}

Everything downstream — proof-hash construction, signer participation, relay submission, backing attestation — is chain-agnostic.


2. Threshold-signature scheme

The signer in opnet-watcher/signer.go is built on github.com/luxfi/crypto/threshold and supports three production schemes selected per destination context:

Scheme Curve Use
CGGMP21 (SchemeCMP) secp256k1 Threshold ECDSA for EVM-compatible destinations — the default relay path.
FROST (SchemeFROST) secp256k1 / ed25519 Threshold Schnorr for Taproot and ed25519-native destinations.
BLS (SchemeBLS) BLS12-381 Threshold BLS for aggregated internal attestations.

CGGMP21 is the canonical choice because it is non-interactive at signing time after a one-time interactive keygen, and it produces signatures that are bit-for-bit identical to any standard secp256k1 ECDSA signature — destination contracts verify with ecrecover and do not need to know that a threshold scheme produced the signature.

Key shares are generated once via the keygen ceremony in pkg/threshold/ (TS-side ceremony coordinator) and distributed to operator HSMs out of band. The Go-side daemon never sees a full private key — only its share — and t-of-n participation is required for every relay signature.


3. The WebhookHandler pattern

A small but load-bearing convention in the Lux ecosystem: any provider / plugin that can receive asynchronous callbacks from an external system (chain RPC push, custodian webhook, oracle feed) implements the optional WebhookHandler capability rather than embedding the callback path inside its primary interface.

The canonical shape (mirrored from forex/pkg/provider/provider.go):

type WebhookHandler interface {
    ParseWebhook(eventType string, body []byte, headers http.Header) (*NormalizedEvent, error)
}

Rules:

  • It is optional — providers/plugins that poll only do not implement it. Consumers do an interface assertion and skip.
  • The handler returns a normalised event in the same shape the poll path emits. The downstream pipeline cannot tell whether an event arrived by polling or by webhook.
  • All authentication / signature verification of the inbound webhook happens inside ParseWebhook, using headers. The caller does not pre-validate.

Adopt this pattern for any new chain plugin that gains a push API. Do not invent a parallel callback interface.


4. Adding a new chain

  1. Drop a new file in opnet-watcher/plugins/<chain>.go implementing ChainPlugin. Use one of the existing plugins as the closest structural reference for your chain family (EVM, Cosmos, UTXO, account-model non-EVM, rollup).
  2. Add the chain ID to cmd/bridge/networks.example.yaml.
  3. Register the plugin in opnet-watcher/main.go so it is wired into the watcher fleet at boot.
  4. If the chain provides a push API, additionally implement WebhookHandler (§3) and register the route in cmd/bridge/api.go.
  5. Write a <chain>_test.go that exercises PollDeposits against a recorded fixture — every existing plugin ships one; the test matrix in CI requires it.

No changes are required to the signer, relay, or destination contract. The plugin contract is the only seam.


5. Public SDK boundary

@luxfi/bridge (the npm package in pkg/bridge/) is the only surface a downstream application is ever expected to import. The Go binaries here are operational infrastructure: an integrating application does not run bridge or opnet-watcher in-process — it talks to a federated relay network of operators who do.

If an integration question is about what an app developer calls, the answer is in pkg/bridge/README.md. If it is about what an operator runs, the answer is in this document.