Aggregate and publish
audience: ai
With t partial signatures in the committee’s
internal stream, the aggregator combines them into
the final threshold signature, commits a public
SignatureOutcome, and delivers the signature
bytes to the requester through the sealed side
channel declared at setup. This
chapter specifies all three steps and the shape
consumers gate admission against.
Aggregate
The aggregator is any committee member — the
protocol does not assign a fixed leader. Whichever
member first observes t partial sigs for a
request runs the scheme-specific combine:
pub fn aggregate<M: SignableMessage>(
cfg: &Config<'_, M>,
partials: &[PartialSignature],
) -> Result<AggregateSignature, AggregateError> {
assert!(partials.len() >= cfg.threshold as usize);
match cfg.scheme {
SignatureScheme::BlsAggregateG1 =>
bls::aggregate_g1(partials),
SignatureScheme::BlsAggregateG2 =>
bls::aggregate_g2(partials),
SignatureScheme::FrostSchnorrSecp256k1 =>
frost_schnorr::combine_secp256k1(partials),
SignatureScheme::FrostSchnorrEd25519 =>
frost_schnorr::combine_ed25519(partials),
SignatureScheme::FrostEcdsaSecp256k1 =>
frost_ecdsa::combine(partials),
SignatureScheme::LagrangeBls =>
lagrange::combine(partials),
}
}
Under non-adversarial assumptions the combine is
deterministic and verifies against the committee’s
published aggregate public key
(Config.committee_pk_digest). A failed combine —
malformed partials, mismatched nonce commits, a
byzantine signer producing garbage — is logged and
the aggregator runs the combine with a different
subset of partials if the count allows.
The public SignatureOutcome
Once a verifying aggregate is in hand, the
aggregator commits a SignatureOutcome on the
organism’s outcome collection. The outcome carries
only metadata; the signature bytes themselves go
through the sealed side channel.
pub struct SignatureOutcome<M: SignableMessage> {
pub request_id: UniqueId,
pub signature_digest: UniqueId,
pub participating_providers: Vec<UniqueId>,
pub aggregate_attestation_level: AttestationLevel,
pub committed_at_unix_secs: u64,
pub _phantom: PhantomData<M>,
}
Four fields a consumer acts on:
request_id— joins the outcome back to the requester’s earlier submission.signature_digest— blake3 of the aggregate signature bytes. Consumers pulling the sealed-channel payload verify the delivered signature hashes to this digest.participating_providers— thetproviders whose partials combined into the aggregate. Consumers can cross-reference against theProviderCardcollection (provider-reads).aggregate_attestation_level— the minimum attestation level across the participating providers. This is the field the consumer’sTicketValidatorgates against (binding).
The minimum — not the majority, not the
declared-on-card — is what the outcome carries. A
request served by four TdxRemote providers and
one TdxLocal provider yields an outcome with
aggregate_attestation_level = TdxLocal. A
consumer whose floor is TdxRemote does not admit
the outcome; the signature is still verifiable,
just not at the consumer’s declared trust level.
The sealed side channel
The actual signature bytes are too big for the public commit — a BLS aggregate is 48 or 96 bytes, a FROST Schnorr sig is 64, and every outcome’s bytes would clog the public Collection for audit-only consumers. They travel through the organism’s sealed side channel:
committee aggregator
│
│ SealedEnvelope {
│ recipient_pk: requester_identity_pk,
│ ciphertext: encrypt_to(signature_bytes,
│ requester_identity_pk),
│ request_id: <matches public outcome>,
│ }
│
▼
sealed channel
│
│ (only the requester's key can open)
▼
requester
│
│ let sig = decrypt(ciphertext);
│ assert_eq!(blake3::hash(&sig), outcome.signature_digest);
The sealed-channel cryptography is standard X25519 + ChaCha20-Poly1305 (the same primitive pattern the compute-bridge uses for encrypted SSH receipts). The requester’s identity key binds to their coalition’s stable id; the committee has no access to the signature bytes after delivery.
Determinism and replay
The aggregate signature is scheme-deterministic for non-interactive schemes and scheme-deterministic-given-nonce-commits for interactive schemes. Two honest aggregators running on the same partials (same subset, same order where order matters) produce the same aggregate.
Replay protection: the
(request_id, aggregate_attestation_level) tuple
in the outcome is unique per request, and the
organism’s state machine rejects a second commit
for the same request_id. A byzantine aggregator
attempting to replay a partial set from a prior
request has its commit rejected by the committee’s
Raft log.
Failure modes
AggregationFailure. Partial sigs failed to combine (byzantine signer, corrupted share). The aggregator commits aRequestFailedentry; consumers observe the public commit and resubmit.SideChannelDeliveryFailure. Aggregate combined, outcome committed, but the sealed envelope did not reach the requester. The requester reads thesignature_digestfrom the public outcome, submits aResendDeliveryrequest, and the aggregator re-seals and resends. This is a liveness concern, not a safety one — the signature already exists.OutcomeSignerMismatch. A consumer verifying the aggregate against the committee’s published public key gets a mismatch. This is the forgery alarm: either the published key is wrong (operator error or compromise), or the committee is byzantine. The consumer halts, unpins the organism in itsTicketValidator, and waits for a rotation.
Forward
Chapter 7 (sustainability) walks what happens when the committee rotates — key refresh, committee-member replacement, retirement markers, and the multi-operator fleet shape that lets consumers fail over when a specific signer-δ deployment retires.