Blindbook protocol
Blindbook is a block-crossing venue for NEAR Intents. Orders are sealed to a matcher that runs inside an Intel TDX enclave (OutLayer), crossed at a verified midpoint in periodic batches, and settled atomically by the NEAR Intents verifier. No server holds orders, keys or funds.
trader ──seal──▶ pool contract (NEAR) ──run_batch──▶ OutLayer TEE: matcher
▲ │ open, validate, cross, plan
└──────── signed result ◀───────────┘
│
└──execute_intents (one call per sub-batch)──▶ intents.near
1. Orders
1.1 Lots
A market has a fixed lot size in base units. An order is a bundle of lots, and each lot is one signed verifier payload holding exactly one token_diff:
| side | diff |
|---|---|
| sell | { base: "-<lot>", quote: "<limit_quote>" } |
| buy | { base: "<lot>", quote: "-<limit_quote>" } |
limit_quote is the trader's limit for that lot: the minimum quote received on a sell, the maximum paid on a buy. Because the verifier applies the signed amounts exactly, no lot can ever settle at a worse price than its limit, whatever the matcher does.
Each lot payload uses the verifier's versioned nonce:
5628f6c6 | 00 | salt (4) | nonce deadline, i64 ns LE (8) | random (15)
The salt is the verifier's current_salt. The intent deadline must be no later than the nonce deadline.
Supported signing standards:
nep413, for NEAR accounts. A named account must first register the key on the verifier withadd_public_key; implicit accounts work as-is.erc191, for EVM wallets.signer_idis the lowercase0xaddress, and the recovery id is0or1.
1.2 Order body
The plaintext sealed inside the envelope:
{
"v": 1,
"market": "wnear-usdc",
"owner": "alice.near",
"side": "sell",
"min_lots": 1,
"lots": [
{ "index": 0, "limit_quote": "354000", "intent": { "standard": "nep413", "payload": { "...": "..." }, "public_key": "ed25519:...", "signature": "ed25519:..." } }
]
}
owner is the verifier account that signed every lot. An order's deadline is the earliest deadline among its lots.
1.3 Envelope
Orders are sealed to the matcher's X25519 key, which pool.get_matcher_key() publishes. The layout:
version 0x01 | ephemeral X25519 public key (32) | XChaCha20 nonce (24) | AEAD ciphertext
key = HKDF-SHA256(ikm = X25519(ephemeral, matcher), salt = eph_pub || matcher_pub, info = "blindbook/v1/order")
aad = version || eph_pub
plain = u32 LE length || body || zero padding to a bucket of 2, 4, 8, 16, 32 or 64 KiB
blindbook-core::envelope (Rust) and @blindbook/sdk (TypeScript) implement this byte for byte. A shared test vector proves it.
1.4 Cancelling
- Before the matcher ingests an order:
pool.cancel_order(id)refunds its storage deposit. - After ingestion: the trader signs a cancel ticket for each remaining lot: the same signer and nonce with
"intents": []. They submit it to the verifier directly, and the nonce is consumed. The matcher drops any lot whose nonce has been used.
This needs no permission from the venue.
2. Pool contract
| Method | Who | What |
|---|---|---|
submit_order(market, sealed_b64) |
anyone, payable | Stores the sealed order. The attached deposit covers its storage and is refunded once the matcher ingests or rejects the order. Emits order_submitted. |
cancel_order(id) |
the submitter | Removes a pending (not yet ingested) order and refunds the deposit. |
run_batch() |
anyone, payable | Sends the pending orders to the matcher through outlayer.request_execution (yield/resume). Unused OutLayer deposit is refunded to the caller. |
on_result(batch_id, order_ids) |
private | Verifies the matcher's signature over the result, refunds storage for ingested and rejected orders, records the batch, then calls execute_intents once per sub-batch. |
on_settled(batch_id) |
private | Records which sub-batches settled and emits batch_settled. |
| views | — | get_config, get_markets, get_matcher_key, get_pending, get_batch, get_batches, get_stats |
The matcher runs as an OutLayer project, pinned to one wasm hash (version_key). Its keys are bound to the pool contract as the calling predecessor, so nobody else can obtain them by running the same code. The deployed projects are listed in §5.
3. Matcher I/O
3.1 Input (input_data, public)
{
"v": 1,
"op": "batch",
"network": "mainnet",
"verifier": "intents.near",
"now_ns": "1794225600000000000",
"markets": [
{ "id": "wnear-usdc", "base": "nep141:wrap.near", "quote": "nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1",
"lot": "100000000000000000000000",
"reference": { "base": "nep141:wrap.near", "quote": "nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1", "probe_base": "10000000000000000000000000", "probe_quote": "36000000" } }
],
"params": { "fee_bps": 5, "max_spread_bps": 150, "max_lots_per_side": 6, "max_sub_batches": 4, "deadline_margin_s": 90 },
"orders": [ { "id": "17", "seq": 17, "market": "wnear-usdc", "sealed": "<base64>" } ]
}
op: "keys"asks the matcher to publish its public keys and ignoresorders.referencenames the assets used to fetch 1Click quotes. On testnet, the test tokens map to their mainnet equivalents.probe_*are the quote sizes for the bid (sellprobe_base) and the ask (buy withprobe_quote).
3.2 Output (callback return value, public)
{ "result": "<canonical JSON string>", "sig": "ed25519:<base58>", "key": "ed25519:<base58>" }
sig is an ed25519 signature by the matcher's result key over sha256(result). The pool checks it against the pinned key with env::ed25519_verify before acting on anything.
result decodes to:
{
"v": 1,
"op": "batch",
"ingested": ["17"],
"rejected": [ { "id": "18", "reason": "invalid" } ],
"batches": [
{ "market": "wnear-usdc", "mid": { "quote": "...", "base": "..." }, "lot_mid_quote": "...", "matched_lots": 2,
"base_volume": "...", "quote_volume": "...", "rebated": "...", "venue_fee": "...", "quotes": ["<1click digest>", "<1click digest>"],
"skipped": null }
],
"settle": [ [ { "standard": "nep413", "...": "..." } ] ],
"book": { "orders": 3, "lots": 9 }
}
settleholds one array of signed payloads per sub-batch, in submission order: sells, then buys, then the venue payload. Each sub-batch nets to zero under the verifier's fee on its own, and the matcher simulates each one against the live verifier before it is included.bookreports aggregate counts only.- For
op: "keys",resultis:
{ "v": 1, "op": "keys", "matcher_x25519": "<hex>", "venue": "ed25519:<base58>", "result_key": "ed25519:<base58>" }
3.3 Privacy
Everything in input_data and in the result is public on chain:
- The input carries only ciphertext and market configuration.
- The result reveals only what settlement reveals anyway: crossed lots, volumes and the reference price.
- Resting orders, their sizes and their limits never leave the enclave.
3.4 Matcher v1 details
Implemented by matcher/ (blindbook-matcher). These fields extend §3.1 and §3.2.
Input additions
venue_account(required forbatch): the verifier account the venue signs as. Thevenuekey fromop: "keys"must be registered on it withadd_public_key. Mainnet:venue.blindbook.near. Testnet:venue.blindbook-dev.testnet.params.quote_max_age_s(optional, default120): the most a 1Click quote'stimestampmay differ from the enclave clock.0turns the check off.networkmust be the build's network (testnetby default,mainnetwith themainnetfeature). Otherwise the result isnetwork_mismatch.- Bounds, otherwise
bad_input:- at most 8 markets and 64 orders per input
max_lots_per_sidefrom 1 to 6max_sub_batchesfrom 1 to 16fee_bps≤ 1000,deadline_margin_s≤ 86400
- The batch time is the later of
now_nsand the enclave clock.
Result additions (op: "batch")
input_sha256: hex sha256 of the exactinput_databytes. The pool can compare it with what it sent, so a result can't be replayed onto another batch.now_ns: the batch time used.deferred: orders neither ingested nor rejected. They stay pending at the pool and come back next batch. This happens when the book is full or a verifier check got no answer.- Per
batches[]entry:sub_batches: how many of this market's sub-batches are insettle.dropped: one reason per sub-batch that crossed but was left out:simulate_failed,simulate_unavailableorvenue_inventory.skipped:nullor one ofquote_unavailable,quote_unverified,quote_stale,bad_reference,no_cross,rpc_unavailable.matched_lots,base_volume,quote_volume,rebatedandvenue_feecount only the sub-batches insettle.
errorappears only on failure. A failed result has emptyingested,rejected,deferred,batchesandsettle,book: null, and leaves the stored book untouched. Codes:bad_input,network_mismatch,storage_unavailable,book_corrupt,book_conflict,book_too_large,key_unavailable,rng_unavailable,clock_invalid,host_error,internal.- Reject reasons stay generic:
invalid,expired,unsupported_market.
Signed bytes. sig = ed25519 (RFC 8032) by the result host key over the 32 bytes sha256(result). Here result is the UTF-8 string exactly as it appears in the output. The pool checks env::ed25519_verify(sig, env::sha256_array(result.as_bytes()), pinned_key). If the result can't be signed at all, the output is {"error":"<code>"} with no sig, and the pool must treat it as a failed batch.
Settlement rules
- The venue payload's deadline and nonce deadline are the batch time + 180 s. Its salt is the verifier's
current_salt. max_sub_batchescaps sub-batches across all markets, in input order. Crossed lots beyond the cap stay resting and are not reported.- A lot leaves the book (goes in flight) only when its sub-batch passes
simulate_intents. - An in-flight lot whose nonce is used counts as filled. An in-flight lot whose nonce is unused goes back to the book only after its venue deadline + 30 s, since its sub-batch might still settle until then.
- When a sub-batch panics in simulation, each of its lots is simulated alone. An order whose lot can't run alone gets a strike, and three strikes drop the order.
- The venue's own base dust (and any quote shortfall) is budgeted against its verifier balance across all included sub-batches.
4. Venue
The venue is a verifier account whose only signing key is held by the enclave. In each sub-batch it signs:
- a
token_diffclosure that balances the sub-batch under the verifier's protocol fee, and - one
transferper trader, returning their price improvement minus the venue fee.
The venue holds a small balance of each token to cover fee dust.
5. Deployments
| Mainnet | Testnet | |
|---|---|---|
| Pool | pool.blindbook.near |
pool.blindbook-dev.testnet |
| Pool code, sha256 | 9f52588546dd689d54848e28046569a63ed5260f3c6ca4692368526ac2dc14f6 |
the same bytes |
| OutLayer project | blindbook.near/blindbook-matcher on outlayer.near |
blindbook-dev.testnet/blindbook-matcher on outlayer.testnet |
version_key (matcher wasm sha256) |
435b786312128aefc2a0a60ffa4d0232a22cfcf0ad31e492075519b909467bc9 |
a352985a6807d716158c8aed651af0882aa9da79542911225df129dead02a980 |
Manifest callers |
pool.blindbook.near |
pool.blindbook-dev.testnet |
| Venue account and key | venue.blindbook.near, ed25519:64XvgXXDj62HyW3p7yTFt4ZuAuVjQKj5nTXHPERxJ5SX |
venue.blindbook-dev.testnet, ed25519:9NiVPnMqhCj1WCWMzTq2cAk1D1GFiWrXqwTyVsVD8jED |
| Verifier | intents.near |
intents-bbcced2c.testnet |
| Market, lot | wnear-usdc, 0.1 NEAR |
tnear-tusdc, 1 tNEAR |
The two matcher builds come from the same source. The mainnet one is built with the mainnet feature, which pins network and the pool in the manifest. pool.get_config() returns the live version_key, result key and venue key for either pool.