Skip to content

Commit eccd1a6

Browse files
committed
fix: tune lightning probe routing
1 parent 4deacd5 commit eccd1a6

9 files changed

Lines changed: 296 additions & 35 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ google-services.json
2626
!debug.keystore
2727
keystore.*
2828
!keystore.properties.template
29+
.vscode

app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.net.Uri
66
import android.os.Binder
77
import android.os.Bundle
88
import android.os.Process
9+
import android.os.SystemClock
910
import androidx.core.os.bundleOf
1011
import dagger.hilt.EntryPoint
1112
import dagger.hilt.InstallIn
@@ -114,16 +115,19 @@ private sealed interface DevCommand {
114115
context = TAG,
115116
)
116117

118+
val startedAt = SystemClock.elapsedRealtime()
117119
return deps.lightningRepo().sendProbeForInvoice(args.bolt11, amountSats)
118120
.fold(
119121
onSuccess = {
120122
deps.lightningRepo().waitForProbeOutcome(it.paymentIds, timeout)
121123
.fold(
122-
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds) },
123-
onFailure = { error -> DevResult.ProbeFailure.from(error, it.paymentIds) },
124+
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds, startedAt.durationMs()) },
125+
onFailure = { error ->
126+
DevResult.ProbeFailure.from(error, it.paymentIds, startedAt.durationMs())
127+
},
124128
)
125129
},
126-
onFailure = { DevResult.ProbeFailure.from(it) },
130+
onFailure = { DevResult.ProbeFailure.from(it, durationMs = startedAt.durationMs()) },
127131
)
128132
}
129133
}
@@ -153,16 +157,19 @@ private sealed interface DevCommand {
153157
context = TAG,
154158
)
155159

160+
val startedAt = SystemClock.elapsedRealtime()
156161
return deps.lightningRepo().sendProbeForNode(args.nodeId, amountSats)
157162
.fold(
158163
onSuccess = {
159164
deps.lightningRepo().waitForProbeOutcome(it.paymentIds, timeout)
160165
.fold(
161-
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds) },
162-
onFailure = { error -> DevResult.ProbeFailure.from(error, it.paymentIds) },
166+
onSuccess = { outcome -> outcome.toDevResult(it.paymentIds, startedAt.durationMs()) },
167+
onFailure = { error ->
168+
DevResult.ProbeFailure.from(error, it.paymentIds, startedAt.durationMs())
169+
},
163170
)
164171
},
165-
onFailure = { DevResult.ProbeFailure.from(it) },
172+
onFailure = { DevResult.ProbeFailure.from(it, durationMs = startedAt.durationMs()) },
166173
)
167174
}
168175
}
@@ -206,6 +213,8 @@ private sealed interface DevResult {
206213
val success: Boolean = true,
207214
val paymentId: String,
208215
val paymentHash: String,
216+
val durationMs: Long,
217+
val routeFeeMsat: ULong?,
209218
val paymentIds: List<String>,
210219
) : DevResult
211220

