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 sits | How often it is paid | Consequence for the pool |
|---|---|---|
| A token transaction (a round) | Three times: mined, rebuilt by this round's PP1, pushed whole as the next round's parent | Rounds are kept small: about 142 KB at test parameters, 396 KB in production |
| A witness's unlocking data | Once: nothing ever holds a witness on the stack again | Large things ride here: note ciphertexts, the verifier body PP1 certifies |
| A separate slot transaction, Y | Once, then pushed once in a witness | The 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.
Four parties, one path
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.
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.
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.
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.
| Message | Direction | Carries | Size |
|---|---|---|---|
| Submission | wallet → pool | A 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 one | 68,972 B |
| Reply | pool → wallet | The id and an outcome: accepted with a round number, refused with a numbered reason and a sentence, or expired | 23–84 B |
| Descriptor | pool feed | Network, the txids of the issuance, witness 0 and Y_0, the aggregation arities, the receipt slots and the spend parameters | 115 B |
| Announcement | pool feed | Round number, the 236-byte header, and the txids of the round, its witness and the slot it pins | 338 B |
| Catch-up | wallet ⇄ pool | Head, frontier, block roots, or a round by number, always answered at the last mined round; refusals say never or later | ≤ 792 KB |
| Mined-round notice | pool → wallet | Sent unasked when the round holding your submission is mined: its txids and the witness's place in its block | 141 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.
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:
| Field | Meaning |
|---|---|
| cmRoot | Root of the commitment tree after this round. |
| nfRoot | Root of the spent-nullifier tree, a keyed sparse tree, after this round. |
| ring | The 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. |
| size | Leaves in the commitment tree. Each production round appends an aligned block of 512. |
| balance | Satoshis PP3 actually holds. PP1 checks the two are equal every round. A pool opens holding 1 satoshi, the dust PP3's output needs. |
| outHash | Hash 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.
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
| Who | Needs to verify their money | Why |
|---|---|---|
| Withdrawal recipient | The round and one merkle proof | The payout is an output of that round, and the verifier ran inside it |
| Note recipient | The witness and the round, two merkle proofs | The ciphertext rides in the witness; outHash binds it from the round |
| Depositor | The round and one merkle proof | The receipt is an output of the round |
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.
- 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 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- proving
Aggregate
A recursive prover folds every spend proof into one root proof and computes the next header.
~245 s GPU
~332 s CPU - 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 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- broadcast
Publish
Y, then the round, then the witness. They chain at zero confirmations and may all land in one block.
~4 s 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.
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.
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.
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.
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.
Inputs · what each spends
Outputs · what each creates
OP_FALSE OP_RETURN cm value, tied by V to receipt slot 044 BInputs
Outputs
Inside input 1's unlocking script
Inputs
Outputs
Inputs
Outputs
cm + 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 satRound 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.
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.
| Script | Locks | Executes when | Does what |
|---|---|---|---|
| V_N | Y_N out0 | round N+1, in2 | Requires the named signer, verifies the proof, rebuilds the round's outputs, checks withdrawals and the balance equation. The gate on money. |
| PP3_N | round N out3 | round N+1, in3 | Makes V unskippable: input 2 must be the outpoint it named, input 4 the next slot's anchor, output 3 the same program. |
| PP1_N | round N out1 | witness N, in1 | Makes V trustworthy: certifies the outpoint PP3_N names holds this pool's verifier with header N. |
| deposit | D out1 | round N+1, in5+ | Requires input 3 to be the PP3_N it names and its own index's output to be its receipt. |
| anchor | Y_N out1 | round N, in4 | Nothing 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.
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)
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.
| Party | Can | Cannot |
|---|---|---|
| A wallet | Deposit, pay, withdraw; refund a deposit no round took in | Read other people's notes; spend a note twice |
| The coordinator | Choose which transfers to include and when; stop the pool | Take money, redirect a payment or withdrawal, forge a spend, read note contents |
| The relay | Delay or drop messages | Read them; wallet messages are sealed end to end |
| The chain | Refuse any round that breaks a rule | See who owns what |
The slot transaction Y is the one piece built outside a round, so it was attacked on its own:
| # | Attack on Y | Worst case | Stands |
|---|---|---|---|
| 1 | Copy the proof from the mempool and spend Y's verifier elsewhere, stranding PP3 | Freeze | closedV demands the named signer's signature |
| 2 | Malleate Y's txid in relay so a different Y is mined | Delay | delay onlythe round spends the original Y's anchor |
| 3 | Double-spend Y's funding, or withhold the anchor key | Delay | delay onlythe pin never exists without Y |
| 4 | Coordinator pins a Y that PP1 will not certify | Freeze | acceptedcoordinator-only, and it can freeze more cheaply anyway; the software refuses to build such a round |
| 5 | Pin a decoy Y_0 at issuance | Theft | closedcreate certifies Y_0 and the issuance spends its anchor |
| 6 | Give Y a second input, extra outputs, or V elsewhere than output 0 | Theft | closedPP1 rebuilds Y whole, so only the fixed shape matches |
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.