Stream publication
audience: ai
Each supported token pair is a separate
Stream<PriceTick>. The enclave commits one tick
per cadence per pair (or skips, when the quorum
floor is not met). This chapter specifies the
stream surface: identifier derivation, commit
cadence, the per-bond attestation, and what the
enclave’s signing key is bound to.
One stream per token pair
The oracle publishes one Stream<PriceTick> per
TokenPair in OracleParameters.pairs
(setup). The stream identifier folds
the oracle’s stable id, the pair’s preimage, and
the commit schema version:
use mosaik::streams::{StreamId, Stream, StreamAcl};
pub fn stream_id_for(pair: &TokenPair) -> StreamId {
StreamId::derive(
ORACLE.stable_id(),
&pair.preimage(),
&PRICE_TICK_SCHEMA_V1,
)
}
PRICE_TICK_SCHEMA_V1 folds the PriceTick layout
byte-for-byte (field order, width, signedness). Any
future schema change is a new SchemaVersion, a
new StreamId, and — because schemas are folded
into OracleParameters through the aggregation
policy’s published constants — a new image.
Consumers migrate on their own cadence.
The pair preimage is canonicalised at the catalog
level: base and quote are sorted lexicographically
inside the TokenPair constructor, so
TokenPair::new("ETH", "USDC") and
TokenPair::new("USDC", "ETH") hash the same
preimage and address the same stream. The pair’s
semantic orientation (ETH priced in USDC vs. USDC
priced in ETH) is a property of the price field
within the tick, not of the stream identifier.
The publish loop
The enclave runs one publish task per supported pair:
pub async fn publish_loop(
pair: &TokenPair,
stream: &Stream<PriceTick>,
sources: &SourceSubscriptions,
policy: &AggregationPolicyId,
cadence: Duration,
) {
let mut seq: u64 = 0;
let mut next_tick = Instant::now() + cadence;
loop {
tokio::time::sleep_until(next_tick.into()).await;
next_tick += cadence;
let fresh = sources.fresh_snapshot(pair);
match aggregate(policy, &fresh) {
AggregationOutcome::Published { price, confidence, source_count, contributing_ids } => {
let tick = PriceTick {
timestamp_ms: wall_clock_ms(),
pair: *pair,
price,
confidence,
source_set_digest: blake3_source_set(&contributing_ids),
source_count,
stale: sources.any_reconnecting(pair),
seq,
};
stream.commit(tick).await.expect("commit failed");
seq += 1;
}
AggregationOutcome::Skip => {
// no commit; consumers see the gap in `seq`.
}
}
}
}
Cadence is per-pair. A fast-moving pair like
ETH/USDC may run at 250 ms cadence; a long-tail
pair may run at 5 s. All cadences are declared in
OracleParameters and folded into Measurements.
The bond-level attestation
When a consumer subscribes to a pair’s stream
(binding), the enclave presents a
TDX self-quote covering MR_TD,
MR_CONFIG_ID, and the owner set — that is,
ORACLE_MEASUREMENTS in full. The consumer’s
TicketValidator verifies the quote against its
pinned Measurements and completes the bond. The
quote is thereafter part of the bond’s state; it
is not re-issued per tick.
The bond-level surface is what makes the trust model cheap: one expensive verification at bond time, plus cheap per-tick signature checks against a key whose derivation is bound to the attested image.
The enclave’s signing key
The signing key used for PriceTick::sign is
derived inside the enclave via TDX key derivation
(mosaik::tee::tdx::derive_key or equivalent).
The derivation folds MR_TD and the
OracleParameters.aggregation_policy identifier,
so:
- A different image (different
MR_TD) derives a different key. Signed ticks from an image a consumer did not admit do not verify against the signing key the consumer expects. - A replay from outside the enclave cannot reconstruct the key without a compatible TDX measurement, which it does not have.
- A Measurements rotation derives a new key; the consumer’s verification after a rotation uses the new key once it has followed the retirement chain (sustainability).
Back-pressure
Mosaik Stream commits are non-blocking for the
publisher: a slow subscriber that cannot keep up
falls behind and re-syncs from a checkpoint on
reconnect. The enclave does not tune its cadence
to the slowest subscriber; cadence is a published
property of the oracle, and admission is
per-subscriber.
A subscriber that falls behind by more than the
oracle’s declared retention window (published in
OracleParameters) misses intervening ticks
entirely. Most consumers sit well inside the
window and never notice.
What publication guarantees
- Every published tick was produced by an enclave whose Measurements the consumer admitted. Attested by the bond-level quote and re-verified per-tick by the signing key derivation.
- Missing ticks mean the oracle had no quorum
or was skipping. Gaps in
seqare visible; a consumer who cares about liveness detects them trivially. - Stream identifiers are stable across
Measurements rotations only if the aggregation
policy and schema are unchanged. A rotation
that changes only the image but keeps the
policy and schema preserves
StreamId; consumers continue reading the same streams from a different key. A rotation that changes the schema changesStreamIdand a consumer must resubscribe.
Forward
Chapter 7 (sustainability) walks what happens when the operator rotates Measurements, retires the instance, or stands up a multi-operator fleet.