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

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 seq are 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 changes StreamId and 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.