Skip to content
The roundVerifyDocsSource
← Back

Running and maintaining Blindband

What it is made of, how to run a round, and what broke while building it.

Running a round

Five commands, in order. Each one refuses to proceed if the previous left something inconsistent.

  1. 01npm run deploy

    Registers the component and re-scopes both sealed maps to every contract id this tenant has ever held. Idempotent — a second run costs nothing.

  2. 02npm run submit -- data/records.json

    One execution carries the whole file. Rejected rows come back with an index and a reason; accepted rows come back as receipts.

  3. 03npm run round -- 2026-q1

    Aggregates inside the enclave, applies the four gates, and writes the response to data/round.json byte for byte.

  4. 04npm run anchor

    Recomputes the digest locally first and refuses to write anything if it does not match. Then posts it to Solana devnet as a memo.

  5. 05npm run verify

    Offline digest, on-chain anchor, and three receipt probes against the contract — one of them a forgery, so a verifier that only says yes is caught.

Repository layout

Three parts, split so the interesting logic can be tested without a node, an enclave, or credits.

contract/src/model.rs
Wire types and the ruleset constants. No host calls.
contract/src/stats.rs
Percentile maths. Pure, unit-tested.
contract/src/policy.rs
The four gates and the aggregation. Pure, unit-tested.
contract/src/ledger.rs
The only module that touches the host. wasm32 only.
agent/src/lib/
Session, invocation, digest and Solana helpers, each with the reasoning for its approach written at the top.
agent/state.json
The only state outside git: tenant DID, contract ids, map names. Reproducible with one deploy.
web/
This site. Reads the round file off disk at build time so the bytes in your browser are the enclave's bytes.

Bugs and platform notes

Everything below cost real time. They are written down so the next person spends it on something else.

