Protocol · TSL1_SP · wire version 3

A shielded pool is one TSL1 token, spent once a round.

This page follows money through the pool: what a wallet sends, how the coordinator folds many transfers into one round, which scripts lock which outputs, and what the chain checks before a single satoshi moves. Sizes and timings are measured, on a regtest node, at test and at production parameters.

production plan: 256 transfers a round header: 236 bytes measured 2026-09-21 to 09-24 0.x pre-release · not audited
01

The pool as a TSL1 token

Every link is a spend, and the chain of spends ends at an outpoint that could only be spent once.

TSL1 is a token layout for BSV built from three locking scripts and a witness transaction. PP1 carries the token's header and checks each transfer's structure. PP2 bridges the token transaction to its witness. PP3 holds the value. The witness proves the transfer that created it, and the next transfer can only spend PP3 once that witness exists.

Shieldpool's pool is one such token. Its tokenId is the funding txid of the genesis round. Its mutable header is the pool's state. Each round is one token transfer carrying up to 256 users' transfers at once. Because PP1 walks the parent chain down to the funding outpoint, a copy of the pool's scripts written into some other transaction can be mined but can never be advanced. That matters here: the set of spent notes belongs to one chain, so a cloned chain would be a double spend.

Where a byte sitsHow often it is paidConsequence for the pool
A token transaction (a round)Three times: mined, rebuilt by this round's PP1, pushed whole as the next round's parentRounds are kept small: about 142 KB at test parameters, 396 KB in production
A witness's unlocking dataOnce: nothing ever holds a witness on the stack againLarge things ride here: note ciphertexts, the verifier body PP1 certifies
A separate slot transaction, YOnce, then pushed once in a witnessThe 1.78 MB STARK verifier lives in Y, outside every round

That is the whole design principle. Keep every round small, and put every large thing either in a witness or in a slot transaction whose bytes are pushed once.

02

Four parties, one path

holds keys

cloak

The wallet. Holds your spending key and your notes, follows block headers itself, proves your spends. Never asks the network about an address, txid or outpoint of yours.

carries messages

ricochet

A store-and-forward relay on libp2p. The only machine your wallet talks to about the pool. Messages wait in a mailbox until the other side collects them.

builds rounds

coordinator

Checks submissions, folds a round's proofs into one, builds and publishes the round, and answers catch-up requests. Untrusted: it can stall the pool, not take from it.

enforces rules

BSV

Runs the verifier in the same transaction that moves money. A round that breaks a rule is not a valid transaction.

Wallets and the coordinator exchange a small set of binary messages over ricochet. Each begins with a version byte and a kind byte, and every length is bounded before a byte of the field is read.

MessageDirectionCarriesSize
Submissionwallet → poolA random 16-byte id, the transfer (spend proof, 56 public lanes, note bundles, an optional withdrawal), and the deposit covenant's transaction when it backs one68,972 B
Replypool → walletThe id and an outcome: accepted with a round number, refused with a numbered reason and a sentence, or expired23–84 B
Descriptorpool feedNetwork, the txids of the issuance, witness 0 and Y_0, the aggregation arities, the receipt slots and the spend parameters115 B
Announcementpool feedRound number, the 236-byte header, and the txids of the round, its witness and the slot it pins338 B
Catch-upwallet ⇄ poolHead, frontier, block roots, or a round by number, always answered at the last mined round; refusals say never or later≤ 792 KB
Mined-round noticepool → walletSent unasked when the round holding your submission is mined: its txids and the witness's place in its block141 B

Nothing a coordinator says is trusted. A wallet opens its ledger from the descriptor's txids and applies announced rounds itself. An announcement whose header or round number does not match what that round's own PP1 carries is refused, naming the disagreement.

03

Notes, nullifiers, and the pool header

