metaspace
Protocol / Withdrawals

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

One withdrawal, seven transactions
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
QuantityValue
Transactions, minimum7
Signature bytes delivered1,088
Instruction data per full push256 bytes
Hashes per push, worst case2,040
Hashes to verify, averageabout 4,300
Hashes in finalise1, 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:

ej=H255−mj(σj),j=chains_filled+1,…e_j = H^{255 - m_j}(\sigma_j), \qquad j = \mathit{chains\_filled} + 1, \ldots

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:

H ⁣(metaspace:commitment:v1 ∥ e1 ∥ ⋯ ∥ e34)=?Vault.commitmentH\!\left(\texttt{metaspace:commitment:v1} \,\|\, e_1 \,\|\, \cdots \,\|\, e_{34}\right) \overset{?}{=} \mathit{Vault.commitment}

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.commitment becomes CnextC_{\mathrm{next}}. Vault.nonce and Vault.rotations each 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:

tnow−topened  ≥  86,400 secondst_{\mathrm{now}} - t_{\mathrm{opened}} \;\ge\; 86{,}400 \text{ seconds}
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.