mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
The lane that did this was killed before it could commit; the tree it left is green and is preserved here rather than lost. Root package 51.5% at the start of the day, 93.3% at its last checkpoint, 98.6% now, with cert, fhe, types and cmd/plugin all passing. Includes the fault-injection suite it was building when it stopped: a node that cannot read its state does not start, and one that cannot enumerate its registry does not either. Not reviewed by me beyond build, vet, gofmt and the full suite under -count=1. The lane's own report is owed and did not arrive, so its mutation count and its verdict on the mpcvm/fhe duplication are unrecorded.
568 lines
21 KiB
Go
568 lines
21 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mpcvm
|
|
|
|
// custody.go — the ceremony lifecycle: how a threshold key comes into being and
|
|
// how it signs, and how each of those becomes a verifiable state transition.
|
|
//
|
|
// # Leaderless by derivation
|
|
//
|
|
// There is no coordinator and no privileged proposer. Every validator that
|
|
// observes the same request independently computes the same ceremony id from
|
|
// the task itself — H(tag ‖ keyID ‖ digest ‖ sorted signers) — so the round
|
|
// messages demultiplex correctly without a coordination round electing anyone.
|
|
// Every honest participant finishes holding the SAME signature, stages the SAME
|
|
// operation, and whichever validator happens to propose the next block carries
|
|
// it; the rest verify it against their own state and agree. Nothing about the
|
|
// outcome depends on who proposed.
|
|
//
|
|
// # Permissionless by construction
|
|
//
|
|
// The committee is the chain's own validator set (Committee below). Joining the
|
|
// signing ring is joining the validator set — there is no allowlist, no
|
|
// operator registry, and no key ceremony gated on an admin. A new validator
|
|
// participates in every key generated after it joins.
|
|
//
|
|
// It does NOT join keys that already exist: there is no resharing ceremony, and
|
|
// a key's participant set is fixed at the DKG that created it. That is a real
|
|
// limit — a custody key survives only while K of its original N remain — and it
|
|
// is stated rather than implied, because a registry with a ReplaceKey and prose
|
|
// about "the resharing path" reads as though the ceremony exists.
|
|
//
|
|
// # Why this is the fix for off-chain signer clusters
|
|
//
|
|
// An off-chain cluster's quorum is whatever each binary happened to be
|
|
// configured with; a config that says 3-of-5 while the binary generated a
|
|
// degree-0 key produces a 1-of-n key and nothing notices until an audit. Here
|
|
// the policy is in consensus state, the degree is derived from it at exactly
|
|
// one place (quorum.Policy.Degree), and a block registering a key whose degree
|
|
// disagrees with a participant's own share is rejected by that participant.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/threshold/pkg/math/curve"
|
|
"github.com/luxfi/threshold/pkg/party"
|
|
"github.com/luxfi/threshold/pkg/quorum"
|
|
cmpconfig "github.com/luxfi/threshold/protocols/cmp/config"
|
|
)
|
|
|
|
// keygenDomainTag domain-separates a keygen ceremony id from a signing ceremony
|
|
// id, so the two can never collide even for identical (key, participant) inputs.
|
|
const keygenDomainTag = "LUX_MPC_KEYGEN_v1"
|
|
|
|
// quorumSelectTag domain-separates the per-task quorum selection hash, so the
|
|
// ranking of a party for one message can never be reused as a ranking for
|
|
// another, nor collide with a ceremony id or a signing preimage.
|
|
const quorumSelectTag = "LUX_MPC_QUORUM_SELECT_v1"
|
|
|
|
// KindCGGMP21 names the threshold-ECDSA protocol used for bridge custody of
|
|
// external wallets. It is the value stored in KeyRecord.Kind.
|
|
const KindCGGMP21 = "cggmp21"
|
|
|
|
var (
|
|
ErrNoCommittee = errors.New("mpcvm: no validator committee available")
|
|
ErrNotParticipant = errors.New("mpcvm: this node is not in the ceremony committee")
|
|
ErrNotInQuorum = errors.New("mpcvm: this node is not in this task's signing quorum")
|
|
ErrPolicyTooLarge = errors.New("mpcvm: policy requires more parties than the committee has")
|
|
)
|
|
|
|
// heldShare is this node's secret material for one custody key, plus the public
|
|
// facts derivable from it. It is the value the participant cross-check reads.
|
|
type heldShare struct {
|
|
kind string
|
|
cmp *cmpconfig.Config
|
|
}
|
|
|
|
// groupKeyAndDegree returns the compressed group public key and the polynomial
|
|
// degree this share was generated with. These are the two facts a participant
|
|
// uses to contradict a mis-declared key registration.
|
|
func (h *heldShare) groupKeyAndDegree() ([]byte, int, error) {
|
|
if h == nil || h.cmp == nil {
|
|
return nil, 0, errors.New("mpcvm: empty share")
|
|
}
|
|
pub, err := h.cmp.PublicPoint().MarshalBinary()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return pub, h.cmp.Threshold, nil
|
|
}
|
|
|
|
// marshal serialises the share for node-private storage.
|
|
func (h *heldShare) marshal() ([]byte, error) {
|
|
if h.cmp == nil {
|
|
return nil, errors.New("mpcvm: no share to store")
|
|
}
|
|
return h.cmp.MarshalBinary()
|
|
}
|
|
|
|
// parseHeldShare reads a node-private share back.
|
|
//
|
|
// The curve comes from the key's protocol, and it is not optional: the stored
|
|
// encoding holds curve points as bare bytes, so the decoder has to be told
|
|
// which curve to decode them on. It used to be handed nil, which the library
|
|
// refuses outright — so parseHeldShare could not succeed for ANY input, and
|
|
// every validator that actually held a share failed to start, with
|
|
// Initialize returning "load key shares: config must be initialized using
|
|
// EmptyConfig". A node that holds no share restarts fine, which is why the
|
|
// path that fails is the one only real committee members take.
|
|
func parseHeldShare(kind string, raw []byte) (*heldShare, error) {
|
|
if kind != KindCGGMP21 {
|
|
return nil, fmt.Errorf("mpcvm: share for unknown protocol %q", kind)
|
|
}
|
|
cfg := cmpconfig.EmptyConfig(curve.Secp256k1{})
|
|
if err := cfg.UnmarshalBinary(raw); err != nil {
|
|
return nil, fmt.Errorf("mpcvm: share for kind %s: %w", kind, err)
|
|
}
|
|
return &heldShare{kind: kind, cmp: cfg}, nil
|
|
}
|
|
|
|
// keygenCeremonyID derives the session id for a key generation. A keygen has no
|
|
// message to bind, so it binds the key id, the policy and the committee: two
|
|
// nodes that disagree about any of those run different ceremonies rather than
|
|
// silently producing incompatible shares of the same session.
|
|
func keygenCeremonyID(keyID string, policy quorum.Policy, participants []party.ID) string {
|
|
h := sha256.New()
|
|
writeTagged(h, keygenDomainTag)
|
|
writeField(h, []byte(keyID))
|
|
writeUint64(h, uint64(policy.K))
|
|
writeUint64(h, uint64(policy.N))
|
|
writeParties(h, participants)
|
|
return "mpc/keygen/" + hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// custodySet chooses the N validators that will hold shares of a key: the N with
|
|
// the most stake, ties broken by node id.
|
|
//
|
|
// Stake decides membership because a node id cannot. An id is a hash of the
|
|
// node's certificate, so ids can be aimed: certificates are cheap to generate
|
|
// offline and one that sorts early is a matter of how many were tried. An order
|
|
// over ids alone would therefore let a party choose its own seats, and seats in
|
|
// every key the chain generates — under a 2-of-3 policy, a quorum of them, held
|
|
// by one party that can then sign off-chain with no block to detect it.
|
|
//
|
|
// Stake is not chosen that way. A seat costs stake proportional to the stake
|
|
// already there, which is the same price as attacking consensus, so custody is
|
|
// not the cheaper target. Ties break lexicographically, which is safe once stake
|
|
// has chosen the set. Selection stays a pure function of replicated state, so
|
|
// every validator derives the same set without negotiating one.
|
|
func (vm *VM) custodySet(ctx context.Context, pchainHeight uint64, n int) ([]party.ID, error) {
|
|
if vm.rt == nil || vm.rt.ValidatorState == nil {
|
|
return nil, ErrNoCommittee
|
|
}
|
|
set, err := vm.rt.ValidatorState.GetValidatorSet(ctx, pchainHeight, vm.netID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mpcvm: validator set at height %d: %w", pchainHeight, err)
|
|
}
|
|
if len(set) < n {
|
|
return nil, fmt.Errorf("%w: need %d parties, validator set has %d",
|
|
ErrPolicyTooLarge, n, len(set))
|
|
}
|
|
|
|
type seat struct {
|
|
id party.ID
|
|
weight uint64
|
|
}
|
|
seats := make([]seat, 0, len(set))
|
|
for nodeID, v := range set {
|
|
var w uint64
|
|
if v != nil {
|
|
w = v.Weight
|
|
}
|
|
seats = append(seats, seat{id: party.ID(nodeID.String()), weight: w})
|
|
}
|
|
// Descending weight; node id breaks ties so the order is total and identical
|
|
// on every validator. The tie-break is still lexicographic, but it now orders
|
|
// only validators of EQUAL stake — grinding an id buys a position among peers
|
|
// whose stake you already had to match.
|
|
sort.Slice(seats, func(i, j int) bool {
|
|
if seats[i].weight != seats[j].weight {
|
|
return seats[i].weight > seats[j].weight
|
|
}
|
|
return seats[i].id < seats[j].id
|
|
})
|
|
|
|
out := make([]party.ID, 0, n)
|
|
for _, s := range seats[:n] {
|
|
out = append(out, s.id)
|
|
}
|
|
// The ceremony id and every later check hash this set, so it must come back in
|
|
// the one canonical order the rest of the package uses.
|
|
return canonicalParties(out), nil
|
|
}
|
|
|
|
// Committee returns the ceremony party set: every validator of this chain at
|
|
// the given P-Chain height, in canonical order.
|
|
//
|
|
// party.ID is the NodeID string, so the mapping from a signer back to the peer
|
|
// that runs it is the exact inverse with no side table to drift out of sync.
|
|
// Canonical ordering makes the set order-independent, which is what lets every
|
|
// validator derive the same ceremony id without exchanging one.
|
|
func (vm *VM) Committee(ctx context.Context, height uint64) ([]party.ID, error) {
|
|
if vm.rt == nil || vm.rt.ValidatorState == nil {
|
|
return nil, ErrNoCommittee
|
|
}
|
|
set, err := vm.rt.ValidatorState.GetValidatorSet(ctx, height, vm.netID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mpcvm: validator set at height %d: %w", height, err)
|
|
}
|
|
if len(set) == 0 {
|
|
return nil, ErrNoCommittee
|
|
}
|
|
out := make([]party.ID, 0, len(set))
|
|
for nodeID := range set {
|
|
out = append(out, party.ID(nodeID.String()))
|
|
}
|
|
return canonicalParties(out), nil
|
|
}
|
|
|
|
// RunKeygen generates a custody key across the committee and stages its
|
|
// registration for the next block.
|
|
//
|
|
// The ceremony is: distributed key generation (no dealer — no party ever holds
|
|
// the whole secret), then a threshold signature by the fresh group key over its
|
|
// own registration digest. That second step is the proof of possession: it
|
|
// demonstrates the committee really can sign with the key it is registering, so
|
|
// a key that would be dead on arrival never reaches the registry, and a
|
|
// proposer cannot register a public key it does not control.
|
|
//
|
|
// Every participant runs this and stages an identical operation. Non-blocking
|
|
// on the caller's behalf is the caller's business; this returns when the
|
|
// ceremony is done.
|
|
func (vm *VM) runKeygen(ctx context.Context, keyID string, policy quorum.Policy, requestingChain string) (*Operation, error) {
|
|
if !policy.Valid() {
|
|
return nil, fmt.Errorf("mpcvm: keygen for %s: undeployable policy %s", keyID, policy)
|
|
}
|
|
vm.mu.RLock()
|
|
pchainHeight := vm.rt.PChainHeight
|
|
selfID := vm.partyID
|
|
vm.mu.RUnlock()
|
|
|
|
participants, err := vm.custodySet(ctx, pchainHeight, policy.N)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !containsParty(participants, selfID) {
|
|
return nil, ErrNotParticipant
|
|
}
|
|
|
|
if has, err := vm.state.HasKey(keyID); err != nil {
|
|
return nil, err
|
|
} else if has {
|
|
return nil, fmt.Errorf("%w: %s", ErrKeyExists, keyID)
|
|
}
|
|
|
|
cid := keygenCeremonyID(keyID, policy, participants)
|
|
|
|
// THE conversion: operator policy -> protocol degree, at one boundary.
|
|
// cmp.Keygen reads its threshold argument as t, the polynomial degree, and
|
|
// a t-degree key needs t+1 signers. Handing it K directly is what produces
|
|
// a (K+1)-of-N key from a config that says K-of-N.
|
|
degree := policy.Degree()
|
|
|
|
cfg, err := vm.runCMPKeygen(ctx, cid, selfID, participants, degree)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mpcvm: keygen %s: %w", keyID, err)
|
|
}
|
|
if cfg.Threshold != degree {
|
|
// The library disagreeing with us about degree is not survivable: the
|
|
// key would silently require a different quorum than the registry says.
|
|
return nil, fmt.Errorf("mpcvm: keygen %s produced degree %d, policy %s requires %d",
|
|
keyID, cfg.Threshold, policy, degree)
|
|
}
|
|
|
|
groupPub, err := cfg.PublicPoint().MarshalBinary()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mpcvm: keygen %s: encode group key: %w", keyID, err)
|
|
}
|
|
address := publicKeyToAddress(groupPub)
|
|
if address == nil {
|
|
return nil, fmt.Errorf("mpcvm: keygen %s produced an underivable address", keyID)
|
|
}
|
|
|
|
rec := &KeyRecord{
|
|
KeyID: keyID,
|
|
Kind: KindCGGMP21,
|
|
Policy: policy,
|
|
Participants: participants,
|
|
GroupPublicKey: groupPub,
|
|
Address: address,
|
|
Generation: 0,
|
|
}
|
|
if err := rec.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
share := &heldShare{kind: KindCGGMP21, cmp: cfg}
|
|
|
|
// Proof of possession: the new key signs its own registration.
|
|
commit := KeyCommitDigest(rec)
|
|
pop, err := vm.thresholdSign(ctx, rec, share, commit[:], participants)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mpcvm: keygen %s: proof of possession: %w", keyID, err)
|
|
}
|
|
|
|
// Store the share only once the key has demonstrably worked. A share for a
|
|
// key that cannot sign is worse than no share: it makes the node claim
|
|
// participation it cannot honour.
|
|
raw, err := share.marshal()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vm.mu.Lock()
|
|
if err := vm.state.PutShare(keyID, raw); err != nil {
|
|
vm.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
vm.shares[keyID] = share
|
|
vm.mu.Unlock()
|
|
|
|
op := &Operation{
|
|
Type: OpTypeKeygen,
|
|
CeremonyID: cid,
|
|
KeyID: keyID,
|
|
RequestingChain: requestingChain,
|
|
Digest: commit[:],
|
|
Artifact: pop,
|
|
Signers: participants,
|
|
Key: rec,
|
|
}
|
|
vm.stage(op)
|
|
|
|
vm.log.Info("custody key generated",
|
|
log.String("keyID", keyID),
|
|
log.String("policy", policy.String()),
|
|
log.Int("degree", degree),
|
|
log.String("address", "0x"+hex.EncodeToString(address)),
|
|
log.String("ceremony", cid),
|
|
)
|
|
return op, nil
|
|
}
|
|
|
|
// RunSign produces a threshold signature over digest with a registered custody
|
|
// key and stages it for the next block.
|
|
//
|
|
// digest must be the exact 32 bytes to sign. M-Chain does not hash on the
|
|
// caller's behalf: the caller knows its own signing domain (an Ethereum tx
|
|
// hash, a bridge release digest), and a chain that re-hashes would produce
|
|
// signatures over a preimage the caller never authorised.
|
|
func (vm *VM) runSign(ctx context.Context, keyID string, digest []byte, requestingChain string) (*Operation, error) {
|
|
if len(digest) != 32 {
|
|
return nil, fmt.Errorf("mpcvm: sign %s: digest is %d bytes, want 32", keyID, len(digest))
|
|
}
|
|
rec, err := vm.state.GetKey(keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vm.mu.RLock()
|
|
share, held := vm.shares[keyID]
|
|
selfID := vm.partyID
|
|
vm.mu.RUnlock()
|
|
if !held {
|
|
return nil, fmt.Errorf("%w: %s", ErrShareNotHeld, keyID)
|
|
}
|
|
if !containsParty(rec.Participants, selfID) {
|
|
return nil, ErrNotParticipant
|
|
}
|
|
|
|
// The signer set is a K-subset of the participants — the quorum the policy
|
|
// actually calls for, not everyone. A 3-of-5 key signs with three parties.
|
|
//
|
|
// The subset is chosen deterministically from the task itself (see
|
|
// quorumFor), because a subset chosen independently by each node would give
|
|
// different nodes different ceremony ids and no ceremony would ever
|
|
// converge. Determinism is what makes a leaderless quorum possible: every
|
|
// node computes the same answer without negotiating one.
|
|
signers := quorumFor(rec, digest)
|
|
if !containsParty(signers, selfID) {
|
|
// This node holds a share but is not in this task's quorum. That is
|
|
// normal and not an error condition for the chain: the ceremony will
|
|
// complete without it, and it will verify the result like any other
|
|
// validator when the block arrives.
|
|
return nil, ErrNotInQuorum
|
|
}
|
|
cid := ceremonyID(keyID, digest, signers)
|
|
|
|
if _, err := vm.state.GetCeremony(cid); err == nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrCeremonyExists, cid)
|
|
}
|
|
|
|
sig, err := vm.thresholdSign(ctx, rec, share, digest, signers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mpcvm: sign %s: %w", keyID, err)
|
|
}
|
|
// Check our own work before asking the network to accept it. A signature
|
|
// that does not verify locally will be rejected by every peer anyway, and
|
|
// finding out here names the ceremony instead of the block.
|
|
if err := verifyGroupSignature(rec.GroupPublicKey, digest, sig); err != nil {
|
|
return nil, fmt.Errorf("mpcvm: sign %s produced a signature that does not verify: %w", keyID, err)
|
|
}
|
|
|
|
op := &Operation{
|
|
Type: OpTypeSign,
|
|
CeremonyID: cid,
|
|
KeyID: keyID,
|
|
RequestingChain: requestingChain,
|
|
Digest: append([]byte(nil), digest...),
|
|
Artifact: sig,
|
|
Signers: signers,
|
|
}
|
|
vm.stage(op)
|
|
|
|
vm.log.Info("custody signature produced",
|
|
log.String("keyID", keyID),
|
|
log.String("policy", rec.Policy.String()),
|
|
log.Int("signers", len(signers)),
|
|
log.String("ceremony", cid),
|
|
)
|
|
return op, nil
|
|
}
|
|
|
|
// thresholdSign runs one CGGMP21 signing ceremony over the gossip transport and
|
|
// returns the canonical 65-byte r‖s‖v signature. Every honest signer returns
|
|
// the same bytes.
|
|
func (vm *VM) thresholdSign(
|
|
ctx context.Context,
|
|
rec *KeyRecord,
|
|
share *heldShare,
|
|
digest []byte,
|
|
signers []party.ID,
|
|
) ([]byte, error) {
|
|
if share == nil || share.cmp == nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrShareNotHeld, rec.KeyID)
|
|
}
|
|
cid := ceremonyID(rec.KeyID, digest, signers)
|
|
router, release, err := vm.ceremonyRouter(ctx, cid, signers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
sig, err := vm.protocolExecutor.RunCMPSign(ctx, cid, share.cmp, signers, digest, router)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(sig.R) != 32 || len(sig.S) != 32 {
|
|
return nil, fmt.Errorf("mpcvm: malformed signature parts r=%d s=%d", len(sig.R), len(sig.S))
|
|
}
|
|
out := make([]byte, 0, 65)
|
|
out = append(out, sig.R...)
|
|
out = append(out, sig.S...)
|
|
return append(out, sig.V), nil
|
|
}
|
|
|
|
// runCMPKeygen runs one distributed key generation over the gossip transport.
|
|
//
|
|
// It goes through the same per-ceremony router as signing rather than the
|
|
// protocol registry's handler, because a handler holds ONE router for the whole
|
|
// VM: two concurrent ceremonies would interleave on it, and a VM that never had
|
|
// one set could not run a keygen at all.
|
|
func (vm *VM) runCMPKeygen(
|
|
ctx context.Context,
|
|
cid string,
|
|
selfID party.ID,
|
|
participants []party.ID,
|
|
degree int,
|
|
) (*cmpconfig.Config, error) {
|
|
router, release, err := vm.ceremonyRouter(ctx, cid, participants)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
return vm.protocolExecutor.RunCMPKeygen(ctx, cid, selfID, participants, degree, router)
|
|
}
|
|
|
|
// ceremonyRouter builds the transport for one ceremony and registers it so
|
|
// inbound gossip for that ceremony finds it. release tears it down.
|
|
func (vm *VM) ceremonyRouter(ctx context.Context, cid string, parties []party.ID) (MessageRouter, func(), error) {
|
|
if vm.sender == nil {
|
|
return nil, nil, errors.New("mpcvm: no warp sender; cross-validator ceremonies unavailable")
|
|
}
|
|
nodes, peers, err := resolveCommittee(parties)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
appNet := newWarpAppNetwork(vm.sender, vm.rt.NodeID, nodes)
|
|
router := newGossipRouter(ctx, cid, vm.partyID, appNet, peers)
|
|
vm.registerSessionRouter(cid, router)
|
|
return router, func() { vm.unregisterSessionRouter(cid) }, nil
|
|
}
|
|
|
|
// stage queues a completed operation for inclusion in the next block and wakes
|
|
// the block builder. Staging is idempotent on ceremony id: a ceremony that
|
|
// completes twice (a re-proposal after a rejected block) stages once, which is
|
|
// what the pool's claim set already refuses.
|
|
func (vm *VM) stage(op *Operation) {
|
|
_ = vm.staged.Add(op)
|
|
}
|
|
|
|
// quorumFor selects the K participants that sign a given task.
|
|
//
|
|
// The selection is a deterministic function of (key, digest) alone, so every
|
|
// node — signer or not — computes the same quorum and therefore the same
|
|
// ceremony id, with no election and no coordinator. Ordering the participants
|
|
// by H(tag ‖ keyID ‖ digest ‖ party) rather than taking the first K in
|
|
// canonical order spreads signing work evenly across the committee instead of
|
|
// loading the same K parties for every transfer, and it means an adversary
|
|
// cannot choose which subset will sign a message it does not control.
|
|
//
|
|
// Liveness note: the quorum is fixed per task, so a task whose selected subset
|
|
// contains an offline party stalls rather than falling back to another subset.
|
|
// Availability-aware reselection needs a deterministic epoch or view number to
|
|
// rotate on — every node must still agree — and that is a follow-up. Safety
|
|
// does not depend on it: a stalled ceremony produces no signature and no state
|
|
// transition.
|
|
func quorumFor(rec *KeyRecord, digest []byte) []party.ID {
|
|
type ranked struct {
|
|
id party.ID
|
|
rank [32]byte
|
|
}
|
|
rs := make([]ranked, 0, len(rec.Participants))
|
|
for _, p := range rec.Participants {
|
|
h := sha256.New()
|
|
writeTagged(h, quorumSelectTag)
|
|
writeField(h, []byte(rec.KeyID))
|
|
writeField(h, digest)
|
|
writeField(h, []byte(p))
|
|
var r [32]byte
|
|
copy(r[:], h.Sum(nil))
|
|
rs = append(rs, ranked{id: p, rank: r})
|
|
}
|
|
sort.Slice(rs, func(i, j int) bool {
|
|
if c := bytes.Compare(rs[i].rank[:], rs[j].rank[:]); c != 0 {
|
|
return c < 0
|
|
}
|
|
return rs[i].id < rs[j].id // total order even on a hash collision
|
|
})
|
|
|
|
k := rec.Policy.K
|
|
if k > len(rs) {
|
|
k = len(rs)
|
|
}
|
|
out := make([]party.ID, 0, k)
|
|
for _, r := range rs[:k] {
|
|
out = append(out, r.id)
|
|
}
|
|
// Canonical order is what the ceremony id and the block record are hashed
|
|
// over; the ranking above only decides membership.
|
|
return canonicalParties(out)
|
|
}
|
|
|
|
func containsParty(ps []party.ID, want party.ID) bool {
|
|
for _, p := range ps {
|
|
if p == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|