Inside the pool, money is held as notes. A note records an amount and the key that may spend it. The chain stores only a commitment to each note, a leaf in a depth-32 Poseidon2 tree. When a note is spent its nullifier is published, nf = H(nk, rho), where nk is derived from the spending key. Nobody can link a nullifier to its commitment, and publishing the same one twice is refused.

The pool's whole public state is one 236-byte header, carried as a single push in PP1:

03264192196204 → 236
FieldMeaning
cmRootRoot of the commitment tree after this round.
nfRootRoot of the spent-nullifier tree, a keyed sparse tree, after this round.
ringThe four most recent cmRoots, newest first. A spend proof may anchor to any of them, so a proof stays valid for about four rounds and a wallet never needs a handshake to submit.
sizeLeaves in the commitment tree. Each production round appends an aligned block of 512.
balanceSatoshis PP3 actually holds. PP1 checks the two are equal every round. A pool opens holding 1 satoshi, the dust PP3's output needs.
outHashHash of this round's note ciphertexts, which travel in the witness. A wallet checks it before trusting anything it decrypts.

A wallet follows the commitment tree for 32 bytes a round. Because every round owns one aligned block of leaves, a note's lower siblings are fixed when its round is mined, and everything above follows from one block root per round. The wallet folds each block root and checks the result against the cmRoot in a header it proved from the chain, so a block root is safe to take from anyone.

04

A payment, end to end

Here is what each cloak command does in protocol terms. The files it writes, such as invoices, proofs and acknowledgements, travel between people over whatever channel they already share. None of them goes through the pool.

Deposit

The wallet builds a deposit transaction D whose covenant output names the live pool's PP3 outpoint, a note commitment, a refund key and a refund block height. Once D is mined, sync submits a transfer with two dummy inputs that mints the note. The next round must spend D at input 5 or later and write the receipt OP_FALSE OP_RETURN cm value at the same index. If no round takes it in, the depositor refunds it from the named height.

cloak address --transparent
cloak deposit --amount 5000
cloak sync   # submits once D is mined
cloak sync   # reads the round; note is spendable

Pay

The payee issues an invoice to a fresh pool address. The payer proves a spend of one note into two outputs, the payee's note and change, and submits it. The coordinator replies with the round that will carry it. When that round is mined a notice arrives, and proof writes a file carrying the round, the witness and their merkle proofs. The payee checks it against their own headers and takes the note.

# payee
cloak invoice new --amount 1200 --out oranges.invoice
# payer
cloak pay oranges.invoice
cloak sync
cloak proof --invoice 3f9a… --out oranges.proof
# payee
cloak check oranges.proof
cloak ack oranges.proof --out oranges.ack

Withdraw

A withdrawal is a transfer whose public values include a (key hash, amount) pair. The round pays it as an ordinary P2PKH output, and the verifier running in that same transaction checks it is exactly what the proof committed to. The destination and amount are visible on chain. Where the money came from is not.

cloak withdraw --amount 1000 --to mzJ9…
cloak sync
WhoNeeds to verify their moneyWhy
Withdrawal recipientThe round and one merkle proofThe payout is an output of that round, and the verifier ran inside it
Note recipientThe witness and the round, two merkle proofsThe ciphertext rides in the witness; outHash binds it from the round
DepositorThe round and one merkle proofThe receipt is an output of the round
05

A round's life, as the coordinator runs it

The stages below are the ones the live widget on the front page reports. Timings are for a production round of 256 transfers on one machine with a GPU.

  1. assembling

    Intake

    Each submission is checked in cost order and refused at the first failure: it decodes, the transfer agrees with itself, its anchor is in the ring, its nullifiers are unspent and unclaimed, a deposit names the live PP3, withdrawals stay within the balance. The proof is verified last.

    ~18 ms a transfer
  2. Close

    Transfers whose anchor left the ring are dropped with an expired reply. The round is padded to its plan size with padding transfers.

    at the deadline
  3. proving

    Aggregate

    A recursive prover folds every spend proof into one root proof and computes the next header.

    ~245 s GPU
    ~332 s CPU
  4. funding

    Build Y, the round and the witness

    The coordinator funds three transactions, builds each once to size it, then again with exact fees and real outpoints.

    ~9 s
  5. Apply to its own ledger

    The coordinator applies the round with a reader's checks before anyone sees it. A transfer that would strand every reader is caught here, while it can still be stopped.

    ~0.7 s
  6. broadcast

    Publish

    Y, then the round, then the witness. They chain at zero confirmations and may all land in one block.

    ~4 s
  7. Announce and notify

    The announcement goes on the feed. Each submitter is sent its round when it is mined.

    on mining

