Peal protocol reference · v0

How Peal guarantees every reveal.

Peal is a programmable encryption network for information that must stay unreadable until a shared condition fires. A browser seals a payload once. A threshold committee later opens the whole batch, publicly and verifiably. Nobody returns for a second reveal transaction. This article walks the protocol end to end: the cryptography that makes early reading impossible, the machinery that makes the reveal guaranteed, how to build on it, and exactly what separates today's devnet from production.

committeen = 5
thresholdt = 3
fixed batchB = 64
crypto overhead64 bytes
share size48 bytes
stall timeout120 s

The problem

Commit-reveal is the standard way to hide information on a public ledger until a deadline: users post a hash now and the preimage later. The pattern is sound cryptography wrapped around a broken assumption, that people will come back. The bidder who lost never opens their commitment, and the auction cannot distinguish a griefing bidder from a crashed one. The voter who saw the early tally shift never reveals. The game player whose move became bad simply forfeits the reveal and eats a smaller penalty than the move would have cost.

Every application built on voluntary reveal inherits the same design tax: reveal windows and their timeout parameters, slashing deposits sized to guess at the value of hiding, incomplete datasets, and a support queue of users who missed their window honestly. The failure is structural. If revealing is a user action, the user can decline it exactly when declining matters most.

commit-reveal reveal is a user action user posts hash(bid, salt) deadline passes everyone waits user must return and reveal a second transaction, voluntarily or they simply do not unopened commitments break the app Peal reveal is a network duty user seals ciphertext, once encrypted in the browser, in wasm the cue fires wall clock or block height, batch freezes the committee reveals the batch any t of n shares, publicly verified every slot opens, including losers no second transaction exists (nothing for the user to abandon)
The structural difference: commit-reveal ends in a step the user can skip. Peal has no such step.

Peal removes the action. The payload is encrypted rather than hashed, and decryption is a network duty rather than a user choice. Before the cue, fewer than t committee operators can recover nothing, and that includes the coordinator that stores the ciphertexts. After the cue, any t valid shares recover every slot in the frozen batch at once. The user's only action is the seal.

Why threshold, not the alternatives

Guaranteed reveal has four other known constructions. Each carries a cost Peal was built to avoid, and each has real uses; the table is a scoping tool, not a dismissal.

approachhow it revealsthe catch
timelock puzzle / VDFanyone grinds sequential computation until the plaintext falls outthe deadline is denominated in compute, not clock time; someone must actually run the grind, and faster hardware moves the deadline
drand tlocka long-running threshold beacon, the League of Entropy, publishes a BLS signature every round; you encrypt to a future round number and anyone decrypts with that round's signaturethe schedule is the beacon's rounds, 3 s or 30 s of wall clock, not your condition, and there is no block-height cue; every ciphertext for a round opens for everyone with the same signature, one at a time, and there is no batch, so nothing pads or hides how many were sealed
TEE-held keysan enclave releases the key at the deadlinethe guarantee is the vendor's attestation chain; one enclave break is a total, silent compromise
naked thresholda committee decrypts each ciphertext on requestper-ciphertext work: n operators each touch every message, which is exactly what kills throughput at batch sizes that matter
peal (batched)a committee posts one constant-size share per operator per batchbatches are fixed at B slots and a threshold of operators must be live; the v0 ceremony still has a trusted dealer

The batching is the point. One 48-byte share per operator opens up to 64 payloads, so the committee's work per reveal is constant while the batch fills. Wall-clock deadlines stay wall-clock. And every share is publicly verifiable against published keys, so a lying operator is caught by arithmetic, not by reputation. Detection is live today; the remedy is not: replacing a caught operator currently means a new ceremony.

Why not drand tlock

tlock deserves its own paragraph because it is the closest neighbour, not a strawman. It is the same primitive family, threshold BLS over BLS12-381. It has run in production since 2020. Its own documentation lists the same applications this site does: sealed-bid auctions, MEV prevention, voting, responsible vulnerability disclosure. And the League of Entropy is a genuinely multi-organisation committee, Cloudflare, EPFL, Protocol Labs, Kudelski Security and around twenty other named members. If what you need is a public wall-clock unlock and nothing else, tlock is the mature answer and you should use it.

