Files
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

79 lines
2.0 KiB
Go

package mchain
import (
"testing"
"time"
)
func TestNewPool_SingleCluster(t *testing.T) {
c := New("http://mpc:9800", 30*time.Second)
p := NewPool(c)
if p == nil {
t.Fatal("NewPool returned nil for non-nil client")
}
if p.Public != c {
t.Errorf("Public = %p, want %p", p.Public, c)
}
if p.Private != c {
t.Errorf("Private = %p, want %p (single-cluster fallback)", p.Private, c)
}
if p.IsSplit() {
t.Error("IsSplit() = true, want false for single-cluster pool")
}
}
func TestNewPool_NilClient(t *testing.T) {
if p := NewPool(nil); p != nil {
t.Errorf("NewPool(nil) = %+v, want nil — MPC-disabled path", p)
}
}
func TestNewSplitPool_TwoClusters(t *testing.T) {
pub := New("http://public-mpc:9800", 30*time.Second)
priv := New("http://private-mpc:9800", 30*time.Second)
p := NewSplitPool(pub, priv)
if p == nil {
t.Fatal("NewSplitPool returned nil for two non-nil clients")
}
if p.Public != pub {
t.Errorf("Public = %p, want %p", p.Public, pub)
}
if p.Private != priv {
t.Errorf("Private = %p, want %p", p.Private, priv)
}
if !p.IsSplit() {
t.Error("IsSplit() = false, want true for two-cluster pool")
}
}
func TestNewSplitPool_NilPrivateFallsBackToPublic(t *testing.T) {
pub := New("http://public-mpc:9800", 30*time.Second)
p := NewSplitPool(pub, nil)
if p == nil {
t.Fatal("NewSplitPool(pub, nil) returned nil — should fall back to single-cluster")
}
if p.Public != pub {
t.Errorf("Public = %p, want %p", p.Public, pub)
}
if p.Private != pub {
t.Errorf("Private = %p, want %p (nil-private fallback)", p.Private, pub)
}
if p.IsSplit() {
t.Error("IsSplit() = true, want false when private fell back to public")
}
}
func TestNewSplitPool_NilPublicReturnsNil(t *testing.T) {
priv := New("http://private-mpc:9800", 30*time.Second)
if p := NewSplitPool(nil, priv); p != nil {
t.Errorf("NewSplitPool(nil, priv) = %+v, want nil — public is mandatory", p)
}
}
func TestPool_NilSafeIsSplit(t *testing.T) {
var p *Pool
if p.IsSplit() {
t.Error("(*Pool)(nil).IsSplit() = true, want false")
}
}