Custom leaf optimization
The SDK implements a configurable Spark leaf optimization process. It supports two optimization policies:
- Maximize unilateral exit efficiency: aims to minimize the number of leaves, reducing costs for unilaterally exiting Bitcoin funds.
- Increase payment speed: maintains multiple copies of each leaf denomination to reduce the need for swaps during Bitcoin payments.
Configuring the optimization policy
The optimization behavior is controlled by the multiplicity setting, an integer value in the range 0-5. Setting it to 0 fully optimizes for unilateral exit efficiency, while values greater than 0 also optimize for payment speed. Higher values prioritize payment speed more aggressively, resulting in higher unilateral exit costs but faster payments, especially for bursts of transactions.
See Configuration to learn how to set the multiplicity.
Impact on payment speed
Multiplicity defines how many copies of each leaf denomination the SDK maintains. A higher multiplicity provides more flexibility in leaf combinations, reducing the frequency of swaps during payments. However, the exact number of swap-free payments depends on transaction amounts and patterns.
With automatic optimization, which is enabled by default, a multiplicity of 1 (the default) works well for most single-user applications with low payment frequency, eliminating the need for swaps in the vast majority of payment scenarios. Higher multiplicities are better suited for high-volume payment processing.
Impact on unilateral exit costs
Maintaining more leaves increases the total cost of unilaterally exiting funds, as each leaf incurs its own exit fee regardless of the leaf's value. This makes small denomination leaves cost-ineffective to exit.
Developer note
Keep multiplicity as low as possible while meeting your performance requirements. A high multiplicity can make unilateral exits prohibitively expensive.
Controlling optimization timing
The optimization process runs as a background task that reorganizes leaves by swapping them to achieve optimal denominations. During this process, funds in leaves being swapped become temporarily unavailable for payments, which can delay transaction processing.
By default, the SDK automatically triggers optimization after each payment (sent or received). For applications requiring more control, you can disable automatic optimization in the configuration and manage it manually as described below.
Start optimization API docs
You can manually trigger the optimization task to start running in the background. If optimization is already running, no new task will be started.
sdk.start_leaf_optimization();
sdk.startLeafOptimization()
sdk.startLeafOptimization()
sdk.StartLeafOptimization();
sdk.startLeafOptimization()
sdk.startLeafOptimization()
sdk.startLeafOptimization();
sdk.start_leaf_optimization()
sdk.StartLeafOptimization()
Cancel optimization API docs
You can cancel an ongoing optimization task and wait for it to stop completely. Optimization is done in rounds, and the current round will complete before stopping.
sdk.cancel_leaf_optimization().await?;
do {
try await sdk.cancelLeafOptimization()
} catch {
print("Failed to cancel optimization: \(error)")
}
try {
sdk.cancelLeafOptimization()
} catch (e: Exception) {
// handle error
}
await sdk.CancelLeafOptimization();
await sdk.cancelLeafOptimization()
await sdk.cancelLeafOptimization()
await sdk.cancelLeafOptimization();
await sdk.cancel_leaf_optimization()
err := sdk.CancelLeafOptimization()
if err != nil {
return err
}
Developer note
The SDK automatically cancels optimization when it would block an immediate payment. Use manual cancellation only when anticipating upcoming payment activity, not when there is an immediate need to make a payment.
Get optimization progress API docs
You can retrieve the current optimization progress to monitor the optimization task.
let progress = sdk.get_leaf_optimization_progress();
info!("Optimization is running: {}", progress.is_running);
info!("Current round: {}", progress.current_round);
info!("Total rounds: {}", progress.total_rounds);
let progress = sdk.getLeafOptimizationProgress()
print("Optimization is running: \(progress.isRunning)")
print("Current round: \(progress.currentRound)")
print("Total rounds: \(progress.totalRounds)")
val progress = sdk.getLeafOptimizationProgress()
println("Optimization is running: ${progress.isRunning}")
println("Current round: ${progress.currentRound}")
println("Total rounds: ${progress.totalRounds}")
var progress = sdk.GetLeafOptimizationProgress();
Console.WriteLine($"Optimization is running: {progress.isRunning}");
Console.WriteLine($"Current round: {progress.currentRound}");
Console.WriteLine($"Total rounds: {progress.totalRounds}");
const progress = sdk.getLeafOptimizationProgress()
console.log(`Optimization is running: ${progress.isRunning}`)
console.log(`Current round: ${progress.currentRound}`)
console.log(`Total rounds: ${progress.totalRounds}`)
const progress = sdk.getLeafOptimizationProgress()
console.log(`Optimization is running: ${progress.isRunning}`)
console.log(`Current round: ${progress.currentRound}`)
console.log(`Total rounds: ${progress.totalRounds}`)
var progress = sdk.getLeafOptimizationProgress();
print("Optimization is running: ${progress.isRunning}");
print("Current round: ${progress.currentRound}");
print("Total rounds: ${progress.totalRounds}");
progress = sdk.get_leaf_optimization_progress()
logging.debug(f"Optimization is running: {progress.is_running}")
logging.debug(f"Current round: {progress.current_round}")
logging.debug(f"Total rounds: {progress.total_rounds}")
progress := sdk.GetLeafOptimizationProgress()
log.Printf("Optimization is running: %v\n", progress.IsRunning)
log.Printf("Current round: %v\n", progress.CurrentRound)
log.Printf("Total rounds: %v\n", progress.TotalRounds)
Optimization events
The SDK emits events to keep your application informed about optimization status. See Listening to events for subscription instructions.
match event {
OptimizationEvent::Started { total_rounds } => {
info!("Optimization started with {} rounds", total_rounds);
}
OptimizationEvent::RoundCompleted { current_round, total_rounds } => {
info!("Optimization round {} of {} completed", current_round, total_rounds);
}
OptimizationEvent::Completed => {
info!("Optimization completed successfully");
}
OptimizationEvent::Cancelled => {
info!("Optimization was cancelled");
}
OptimizationEvent::Failed { error } => {
info!("Optimization failed: {}", error);
}
OptimizationEvent::Skipped => {
info!("Optimization was skipped because leaves are already optimal");
}
}
switch event {
case .started(let totalRounds):
print("Optimization started with \(totalRounds) rounds")
case .roundCompleted(let currentRound, let totalRounds):
print("Optimization round \(currentRound) of \(totalRounds) completed")
case .completed:
print("Optimization completed successfully")
case .cancelled:
print("Optimization was cancelled")
case .failed(let error):
print("Optimization failed: \(error)")
case .skipped:
print("Optimization was skipped because leaves are already optimal")
}
when (optimizationEvent) {
is OptimizationEvent.Started -> {
// Log.v("Breez", "Optimization started with ${optimizationEvent.totalRounds} rounds")
}
is OptimizationEvent.RoundCompleted -> {
// Log.v("Breez", "Optimization round ${optimizationEvent.currentRound} of ${optimizationEvent.totalRounds} completed")
}
is OptimizationEvent.Completed -> {
// Log.v("Breez", "Optimization completed successfully")
}
is OptimizationEvent.Cancelled -> {
// Log.v("Breez", "Optimization was cancelled")
}
is OptimizationEvent.Failed -> {
// Log.v("Breez", "Optimization failed: ${optimizationEvent.error}")
}
is OptimizationEvent.Skipped -> {
// Log.v("Breez", "Optimization was skipped because leaves are already optimal")
}
}
switch (event.type) {
case 'started': {
console.log(`Optimization started with ${event.totalRounds} rounds`)
break
}
case 'roundCompleted': {
console.log(`Optimization round ${event.currentRound} of ${event.totalRounds} completed`)
break
}
case 'completed': {
console.log('Optimization completed successfully')
break
}
case 'cancelled': {
console.log('Optimization was cancelled')
break
}
case 'failed': {
console.log(`Optimization failed: ${event.error}`)
break
}
case 'skipped': {
console.log('Optimization was skipped because leaves are already optimal')
break
}
}
if (optimizationEvent.tag === OptimizationEvent_Tags.Started) {
console.log(`Optimization started with ${optimizationEvent.inner.totalRounds} rounds`)
} else if (optimizationEvent.tag === OptimizationEvent_Tags.RoundCompleted) {
console.log(`Optimization round ${optimizationEvent.inner.currentRound} of ${optimizationEvent.inner.totalRounds} completed`)
} else if (optimizationEvent.tag === OptimizationEvent_Tags.Completed) {
console.log('Optimization completed successfully')
} else if (optimizationEvent.tag === OptimizationEvent_Tags.Cancelled) {
console.log('Optimization was cancelled')
} else if (optimizationEvent.tag === OptimizationEvent_Tags.Failed) {
console.log(`Optimization failed: ${optimizationEvent.inner.error}`)
} else if (optimizationEvent.tag === OptimizationEvent_Tags.Skipped) {
console.log('Optimization was skipped because leaves are already optimal')
}
if isinstance(optimization_event, OptimizationEvent.STARTED):
logging.debug(f"Optimization started with {optimization_event.total_rounds} rounds")
elif isinstance(optimization_event, OptimizationEvent.ROUND_COMPLETED):
logging.debug(f"Optimization round {optimization_event.current_round} of "
f"{optimization_event.total_rounds} completed")
elif isinstance(optimization_event, OptimizationEvent.COMPLETED):
logging.debug("Optimization completed successfully")
elif isinstance(optimization_event, OptimizationEvent.CANCELLED):
logging.debug("Optimization was cancelled")
elif isinstance(optimization_event, OptimizationEvent.FAILED):
logging.debug(f"Optimization failed: {optimization_event.error}")
elif isinstance(optimization_event, OptimizationEvent.SKIPPED):
logging.debug("Optimization was skipped because leaves are already optimal")
switch event := optimizationEvent.(type) {
case breez_sdk_spark.OptimizationEventStarted:
log.Printf("Optimization started with %v rounds\n", event.TotalRounds)
case breez_sdk_spark.OptimizationEventRoundCompleted:
log.Printf("Optimization round %v of %v completed\n", event.CurrentRound, event.TotalRounds)
case breez_sdk_spark.OptimizationEventCompleted:
log.Printf("Optimization completed successfully\n")
case breez_sdk_spark.OptimizationEventCancelled:
log.Printf("Optimization was cancelled\n")
case breez_sdk_spark.OptimizationEventFailed:
log.Printf("Optimization failed: %v\n", event.Error)
case breez_sdk_spark.OptimizationEventSkipped:
log.Printf("Optimization was skipped because leaves are already optimal\n")
}