The offboard module provides functionality for cooperatively exiting VTXOs to on-chain Bitcoin addresses. This enables users to cash out of the Ark with server cooperation, creating an on-chain transaction.
Overview
Offboarding allows users to:
Exit to on-chain : Convert VTXOs to regular Bitcoin UTXOs
Batch exits : Server can batch multiple offboards in one transaction
Cooperative exits : Faster and cheaper than unilateral exits
Forfeit signatures : Users sign forfeits before receiving funds
The offboard process uses forfeit transactions to protect the server from partial exit attacks.
OffboardRequest
Specifies the destination and parameters for an offboard.
pub struct OffboardRequest {
pub script_pubkey : ScriptBuf ,
pub net_amount : Amount ,
pub deduct_fees_from_gross_amount : bool ,
pub fee_rate : FeeRate ,
}
Fields:
script_pubkey: Destination scriptPubkey (where funds will be sent)
net_amount: Target amount in satoshis
deduct_fees_from_gross_amount: If true, fees come from input; if false, added on top
fee_rate: Fee rate used for calculating the offboard fee
Methods
pub fn validate ( & self ) -> Result <(), InvalidOffboardRequestError >
Validate that the offboard has a standard (relay-able) output script.
pub fn to_txout ( & self ) -> TxOut
Convert the request into a transaction output.
Errors
pub enum InvalidOffboardRequestError {
// Non-standard output (won't relay)
}
OffboardForfeitContext
Context for signing and validating forfeit transactions.
pub struct OffboardForfeitContext <' a , V > {
input_vtxos : & ' a [ V ],
offboard_tx : & ' a Transaction ,
}
Generic V : Any type that implements AsRef<Vtxo>
Creating Context
pub fn new (
input_vtxos : & ' a [ V ],
offboard_tx : & ' a Transaction ,
) -> Self
Create context with input VTXOs and the offboard transaction.
Panics if input_vtxos is empty.
Validating Offboard Transaction
pub fn validate_offboard_tx (
& self ,
req : & OffboardRequest ,
) -> Result <(), InvalidOffboardTxError >
Validate that the offboard transaction matches the request.
Checks:
Offboard output (vout 0) matches request scriptPubkey and amount
Connector output (vout 1) has sufficient value for forfeit connectors
Required: 330 sats * number_of_inputs
Offboard Transaction Errors
pub enum InvalidOffboardTxError {
// String-based error with detailed message
}
User: Signing Forfeits
pub fn user_sign_forfeits (
& self ,
keys : & [ impl Borrow < Keypair >],
server_nonces : & [ PublicNonce ],
) -> OffboardForfeitSignatures
Sign forfeit transactions for all input VTXOs.
keys: User keypairs for the VTXO pubkeys (in order of input VTXOs)
server_nonces: Server’s public nonces (one per input VTXO)
Panics if:
Wrong number of keys or nonces
validate_offboard_tx() would have returned an error
Always call validate_offboard_tx() first!
OffboardForfeitSignatures
pub struct OffboardForfeitSignatures {
pub public_nonces : Vec < PublicNonce >,
pub partial_signatures : Vec < PartialSignature >,
}
User’s forfeit signatures to send to the server.
Server: Checking and Finalizing Forfeits
pub fn check_finalize_transactions (
& self ,
server_key : & Keypair ,
connector_key : & Keypair ,
server_pub_nonces : & [ PublicNonce ],
server_sec_nonces : Vec < SecretNonce >,
user_pub_nonces : & [ PublicNonce ],
user_partial_sigs : & [ PartialSignature ],
) -> Result < Vec < Transaction >, InvalidUserPartialSignatureError >
Check user’s partial signatures and finalize forfeit transactions.
server_key: Server’s VTXO signing key
connector_key: Key for spending connector outputs
server_pub_nonces: Server’s public nonces (one per input)
server_sec_nonces: Server’s secret nonces (consumed)
user_pub_nonces: User’s public nonces (from OffboardForfeitSignatures)
user_partial_sigs: User’s partial signatures (from OffboardForfeitSignatures)
Panics if:
Wrong number of nonces or signatures
validate_offboard_tx() would have returned an error
Returns:
Vector of fully signed forfeit transactions (one per input VTXO)
Invalid User Signature Error
pub struct InvalidUserPartialSignatureError {
pub vtxo : VtxoId ,
}
Indicates which VTXO had an invalid partial signature.
Forfeit Transaction Structure
Forfeits protect the server by ensuring users can’t perform partial exit attacks.
Inputs:
0: VTXO output (user + server MuSig2)
1: Connector output (from offboard tx)
Outputs:
0: Server pubkey (vtxo_amount + connector_dust)
1: Fee anchor (P2A)
Purpose: If user broadcasts the offboard tx but not the full round, server can claim the VTXO using this forfeit.
When multiple VTXOs are spent:
Offboard Tx
↓
Connector Output
↓
[Intermediate Connector Tx] ← Deterministic fanout
↓ ↓ ↓
Individual Connectors
↓ ↓ ↓
[Forfeit Tx 1] [Forfeit Tx 2] [Forfeit Tx 3]
Each forfeit uses its own connector from the intermediate transaction.
Offboard Transaction Output Indices
pub const OFFBOARD_TX_OFFBOARD_VOUT : usize = 0 ;
pub const OFFBOARD_TX_CONNECTOR_VOUT : usize = 1 ;
Offboard transaction structure:
Output 0: User’s offboard destination (from OffboardRequest)
Output 1: Connector for forfeit transactions (330 sats × number of inputs)
Output 2+: Optional (change, other destinations)
Usage Examples
User: Creating an Offboard Request and Signing Forfeits
let user_key = Keypair :: new ( SECP256K1_CONTEXT , & mut rng );
let input_vtxos : Vec < Vtxo > = // ... VTXOs to spend
// 1. Create offboard request
let offboard_req = OffboardRequest {
script_pubkey : destination_address . script_pubkey (),
net_amount : Amount :: from_sat ( 90_000 ),
deduct_fees_from_gross_amount : true ,
fee_rate : FeeRate :: from_sat_per_vb ( 10 ) . unwrap (),
};
// Validate the request
offboard_req . validate () ? ;
// 2. Send request to server, receive offboard_tx and server_nonces
let offboard_tx : Transaction = // ... from server
let server_nonces : Vec < PublicNonce > = // ... from server
// 3. Create forfeit context
let forfeit_ctx = OffboardForfeitContext :: new (
& input_vtxos ,
& offboard_tx ,
);
// 4. Validate the offboard transaction
forfeit_ctx . validate_offboard_tx ( & offboard_req ) ? ;
// 5. Sign forfeits
let user_keys = vec! [ & user_key ]; // One key per input VTXO
let forfeit_sigs = forfeit_ctx . user_sign_forfeits (
& user_keys ,
& server_nonces ,
);
// 6. Send forfeit_sigs to server
// forfeit_sigs.public_nonces
// forfeit_sigs.partial_signatures
// Server will finalize forfeits and broadcast offboard tx
Server: Processing Offboard and Finalizing Forfeits
let server_key : Keypair = // ...
let connector_key : Keypair = // ... for spending connectors
let offboard_req : OffboardRequest = // ... from user
let input_vtxos : Vec < Vtxo > = // ... user's VTXOs
// 1. Construct offboard transaction
let offboard_tx = Transaction {
version : bitcoin :: transaction :: Version ( 3 ),
input : vec! [
// ... user's VTXOs as inputs
],
output : vec! [
offboard_req . to_txout (), // vout 0: user's destination
TxOut { // vout 1: connector
value : P2TR_DUST * input_vtxos . len () as u64 ,
script_pubkey : connector_spk ,
},
// ... optional change output
],
};
// 2. Generate server nonces
let ( server_sec_nonces , server_pub_nonces ) : ( Vec < _ >, Vec < _ >) =
( 0 .. input_vtxos . len ())
. map ( | _ | musig :: nonce_pair ( & server_key ))
. unzip ();
// 3. Send to user:
// - offboard_tx
// - server_pub_nonces
// ... receive user_forfeit_sigs from user ...
// 4. Create forfeit context
let forfeit_ctx = OffboardForfeitContext :: new (
& input_vtxos ,
& offboard_tx ,
);
// 5. Validate (should match what user validated)
forfeit_ctx . validate_offboard_tx ( & offboard_req ) ? ;
// 6. Check and finalize forfeits
let forfeit_txs = forfeit_ctx . check_finalize_transactions (
& server_key ,
& connector_key ,
& server_pub_nonces ,
server_sec_nonces ,
& user_forfeit_sigs . public_nonces,
& user_forfeit_sigs . partial_signatures,
) ? ;
// 7. Store forfeit transactions and broadcast offboard
// Store forfeit_txs in database (safety net against partial exits)
// Broadcast offboard_tx to network
let vtxo1 : Vtxo = // ... 50k sats
let vtxo2 : Vtxo = // ... 30k sats
let vtxo3 : Vtxo = // ... 20k sats
let input_vtxos = vec! [ vtxo1 , vtxo2 , vtxo3 ];
let total_input = Amount :: from_sat ( 100_000 );
// Offboard 95k (5k for fees)
let offboard_req = OffboardRequest {
script_pubkey : destination_spk ,
net_amount : Amount :: from_sat ( 95_000 ),
deduct_fees_from_gross_amount : true ,
fee_rate : FeeRate :: from_sat_per_vb ( 5 ) . unwrap (),
};
// Connector output needs 3 × 330 = 990 sats minimum
let connector_amount = P2TR_DUST * 3 ;
let offboard_tx = Transaction {
// ...
output : vec! [
offboard_req . to_txout (), // 95k sats
TxOut { // 990 sats connector
value : connector_amount ,
script_pubkey : connector_spk ,
},
TxOut { // 4010 sats change
value : total_input - offboard_req . net_amount - connector_amount ,
script_pubkey : change_spk ,
},
],
};
// Create intermediate connector tx for individual forfeits
// (this happens automatically in check_finalize_transactions)
// Each forfeit will spend from the intermediate connector tx
// forfeit_txs[0] spends intermediate connector output 0 + vtxo1
// forfeit_txs[1] spends intermediate connector output 1 + vtxo2
// forfeit_txs[2] spends intermediate connector output 2 + vtxo3
Important Notes
Users must sign forfeits before the server broadcasts the offboard transaction. Without forfeits, users could perform partial exit attacks.
Forfeit transactions are a safety mechanism. They’re only broadcast if a user tries to cheat by exiting early from a round.
The connector output must have exactly 330 sats × number_of_inputs. This provides one dust output per forfeit transaction.
Always validate the offboard transaction with validate_offboard_tx() before signing forfeits. This ensures the transaction matches what you requested.
The check_finalize_transactions method consumes server secret nonces. They cannot be reused. Generate fresh nonces for each offboard attempt.