Peal differs in three specific ways. The condition is yours. A tlock ciphertext opens at a beacon round, which is a point in wall-clock time; a Peal condition is a deadline or a block height on a chain you name, and the cue is a row the coordinator fires rather than a signature somebody else's schedule produces. The unit is a round, not a ciphertext. Up to 64 payloads share one batch; one 48-byte share per operator opens all of them, so committee traffic per reveal is constant while the batch fills. The batch is padded with decoys, so while a round is open nothing says how many were sealed, and once it opens the count is still padded. tlock has no batch: each ciphertext is independent, and a public count of ciphertexts encrypted to a round is exactly the number a competitor in a sealed auction wants. It is HTTP with no chain. Three calls seal and open a round; a chain enters only when you anchor a root to one.

Two things this comparison must not say. Neither scheme is post-quantum: both rest on pairings, and drand says so of tlock in its own docs. And batching saves coordination and bandwidth, not pairings: the paper is explicit that decryption costs a few pairings per ciphertext in every scheme, its own included. What Peal does not have yet is distributed key generation: until the dealer ceremony is replaced, tlock's trust model is the stronger one.

Sources: drand, timelock encryption (mechanism, the applications list, the stated limitations) · drand/tlock (round or duration as the only triggers) · drand, about (beacon cadence, League of Entropy membership) · Policharla, A Simple Batched Threshold Encryption Scheme, ePrint 2026/760 (48-byte shares, the per-ciphertext pairing floor).

What one payload goes through

  1. Fetch committee parameters

    The SDK downloads the public parameters, checks their SHA-256 digest, and caches them. The parameter set fixes n, t and B for the committee's lifetime, and the wasm module re-parses it independently, so a tampered parameter blob fails twice.

    GET /v0/committees/:id
  2. Seal in the browser

    Wasm runs the Fujisaki-Okamoto transform locally. Only the sealed wire bytes ever leave the device; for a short text payload that is about 110 bytes total. The plaintext never exists outside the tab that typed it.

    seal(payload, conditionId)
  3. Content-address the ciphertext

    The coordinator parses the wire format, enforces the payload cap, computes ct_hash = sha256(wire), and stores the blob under its condition. The hash is the stable handle an application keeps; submission order does not choose the slot.

    POST /v0/ciphertexts
  4. The cue fires, the batch freezes

    A wall-clock or block-height condition fires. The coordinator pads to 64 slots with self-sealed dummies, sorts every ciphertext by hash, assigns positions, and makes the batch immutable. Positions are a pure function of the ciphertext set: two coordinators given the same seals produce the same board.

    pending → frozen
  5. Operators post one share each

    Each outbound-only node polls for frozen work, decrypts its local keystore, and computes one 48-byte partial for the entire batch. The share is the same size whether the batch holds one sealed order or sixty-four.

    GET /v0/work · POST /v0/shares
  6. Verify, combine, recover

    Every share must pass a public pairing check before it counts. Any t valid shares are Lagrange-combined once for the whole batch, and a per-slot integrity check isolates any mauled ciphertext to its own slot.

    frozen → revealed
  7. Publish an auditable record

    The reveal carries every slot, its validity bit, the full operator share log with timings, and a merkle root over position and payload. Anyone can recompute the root from the published batch.

    GET /v0/reveals/:id
pending accepting seals cue fires frozen collecting shares t valid shares revealed immutable, auditable fewer than t in 120 s stalled one late valid share
The condition state machine. Stalled is honest, not terminal: a late valid share resumes recovery, and a condition is never falsely revealed.

The condition engine

The coordinator's scheduler is a 500 ms tick loop, not a callback registry. Each pass it freezes any pending wall-clock condition whose time has come, polls configured RPC endpoints with eth_blockNumber for block-height conditions, finalizes any frozen batch that has reached t verified shares, and marks conditions stalled when the reveal timeout (default 120 seconds, REVEAL_TIMEOUT_SECS) passes without them.

Two properties matter more than the loop itself. Freezing is deterministic: positions derive from sorting ciphertext hashes, so the frozen board is reproducible by anyone holding the ciphertext set, and an integration test asserts two coordinators agree. And every step is idempotent against restart: state reloads from the database, the expensive cross-terms are recomputed rather than trusted from cache, and operator nodes simply repoll. Killing the coordinator mid-reveal delays the reveal; it cannot corrupt it.

The cryptography

Peal wraps Commonware's batched threshold encryption (eprint 2026/760 by Guru Vamsi Policharla, implemented in simple-bte) without modifying the scheme. The wrapper adds conditions, persistence, operator transport, share verification, and public records. Nothing below is novel cryptography; that is deliberate.