These stay in English in every language, so they can be forwarded to Terminal 3 exactly as written — a translated error string does not match anything in their logs.

  • BB-01Platform

    The flat invoke() path rejects a sandbox-claimed API key

    Symptom
    `invalid api key: malformed api key token`, returned for every request, regardless of body or headers.
    Cause
    `invoke()` expects an opaque `t3n_key_…` token issued by organisation agent provisioning. A key claimed from the community sandbox page is a hex secp256k1 key intended for the SIWE session flow. The two are not interchangeable, and the error names neither.
    Fix
    Use the authenticated session instead: `tenant.contracts.execute(tail, { version, functionName, input })`. It costs one handshake per process, which is irrelevant for a round that runs quarterly.
    Cost
    Roughly half a day, most of it spent assuming the request body was wrong.
  • BB-02Platform

    The sandbox trust manifest does not parse, so attestation cannot be verified

    Symptom
    `Trust manifest at https://cn-api.sg.testnet.t3n.terminal3.io/api/trust-manifest is malformed.` The SDK then refuses to proceed unless `T3N_ALLOW_UNVERIFIED_NODE=true` is set.
    Cause
    The manifest served by the sandbox control node is not in the shape the SDK expects. The SDK's behaviour is correct — it declines to trust a node it cannot attest.
    Fix
    None available from the client side. Every run on the sandbox therefore carries an `unsafe_trust_server` warning, which the agent prints rather than suppresses.
    Cost
    Not blocking, but it undercuts the demo: the one property a TEE platform sells is the property that cannot currently be checked on its own sandbox.
  • BB-03Platform

    A KV map's ACL names contract ids, and a version bump mints a new one

    Symptom
    After registering v0.2.0, every read failed with `kv_store.get on 'z:…:bb-rounds' read denied: access denied: TenantContract(did:t3n:…/871) cannot read map` — and the log ring came back empty, because the contract dies during instantiation before it can log anything.
    Cause
    The maps were created with `readers: { only: [870] }`, the id of the previous build. Registering a new version produces a new contract id, and the old ACL does not follow it.
    Fix
    Track every id the tenant has registered in `state.json` and re-scope both maps to the union with `tenant.maps.update()` on each deploy. The method exists in the SDK but is not mentioned in the KV maps guide.
    Cost
    The longest single block of the build. The empty log ring is what made it expensive — the failure gives you nothing to search for.
  • BB-04Platform

    A map created without `readers` succeeds, then denies every read

    Symptom
    `create` returns cleanly. Every subsequent `kv_store.get` fails with AccessDenied.
    Cause
    The KV governor defaults to deny. An omitted `readers` set is not an open map, it is a closed one.
    Fix
    Always pass `readers` and `writers` explicitly at creation. Worth a line in the docs, since the failure surfaces far from the call that caused it.
    Cost
    An hour, and it would have been more without BB-03 already teaching us to suspect the ACL.
  • BB-05Platform

    An execution locks half the sandbox allocation regardless of what it spends

    Symptom
    A sandbox allocation is 20,000,000,000 base units. Each contract execution locks 10,000,000,000 while it runs, though a real round settles at roughly 100–180,000,000.
    Cause
    The lock is a worst-case reservation, not a charge. It is released on completion.
    Fix
    Batch aggressively. Submitting 117 rows one call at a time would need 117 executions; `submit-batch` does it in one for 5.9B. The constraint shaped the contract's interface, which is worth saying out loud in the docs.
    Cost
    No lost time, but it silently rules out the obvious per-row API design.
  • BB-06Ours

    The verifier could not read the anchor it had just written

    Symptom
    `Memo on 5uczxVJU… is not a Blindband anchor.` — reported against a transaction that was, in fact, a Blindband anchor.
    Cause
    SPL Memo v2 echoes the payload into the program log as a JSON *string literal*: `Memo (len 157): "{\"p\":\"blindband\"…}"`. Slicing between the outer quotes yields text with the escapes still in it, which fails to parse.
    Fix
    Read the decoded instruction data from `getParsedTransaction` instead of the log echo, and unescape properly in the fallback path. The instruction data is the committed bytes; the log is a convenience.
    Cost
    An hour, and a genuinely alarming few minutes in which the tool appeared to be reporting tampering.
  • BB-07Ours

    A byte-order mark in state.json silently re-registered the contract

    Symptom
    `contract version invalid: version 0.2.0 is not higher than current version 0.2.0`.
    Cause
    `readState()` caught the JSON parse failure and returned `{}`, so the deploy believed nothing had been registered yet. The parse failed because the file had been rewritten from PowerShell, which adds a UTF-8 BOM by default.
    Fix
    Write the file BOM-free. The deeper fix is that a state file which fails to parse should stop the deploy rather than being treated as an empty state.
    Cost
    Half an hour of doubting the platform for something local.
  • BB-08Ours

    A default build target in .cargo/config.toml made cargo test impossible

    Symptom
    `cargo test` compiled everything, then died with `could not execute process … z_blindband-….wasm` / `%1 is not a valid Win32 application. (os error 193)`.
    Cause
    `.cargo/config.toml` set `[build] target = "wasm32-wasip2"` so the component build could be typed more briefly. That default applies to `cargo test` too: cargo built the test harnesses as wasm and then tried to execute them as native binaries.
    Fix
    Delete the default target and name it on the one command that needs it — `cargo build --target wasm32-wasip2 --release`, which is what the deploy script already looked for. Plain `cargo test` now runs the 23 host tests.
    Cost
    Small in minutes, large in consequence: the repository claimed the gates were covered by a command that did not run. Found by actually running every command in our own documentation before publishing.
  • BB-09Ours

    state.json was committed, so the first command failed for everyone but us

    Symptom
    A clean clone carries `agent/state.json` naming our tenant DID and contract ids 870/871. On someone else's key, `npm run deploy` prints `contract : reusing z:efd91540…:blindband (id 871)` — a namespace that key does not own — registers nothing, and scopes their two maps to `readers: { only: [870, 871] }`. The first `submit` then dies with AccessDenied and an empty log ring.
    Cause
    `.gitignore` covered `data/*.json` and `.env` but not `state.json`, and `deploy.ts` trusted whatever the file said without checking it against the tenant it had just connected as.
    Fix
    Untrack `state.json`, and make the deploy compare `state.tenantDid` with the connected DID — foreign state is announced and discarded rather than used. An unparseable state file is now fatal too, which is the deeper fix BB-07 identified and did not make at the time.
    Cost
    Nothing to us, which is exactly the problem: we only found it by cloning our own public repository and reading it as a stranger. It would have failed for every judge, in the most confusing way this platform offers.
  • BB-10Ours

    The sample data the README tells you to submit was not in the repository

    Symptom
    `npm run submit -- data/records.json` is the second command in our own quickstart. A clean clone has no `agent/data/` directory at all.
    Cause
    `.gitignore` held `data/*.json` with a `!data/.gitkeep` exception, and `.gitkeep` was never created — so git dropped the directory entirely. The rule was written to keep generated round and receipt files out, and it took the one input file with them.
    Fix
    Ignore the four generated artefacts by name and track `records.json`. The 117 rows are synthetic — nine members named `member-01`…`member-09` — so there is nothing to withhold.
    Cost
    Found in the same pass as BB-09. Between them, the pipeline was reproducible by nobody, while the repository claimed otherwise in three places.
  • BB-11Platform

    A sandbox tenant cannot provision the delegated agent identity the SDK offers it

    Symptom
    `client.createAgent(did, "blindband-round-runner")` on a sandbox-claimed tenant returns `RPC Error: organisation has no policy meta`. The method is present and callable; the tier is not.
    Cause
    `createAgent` takes an *organisation* DID and requires the caller to be one of its admins. A DID claimed from the community sandbox page is not an organisation and carries no policy metadata, so there is no organisation for the agent to belong to. This is the same tier boundary as BB-01, seen from the other side: the opaque `t3n_key_…` that the flat `invoke()` path wants is precisely what `createAgent` returns.
    Fix
    None available from this tier. Rounds run under the tenant identity, and the agent reports which identity ran a round rather than implying otherwise. `npm run probe:agent` re-runs the check in one call, so whoever inherits this can tell in seconds whether their tier has lifted.
    Cost
    No lost time — but it is the single largest gap between what this is and what an agent on this platform is supposed to be, and it is not closable by writing better code.
  • BB-12Ours

    The landing page was blank with JavaScript disabled

    Symptom
    A crawler, a reader with scripting off, or a screenshot taken before hydration got the header, the footer, and nothing in between. `curl` on the built page shows why: every section ships as `style="opacity:0;transform:translateY(14px)"`.
    Cause
    The one entrance animation on the site starts each section at zero opacity and animates it in when it scrolls into view. That initial style is server-rendered, so the page's whole argument — the round, the gates, the verifier — depended on JavaScript running to become visible at all. Nothing in the browser ever showed it, because in a browser the animation always ran.
    Fix
    The animated wrapper carries a `data-reveal` marker and the document head carries a `<noscript>` rule that forces every one of them back to full opacity. The same commit made the light palette a `prefers-color-scheme` media query as well as a JavaScript-set attribute, so a light-configured machine gets a light page before hydration rather than a dark flash or nothing at all.
    Cost
    Found while building the screenshot script, not while using the site — which is the point. Half of the captures came back as an empty gradient, and the flaky tool turned out to be the honest witness.

Continuing or handing over

I would like to keep running this and take it toward a real consortium pilot. If Terminal 3 would rather host it, the handover is small: the repository is self-contained, the deploy script is idempotent, and the only state outside git is the tenant DID, the contract identity and the map access lists — all of which are recorded in state.json and reproducible with a single deploy.