Exit Overview
Exits are how users withdraw funds from Ark back to on-chain Bitcoin. There are two types:
Cooperative Exits (Offboard) : Work with the server for efficient on-chain settlement
Unilateral Exits : Independent recovery without server cooperation
Cooperative exits are cheaper and faster, but unilateral exits are your ultimate safety guarantee - you can always recover your funds, even if the server disappears.
Cooperative Exits (Offboard)
How Offboarding Works
The server includes your payment in the next round transaction:
1. Submit offboard request with VTXOs
2. Server includes output in next round
3. Round transaction confirms on-chain
4. Receive Bitcoin at specified address
Advantages :
Low fees (shared among round participants)
Fast (next round, typically 5-15 minutes)
Simple process
No VTXO exit chain needed
Trade-offs :
Requires server cooperation
Subject to round timing
Round fees apply (from ArkInfo.fees)
Offboard vs Send On-chain
Two cooperative methods exist:
Offboard : Entire VTXO(s) sent on-chain
Input: 1 VTXO (10,000 sats)
Output: On-chain payment (9,500 sats after fees)
Change: None
Send On-chain : Specific amount with change
Input: 1 VTXO (10,000 sats)
Output: On-chain payment (6,000 sats)
Change: 3,500 sats (new VTXO)
Unilateral Exits
Unilateral exits are the core security mechanism of Ark. The implementation is in bark/src/exit/mod.rs.
Exit System Architecture
pub struct Exit {
tx_manager : ExitTransactionManager ,
persister : Arc < dyn BarkPersister >,
chain_source : Arc < ChainSource >,
movement_manager : Arc < MovementManager >,
exit_vtxos : Vec < ExitVtxo >,
}
Components :
Transaction Manager : Constructs and broadcasts exit transactions
Persister : Stores exit state across restarts
Chain Source : Monitors blockchain confirmations
Exit VTXOs : Tracks each VTXO’s exit progress
Exit State Machine
Each exiting VTXO progresses through states:
pub enum ExitState {
Start ( ExitStartState ), // Just marked for exit
Processing ( ExitProcessingState ), // Broadcasting transactions
AwaitingDelta ( ExitAwaitingDeltaState ), // Waiting for timelock
Claimable ( ExitClaimableState ), // Ready to claim
ClaimInProgress ( ExitClaimInProgressState ),
Claimed ( ExitClaimedState ), // Funds recovered
}
Exit Lifecycle
1. Start Exit
exit . start_exit_for_vtxos ( & [ vtxo ]) . await ? ;
Marks VTXOs for exit:
Validates VTXO is above dust limit (330 sats)
Stores exit entry in database
Changes VTXO state to “Spent”
Creates movement record
Dust protection :
if vtxo . amount () < P2TR_DUST {
return Err ( ExitError :: DustLimit {
vtxo : vtxo . amount (),
dust : P2TR_DUST ,
});
}
2. Initialize Exit
exit . sync_no_progress ( & onchain_wallet ) . await ? ;
Prepares exit transactions:
Constructs full exit transaction chain
Tracks transaction IDs (TXIDs)
Validates exit path
Prepares fee anchors
3. Progress Exit
exit . progress_exits ( & wallet , & mut onchain_wallet , fee_rate ) . await ? ;
Broadcasting phase :
Progress Step 1: Broadcast root exit transaction
→ Includes CPFP fee anchor
→ Wait for confirmation
Progress Step 2: Wait for exit_delta blocks
→ Relative timelock from parent confirmation
→ Typically 144-2016 blocks
Progress Step 3: Broadcast next level transaction
→ Repeat for each level in tree
→ Apply fee-bumping if needed
Progress Step N: Final VTXO output confirmed
→ State: Claimable
4. Claim Funds
let drain_addr = Address :: from_str ( "bc1p..." ) ? ;
let psbt = exit . drain_exits ( & claimable , & wallet , drain_addr , fee_rate ) . await ? ;
Final claim transaction:
Spends claimable exit outputs
Sends to recovery address
Applies appropriate fees
Marks exit as complete
Exit Transaction Structure
Each exit transaction follows this pattern:
pub fn create_exit_tx (
prevout : OutPoint ,
output : TxOut ,
signature : Option < & Signature >,
fee : Amount ,
) -> Transaction
Transaction layout:
Exit Transaction
Inputs:
[0] Previous exit output (or chain anchor)
Outputs:
[0] Next level output (or final VTXO output)
[1] Fee Anchor (P2A, amount based on fee rate)
Fixed weight : Each exit transaction is exactly 124 vBytes:
pub const EXIT_TX_WEIGHT : Weight = Weight :: from_vb_unchecked ( 124 );
Exit Delta (Timelock)
The exit_delta parameter (from ArkInfo) enforces a relative timelock:
pub vtxo_exit_delta : BlockDelta // Typically 144-2016 blocks
Purpose :
Gives server time to refresh VTXOs cooperatively
Prevents spam attacks on chain
Allows orderly shutdown if server goes offline
Example timeline (exit_delta = 144):
Block 800,000: Parent tx confirms
Block 800,144: Exit tx becomes spendable (+ exit_delta)
Block 800,144+: Can broadcast next level
Script implementation :
pub fn delayed_sign ( delay_blocks : BlockDelta , pubkey : XOnlyPublicKey ) -> ScriptBuf {
let csv = Sequence :: from_height ( delay_blocks );
Script :: builder ()
. push_int ( csv . to_consensus_u32 () as i64 )
. push_opcode ( OP_CSV )
. push_opcode ( OP_DROP )
. push_x_only_key ( & pubkey )
. push_opcode ( OP_CHECKSIG )
. into_script ()
}
Fee Management
CPFP Fee-Bumping
All exit transactions include a fee anchor:
pub fn fee_anchor_with_amount ( fee : Amount ) -> TxOut {
TxOut {
script_pubkey : ScriptBuf :: new_p2tr_tweaked ( /* ... */ ),
value : fee ,
}
}
CPFP process :
Exit transaction broadcasts with low fee
Create child transaction spending fee anchor
Child pays for both (package relay)
Miners include entire package
Fee Rate Selection
pub async fn progress_exits (
fee_rate_override : Option < FeeRate >,
) -> Result <()>
Fee selection strategy :
No override : Use chain_source.fee_rates().fast
With override : Use specified rate
RBF bumping : Must exceed previous fee by increment
Cost calculation :
let total_cost = exit_depth × EXIT_TX_WEIGHT × fee_rate ;
// Example: depth 3, 10 sat/vB
total_cost = 3 × 124 vB × 10 sat / vB = 3 , 720 sats
Exit Transaction Packages
The system tracks related transactions together:
pub struct ExitTransactionPackage {
pub exit : TransactionInfo , // Main exit transaction
pub child : Option < ChildTransactionInfo >, // CPFP child if needed
}
pub struct TransactionInfo {
pub txid : Txid ,
pub tx : Transaction ,
}
Package features :
Atomic broadcast (both or neither)
Fee coordination
RBF replacement tracking
Status monitoring
Exit Monitoring
Checking Exit Status
let status = exit . get_exit_status (
vtxo_id ,
include_history : true ,
include_transactions : true ,
) . await ? ;
Returns:
pub struct ExitTransactionStatus {
pub vtxo_id : VtxoId ,
pub state : ExitState ,
pub history : Option < Vec < ExitState >>, // State transitions
pub transactions : Vec < ExitTransactionPackage >,
}
Exit Progress Status
pub struct ExitProgressStatus {
pub vtxo_id : VtxoId ,
pub state : ExitState ,
pub error : Option < ExitError >, // If progress failed
}
Claimable Tracking
let claimable = exit . list_claimable ();
let pending_total = exit . pending_total ();
let has_pending = exit . has_pending_exits ();
Common Exit Scenarios
Scenario 1: Server Goes Offline
1. Server stops responding
2. VTXOs approaching expiry
3. Start unilateral exit for all VTXOs
4. Wait for confirmations + exit_delta
5. Claim funds to recovery wallet
Timeline (exit_delta = 144):
T+0: Start exits, broadcast first level
T+10min: First level confirms (1 conf)
T+24h: Exit delta expires (144 blocks)
T+24h: Broadcast second level
T+24.5h: Second level confirms
T+48h: Exit delta expires again
T+48h: Broadcast final level
T+48.5h: Funds claimable
Scenario 2: Emergency Exit Single VTXO
// Exit just one specific VTXO
let vtxo = wallet . get_vtxo ( vtxo_id ) ? ;
exit . start_exit_for_vtxos ( & [ vtxo ]) . await ? ;
loop {
exit . sync ( & wallet , & mut onchain_wallet ) . await ? ;
exit . progress_exits ( & wallet , & mut onchain_wallet , None ) . await ? ;
if exit . get_exit_vtxo ( vtxo_id ) . unwrap () . is_claimable () {
break ;
}
sleep ( Duration :: from_secs ( 600 )) . await ; // Check every 10 min
}
let drain_addr = onchain_wallet . new_address () ? ;
let psbt = exit . drain_exits (
& [ exit . get_exit_vtxo ( vtxo_id ) . unwrap ()],
& wallet ,
drain_addr ,
None ,
) . await ? ;
onchain_wallet . broadcast_psbt ( psbt ) . await ? ;
Scenario 3: Batch Exit All Funds
// Exit entire wallet
exit . start_exit_for_entire_wallet () . await ? ;
let all_claimable_height = exit . all_claimable_at_height () . await ;
println! ( "All funds claimable at height: {:?}" , all_claimable_height );
Error Handling
Common exit errors:
pub enum ExitError {
DustLimit { vtxo : Amount , dust : Amount },
InsufficientConfirmedFunds { required : Amount , available : Amount },
VtxoNotClaimable { vtxo : VtxoId },
ClaimMissingInputs ,
ClaimFeeExceedsOutput { needed : Amount , output : Amount },
// ... more variants
}
Error recovery strategies :
VTXO is too small to exit profitably. Consider:
Combining with other VTXOs in a round first
Spending off-chain to another user
Waiting for lower fee rates
InsufficientConfirmedFunds
Not enough on-chain funds for CPFP. Solutions:
Wait for existing UTXOs to confirm
Deposit more funds to on-chain wallet
Use lower fee rates (slower confirmation)
Exit not ready yet. Check:
Has parent transaction confirmed?
Has exit_delta passed since parent confirmation?
Is exit state progressed properly?
Exit Best Practices
Monitor VTXO expiries and exit well before expiry
Keep on-chain wallet funded for CPFP
Test exit process on testnet/signet first
Store exit state persistently
Verify all exit transactions before broadcast
Use fee-bumping when necessary
Exit dust VTXOs during high fees
Clear exit state without claiming funds
Assume exits will be fast
Forget about exit_delta timelock
Broadcast without validating signatures
Rely solely on cooperative exits
Typical timelines :
Exit Depth Confirmations Exit Delta (blocks) Total Time (estimate) 1 (board) 1 144 ~24 hours 3 (round) 3 144 × 3 ~72 hours 5 (arkoor) 5 144 × 5 ~120 hours
Assuming 10-minute blocks and 144-block exit_delta
Cost comparison (10 sat/vB):
Exit Type Transactions Weight Cost Cooperative 0 (included in round) ~140 vB ~200 sats (shared) Unilateral Depth 1 1 124 vB 1,240 sats Unilateral Depth 3 3 372 vB 3,720 sats Unilateral Depth 5 5 620 vB 6,200 sats
Exit Transaction Validation
Before broadcasting, validate:
vtxo . validate ( & chain_anchor_tx ) ? ;
for exit_tx in vtxo . transactions () {
// Check signatures
// Verify output scripts
// Confirm fee anchors
// Validate amounts
}
Validation checklist :
✓ Chain anchor confirms on-chain
✓ All signatures valid
✓ Exit delta correctly enforced
✓ Fee anchors present
✓ Output amounts match expectations
✓ No unexpected spend paths
✓ All outputs are standard
Emergency Exit Procedure
If the server becomes malicious or unresponsive:
Immediate Action
exit . start_exit_for_entire_wallet () . await ? ;
Monitor Progress
// Run in loop until complete
exit . sync ( & wallet , & mut onchain ) . await ? ;
exit . progress_exits ( & wallet , & mut onchain , Some ( high_fee_rate )) . await ? ;
Claim When Ready
let claimable = exit . list_claimable ();
if ! claimable . is_empty () {
let psbt = exit . drain_exits ( & claimable , & wallet , recovery_addr , None ) . await ? ;
broadcast ( psbt ) . await ? ;
}
Verify Recovery
Check recovery address receives funds
Verify amounts match expectations (minus fees)
Archive exit records for auditing
Further Reading
Ark Protocol Overall protocol architecture
VTXOs Understanding VTXO structure