The wire format

A sealed ciphertext is a fixed header and a masked body. The cryptographic overhead is 64 bytes: a 48-byte KEM header and a 16-byte key mask. Framing adds 9 more.

magic + type"BTE0" || 0x01, 5 bytes
KEM headerct₀ = [k]₁, 48 bytes compressed G1
masked keyct₁ = Hₖ([k · τB+1]ₜ) xor K, 16 bytes
masked bodyct₂ = Hₘ(K) xor payload
integrityk = Hᵣ(K, payload), verify [k]₁ = ct₀

The last line is the Fujisaki-Okamoto discipline that makes the scheme non-malleable in practice: the ephemeral scalar is not random at decryption time, it is re-derived from the recovered plaintext. A ciphertext anyone tampered with recovers to a plaintext whose re-derived scalar fails to reproduce the KEM header, so the slot is flagged corrupt and quarantined without poisoning the other 63.

The punctured setup

The ceremony publishes powers of a secret τ with one deliberate hole. Everyone can encrypt toward the missing power. Nobody holds it. That hole is the entire trick.

publishedj]₂ for j = 0 … 2B, except j = B+1 (zeroed)
encryption keyek = [τB+1]ₜ, target group only
dealt to operatorsShamir shares of τ¹ … τB, threshold t of n
per-operator publicverification values vᵢʲ

Why it works, in one paragraph. Sealing masks the payload key with [k · τB+1]ₜ. That value lives in the pairing's target group, where you cannot climb from j] to j+1]; the one power that would let you compute it directly was zeroed out of the published set, and its preimage was destroyed at setup (in v0, that destruction is the dealer's promise; see the trust model). What the batch structure buys is a way to route around the hole: pairing each ciphertext header against the right published powers produces cross-terms in which the unknown power appears only multiplied by the operators' secret-shared low powers. Add t operators' contributions and the unknown cancels into something computable; with fewer than t, it is still hidden behind Shamir secrecy. The published powers run to 2B precisely so those cross-terms exist for every slot position.

This is also why every batch is exactly B slots. The setup material and the FFT domain are sized to B at the ceremony; a batch of three cannot be decrypted, so the coordinator pads with self-sealed dummies rather than shrinking the math.

Shares and public verification

operator's sharepdⱼ = Σᵢ σⱼᵢ · ctᵢ,₀
public checke(pdⱼ, g₂) = Πᵢ e(ctᵢ,₀, vⱼᵢ)
recoveryLagrange-combine any t valid shares at x = 0

Operator j runs one multi-scalar multiplication over the frozen headers; the result is a single compressed G1 point, 48 bytes, covering the whole batch. The coordinator checks each partial against public verification keys before it counts: a forged share fails the pairing equation, is recorded as rejected under the operator's identity, and never reaches the threshold. Verification needs no secrets, so anyone can re-run it.

Pipelined recovery

The expensive part of decryption does not wait for operators. The FFT cross-terms (O(B log B) group operations plus O(B) pairings, never the naive loop) depend only on the frozen ciphertexts and the public parameters, so they compute at freeze time while shares are still in flight; an integration test asserts pre-decrypt finishes before the first share exists. When t verified shares land, finalize is one Lagrange interpolation at x = 0 plus the per-slot FO re-check. On the public devnet, pre-decrypt runs in roughly 250 ms and finalize in 40 to 150 ms for a full 64-slot batch, so the user-visible gap between cue and plaintext is the share round-trip, not the math.

The commitment

leafsha256(position_le_u32 || payload)
parentsha256(left || right), odd node promoted
padding payload"BTE_DUMMY_V0:" || 16 random bytes

The merkle root binds every slot, padding included, so a published reveal cannot be edited without detection. Padding slots are real self-sealed ciphertexts with a tagged random payload, unique per batch, and operators do identical work on them, so the padding is not a shortcut. Anyone can download the batch from the reveal endpoint, recompute the root, and compare it with the published or onchain-anchored value.

The ceremony and the keys

v0 key generation is a single offline dealer running simple-bte::crs::setup: it samples τ, computes the punctured public parameters, Shamir-deals the shares of each power to the n operators, writes each share into a password-encrypted keystore (argon2id key derivation, ChaCha20-Poly1305 sealing), publishes the parameter blob, and destroys τ. The parameter set is content-addressed: its SHA-256 digest is the committee id, which is why an application can pin one committee and reject every other.

