Skip to main content
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.
lib/src/arkoor/mod.rs
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:
lib/src/arkoor/mod.rs
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.
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.

Validation Errors

lib/src/arkoor/mod.rs
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.
lib/src/arkoor/mod.rs
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.
lib/src/arkoor/mod.rs
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.
lib/src/arkoor/mod.rs
pub struct ArkoorCosignResponse {
    pub server_pub_nonces: Vec<PublicNonce>,
    pub server_partial_sigs: Vec<PartialSignature>,
}

Signing Errors

lib/src/arkoor/mod.rs
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.

Build docs developers (and LLMs) love