Files
bridge/internal/mchain/pool.go
T
zeekay 2b28dc81b9 feat(bchain,mchain): LP-333 signer-set/epoch + mchain release-wallet pool
Two additive integrations from origin/whispers/bridgev2 — both pure
insertions, no consumer-contract changes.

LP-333 (internal/bchain/client.go):
  Adds SignerMember, SignerSetInfo, EpochInfo + GetSignerSetInfo /
  GetEpoch methods on the b-chain JSON-RPC client. The bridge becomes
  a CONSUMER of BridgeVM signer-set state: it observes which MPC
  nodes are in the active quorum, the threshold, and epoch
  transitions, then points new keygen/signing at the current epoch's
  members. mpcd remains authoritative on key share material; BridgeVM
  is the membership + epoch ledger.

  Methods are read-only — the bridge never proposes rotations. Node
  operators do that out-of-band via BridgeVM governance. Surface lives
  on the same `${nodeUrl}/ext/bc/B/rpc` endpoint as bridge_getInfo,
  not on ThresholdVM.

internal/mchain/pool.go:
  Adds Pool — round-robin release-wallet selector that the daemon
  drains for destination-chain broadcasts. Pure additive; existing
  callers don't see it until they choose to consume it.

Tests: bchain (2.3s) + mchain (2.6s) all green. Includes
TestGetSignerSetInfo_HappyPath + TestGetEpoch round-trips against the
mock node.

NOT included in this PR (deliberately scoped out):
  - internal/mchain/client.go modifications — branch's version is
    behind main (no DashboardURL/Curve/sessions). Main's BTC support
    already supersedes the branch's testnet-stopgap path.
  - internal/mchain/btc_address.go — branch's file is a TESTNET
    base58 stopgap; main's file is the production bech32 derivation
    (deriveBTCBech32Address, compressSecp256k1Pubkey). Different
    purposes, main's wins.

This is the second slice of the whispers/bridgev2 integration. Per
"x.x.x+1, never lazy major" the bridge's next tag is v1.2.0 once the
substantive features land.
2026-05-28 20:17:07 -07:00

82 lines
3.2 KiB
Go

package mchain
// pool.go — layered MPC routing.
//
// REQUIREMENTS.md §6 + the SDK's BridgeMPCConfig declare two MPC URL
// slots:
//
// publicUrl — m-chain, public/permissionless validator MPC. Per-swap
// deposit-wallet keygen + refund signing run here. Blast
// radius on compromise is a single in-flight swap because
// a fresh wallet is minted every time.
//
// privateUrl — Lux treasury cluster, smaller internal quorum. Long-
// lived release-wallet keygen + settlement signing run
// here. These wallets hold operator-funded liquidity, so
// they need both a smaller-quorum threshold (faster +
// tighter access) and persistence so funds don't strand
// across restarts.
//
// The role-based split lets a tenant point the layered cosigner stack
// at the right cluster for the right operation without the bridge
// embedding any "is this a treasury op?" decision in the call path.
//
// Back-compat: when only publicUrl is configured, NewPool sets Private
// to the same *Client. Every existing single-cluster deploy keeps
// working without flag changes; the split is purely additive.
// Pool routes MPC calls between the public (m-chain) and private
// (treasury) clusters. Both fields are non-nil after construction via
// NewPool / NewSplitPool — single-cluster deploys get Private == Public.
//
// Pool is concurrency-safe iff both Client values are (which they are —
// *Client docstring guarantees this).
type Pool struct {
// Public handles per-swap deposit-wallet keygen + signing FROM the
// deposit wallet (i.e. refund-leg signing). User-funded; ephemeral.
Public *Client
// Private handles release-wallet keygen + signing FROM the release
// wallet (i.e. settlement-leg signing). Treasury-funded; long-lived.
// In single-cluster deploys, Private == Public.
Private *Client
}
// NewPool constructs a single-cluster pool. Both Public and Private
// point at the same *Client. Use this when only the legacy --mpc-url
// flag is set; the operator has not opted into a treasury cluster yet.
//
// Passing a nil Client returns nil — callers should treat the pool as
// "MPC disabled" the same way they treat a nil *Client today.
func NewPool(c *Client) *Pool {
if c == nil {
return nil
}
return &Pool{Public: c, Private: c}
}
// NewSplitPool constructs a two-cluster pool. When private is nil it
// falls back to single-cluster (Private == Public), matching NewPool.
// Passing a nil public returns nil — the public cluster is mandatory
// for any swap to proceed (deposit wallet keygen lives there).
func NewSplitPool(public, private *Client) *Pool {
if public == nil {
return nil
}
if private == nil {
return &Pool{Public: public, Private: public}
}
return &Pool{Public: public, Private: private}
}
// IsSplit reports whether the pool has distinct public + private
// clients. False when single-cluster (Private == Public). Surfaced
// for startup logging + /health so operators can confirm their
// --mpc-private-url flag actually took effect.
func (p *Pool) IsSplit() bool {
if p == nil {
return false
}
return p.Public != p.Private
}