Close to mined, production: 258 to 261 s measured, of which about 245 s is aggregation. The node validated each round, which runs the verifier, in 914 to 963 ms.

06

Rounds and witnesses alternate; slot transactions drop in from above

A script is named for the output it locks, and it executes only when a later transaction spends that output. PP3_Nlocks round N's output 3 and runs when round N+1 spends it at input 3. The map keeps those two positions apart: a box's outputs are what it creates, an arrow arriving at a box is what consumes them.

SLOT TRANSACTIONS ROUNDS AND WITNESSES Y_0 413 KB · V Y_1 413 KB · V Y_2 413 KB · V round 3 Round 0 issuance · genesis 68 KB · 2 in · 5 out Witness 0 proves round 0 498 KB · 3 in · 1 out Round 1 deposit 500 · PP3 501 sat 142 KB · 6 in · 6 out Witness 1 proves round 1 574 KB · 3 in · 1 out Round 2 withdraw 300 · PP3 201 sat 141 KB · 5 in · 6 out W 2 648 KB out1,2out0out1,2out0 → in1,2→ in1→ in1,2→ in1 PP3 out3 → in3 · carries the balance PP3 out3 → in3 · carries the balance V out0 → in2V out0 → in2 PP3 pinsPP3 pinsPP3 pins PP1 certifiesPP1 certifiesPP1 certifies anchorout1 → in1 anchorout1 → in4 anchorout1 → in4 D 1.5 KB · 500 sat out1 → in5 Input 0 of every round and every witness is a coordinator P2PKH funding output, left out of the arrows. Solid = spend. Dotted = a claim about an outpoint not yet spent. Grey = the anchor: each round spends output 1 of the Y its own PP3 pins. Sizes are the test chain's: 4 transfers a round, test proof parameters. The anatomy below gives production sizes beside them.

Three relationships appear, and telling them apart is the design. A solid arrow is a spend. A dotted line is a claim: PP3 pins an outpoint the next round must spend, and PP1 certifies what the script at that outpoint actually is. The grey anchor makes a pin impossible until the slot it names is mined.

07

Six kinds of script, colour-coded throughout

PP1_SP16,924 B

Locks round out1, runs when the witness spends it. Rebuilds its own round byte for byte and certifies the next verifier slot by rebuilding that slot transaction whole. The create branch certifies Y_0 the same way.

PP21,366 B

Locks round out2, runs in the witness. Forces every witness input to come from this round and the witness output to pay the owner.

PP349,195 B

Locks round out3 and holds the pool balance. Pins the verifier slot the next round must spend, requires that round to spend the anchor of the slot its successor pins, and requires output 3 to run this same program. No owner, no burn path.

V1,782,796 B prod

Locks Y's out0, one satoshi. Runs as input 2 of the round it verifies. Checks the signer, the round's publics against both headers, the balance step, every output against hashOutputs, and the root STARK proof. 823,536 opcodes in production, under the 1M limit.

Deposit covenant1,310 B

Locks the depositor's output. Names PP3_N's outpoint, so only round N+1 can take it in, and only by writing its receipt at the same index. Otherwise it refunds to the depositor from a block height, never a timestamp.

P2PKH and metadata

Change at out0, OP_FALSE OP_RETURN metadata at out4, receipts then withdrawals from out5, the witness's ModP2PKH output, and each Y's one-satoshi anchor. V fixes their bytes.

