Files
zooqueen 861db0f621 a request nobody hears is not a quorum problem, and a secret needs a way in
Two halves of opening a secret were missing, and both showed up the first time
this ran against the real ring rather than an in-process one.

The request reached no party. Parties subscribed to "mpc.reveal_request.*" and
the requester published to "mpc.reveal_request.<session>". This transport
delivers by exact map lookup on the topic — it has no wildcards — so the
subscription matched nothing, every node stayed silent, and the opening failed
as "0 of 2 answered" with all three shares present and nobody having been asked.
That reads like a quorum fault and is a routing one. Both sides name one literal
now; the session rides in the body, where a reader wants it anyway. The pattern
came from the NATS-era topics next door, where wildcards do work.

And nothing could seal. mpcrek requires a sealed root to bring and no tool in
the estate produced one, so the protocol had no beginning. reveal.Seal takes the
published group key and a secret — no share, no node, no round, because writing
to the committee needs none of that — and `mpcd seal` is that from a shell, with
the secret on stdin so it never becomes an argument other processes can read.

Sealing to the identity is refused. It decodes as a valid point, so the decoder
lets it through, and every answer verifies against it: a secret sealed there is
readable by anyone. A test asked for that and the first version did not do it.
2026-08-21 00:57:48 -07:00

58 lines
2.5 KiB
Go

package event
// Opening a secret sealed to a wallet's share set.
//
// Signing is a protocol: rounds, state, every party waiting on the others.
// Opening is not. Each party computes its answer from its own share and the
// ciphertext alone, so this is a fan-out and a fan-in with nothing in between —
// no session to build, no round to sequence, no party that must wait to speak.
//
// That is why these are their own topics rather than another message on the
// signing stream: a request that needs no session should not be routed through
// the machinery that keeps them.
const (
RevealPublisherStream = "mpc-reveal"
RevealConsumerStream = "mpc-reveal-consumer"
// RevealRequestTopic is what every party listens to. One request reaches
// them all and each answers independently.
//
// One literal topic, not a pattern. The consensus transport delivers by
// exact map lookup — it has no wildcards — so a party subscribed to
// "mpc.reveal_request.*" hears a request published under
// "mpc.reveal_request.<session>" never, and the opening times out with
// every share present and nobody having been asked. The session travels in
// the request body, where a reader needs it anyway.
RevealRequestTopic = "mpc.reveal_request"
// RevealAnswerTopic carries the answers back, one topic per session, so two
// openings in flight cannot be combined into one. Both sides build the same
// literal from the same base, so no pattern is involved here either.
RevealAnswerTopicBase = "mpc.reveal_answer"
)
// RevealRequest asks the share set behind KeyID to open Ciphertext.
//
// It carries no threshold and no party list. Both are properties of the share
// set, which every party already holds and the combiner reads from the same
// place — a request that could name its own threshold could name one.
type RevealRequest struct {
SessionID string `json:"session_id"`
OrgID string `json:"org_id"`
KeyID string `json:"key_id"`
Ciphertext []byte `json:"ciphertext"`
}
// RevealAnswer is one party's contribution, or its reason for having none.
//
// A party that cannot answer says so rather than staying silent, because the
// difference between "this node holds no share for that key" and "this node is
// gone" is the difference between a request that will never succeed and one
// worth waiting out.
type RevealAnswer struct {
SessionID string `json:"session_id"`
PartyID string `json:"party_id"`
Answer []byte `json:"answer,omitempty"`
Error string `json:"error,omitempty"`
}