Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 — the t providers whose partials combined into the aggregate. Consumers can cross-reference against the ProviderCard collection (provider-reads).
  • aggregate_attestation_level — the minimum attestation level across the participating providers. This is the field the consumer’s TicketValidator gates 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 a RequestFailed entry; consumers observe the public commit and resubmit.
  • SideChannelDeliveryFailure. Aggregate combined, outcome committed, but the sealed envelope did not reach the requester. The requester reads the signature_digest from the public outcome, submits a ResendDelivery request, 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 its TicketValidator, 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.