Protocol
Withdrawals
Open, push, finalise: how a 1,088-byte signature crosses the chain in seven transactions.
A withdrawal cannot be one transaction. The signature is 1,088 bytes and verifying it is a few thousand hashes. So it is three instructions across at least seven transactions, and the order is the security property: the intent is written down before any signature material is revealed.
The sequence
1 open_withdrawal amount, destination, next_commitment
2 push_signature chains 1 .. 8
3 push_signature chains 9 .. 16
4 push_signature chains 17 .. 24
5 push_signature chains 25 .. 32
6 push_signature chains 33 .. 34
7 finalize_withdrawal pays destination, rotates the lock, closes the request| Quantity | Value |
|---|---|
| Transactions, minimum | 7 |
| Signature bytes delivered | 1,088 |
| Instruction data per full push | 256 bytes |
| Hashes per push, worst case | 2,040 |
| Hashes to verify, average | about 4,300 |
| Hashes in finalise | 1, over 34 endpoints and the tag |
open_withdrawal
Creates a WithdrawalRequest at the address derived from ["request", vault, nonce], so exactly one request can exist per vault state. It records the amount, the destination, the next commitment, who paid the rent, and the time. Nothing about the signature is revealed yet.
Rejected with ZeroAmount if the amount is 0, ZeroCommitment if the next commitment is zero, and CommitmentReused if it equals the current one.
push_signature
Each push carries between 1 and 8 chain values, in order. The program recomputes the digest from the request, computes the chain lengths, and walks each value to its endpoint:
The endpoints are stored on the request and chains_filled advances. Chains cannot arrive out of order, cannot be skipped and cannot be re-sent, because replacing an accepted chain would let two signatures be mixed into one commitment check. With 8 per push, 34 chains take pushes of 8, 8, 8, 8 and 2.
The digest is read from the request, not from the instruction, so there is nothing for the submitter to vary.
finalize_withdrawal
Requires all 34 endpoints and a nonce that still matches the vault. Then the check the program exists for:
If it holds, in this order:
- The vault pays the destination. It keeps at least its rent-exempt minimum, or the instruction fails with
InsufficientFunds. Vault.commitmentbecomes .Vault.nonceandVault.rotationseach increase by one.- The request is closed and its rent returned to whoever paid it.
No signer is required. The destination account is constrained to request.destination.
cancel_withdrawal
Reclaims a request that will never finalise. Permissionless. A request whose nonce no longer matches the vault closes at once. A current request must have expired:
REQUEST_EXPIRY_SECONDS is 60 · 60 · 24. Rent returns to the original payer. Otherwise NotExpired.Why anyone may finalise
Someone watching the mempool can take the revealed chains and submit finalize_withdrawal themselves. Every field they could want to change is inside the digest the chains sign (see The digest and what it binds). The destination account is further constrained to request.destination, and the rent goes to request.payer, not to the finaliser. So all a stranger can do is pay the owner’s chosen destination, slightly sooner, at their own expense. There is nothing to protect, which is why no signer is asked for.
Liveness
One vault, one pending request. A request that is opened and abandoned holds the vault’s nonce until it expires, 24 hours later. That is a deliberate trade: cancelling a live request mid-push would be a denial of service against the owner, so a live request can only be cancelled after it expires.