Operator nodes never hand their keystores to anyone. A node decrypts its keystore locally at startup, holds the share in memory, and speaks only outbound HTTP to the coordinator. The coordinator cannot reach into a node, and a stolen coordinator database contains ciphertexts and public data, not key material.

The dealer is the v0 compromise, stated plainly: whoever ran the ceremony could have kept τ and could decrypt everything early. That is acceptable for a devnet and unacceptable for value. The production replacement is a distributed key generation in which the trapdoor never exists on any single machine, plus proactive resharing so operators can rotate without changing the public key applications pinned.

Two privacy layers

The network proves when. The link decides who. Threshold reveal is deliberately public: after the cue, every slot's plaintext is on the record so anyone can verify the batch. That is exactly right for auctions and votes, whose fairness depends on every participant reading the same reveal, and wrong for a personal note. So a second, purely client-side layer exists on top.

private payload"BTEP1" || iv (12 bytes) || AES-128-GCM(key, text)
share link#/s/<code>/<key>

Before sealing, the browser wraps a personal payload in AES-128-GCM. The key never reaches any server: it rides only in the share link's hash fragment, which browsers do not transmit. The network still proves when the seal opened; only people holding the full link learn what it said. The explorer marks such slots private instead of printing ciphertext, and a link that lost its key segment gets a clear error rather than garbage. The trade is explicit: there is no recovery path. Lose every copy of the link and the content stays unreadable, by construction.

Architecture

The design separates the public edge from secret-bearing operators. The browser and explorer are public. The coordinator is a scheduler and aggregator that never sees pre-cue plaintext. Operator nodes accept no inbound connections and never expose their keystores.

dapp + SDK seals in wasm ciphertext TLS edge explorer + rate limit /v0 coordinator freeze · verify · recover database + volume ciphertexts · reveals root onchain anchor optional · reveal root the committee · any 3 of 5 reveal node 1 node 2 node 3 node 4 node 5 keystore keystore keystore keystore keystore poll work, post shares outbound only
Operator nodes connect outbound only; the coordinator can never reach into a keystore. Any 3 of the 5 shares complete a reveal.
componentrole
bte-sdkfetches and digest-checks parameters, seals in wasm, submits ciphertexts, waits for reveals, optionally verifies the anchored root
bte-coordinatorcondition engine, SQLite state machine, deterministic freeze, pipelined pre-decrypt, pairing checks, REST /v0
bte-nodepolls outbound for frozen work, decrypts its local argon2id + ChaCha20 keystore, computes one partial, posts it
BteAnchor.solcommits ciphertext hashes to conditions and records the final merkle root from an authorized publisher

The public API, in one table

endpointcallerpurpose
GET /v0/committees/:ideveryonepublic parameters, digest-checked by clients
POST /v0/conditionsappscreate a cue: at a time, in N seconds, or at a block height, with an optional tag
GET /v0/conditions[/:id]everyonestatus, fires_at, counts, tag, per-batch share progress
POST /v0/ciphertextsappssubmit a sealed blob to a pending condition
GET /v0/workoperatorsfrozen batches awaiting this operator's share
POST /v0/sharesoperatorssubmit a 48-byte partial; pairing-verified on arrival
GET /v0/reveals/:ideveryone404 until revealed, then slots, share log, timings, merkle root

The reveals endpoint returning 404 before the cue is itself an invariant with a test behind it: the test greps the coordinator's database for plaintext bytes while a condition is pending and must find none.

The byzantine drill

The claim that verification catches liars is tested, not asserted. The repo ships a drill (just demo-byzantine) that runs the full sealed-bid auction with two failures injected at once: operator 2 runs a byzantine build that submits a corrupt share, and operator 5 is killed mid-flow. The recorded outcome, verified through the API rather than the demo's own output: operator 2's share arrives, fails the pairing equation, and is stored as rejected under its identity; operator 5 never submits; the reveal completes from the three honest shares. The auction's winner is identical to the clean run.

That is the failure envelope in one sentence: up to n - t operators can lie or die simultaneously, attribution is automatic, and the reveal is bit-exact regardless of which honest subset supplied the shares.

Building on it

The product path is four calls: create a condition, seal locally, store the returned hash, wait for the reveal.