@@ -215,12 +224,19 @@ private sealed interface DevResult {
215224
val message: String? = null,
216225
val paymentId: String? = null,
217226
val paymentHash: String? = null,
218-
val shortChannelId: ULong? = null,
227+
val shortChannelId: String? = null,
228+
val durationMs: Long,
229+
val routeFeeMsat: ULong? = null,
219230
val paymentIds: List<String> = emptyList(),
220231
) : DevResult {
221232
companion object {
222-
fun from(error: Throwable, paymentIds: Set<String> = emptySet()) = ProbeFailure(
233+
fun from(
234+
error: Throwable,
235+
paymentIds: Set<String> = emptySet(),
236+
durationMs: Long,
237+
) = ProbeFailure(
223238
message = error.message,
239+
durationMs = durationMs,
224240
paymentIds = paymentIds.toList(),
225241
)
226242
}
@@ -243,6 +259,7 @@ private sealed interface DevResult {
243259
val graphChannelCount: Int? = null,
244260
val latestRgsSyncTimestamp: ULong? = null,
245261
val latestPathfindingScoresSyncTimestamp: ULong? = null,
262+
val probeRuntimeConfig: ProbeRuntimeConfig,
246263
) : DevResult {
247264
companion object {
248265
fun from(readiness: NodeProbeReadiness) = ProbeReadiness(
@@ -261,29 +278,91 @@ private sealed interface DevResult {
261278
graphChannelCount = readiness.graphChannelCount,
262279
latestRgsSyncTimestamp = readiness.latestRgsSyncTimestamp,
263280
latestPathfindingScoresSyncTimestamp = readiness.latestPathfindingScoresSyncTimestamp,
281+
probeRuntimeConfig = ProbeRuntimeConfig.from(readiness.probeRuntimeConfig),
264282
)
265283
}
266284
}
267285

286+
@Serializable
287+
data class ProbeRuntimeConfig(
288+
val sampleAmountMsat: ULong,
289+
val route: ProbeRouteConfig,
290+
val scoring: ProbeScoringConfig,
291+
) {
292+
companion object {
293+
fun from(config: to.bitkit.services.ProbeRuntimeConfig) = ProbeRuntimeConfig(
294+
sampleAmountMsat = config.sampleAmountMsat,
295+
route = ProbeRouteConfig(
296+
maxTotalRoutingFeeMsat = config.route.maxTotalRoutingFeeMsat,
297+
maxTotalCltvExpiryDelta = config.route.maxTotalCltvExpiryDelta,
298+
maxPathCount = config.route.maxPathCount,
299+
maxChannelSaturationPowerOfHalf = config.route.maxChannelSaturationPowerOfHalf,
300+
),
301+
scoring = ProbeScoringConfig(
302+
basePenaltyMsat = config.scoring.basePenaltyMsat,
303+
basePenaltyAmountMultiplierMsat = config.scoring.basePenaltyAmountMultiplierMsat,
304+
liquidityPenaltyMultiplierMsat = config.scoring.liquidityPenaltyMultiplierMsat,
305+
liquidityPenaltyAmountMultiplierMsat = config.scoring.liquidityPenaltyAmountMultiplierMsat,
306+
historicalLiquidityPenaltyMultiplierMsat =
307+
config.scoring.historicalLiquidityPenaltyMultiplierMsat,
308+
historicalLiquidityPenaltyAmountMultiplierMsat =
309+
config.scoring.historicalLiquidityPenaltyAmountMultiplierMsat,
310+
antiProbingPenaltyMsat = config.scoring.antiProbingPenaltyMsat,
311+
consideredImpossiblePenaltyMsat = config.scoring.consideredImpossiblePenaltyMsat,
312+
linearSuccessProbability = config.scoring.linearSuccessProbability,
313+
probingDiversityPenaltyMsat = config.scoring.probingDiversityPenaltyMsat,
314+
),
315+
)
316+
}
317+
}
318+
319+
@Serializable
320+
data class ProbeRouteConfig(
321+
val maxTotalRoutingFeeMsat: ULong?,
322+
val maxTotalCltvExpiryDelta: UInt,
323+
val maxPathCount: Int,
324+
val maxChannelSaturationPowerOfHalf: Int,
325+
)
326+
327+
@Serializable
328+
data class ProbeScoringConfig(
329+
val basePenaltyMsat: ULong,
330+
val basePenaltyAmountMultiplierMsat: ULong,
331+
val liquidityPenaltyMultiplierMsat: ULong,
332+
val liquidityPenaltyAmountMultiplierMsat: ULong,
333+
val historicalLiquidityPenaltyMultiplierMsat: ULong,
334+
val historicalLiquidityPenaltyAmountMultiplierMsat: ULong,
335+
val antiProbingPenaltyMsat: ULong,
336+
val consideredImpossiblePenaltyMsat: ULong,
337+
val linearSuccessProbability: Boolean,
338+
val probingDiversityPenaltyMsat: ULong,
339+
)
340+
268341
@Serializable data class Error(val message: String? = null) : DevResult
269342

270343
fun toBundle() = bundleOf(KEY_RESULT to DEV_JSON.encodeToString(this))
271344
}
272345

273-
private fun ProbeOutcome.toDevResult(paymentIds: Set<String>): DevResult = when (this) {
346+
private fun ProbeOutcome.toDevResult(paymentIds: Set<String>, durationMs: Long): DevResult = when (this) {
274347
is ProbeOutcome.Success -> DevResult.ProbeSuccess(
275348
paymentId = paymentId,
276349
paymentHash = paymentHash,
350+
durationMs = durationMs,
351+
routeFeeMsat = routeFeeMsat,
277352
paymentIds = paymentIds.toList(),
278353
)
279354
is ProbeOutcome.Failure -> DevResult.ProbeFailure(
280355
message = "Probe failed",
281356
paymentId = paymentId,
282357
paymentHash = paymentHash,
283-
shortChannelId = shortChannelId,
358+
shortChannelId = shortChannelId?.toString(),
359+
durationMs = durationMs,
360+
routeFeeMsat = routeFeeMsat,
284361
paymentIds = paymentIds.toList(),
285362
)
286363
}
287364

288365
private inline fun <reified T> String?.deserialize(): T =
289366
if (isNullOrBlank()) Json.decodeFromString("{}") else Json.decodeFromString(this)
367+
368+
private fun Long.durationMs() = SystemClock.elapsedRealtime() - this

app/src/main/java/to/bitkit/repositories/LightningRepo.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import to.bitkit.services.LnurlService
8585
import to.bitkit.services.LnurlWithdrawResponse
8686
import to.bitkit.services.LspNotificationsService
8787
import to.bitkit.services.NodeEventHandler
88+
import to.bitkit.services.ProbeRuntimeConfig
8889
import to.bitkit.utils.AppError
8990
import to.bitkit.utils.Logger
9091
import to.bitkit.utils.ServiceError
@@ -571,8 +572,13 @@ class LightningRepo @Inject constructor(
571572

572573
private suspend fun recordProbeOutcome(event: Event) {
573574
val outcome = when (event) {
574-
is Event.ProbeSuccessful -> ProbeOutcome.Success(event.paymentId, event.paymentHash)
575-
is Event.ProbeFailed -> ProbeOutcome.Failure(event.paymentId, event.paymentHash, event.shortChannelId)
575+
is Event.ProbeSuccessful -> ProbeOutcome.Success(event.paymentId, event.paymentHash, event.routeFeeMsat)
576+
is Event.ProbeFailed -> ProbeOutcome.Failure(
577+
event.paymentId,
578+
event.paymentHash,
579+
event.shortChannelId,
580+
event.routeFeeMsat,
581+
)
576582
else -> return
577583
}
578584

@@ -1637,6 +1643,7 @@ class LightningRepo @Inject constructor(
16371643
graphChannelCount = graph?.channelCount,
16381644
latestRgsSyncTimestamp = graph?.latestRgsSyncTimestamp,
16391645
latestPathfindingScoresSyncTimestamp = state.nodeStatus?.latestPathfindingScoresSyncTimestamp,
1646+
probeRuntimeConfig = lightningService.probeRuntimeConfig(),
16401647
syncHealthy = state.isSyncHealthy,
16411648
)
16421649
}
@@ -1761,6 +1768,7 @@ data class ProbeReadiness(
17611768
val graphChannelCount: Int?,
17621769
val latestRgsSyncTimestamp: ULong?,
17631770
val latestPathfindingScoresSyncTimestamp: ULong?,
1771+
val probeRuntimeConfig: ProbeRuntimeConfig,
17641772
val syncHealthy: Boolean,
17651773
) {
17661774
val ready: Boolean
@@ -1779,11 +1787,13 @@ sealed interface ProbeOutcome {
17791787
data class Success(
17801788
override val paymentId: PaymentId,
17811789
override val paymentHash: PaymentHash,
1790+
val routeFeeMsat: ULong?,
17821791
) : ProbeOutcome
17831792

17841793
data class Failure(
17851794
override val paymentId: PaymentId,
17861795
override val paymentHash: PaymentHash,
17871796
val shortChannelId: ULong?,
1797+
val routeFeeMsat: ULong?,
17881798
) : ProbeOutcome
17891799
}

app/src/main/java/to/bitkit/services/LightningService.kt

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import org.lightningdevkit.ldknode.PaymentDetails
3838
import org.lightningdevkit.ldknode.PaymentId
3939
import org.lightningdevkit.ldknode.PeerDetails
4040
import org.lightningdevkit.ldknode.PublicKey
41+
import org.lightningdevkit.ldknode.RouteParametersConfig
42+
import org.lightningdevkit.ldknode.ScoringFeeParameters
4143
import org.lightningdevkit.ldknode.SpendableUtxo
4244
import org.lightningdevkit.ldknode.Txid
4345
import org.lightningdevkit.ldknode.defaultConfig
@@ -89,6 +91,22 @@ class LightningService @Inject constructor(
8991
companion object {
9092
private const val TAG = "LightningService"
9193
private const val NODE_ID_PREVIEW_LEN = 20
94+
private const val PROBE_MAX_TOTAL_ROUTING_FEE_DIVISOR = 100uL
95+
private const val PROBE_MAX_TOTAL_ROUTING_FEE_BASE_MSAT = 50_000uL
96+
private const val PROBE_MAX_TOTAL_CLTV_EXPIRY_DELTA = 2_016u
97+
private const val PROBE_MAX_PATH_COUNT = 10u
98+
private const val PROBE_MAX_CHANNEL_SATURATION_POWER_OF_HALF = 2u
99+
private const val PROBE_CONFIG_SAMPLE_AMOUNT_MSAT = 80_000_000uL
100+
101+
private const val SCORING_BASE_PENALTY_MSAT = 50_000uL
102+
private const val SCORING_BASE_PENALTY_AMOUNT_MULTIPLIER_MSAT = 131_072uL
103+
private const val SCORING_LIQUIDITY_PENALTY_MULTIPLIER_MSAT = 10_000uL
104+
private const val SCORING_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT = 10_000uL
105+
private const val SCORING_HISTORICAL_LIQUIDITY_PENALTY_MULTIPLIER_MSAT = 10_000uL
106+
private const val SCORING_HISTORICAL_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT = 20_000uL
107+
private const val SCORING_ANTI_PROBING_PENALTY_MSAT = 250uL
108+
private const val SCORING_CONSIDERED_IMPOSSIBLE_PENALTY_MSAT = 1_000_000_000_000uL
109+
private const val SCORING_PROBING_DIVERSITY_PENALTY_MSAT = 50_000uL
92110
}
93111

94112
@Volatile
@@ -168,6 +186,7 @@ class LightningService @Inject constructor(
168186
configureChainSource(customServerUrl)
169187
configureGossipSource(customRgsServerUrl)
170188
configureScorerSource()
189+
setScoringFeeParams(scorerFeeParameters())
171190
setAddressType(selectedType)
172191
setAddressTypesToMonitor(monitoredTypes)
173192

@@ -808,7 +827,10 @@ class LightningService @Inject constructor(
808827

809828
return ServiceQueue.LDK.background {
810829
runCatching {
811-
val handles = node.bolt11Payment().sendProbes(bolt11Invoice, null)
830+
val handles = node.bolt11Payment().sendProbes(
831+
bolt11Invoice,
832+
invoiceAmountMsat?.let { probeRouteParameters(it) },
833+
)
812834
Result.success(handles.map { it.paymentId }.toSet())
813835
}.getOrElse {
814836
dumpNetworkGraphInfo(bolt11)
@@ -832,7 +854,11 @@ class LightningService @Inject constructor(
832854

833855
return ServiceQueue.LDK.background {
834856
runCatching {
835-
val handles = node.bolt11Payment().sendProbesUsingAmount(bolt11Invoice, amountMsat, null)
857+
val handles = node.bolt11Payment().sendProbesUsingAmount(
858+
bolt11Invoice,
859+
amountMsat,
860+
probeRouteParameters(amountMsat),
861+
)
836862
Result.success(handles.map { it.paymentId }.toSet())
837863
}.getOrElse {
838864
dumpNetworkGraphInfo(bolt11)
@@ -860,6 +886,55 @@ class LightningService @Inject constructor(
860886
}
861887
// endregion
862888

889+
private fun probeRouteParameters(amountMsat: ULong) = RouteParametersConfig(
890+
maxTotalRoutingFeeMsat = amountMsat / PROBE_MAX_TOTAL_ROUTING_FEE_DIVISOR +
891+
PROBE_MAX_TOTAL_ROUTING_FEE_BASE_MSAT,
892+
maxTotalCltvExpiryDelta = PROBE_MAX_TOTAL_CLTV_EXPIRY_DELTA,
893+
maxPathCount = PROBE_MAX_PATH_COUNT.toUByte(),
894+
maxChannelSaturationPowerOfHalf = PROBE_MAX_CHANNEL_SATURATION_POWER_OF_HALF.toUByte(),
895+
)
896+
897+
private fun scorerFeeParameters() = ScoringFeeParameters(
898+
basePenaltyMsat = SCORING_BASE_PENALTY_MSAT,
899+
basePenaltyAmountMultiplierMsat = SCORING_BASE_PENALTY_AMOUNT_MULTIPLIER_MSAT,
900+
liquidityPenaltyMultiplierMsat = SCORING_LIQUIDITY_PENALTY_MULTIPLIER_MSAT,
901+
liquidityPenaltyAmountMultiplierMsat = SCORING_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT,
902+
historicalLiquidityPenaltyMultiplierMsat = SCORING_HISTORICAL_LIQUIDITY_PENALTY_MULTIPLIER_MSAT,
903+
historicalLiquidityPenaltyAmountMultiplierMsat = SCORING_HISTORICAL_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT,
904+
antiProbingPenaltyMsat = SCORING_ANTI_PROBING_PENALTY_MSAT,
905+
consideredImpossiblePenaltyMsat = SCORING_CONSIDERED_IMPOSSIBLE_PENALTY_MSAT,
906+
linearSuccessProbability = false,
907+
probingDiversityPenaltyMsat = SCORING_PROBING_DIVERSITY_PENALTY_MSAT,
908+
)
909+
910+
fun probeRuntimeConfig(
911+
sampleAmountMsat: ULong = PROBE_CONFIG_SAMPLE_AMOUNT_MSAT,
912+
) = ProbeRuntimeConfig(
913+
sampleAmountMsat = sampleAmountMsat,
914+
route = probeRouteParameters(sampleAmountMsat).let {
915+
ProbeRouteConfig(
916+
maxTotalRoutingFeeMsat = it.maxTotalRoutingFeeMsat,
917+
maxTotalCltvExpiryDelta = it.maxTotalCltvExpiryDelta,
918+
maxPathCount = it.maxPathCount.toInt(),
919+
maxChannelSaturationPowerOfHalf = it.maxChannelSaturationPowerOfHalf.toInt(),
920+
)
921+
},
922+
scoring = scorerFeeParameters().let {
923+
ProbeScoringConfig(
924+
basePenaltyMsat = it.basePenaltyMsat,
925+
basePenaltyAmountMultiplierMsat = it.basePenaltyAmountMultiplierMsat,
926+
liquidityPenaltyMultiplierMsat = it.liquidityPenaltyMultiplierMsat,
927+
liquidityPenaltyAmountMultiplierMsat = it.liquidityPenaltyAmountMultiplierMsat,
928+
historicalLiquidityPenaltyMultiplierMsat = it.historicalLiquidityPenaltyMultiplierMsat,
929+
historicalLiquidityPenaltyAmountMultiplierMsat = it.historicalLiquidityPenaltyAmountMultiplierMsat,
930+
antiProbingPenaltyMsat = it.antiProbingPenaltyMsat,
931+
consideredImpossiblePenaltyMsat = it.consideredImpossiblePenaltyMsat,
932+
linearSuccessProbability = it.linearSuccessProbability,
933+
probingDiversityPenaltyMsat = it.probingDiversityPenaltyMsat,
934+
)
935+
},
936+
)
937+
863938
// region utxo selection
864939
suspend fun listSpendableOutputs(): Result<List<SpendableUtxo>> {
865940
val node = this.node ?: throw ServiceError.NodeNotSetup()
@@ -1157,6 +1232,32 @@ data class NetworkGraphInfo(
11571232
val latestRgsSyncTimestamp: ULong?,
11581233
)
11591234

1235+
data class ProbeRuntimeConfig(
1236+
val sampleAmountMsat: ULong,
1237+
val route: ProbeRouteConfig,
1238+
val scoring: ProbeScoringConfig,
1239+
)
1240+
1241+
data class ProbeRouteConfig(
1242+
val maxTotalRoutingFeeMsat: ULong?,
1243+
val maxTotalCltvExpiryDelta: UInt,
1244+
val maxPathCount: Int,
1245+
val maxChannelSaturationPowerOfHalf: Int,
1246+
)
1247+
1248+
data class ProbeScoringConfig(
1249+
val basePenaltyMsat: ULong,
1250+
val basePenaltyAmountMultiplierMsat: ULong,
1251+
val liquidityPenaltyMultiplierMsat: ULong,
1252+
val liquidityPenaltyAmountMultiplierMsat: ULong,
1253+
val historicalLiquidityPenaltyMultiplierMsat: ULong,
1254+
val historicalLiquidityPenaltyAmountMultiplierMsat: ULong,
1255+
val antiProbingPenaltyMsat: ULong,
1256+
val consideredImpossiblePenaltyMsat: ULong,
1257+
val linearSuccessProbability: Boolean,
1258+
val probingDiversityPenaltyMsat: ULong,
1259+
)
1260+
11601261
class TrustedPeerForceCloseException : AppError(
11611262
"Cannot force close channel with trusted peer. Force close is disabled for Blocktank LSP channels."
11621263
)

0 commit comments

Comments
 (0)