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

Binding consumers

audience: ai

A consumer that wants signatures produced by signer-δ does four things:

  1. Reference the signer organism in its own CoalitionConfig via OrganismRef.
  2. Compose a TicketValidator clause that gates admission on SignatureOutcome::admissible(floor) against the consumer’s declared attestation floor.
  3. Subscribe to the organism’s outcome collection.
  4. Submit a SignatureRequest and read the corresponding SignatureOutcome.

The organism operator does not participate in the handshake beyond publishing the three values from setup.

Referencing the organism

use coalition::{OrganismRef, UniqueId};

pub const SIGNER_CONSUMED: OrganismRef<'static> =
    OrganismRef::by_stable_id(
        "signer",
        SIGNER_STABLE_ID, // from the signer operator's release page
    );

A consumer that wants to track committee-rotation events pins only the stable id; a consumer that wants to lock to a specific committee instance pins both the stable id and the current content hash.

Composing a TicketValidator with an attestation floor

The consumer’s own validator composes two clauses: the organism’s ACL (so bonds form against the expected committee) and an attestation-floor gate on any committed SignatureOutcome the consumer acts on.

use mosaik::tickets::{TicketValidator, And};
use signer::{AttestationLevel, OpaqueMessage, SignatureOutcome};

pub fn signer_consumer_validator(
    floor: AttestationLevel,
) -> TicketValidator<'static> {
    TicketValidator::compose(
        // Bond acceptance — any signer-δ committee member.
        SIGNER_CONSUMED.any_committee_member(),
        // Predicate on every received outcome: admit iff
        // the aggregate attestation level meets the
        // consumer's floor.
        OutcomePredicate::new(
            move |o: &SignatureOutcome<OpaqueMessage>|
                o.admissible(floor)
        ),
    )
}

OutcomePredicate is the consumer’s own wrapper — the substrate TicketValidator admits predicates over received messages; the consumer wraps SignatureOutcome::admissible verbatim.

Selecting a floor

The attestation ladder is totally ordered (provider-reads documents the full taxonomy). A consumer picks the floor that matches the consequence of a forged signature:

Use caseFloor recommendation
Staging / non-financial signaturesSoftware
Dev-environment aggregationTdxLocal
Production signatures consumed by a third-party verifierTdxRemote
Signatures that feed cross-org attestation chainsTdxRaTlsBidirectional
Signatures whose forgery would compromise consumer keysTdxPlusReproducibleBuild

The choice is per consumer. A single signer-δ deployment serves the full spectrum; consumers that disagree on the floor admit different subsets of the same outcome stream.

Submitting a request and reading the outcome

use std::marker::PhantomData;
use signer::{OpaqueMessage, SignatureRequest, SignatureScheme};

let network: Arc<Network> = /* from coalition bootstrap */;
let validator = signer_consumer_validator(AttestationLevel::TdxRemote);

// Submit.
let req: SignatureRequest<OpaqueMessage> = SignatureRequest {
    request_id:         UniqueId::hash(b"req-001"),
    requester:          SEARCHER_ALPHA.stable_id(),
    message_digest:     UniqueId::hash(&message_body),
    scheme:             SignatureScheme::BlsAggregateG1,
    deadline_unix_secs: now_unix() + 10,
    _phantom:           PhantomData,
};
signer::Signer::<OpaqueMessage>::request(&network, &SIGNER_CFG)
    .await?
    .submit(req, validator.clone())
    .await?;

// Read the outcome.
let mut outcomes = signer::Signer::<OpaqueMessage>::outcomes(
    &network, &SIGNER_CFG,
).await?.subscribe(validator).await?;

while let Some(outcome) = outcomes.next().await {
    if outcome.request_id == req.request_id {
        // `validator` already rejected the outcome
        // if its aggregate_attestation_level did not
        // meet the floor, so this branch only fires
        // on admissible outcomes.
        process_signature(outcome.signature_digest).await?;
        break;
    }
}

Two properties of the subscribe pipeline are worth naming:

  • Gate happens at the bond, not at the application. The consumer’s validator rejects inadmissible outcomes before the application sees them; there is no per-message if admissible check in the business logic.
  • Signature bytes arrive out-of-band. The public SignatureOutcome carries a digest only. The actual signature is delivered through a sealed side channel (through the committee’s encrypted- response shape, similar to web2’s encrypted SSH receipts). The digest in the outcome pins what the side-channel payload must verify to.

What the consumer can and cannot assume

  • Every admitted outcome’s participating providers meet the consumer’s floor. The aggregate attestation level is the minimum across participants; if the aggregate meets the floor, every individual signer did.
  • Aggregate verification remains the consumer’s job. admissible(floor) is the attestation check. The consumer still BLS-verifies the aggregate signature against the committee’s published public key; the organism commits the signature digest, not a “this verifies” assertion.
  • The floor can rise over time without a republish. A consumer hardening its posture rebuilds its validator with a higher floor; the existing bond to the organism still works, the validator just accepts fewer outcomes. No coordination with the signer operator is required.

Forward

Chapter 3 (provider-reads) is the inverse perspective: the consumer reads the provider-market collection to audit which attestation levels the committee currently carries, independently of any specific outcome.