typescript
import { BteClient } from 'bte-sdk';

// one client, pointed at the network
const peal = new BteClient({ url: 'https://peal.network' });

// a cue 60 seconds out, tagged so this app finds its own rounds
const conditionId = await peal.condition({ in: 60, tag: 'auction-v1' });

// seal locally; bind the app context inside the encrypted bytes
const sealed = await peal.seal(JSON.stringify({
  app: 'auction-v1',
  conditionId,                      // replay binding
  lotId: 'lot-42',
  bid: 815,
  nonce: crypto.randomUUID()
}), conditionId);

// the network does the rest; no second transaction exists
const reveal = await peal.waitForReveal(conditionId);
const slot = reveal.slots.find((s) => s.ctHash === sealed.ctHash);

if (!slot?.valid) throw new Error('sealed bid did not recover');
console.log(slot.text);
  • Pin the committee digest. Ship the expected public-parameter digest with your app; do not silently accept a coordinator-selected committee.
  • Bind the payload. Put the condition id, app domain, action type, and a nonce inside the encrypted bytes and validate them after reveal, so a copied ciphertext cannot be replayed into a different context undetected.
  • Persist the ciphertext hash. It is the stable handle for the commitment. Store it in your database or anchor it onchain before the cue.
  • Tag your conditions. The optional tag (up to 32 characters of a-z 0-9 : _ -) lets your app find its own rounds and never join a stranger's. Peal Network's explorer keeps bid rounds, vote rounds, and capsules apart this way.
  • Treat reveal as asynchronous. Poll or index; handle pending, frozen, stalled, revealed, and per-slot corrupt states explicitly.
  • Layer client-side encryption for personal data. The network reveal is public; if only the recipient should read the content, encrypt inside the payload and carry the key in your own channel, as private capsules do in the link fragment.

Failure behavior is explicit

failurebehavior
one node offlinereveal continues with any 3 of 5
one forged sharepairing check rejects it; the operator identity stays visible in the log
one mauled ciphertextthat slot is marked corrupt; the other 63 recover
coordinator restartstate reloads from the database, pre-decrypt recomputes, nodes repoll
fewer than t sharesthe condition stalls after 120 s; it is never falsely revealed
late valid sharea stalled condition completes automatically

The numbers

Measured on the public devnet (n = 5, t = 3, B = 64), not estimated.

quantityvaluenote
seal overhead64 bytes48-byte KEM header + 16-byte key mask
wire size, short text~110 bytesframing adds magic, type, length
operator share48 bytesone compressed G1 point per operator per batch, any fill
pre-decrypt~250 mspipelined away: runs at freeze, before any share exists
finalize40 to 150 msuser-visible: Lagrange combine + per-slot FO re-check
engine tick500 msfreeze scheduling granularity
stall timeout120 sREVEAL_TIMEOUT_SECS; late shares still complete
rate limit50 rps, burst 400per client IP, token bucket

Production posture

The current stack runs a transparent public devnet: a real threshold committee, public share verification, durable state on a mounted volume, recovery after restart, TLS, rate limiting, and honest stall states. The decisive blocker for real value is the ceremony.

layerv0 todayproduction target
key generationoffline trusted dealeraudited DKG; no machine ever knows the whole trapdoor
operator lifecyclenew ceremony to replace oneproactive resharing and rotation under a stable public key
availabilitycoordinator database + volumereplicated store plus blob or calldata copies of ciphertexts
accountabilityinvalid shares attributablestake, slashing, signed work receipts
verificationoffchain pairing check, anchored rootEIP-2537 onchain verification of shares and combination
operationshealth endpoint, structured logsSLOs, metrics, paging, tracing, backups
securityunaudited prototypeindependent audits and ceremony review

The trust model, stated precisely

You do not trust

  • the coordinator with pre-cue plaintext
  • any coalition smaller than the threshold
  • an operator's claim that its share is valid
  • the explorer's arithmetic; recompute the root yourself

V0 still requires trust

  • the dealer did not retain or leak τ
  • fewer than three of the five operators collude
  • at least t operators answer after the cue
  • the coordinator includes every submitted ciphertext
  • the deployment preserves ciphertext availability

v0 is dealer-trusted and unaudited. Use it for testnets, demos, and integration work. DKG and an independent audit are prerequisites for a stronger claim.

the paper simple-bte Peal source the live explorer