mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
The embedded threshold dispatcher's Sign_Ctx_Profile consults a
thresholdd.ChainProfileResolver before allowing the single-party dealer
shortcut on ctx-bound signing (pulsar/magnetar sign_ctx). mpcd built the
ZapServer with NO resolver, so the gate fell to its documented fail-OPEN
branch — every chainID, strict-PQ chains included, slipped the gate.
Wire the node's chain-profile posture into the dispatcher BEFORE Start:
- --chain-profile (MPC_CHAIN_PROFILE), default strict-pq → fail-closed.
Any non-empty chainID not allow-listed resolves to ProfileStrictPQ and
the dealer shortcut is REFUSED.
- --legacy-chains (MPC_LEGACY_CHAINS): explicit allow-list of chainIDs
permitted the shortcut (dev / non-PQ chains co-served by a strict node).
Resolver is the canonical thresholdd.NewStaticChainProfileResolver — no
bespoke type, no luxfi/consensus/config dependency.
Residual: thresholdd.RefuseUnderStrictPQ short-circuits empty chainID to
PASS before consulting the resolver; that short-circuit lives inside the
threshold gate (threshold owns the listener, read-only here). The resolver
wired here is already fail-closed for "" (ResolveChainProfile("") ==
ProfileStrictPQ), so the residual closes the instant the threshold gate
routes "" through the resolver — a one-line change in
threshold/pkg/thresholdd/profile.go (drop `|| chainID == ""`).
Tests: TestStrictPQGate_FailClosed / _LegacyDefaultPermissive drive the
real gate proving refuse-by-default + allow-list opt-out + empty/unknown
fail-closed at the resolver boundary.
Also drops the stale "bls" scheme from the dispatcher start log (BLS
custody was ripped in H-2).
101 lines
4.3 KiB
Go
101 lines
4.3 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/luxfi/threshold/pkg/thresholdd"
|
|
)
|
|
|
|
// chain_profile.go — mpcd's chain-profile posture for the embedded
|
|
// threshold dispatcher's strict-PQ gate (closes Red M-2).
|
|
//
|
|
// The threshold dispatcher's Sign_Ctx_Profile consults a
|
|
// thresholdd.ChainProfileResolver before allowing the single-party
|
|
// dealer / single-validator shortcut on a ctx-bound classical-shape
|
|
// primitive (pulsar.sign_ctx, magnetar.sign_ctx). With NO resolver
|
|
// wired the gate fails OPEN — every chainID, including strict-PQ
|
|
// chains, slips through (the M-2 finding). mpcd is the embedder the
|
|
// threshold package documents as the owner of this policy: it supplies
|
|
// the resolver from the node's configured posture so the gate fails
|
|
// CLOSED by default.
|
|
//
|
|
// Decomplected: this file owns ONLY the config→Profile mapping and the
|
|
// resolver construction. The gate mechanism (when to consult the
|
|
// resolver, what the refusal means) stays in luxfi/threshold. No
|
|
// bespoke resolver type, no luxfi/consensus/config dependency — we use
|
|
// the canonical thresholdd.NewStaticChainProfileResolver the package
|
|
// exposes precisely for embedders.
|
|
//
|
|
// Residual (empty chainID): thresholdd.RefuseUnderStrictPQ short-
|
|
// circuits to PASS when the client-supplied chainID is "" — BEFORE the
|
|
// resolver is consulted — so a context-less sign_ctx still slips the
|
|
// gate even on a strict-PQ node. That short-circuit lives inside the
|
|
// threshold gate, on a listener threshold owns; mpc cannot intercept
|
|
// it. The resolver built here is already fail-closed for the empty key
|
|
// (ResolveChainProfile("") == ProfileStrictPQ under a strict-PQ
|
|
// default), so the residual closes the instant the threshold gate
|
|
// routes "" through the resolver instead of special-casing it. See the
|
|
// Red Handoff note for the one-line threshold change.
|
|
|
|
// chainProfileFromString maps the operator-facing --chain-profile value
|
|
// to the thresholdd.Profile the strict-PQ gate reasons about.
|
|
//
|
|
// Fail-secure by construction: an empty / unset value resolves to
|
|
// ProfileStrictPQ. An mpcd node refuses the dealer shortcut on ctx-
|
|
// bound signing unless an operator deliberately downgrades posture
|
|
// (legacy-compat) or allow-lists specific chains (--legacy-chains).
|
|
//
|
|
// "fips" is an alias for strict-pq: luxfi/consensus/config ProfileFIPS
|
|
// (0x03) is IsPQ()==true and equally forbids the dealer shortcut.
|
|
func chainProfileFromString(s string) (thresholdd.Profile, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "", "strict-pq", "strictpq", "strict", "pq", "fips":
|
|
return thresholdd.ProfileStrictPQ, nil
|
|
case "legacy-compat", "legacy", "permissive", "compat":
|
|
return thresholdd.ProfileLegacyCompat, nil
|
|
case "unknown", "open":
|
|
return thresholdd.ProfileUnknown, nil
|
|
default:
|
|
return thresholdd.ProfileUnknown,
|
|
fmt.Errorf("unrecognised chain-profile %q (want strict-pq|legacy-compat|unknown)", s)
|
|
}
|
|
}
|
|
|
|
// buildChainProfileResolver constructs the ChainProfileResolver the
|
|
// embedded threshold dispatcher consults on every sign_ctx.
|
|
//
|
|
// - def is the node-wide default posture (from --chain-profile): the
|
|
// profile returned for any chainID NOT in the allow-list, including
|
|
// the empty chainID.
|
|
// - legacyChains is the explicit allow-list of chainIDs permitted the
|
|
// dealer shortcut even when the node default is strict-PQ (e.g. a
|
|
// dev / non-PQ chain co-served by a strict-PQ node).
|
|
//
|
|
// The returned resolver is the canonical
|
|
// thresholdd.NewStaticChainProfileResolver — concurrent-safe, no
|
|
// bespoke type.
|
|
func buildChainProfileResolver(def thresholdd.Profile, legacyChains []string) thresholdd.ChainProfileResolver {
|
|
overrides := make(map[string]thresholdd.Profile, len(legacyChains))
|
|
for _, id := range legacyChains {
|
|
if id = strings.TrimSpace(id); id != "" {
|
|
overrides[id] = thresholdd.ProfileLegacyCompat
|
|
}
|
|
}
|
|
return thresholdd.NewStaticChainProfileResolver(def, overrides)
|
|
}
|
|
|
|
// splitCSV splits a comma- or space-separated flag value into trimmed,
|
|
// non-empty tokens. Empty input yields a nil slice.
|
|
func splitCSV(s string) []string {
|
|
fields := strings.FieldsFunc(s, func(r rune) bool { return r == ',' || r == ' ' })
|
|
out := make([]string, 0, len(fields))
|
|
for _, f := range fields {
|
|
if f = strings.TrimSpace(f); f != "" {
|
|
out = append(out, f)
|
|
}
|
|
}
|
|
return out
|
|
}
|