08

Round 1 and its witness, input by input

After genesis every round has the same five inputs and five outputs. What varies is the tail: a deposit covenant for each deposit from input 5, and from output 5 its receipt and then each withdrawal.

test4 transfers a round, 2 receipt slots, test proof parameters prod256 transfers a round, 8 receipt slots, production parameters
Round 1token transactiontest141,651 Bprod395,685 B

Inputs · what each spends

0P2PKHcoordinator fundingpays the fee107 B
1ModP2PKHwitness 0 out0the coordinator's signature authorises the round107 B
2VY_0 out0carries only what unlocks V: root proof, output bytes, signer's signature, preimage. V's body stays in Y_0, which is why a round stays smalltest23,394 Bprod277,428 B
3PP3round 0 out3carries the balance forward; its SIGHASH_SINGLE preimage lets it demand output 3 run the same program49,640 B
4P2PKHY_1 out1, the anchorso the pin in this round's PP3 can only exist once Y_1 is mined106 B
5depositD out1input 3 is the PP3 it names; output 5 is its receipt418 B

Outputs · what each creates

0P2PKHchange25 B
1PP1_SPheader 1ownerPKH, tokenId, verifierBodyHash, genesisHeader, header16,924 B · 1 sat
2PP2witness bridge1,366 B · 1 sat
3PP3pins Y_1holds the pool balance49,195 B · 501 sat
4OP_RETURNmetadata2 B
5OP_RETURNreceiptOP_FALSE OP_RETURN cm value, tied by V to receipt slot 044 B
Witness 1inductive prooftest574,451 Bprod2,216,755 B

Inputs

0P2PKHcoordinator funding107 B
1PP1round 1 out1where PP1 executes; its large pushes are belowtest574,139 Bprod2,216,443 B
2PP2round 1 out234 B

Outputs

0ModP2PKHto the coordinatorround 2 must spend this, which orders the chain26 B · 1 sat

Inside input 1's unlocking script

—verifier body, with the header and Y_1's partsPP1 certifies Y_1 by rebuilding it and matching the txid PP3 pins, which takes every byte: 72% of this witness, 80% in productiontest413,382 Bprod1,782,976 B
—round 1's inputsall six with their unlocking scriptstest74,030 Bprod328,064 B
—parent round, pushed wholePP1 reads input 3's outpoint from bytes the chain fixed67,867 B
—sighash preimage, PP2 output18,460 B
—note ciphertexts and receiptsbound by header 1's outHash. A full production round carries 512 hybrid-KEM bundles, about 0.94 MBtest214 Bprod18,856 B
Y_1slot transactiontest413,406 Bprod1,783,000 B

Inputs

0any funding, from anyoneexactly one input with a scriptSig under 253 B, so it cannot swallow V148 B

Outputs

0V_1header 1 + signer + bodythe signer is round 1's owner; V demands its SIGHASH_ALL signature, so a copied proof cannot spend V anywhere but round 2test413,201 Bprod1,782,796 B
1P2PKHanchorspent by round 1 at input 425 B · 1 sat
Ddeposit1,513 B

Inputs

0P2PKHthe depositor's coins106 B

Outputs

0P2PKHchange25 B
1depositcm + PP3_0's outpoint + refund key hash + refund height + bodyvalid for round 1 only; left out, it refunds from the named height1,310 B · 500 sat

Round 0, the issuance, spends the funding outpoint that becomes the tokenId, and Y_0's anchor. Round 2 has the same five inputs one round on, and pays a 300-satoshi withdrawal at output 5 as PP3 steps from 501 to 201.

09

Three scripts, none sufficient alone

PP1 on round N+1's output 1 does not run when that round is mined. It runs later, when witness N+1 spends it, by which time the round's withdrawals are already spendable. So PP1 can never gate its own round's money, and the gate is V, in the same transaction as the payout.

