Binding consumers
audience: ai
A consumer that wants signatures produced by
signer-δ does four things:
- Reference the signer organism in its own
CoalitionConfigviaOrganismRef. - Compose a
TicketValidatorclause that gates admission onSignatureOutcome::admissible(floor)against the consumer’s declared attestation floor. - Subscribe to the organism’s outcome collection.
- Submit a
SignatureRequestand read the correspondingSignatureOutcome.
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 case | Floor recommendation |
|---|---|
| Staging / non-financial signatures | Software |
| Dev-environment aggregation | TdxLocal |
| Production signatures consumed by a third-party verifier | TdxRemote |
| Signatures that feed cross-org attestation chains | TdxRaTlsBidirectional |
| Signatures whose forgery would compromise consumer keys | TdxPlusReproducibleBuild |
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 admissiblecheck in the business logic. - Signature bytes arrive out-of-band. The public
SignatureOutcomecarries 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.