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 to the Compute market

audience: ai

The bridge has a CoalitionConfig for bridge-β and a boot TOML (setup). This chapter walks the handshake: the boot flow, the TDX self-quote, the Compute-module resolution, the shuffle channel opening, and the provider-card publication. Once the card is live the market can route grants to the bridge.

The convergence mechanism from ai/emergent-coordination.md — coordination markets shows up on the provider side here. The bridge’s TicketValidator admits the Compute module’s clearing rule (the module’s Config fingerprint) and the module’s scheduler- committee Measurements (pinned TDX). Two TDX-attested bridges admitting the same module Config agree on the same clearing rule.

The boot flow

The entry point is src/main.rs (examples/compute-bridge/src/main.rs):

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 1. Load boot config.
    let cfg = config::BootConfig::load_from_env()?;

    // 2. TDX self-quote. Without a matching
    //    Measurements set the provider card will
    //    not match this binary and the Compute
    //    committee will reject registration.
    let tdx_quote = tdx::self_quote().await?;

    // 3. Shared mosaik network handle.
    let network = Arc::new(
        Network::new(mosaik::builder::UNIVERSE).await?,
    );

    // 4. Resolve the Compute module on the coalition.
    //    No Compute module, no bridge.
    let compute_cfg = cfg.coalition.compute()?;

    // 5. Shuffle channel for anonymised request/reply.
    let zipnet = zipnet_io::ZipnetChannel::open(
        &network, &cfg.zipnet,
    ).await?;

    // 6. Fleet from the operator's enabled backends.
    let fleet = backends::Fleet
        ::from_boot_config(&cfg.backends).await?;

    // 7. Operator dashboard on localhost (non-essential;
    //    failures log and do not abort).
    let dashboard = Arc::new(
        dashboard::Dashboard::new(
            &cfg.dashboard, fleet.clone(),
        ),
    );
    tokio::spawn({
        let d = dashboard.clone();
        async move { let _ = d.spawn().await; }
    });

    // 8. Provider loop: register the card, accept
    //    grants, return receipts.
    let provider = provider::Provider::new(
        network, compute_cfg, tdx_quote, zipnet,
        fleet, cfg.provider, dashboard,
    );
    provider.run().await
}

Steps (1)–(6) run in order and each blocks the next. Steps (7) and (8) run in the same tokio runtime.

TDX self-quote

Step (2) is load-bearing for the provider card. The scheduler committee verifies the quote against the declared Measurements before accepting the card.

From src/tdx.rs:

pub async fn self_quote() -> anyhow::Result<tdx::Quote> {
    // Calls into the TDX guest's get_quote MMIO/IOCTL
    // via the mosaik::tee::tdx wrapper. The quote's
    // report_data folds:
    //   - the provider's ed25519 public key,
    //   - the bridge organism's Config.content hash,
    //   - a boot nonce so quotes can't be replayed.
    mosaik::tee::tdx::get_quote(report_data()).await
}

The quote is signed by the TDX provisioning-cert chain (traceable to Intel; local attackers cannot forge it). Its report_data binds the provider’s ed25519 public key, so later commits the provider signs are tied to the same key the quote attests. The boot nonce prevents replay across reboots.

The bridge’s TicketValidator

The validator declares what the bridge accepts bonds from and what Measurements it requires of counterparts. Three clauses carry the chapter:

use coalition::{TicketValidator, Tdx};
use coalition::acl::ComputeModule;

fn bridge_ticket_validator()
    -> TicketValidator<'static>
{
    TicketValidator::new()
        // Scheduler committee must run the pinned
        // TDX image. This is the bridge agreeing to
        // the module's clearing rule.
        .require_ticket(
            Tdx::new().require_mrtd(
                COMPUTE_MOD.module_image,
            ),
        )

        // Bridge accepts grants only from the exact
        // Compute module Config fingerprint it
        // booted against. A module rotation requires
        // an explicit rebind via the retirement
        // chain; the bridge does not silently follow
        // scheduler rotations.
        .require_ticket(
            ComputeModule::pin(
                compute_cfg.stable_id(),
            ),
        )

        // The module's own ACL gates who may request
        // grants. The bridge does not add a separate
        // requester ACL; admitting the module's is
        // enough.
        .verify_peer(
            ComputeModule::requester_acl(
                compute_cfg.stable_id(),
            ),
        )
}

The bridge does not bond against other providers. The coordination market is the mediating policy; competing providers are observed (see market-reads) but not bonded to.

Publishing the provider card

provider.run() starts by publishing the card. The card is what the market sees; it is folded into the module’s Collection<ProviderId, ProviderCard> and consulted on every clearing round.

From src/provider.rs:

async fn register_provider_card(&self)
    -> anyhow::Result<ProviderId>
{
    let capabilities = self.fleet.capabilities().await?;

    let card = ProviderCard {
        provider_id:    self.provider_id(),
        tdx_quote:      self.tdx_quote.clone(),
        capabilities,
        declared_rates: self.config.declared_rates,
        zipnet_reply:   self.zipnet.reply_pointer(),
        refreshed_at:   self.network.almanac().tick(),
    };

    coalition_compute::register(
        &self.network, self.compute, card,
    ).await
}

What the card folds:

  • provider_id — blake3 of the bridge’s ed25519 public key, stable across restarts because the key comes from a TDX-sealed seed.
  • tdx_quote — the quote from src/tdx.rs.
  • capabilities — the union of backends’ declared regions, TDX-capable flag, max CPU and RAM (chapter 4 walks the per-backend contribution).
  • declared_rates — the posted $/core-hour and $/GiB-hour per backend. This is the competitive surface (chapter 3).
  • zipnet_reply — the shuffle channel the bridge listens on.
  • refreshed_at — the Almanac tick. Scheduler treats cards stale after a declared max-age; the bridge refreshes on capacity change.

Smoke test

With the card registered the operator checks that it is visible on the market before walking away:

use futures::StreamExt;
use tokio::time::{timeout, Duration};

let mut subscribers = network
    .subscribe::<ProviderCardCollection>(
        compute_cfg.stable_id(),
    ).await?;

let check = async {
    while let Some(ev) = subscribers.next().await {
        if ev.provider_id == provider.provider_id() {
            tracing::info!("card live in market");
            return Ok::<_, anyhow::Error>(());
        }
    }
    anyhow::bail!("card not observed in market")
};

timeout(Duration::from_secs(30), check).await??;

Common failures:

  • TdxQuoteRejected: Measurements in the quote do not match the module’s admission policy. Rebuild against the pinned coalition-compute version the module admits, or wait for the admission rotation.
  • StaleModuleConfig: compute_cfg is older than the module currently runs. Pull the coalition’s updated handshake page, re-read compute_cfg.stable_id(), restart.

Not yet

The validator has no ReputationFloor clause yet; chapter 7 adds it once scoring history exists. The bridge is registered but has not handled a grant; chapter 5 picks that up. Market reads come next (chapter 3).

Cross-references