Partial-signing in the committee
audience: ai
This chapter walks what happens inside the signer
committee when a SignatureRequest arrives: how
the committee admits the request, how each
provider produces a partial signature, and how
partial sigs converge toward the aggregate. The
committee-internal aggregation and the public
commit live in aggregation.
The request pipeline
requester committee verifier
│ │ │
│ SignatureRequest │ │
│ (message_digest, scheme) │ │
│─────────────────────────────>│ │
│ │ │
│ │ partial_sig(member i) │
│ │ ◄──────── × committee_size │
│ │ │
│ │ aggregate (off-stream) │
│ │ ──────────────────────────── │
│ │ │
│ SignatureOutcome │ │
│<─────────────────────────────│ │
│ signature_digest, │ │
│ participating_providers, │ │
│ aggregate_attestation_level │ │
│ │ │
│ │ signature_bytes (sealed) │
│ │ ─────────────────────────────>
Request admission
A SignatureRequest is admitted iff:
- ACL. The requester’s ticket composes cleanly
with the organism’s
TicketValidator. The check is the same mosaik primitive every other organism uses; nothing signer-specific. - Scheme match.
req.schemeequalsConfig.scheme. A request for an unsupported scheme is rejected at the stream ACL; it never reaches the committee. - Body size. The side-channel body (delivered
alongside the request digest via the operator-
declared sealed channel) is ≤
M::MAX_BODY_BYTES. - Deadline.
req.deadline_unix_secsis in the future. Late requests are dropped rather than queued past their deadline.
Admission is cheap; the expensive work is the per-member partial-signing round below.
Per-member partial signing
Each committee member, on admitting the request, runs the scheme-specific partial-sign function:
// Inside the provider's TDX guest (for any
// attestation level above Software).
pub fn partial_sign<M: SignableMessage>(
req: &SignatureRequest<M>,
body: &[u8], // from sealed channel
share_sk: &SchemeSecretShare,
) -> PartialSignature {
assert_eq!(UniqueId::hash(body), req.message_digest);
match req.scheme {
SignatureScheme::BlsAggregateG1 =>
bls::partial_sign_g1(body, share_sk),
SignatureScheme::BlsAggregateG2 =>
bls::partial_sign_g2(body, share_sk),
SignatureScheme::FrostSchnorrSecp256k1 =>
frost_schnorr::partial_sign_secp256k1(
body, share_sk, ¤t_nonce_commit(),
),
SignatureScheme::FrostSchnorrEd25519 =>
frost_schnorr::partial_sign_ed25519(
body, share_sk, ¤t_nonce_commit(),
),
SignatureScheme::FrostEcdsaSecp256k1 =>
frost_ecdsa::partial_sign(
body, share_sk, ¤t_nonce_commit(),
),
SignatureScheme::LagrangeBls =>
lagrange::partial_sign(body, share_sk),
}
}
Two protocol families, one shape in the book. The
member’s result is a PartialSignature committed
to a committee-internal stream (not the public
surface); aggregation runs off that stream.
Non-interactive vs. interactive schemes
The SignatureScheme::is_non_interactive()
predicate separates the two families:
-
Non-interactive — BLS12-381 (both curves) and Shamir-Lagrange. A partial signature is self-contained; the committee aggregator can combine partials at any time in any order once the threshold is reached.
-
Interactive — FROST variants (Schnorr and ECDSA). Partial signatures reference a nonce commit the committee must run one round ahead of time. A missing nonce commit forces a retry, which the committee amortises across a window of upcoming requests.
The organism’s committee ships a nonce-commit
rotation loop iff Config.scheme.is_non_interactive()
is false. For BLS deployments the loop is absent
and the commit bandwidth is zero.
if !Config::<M>::scheme(&cfg).is_non_interactive() {
spawn_nonce_commit_rotator(&committee_handle);
}
Partial-sig visibility
The committee-internal stream carrying
PartialSignatures is not publicly readable.
Its ACL admits only bonded committee members.
Reasons:
- Partial-sig forgery window. A partial signature tied to a request id is meaningful only to the aggregator; leaking it early lets a byzantine aggregator claim the signature without running the full threshold. Restricting the audience to current committee members bounds the attack surface.
- Cross-committee correlation. A provider that serves multiple signer deployments could leak one deployment’s partial sigs into another’s aggregator. The ACL composition pins each deployment’s committee to its own stream.
The publicly readable artefact from this rung is
the SignatureOutcome committed in
aggregation; partial sigs are
committee-internal by design.
Member failure and threshold slack
The threshold is t-of-n; the committee
tolerates n − t absent members per request. A
request with fewer than t partial sigs at the
deadline commits no outcome. The committee emits a
RequestTimedOut entry (with the request id and
the count achieved) so the requester can resubmit
or escalate.
Non-adversarial absence — a committee member restarting, a transient network fault — resolves at the next cadence without operator intervention. Persistent absence triggers a committee rotation (sustainability).
Determinism
The partial-sign function is deterministic given
(body, share_sk) for non-interactive schemes and
deterministic given
(body, share_sk, nonce_commit) for interactive
schemes. Two honest providers running the same
image against the same share produce the same
partial signature. Aggregators treat a non-
matching partial from a non-interactive provider
as a byzantine fault and exclude the provider from
the round.
Forward
Chapter 6 (aggregation) walks
what happens after t partial sigs are in hand:
the aggregate combine, the SignatureOutcome
commit, and the sealed delivery of the signature
bytes back to the requester.