ScriptLocksExecutes whenDoes what
V_NY_N out0round N+1, in2Requires the named signer, verifies the proof, rebuilds the round's outputs, checks withdrawals and the balance equation. The gate on money.
PP3_Nround N out3round N+1, in3Makes V unskippable: input 2 must be the outpoint it named, input 4 the next slot's anchor, output 3 the same program.
PP1_Nround N out1witness N, in1Makes V trustworthy: certifies the outpoint PP3_N names holds this pool's verifier with header N.
depositD out1round N+1, in5+Requires input 3 to be the PP3_N it names and its own index's output to be its receipt.
anchorY_N out1round N, in4Nothing in its script. Because the pinning round spends it, Y_N is mined before the pin exists.

PP3 supplies the timing and PP1 supplies the sight. The ordering holds because round N+2 spends witness N+1's output 0, so the certificate always exists before the pin it describes is tested.

10

Wallets prove their spends; the coordinator folds them

At the edge

The wallet proves its own spend

A spend needs a nullifier, and the nullifier key comes from the spending key. Only the key holder can make one, so the coordinator cannot prove on anyone's behalf.

Produces
one spend proof per transfer, 63,512 B
Takes
about 1.2 s to prove on a laptop core
Anchors to
any of the header's 4 recent cmRoots

At the coordinator

A recursive prover folds the round into one proof

Every spend in the round is aggregated into a single root proof and the new header. Y_N carries one verifier, which checks that one proof when the next round spends it.

Verifies
each submission at intake in ~20 ms
Folds
256 transfers in ~245 s on one GPU machine
Next
a prover farm: eight provers would bring a round near 150 s (not built)
11

What a coordinator can and cannot do

A round can only be mined if V accepted it. The design's failure mode for a dishonest coordinator is that the pool stops, not that funds move. Withdrawals a round already paid stay paid even if the pool is never advanced again.

PartyCanCannot
A walletDeposit, pay, withdraw; refund a deposit no round took inRead other people's notes; spend a note twice
The coordinatorChoose which transfers to include and when; stop the poolTake money, redirect a payment or withdrawal, forge a spend, read note contents
The relayDelay or drop messagesRead them; wallet messages are sealed end to end
The chainRefuse any round that breaks a ruleSee who owns what

The slot transaction Y is the one piece built outside a round, so it was attacked on its own:

#Attack on YWorst caseStands
1Copy the proof from the mempool and spend Y's verifier elsewhere, stranding PP3FreezeclosedV demands the named signer's signature
2Malleate Y's txid in relay so a different Y is minedDelaydelay onlythe round spends the original Y's anchor
3Double-spend Y's funding, or withhold the anchor keyDelaydelay onlythe pin never exists without Y
4Coordinator pins a Y that PP1 will not certifyFreezeacceptedcoordinator-only, and it can freeze more cheaply anyway; the software refuses to build such a round
5Pin a decoy Y_0 at issuanceTheftclosedcreate certifies Y_0 and the issuance spends its anchor
6Give Y a second input, extra outputs, or V elsewhere than output 0TheftclosedPP1 rebuilds Y whole, so only the fixed shape matches
12

What is real today

  • The verifier is real at both scales. Removing any one of its checks lets that check's attack through. A regtest node accepted production rounds running V in 914 to 963 ms, against its 1,000 ms limit for a non-standard transaction. That is the thinnest margin in the system.
  • Rounds need relaxed node policy. Every witness's PP1 unlock is larger than the default 500,000-byte maxscriptsizepolicy, and production witnesses go in over node RPC because ARC cannot yet parse scriptSigs that large.
  • Deposits, payments and withdrawals are built end to end, through the coordinator, over protocol version 3, on a regtest chain at both scales.
  • Wallet restore from seed alone is not built. Back up the wallet directory.
  • Nothing here has been audited. Use amounts you can afford to lose.

The full design record, with every measurement and the reasoning behind each choice, is docs/ZK_SHIELDED_POOL_TSL1_DESIGN.md in tstokenlib.