The arkoor (“Ark out-of-round”) module provides functionality for spending VTXOs cooperatively with the server outside of regular rounds. This enables instant payments and transfers without waiting for the next round.
Overview
Arkoor transactions allow users to:
Send payments instantly to other users
Spend VTXOs without waiting for rounds
Create new VTXOs with cooperative server signatures
Use checkpoints to protect against partial exit attacks
The core construct is ArkoorBuilder, designed for use by both clients and servers with a state-machine API.
ArkoorBuilder
A builder for constructing out-of-round transactions with multiple states.
pub struct ArkoorBuilder < S : BuilderState > {
input : Vtxo ,
outputs : Vec < ArkoorDestination >,
isolated_outputs : Vec < ArkoorDestination >,
checkpoint_data : Option <( Transaction , Txid )>,
unsigned_arkoor_txs : Vec < Transaction >,
unsigned_isolation_fanout_tx : Option < Transaction >,
sighashes : Vec < TapSighash >,
input_tweak : TapTweakHash ,
checkpoint_policy_tweak : TapTweakHash ,
new_vtxo_ids : Vec < VtxoId >,
// ... nonces and signatures (state-dependent)
}
Builder States
The builder uses a state machine to ensure correct usage:
pub mod state {
pub struct Initial ; // Initial state
pub struct UserGeneratedNonces ; // User path: nonces generated
pub struct UserSigned ; // User path: fully signed
pub struct ServerCanCosign ; // Server path: can cosign
pub struct ServerSigned ; // Server path: signed
}
State Transitions
User Path:
Initial → generate_user_nonces() → UserGeneratedNonces → user_cosign() → UserSigned
Server Path:
Initial → set_user_pub_nonces() → ServerCanCosign → server_cosign() → ServerSigned
Construction (Initial State)
Creating a Builder
pub fn new_with_checkpoint (
input : Vtxo ,
outputs : Vec < ArkoorDestination >,
isolated_outputs : Vec < ArkoorDestination >,
) -> Result < Self , ArkoorConstructionError >
Create a builder with checkpoint transaction for security.
Checkpoints protect against partial exit attacks:
Each output gets its own checkpoint VTXO
Server can broadcast a single checkpoint to claim funds on expiry
Other users unaffected if one user exits
Adds one extra transaction to the exit path
pub fn new_without_checkpoint (
input : Vtxo ,
outputs : Vec < ArkoorDestination >,
isolated_outputs : Vec < ArkoorDestination >,
) -> Result < Self , ArkoorConstructionError >
Create a builder without checkpoint transaction (more efficient but less secure).
pub fn new_with_checkpoint_isolate_dust (
input : Vtxo ,
outputs : Vec < ArkoorDestination >,
) -> Result < Self , ArkoorConstructionError >
Create with checkpoint and automatic dust isolation.
Automatically separates dust outputs (< 330 sats) from non-dust:
Dust outputs combined into single checkpoint output
Fanout transaction splits combined output into final VTXOs
Prevents dust from making all outputs non-standard
May split outputs to reach dust threshold
Validation Errors
pub enum ArkoorConstructionError {
Unbalanced { input : Amount , output : Amount },
Dust ,
NoOutputs ,
TooManyInputs ,
}
Construction validates:
Input/output balance : Must be exactly equal (zero on-chain fees)
At least one output : Required for valid arkoor
Dust threshold : Isolated outputs must sum to ≥330 sats
Common Methods (All States)
pub fn input ( & self ) -> & Vtxo
Access the input VTXO being spent.
pub fn normal_outputs ( & self ) -> & [ ArkoorDestination ]
Access regular (non-isolated) output destinations.
pub fn isolated_outputs ( & self ) -> & [ ArkoorDestination ]
Access dust-isolated output destinations.
pub fn all_outputs ( & self ) -> impl Iterator < Item = & ArkoorDestination >
Iterator over all output destinations (normal + isolated).
pub fn build_unsigned_vtxos ( & self ) -> impl Iterator < Item = Vtxo >
Build all final VTXOs without signatures (for preview/validation).
pub fn build_unsigned_internal_vtxos ( & self ) -> impl Iterator < Item = ServerVtxo >
Build intermediate checkpoint and isolation VTXOs (server-internal).
pub fn spend_info ( & self ) -> Vec <( VtxoId , Txid )>
Returns pairs of (spent VTXO ID, spending transaction ID).
pub fn virtual_transactions ( & self ) -> Vec < Txid >
Returns txids of all virtual transactions:
Checkpoint tx (if enabled)
Arkoor txs (one per normal output or single combined)
Isolation fanout tx (if dust isolation active)
User Flow
Step 1: Generate Nonces (UserGeneratedNonces)
pub fn generate_user_nonces (
self ,
user_keypair : Keypair ,
) -> ArkoorBuilder < state :: UserGeneratedNonces >
Generate MuSig2 nonces for signing. Transitions to UserGeneratedNonces state.
pub fn user_pub_nonces ( & self ) -> & [ PublicNonce ]
Access the generated public nonces (needed for cosign request).
pub fn cosign_request ( & self ) -> ArkoorCosignRequest < Vtxo >
Create a cosign request to send to the server.
Step 2: Cosign and Build (UserSigned)
pub fn user_cosign (
self ,
user_keypair : & Keypair ,
server_cosign_data : & ArkoorCosignResponse ,
) -> Result < ArkoorBuilder < state :: UserSigned >, ArkoorSigningError >
Combine user and server partial signatures. Validates server signatures before proceeding.
pub fn build_signed_vtxos ( & self ) -> Vec < Vtxo >
Build final fully-signed VTXOs (only in UserSigned state).
Server Flow
From Cosign Request (ServerCanCosign)
pub fn from_cosign_request (
cosign_request : ArkoorCosignRequest < Vtxo >,
) -> Result < ArkoorBuilder < state :: ServerCanCosign >, ArkoorSigningError >
Create a builder from a user’s cosign request. Validates the request structure.
Server Cosigning (ServerSigned)
pub fn server_cosign (
self ,
server_keypair : & Keypair ,
) -> Result < ArkoorBuilder < state :: ServerSigned >, ArkoorSigningError >
Generate server’s partial signatures. Verifies the keypair matches the VTXO’s server pubkey.
pub fn cosign_response ( & self ) -> ArkoorCosignResponse
Get the cosign response to send back to the user.
pub fn user_pub_nonces ( & self ) -> Vec < PublicNonce >
pub fn server_partial_signatures ( & self ) -> Vec < PartialSignature >
Access server’s nonces and partial signatures.
ArkoorDestination
Specifies where an arkoor output should go.
pub struct ArkoorDestination {
pub total_amount : Amount ,
pub policy : VtxoPolicy ,
}
Multiple destinations with the same policy are created as separate VTXOs (arkoor doesn’t support multiple inputs).
ArkoorCosignRequest
User’s request for the server to cosign.
pub struct ArkoorCosignRequest < V > {
pub user_pub_nonces : Vec < PublicNonce >,
pub input : V ,
pub outputs : Vec < ArkoorDestination >,
pub isolated_outputs : Vec < ArkoorDestination >,
pub use_checkpoint : bool ,
}
Methods:
pub fn new (
user_pub_nonces : Vec < PublicNonce >,
input : V ,
outputs : Vec < ArkoorDestination >,
isolated_outputs : Vec < ArkoorDestination >,
use_checkpoint : bool ,
) -> Self
pub fn all_outputs ( & self ) -> impl Iterator < Item = & ArkoorDestination >
Iterator over all output destinations.
// For VtxoId variant:
pub fn with_vtxo ( self , vtxo : Vtxo ) -> Result < ArkoorCosignRequest < Vtxo >, & ' static str >
Attach the actual VTXO to a request that only had the ID.
ArkoorCosignResponse
Server’s cosigning response.
pub struct ArkoorCosignResponse {
pub server_pub_nonces : Vec < PublicNonce >,
pub server_partial_sigs : Vec < PartialSignature >,
}
Signing Errors
pub enum ArkoorSigningError {
ArkoorConstructionError ( ArkoorConstructionError ),
InvalidNbUserNonces { expected : usize , got : usize },
InvalidNbServerNonces { expected : usize , got : usize },
IncorrectKey { expected : PublicKey , got : PublicKey },
InvalidNbServerPartialSigs { expected : usize , got : usize },
InvalidPartialSignature { index : usize },
// ...
}
Usage Examples
Client: Creating an Arkoor Payment
let alice_keypair : Keypair = // ...
let alice_vtxo : Vtxo = // ...
let bob_pubkey : PublicKey = // ...
// Define where the payment goes
let outputs = vec! [
ArkoorDestination {
total_amount : Amount :: from_sat ( 80_000 ),
policy : VtxoPolicy :: new_pubkey ( bob_pubkey ),
},
ArkoorDestination {
total_amount : Amount :: from_sat ( 20_000 ),
policy : VtxoPolicy :: new_pubkey ( alice_keypair . public_key ()), // change
},
];
// Build with checkpoint for security
let builder = ArkoorBuilder :: new_with_checkpoint (
alice_vtxo ,
outputs ,
vec! [], // no isolation
) ? ;
// Generate nonces
let builder = builder . generate_user_nonces ( alice_keypair );
// Create request for server
let cosign_request = builder . cosign_request ();
// Send cosign_request to server...
// Receive cosign_response from server...
// Finalize and build VTXOs
let builder = builder . user_cosign ( & alice_keypair , & cosign_response ) ? ;
let vtxos = builder . build_signed_vtxos ();
// vtxos[0] = Bob's 80k VTXO
// vtxos[1] = Alice's 20k change VTXO
Server: Cosigning a Request
let server_keypair : Keypair = // ...
let cosign_request : ArkoorCosignRequest < Vtxo > = // ... from client
// Validate and build from request
let builder = ArkoorBuilder :: from_cosign_request ( cosign_request ) ? ;
// Cosign
let builder = builder . server_cosign ( & server_keypair ) ? ;
// Get response to send back to client
let response = builder . cosign_response ();
// Store internal VTXOs for tracking
let internal_vtxos : Vec < ServerVtxo > = builder . build_unsigned_internal_vtxos () . collect ();
let spend_info = builder . spend_info ();
// Store spend_info and internal_vtxos in database...
Automatic Dust Handling
let outputs = vec! [
ArkoorDestination {
total_amount : Amount :: from_sat ( 50_000 ), // normal
policy : VtxoPolicy :: new_pubkey ( pubkey1 ),
},
ArkoorDestination {
total_amount : Amount :: from_sat ( 200 ), // dust
policy : VtxoPolicy :: new_pubkey ( pubkey2 ),
},
ArkoorDestination {
total_amount : Amount :: from_sat ( 150 ), // dust
policy : VtxoPolicy :: new_pubkey ( pubkey3 ),
},
];
// Automatically isolates dust
let builder = ArkoorBuilder :: new_with_checkpoint_isolate_dust (
input_vtxo ,
outputs ,
) ? ;
// Dust outputs combined: 200 + 150 = 350 sats (above 330 threshold)
// Normal outputs: 50_000 sats
// Isolation fanout tx splits combined dust into final VTXOs
assert_eq! ( builder . normal_outputs () . len (), 1 ); // 50k output
assert_eq! ( builder . isolated_outputs () . len (), 2 ); // 200 + 150 isolated
Transaction Structure
With Checkpoints
Input VTXO
↓
[Checkpoint Tx] ← Single transaction with M outputs
↓ ↓ ↓
[Arkoor Tx 1] [Arkoor Tx 2] ... [Arkoor Tx M]
↓ ↓ ↓
VTXO 1 VTXO 2 VTXO M
With dust isolation:
Input VTXO
↓
[Checkpoint Tx] ← M normal outputs + 1 combined dust output
↓ ↓ ↓ ↓
[Arkoor Txs] [Isolation Fanout Tx]
↓ ↓ ↓
Dust VTXOs
Without Checkpoints
Input VTXO
↓
[Single Arkoor Tx] ← All outputs in one transaction
↓ ↓ ↓
VTXO 1 VTXO 2 VTXO 3
With dust isolation:
Input VTXO
↓
[Arkoor Tx] ← Normal outputs + 1 combined dust output
↓ ↓ ↓
VTXOs [Isolation Fanout Tx]
↓ ↓ ↓
Dust VTXOs
Important Notes
Arkoor transactions require exact balance : input amount must equal total output amount. No on-chain fees are paid (uses ephemeral anchors).
Checkpoints add one transaction to the exit path but provide strong protection against partial exit attacks. Recommended for production use.
Dust isolation automatically triggers when outputs mix dust and non-dust amounts. The algorithm may split outputs to meet the 330-sat threshold.
Use build_unsigned_vtxos() to preview results before signing. All VTXOs can be validated with their chain anchor transaction.