Skip to content

Commit 411d48d

Browse files
committed
fix: tune lightning probe routing (#1052)
* feat: show probe route fee * fix: tune scorer fee params * fix: address probe compile errors * fix: use scorer defaults
1 parent 14916f6 commit 411d48d

10 files changed

Lines changed: 121 additions & 28 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
*.iml
22
.gradle
33
.idea
4+
.vscode
45
!.idea/codeStyles/
56
.kotlin
67
.DS_Store

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ private sealed interface DevCommand {
149149
val timeout = args.timeoutSeconds.coerceAtLeast(1).seconds
150150

151151
Logger.info(
152-
"Sending keysend probe for target '${args.targetName ?: "unknown"}' nodeId='${args.nodeId}' amountSats='$amountSats'",
152+
"Sending keysend probe for target '${args.targetName ?: "unknown"}' " +
153+
"nodeId='${args.nodeId}' amountSats='$amountSats'",
153154
context = TAG,
154155
)
155156

@@ -215,7 +216,7 @@ private sealed interface DevResult {
215216
val message: String? = null,
216217
val paymentId: String? = null,
217218
val paymentHash: String? = null,
218-
val shortChannelId: ULong? = null,
219+
val shortChannelId: String? = null,
219220
val paymentIds: List<String> = emptyList(),
220221
) : DevResult {
221222
companion object {

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,13 @@ class LightningRepo @Inject constructor(
559559

560560
private suspend fun recordProbeOutcome(event: Event) {
561561
val outcome = when (event) {
562-
is Event.ProbeSuccessful -> ProbeOutcome.Success(event.paymentId, event.paymentHash)
563-
is Event.ProbeFailed -> ProbeOutcome.Failure(event.paymentId, event.paymentHash, event.shortChannelId)
562+
is Event.ProbeSuccessful -> ProbeOutcome.Success(event.paymentId, event.paymentHash, event.routeFeeMsat)
563+
is Event.ProbeFailed -> ProbeOutcome.Failure(
564+
event.paymentId,
565+
event.paymentHash,
566+
event.shortChannelId?.toString(),
567+
event.routeFeeMsat,
568+
)
564569
else -> return
565570
}
566571

@@ -1783,11 +1788,13 @@ sealed interface ProbeOutcome {
17831788
data class Success(
17841789
override val paymentId: PaymentId,
17851790
override val paymentHash: PaymentHash,
1791+
val routeFeeMsat: ULong?,
17861792
) : ProbeOutcome
17871793

17881794
data class Failure(
17891795
override val paymentId: PaymentId,
17901796
override val paymentHash: PaymentHash,
1791-
val shortChannelId: ULong?,
1797+
val shortChannelId: String?,
1798+
val routeFeeMsat: ULong?,
17921799
) : ProbeOutcome
17931800
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ 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.ScoringFeeParameters
4142
import org.lightningdevkit.ldknode.SpendableUtxo
4243
import org.lightningdevkit.ldknode.Txid
4344
import org.lightningdevkit.ldknode.defaultConfig
@@ -89,6 +90,25 @@ class LightningService @Inject constructor(
8990
companion object {
9091
private const val TAG = "LightningService"
9192
private const val NODE_ID_PREVIEW_LEN = 20
93+
private const val SCORING_BASE_PENALTY_MSAT = 50_000uL
94+
private const val SCORING_LIQUIDITY_PENALTY_MULTIPLIER_MSAT = 10_000uL
95+
private const val SCORING_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT = 10_000uL
96+
private const val SCORING_HISTORICAL_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT = 20_000uL
97+
private const val SCORING_CONSIDERED_IMPOSSIBLE_PENALTY_MSAT = 1_000_000_000_000uL
98+
private const val SCORING_PROBING_DIVERSITY_PENALTY_MSAT = 60_000uL
99+
100+
private val DEFAULT_SCORING_FEE_PARAMETERS = ScoringFeeParameters(
101+
basePenaltyMsat = 1_024uL,
102+
basePenaltyAmountMultiplierMsat = 131_072uL,
103+
liquidityPenaltyMultiplierMsat = 0uL,
104+
liquidityPenaltyAmountMultiplierMsat = 0uL,
105+
historicalLiquidityPenaltyMultiplierMsat = 10_000uL,
106+
historicalLiquidityPenaltyAmountMultiplierMsat = 1_250uL,
107+
antiProbingPenaltyMsat = 250uL,
108+
consideredImpossiblePenaltyMsat = 100_000_000_000uL,
109+
linearSuccessProbability = false,
110+
probingDiversityPenaltyMsat = 0uL,
111+
)
92112
}
93113

94114
@Volatile
@@ -168,6 +188,7 @@ class LightningService @Inject constructor(
168188
configureChainSource(customServerUrl)
169189
configureGossipSource(customRgsServerUrl)
170190
configureScorerSource()
191+
setScoringFeeParams(scorerFeeParameters())
171192
setAddressType(selectedType)
172193
setAddressTypesToMonitor(monitoredTypes)
173194

@@ -860,6 +881,16 @@ class LightningService @Inject constructor(
860881
}
861882
// endregion
862883

884+
private fun scorerFeeParameters(): ScoringFeeParameters = DEFAULT_SCORING_FEE_PARAMETERS.copy(
885+
basePenaltyMsat = SCORING_BASE_PENALTY_MSAT,
886+
liquidityPenaltyMultiplierMsat = SCORING_LIQUIDITY_PENALTY_MULTIPLIER_MSAT,
887+
liquidityPenaltyAmountMultiplierMsat = SCORING_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT,
888+
historicalLiquidityPenaltyAmountMultiplierMsat =
889+
SCORING_HISTORICAL_LIQUIDITY_PENALTY_AMOUNT_MULTIPLIER_MSAT,
890+
consideredImpossiblePenaltyMsat = SCORING_CONSIDERED_IMPOSSIBLE_PENALTY_MSAT,
891+
probingDiversityPenaltyMsat = SCORING_PROBING_DIVERSITY_PENALTY_MSAT,
892+
)
893+
863894
// region utxo selection
864895
suspend fun listSpendableOutputs(): Result<List<SpendableUtxo>> {
865896
val node = this.node ?: throw ServiceError.NodeNotSetup()

app/src/main/java/to/bitkit/ui/screens/settings/ProbingToolScreen.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,11 @@ private fun ProbingToolContent(
186186
iconRes = R.drawable.ic_clock,
187187
value = "${result.durationMs} ms",
188188
)
189-
result.estimatedFeeSats?.let { fee ->
189+
result.routeFeeMsat?.let { fee ->
190190
SettingsTextButtonRow(
191-
title = "Estimated Fee",
191+
title = "Route Fee",
192192
iconRes = R.drawable.ic_coins,
193-
value = "$fee sats",
193+
value = "$fee msat",
194194
)
195195
}
196196
result.errorMessage?.let { error ->
@@ -216,7 +216,7 @@ private fun Preview() {
216216
probeResult = ProbeResult(
217217
success = true,
218218
durationMs = 342,
219-
estimatedFeeSats = 5uL,
219+
routeFeeMsat = 5_000uL,
220220
),
221221
)
222222
)

app/src/main/java/to/bitkit/viewmodels/ProbingToolViewModel.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ class ProbingToolViewModel @Inject constructor(
161161
dispatch
162162
.onSuccess { probe ->
163163
lightningRepo.waitForProbeOutcome(probe.paymentIds)
164-
.onSuccess { handleProbeOutcome(startTime, it, bolt11, amountSats) }
164+
.onSuccess { handleProbeOutcome(startTime, it) }
165165
.onFailure { handleProbeFailure(startTime, it) }
166166
}
167167
.onFailure { handleProbeFailure(startTime, it) }
@@ -257,8 +257,6 @@ class ProbingToolViewModel @Inject constructor(
257257
private suspend fun handleProbeOutcome(
258258
startTime: Long,
259259
outcome: ProbeOutcome,
260-
invoice: String?,
261-
amountSats: ULong?,
262260
) {
263261
val durationMs = Clock.System.nowMs() - startTime
264262
when (outcome) {
@@ -268,13 +266,12 @@ class ProbingToolViewModel @Inject constructor(
268266
context = TAG,
269267
)
270268

271-
val estimatedFee = invoice?.let { getEstimatedFee(it, amountSats) }
272269
_uiState.update {
273270
it.copy(
274271
probeResult = ProbeResult(
275272
success = true,
276273
durationMs = durationMs,
277-
estimatedFeeSats = estimatedFee,
274+
routeFeeMsat = outcome.routeFeeMsat,
278275
)
279276
)
280277
}
@@ -294,6 +291,7 @@ class ProbingToolViewModel @Inject constructor(
294291
probeResult = ProbeResult(
295292
success = false,
296293
durationMs = durationMs,
294+
routeFeeMsat = outcome.routeFeeMsat,
297295
errorMessage = message,
298296
)
299297
)
@@ -379,6 +377,6 @@ data class ProbingToolUiState(
379377
data class ProbeResult(
380378
val success: Boolean,
381379
val durationMs: Long,
382-
val estimatedFeeSats: ULong? = null,
380+
val routeFeeMsat: ULong? = null,
383381
val errorMessage: String? = null,
384382
)

app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,67 +1323,113 @@ class LightningRepoTest : BaseUnitTest() {
13231323
val onEvent = startNodeAndCaptureEvents()
13241324

13251325
val result = async { sut.waitForProbeOutcome(setOf(probePaymentA)) }
1326-
onEvent(Event.ProbeSuccessful(paymentId = probePaymentA, paymentHash = probeHashA))
1326+
onEvent(Event.ProbeSuccessful(paymentId = probePaymentA, paymentHash = probeHashA, routeFeeMsat = 123uL))
13271327

13281328
val outcome = result.await().getOrThrow()
13291329
assertIs<ProbeOutcome.Success>(outcome)
13301330
assertEquals(probePaymentA, outcome.paymentId)
13311331
assertEquals(probeHashA, outcome.paymentHash)
1332+
assertEquals(123uL, outcome.routeFeeMsat)
13321333
}
13331334

13341335
@Test
13351336
fun `waitForProbeOutcome returns cached success when event arrives before wait`() = test {
13361337
val onEvent = startNodeAndCaptureEvents()
1337-
onEvent(Event.ProbeSuccessful(paymentId = probePaymentA, paymentHash = probeHashA))
1338+
onEvent(Event.ProbeSuccessful(paymentId = probePaymentA, paymentHash = probeHashA, routeFeeMsat = 123uL))
13381339

13391340
val outcome = sut.waitForProbeOutcome(setOf(probePaymentA)).getOrThrow()
13401341

13411342
assertIs<ProbeOutcome.Success>(outcome)
13421343
assertEquals(probePaymentA, outcome.paymentId)
13431344
assertEquals(probeHashA, outcome.paymentHash)
1345+
assertEquals(123uL, outcome.routeFeeMsat)
13441346
}
13451347

13461348
@Test
13471349
fun `waitForProbeOutcome returns last failure only after all tracked probes fail`() = test {
13481350
val onEvent = startNodeAndCaptureEvents()
13491351
val result = async { sut.waitForProbeOutcome(setOf(probePaymentA, probePaymentB)) }
13501352

1351-
onEvent(Event.ProbeFailed(paymentId = probePaymentA, paymentHash = probeHashA, shortChannelId = 1uL))
1352-
onEvent(Event.ProbeFailed(paymentId = probePaymentB, paymentHash = probeHashB, shortChannelId = 2uL))
1353+
onEvent(
1354+
Event.ProbeFailed(
1355+
paymentId = probePaymentA,
1356+
paymentHash = probeHashA,
1357+
shortChannelId = 1uL,
1358+
routeFeeMsat = 123uL,
1359+
)
1360+
)
1361+
onEvent(
1362+
Event.ProbeFailed(
1363+
paymentId = probePaymentB,
1364+
paymentHash = probeHashB,
1365+
shortChannelId = 990_718_250_873_192_449uL,
1366+
routeFeeMsat = 456uL,
1367+
)
1368+
)
13531369

13541370
val outcome = result.await().getOrThrow()
13551371
assertIs<ProbeOutcome.Failure>(outcome)
13561372
assertEquals(probePaymentB, outcome.paymentId)
13571373
assertEquals(probeHashB, outcome.paymentHash)
1358-
assertEquals(2uL, outcome.shortChannelId)
1374+
assertEquals("990718250873192449", outcome.shortChannelId)
1375+
assertEquals(456uL, outcome.routeFeeMsat)
13591376
}
13601377

13611378
@Test
13621379
fun `waitForProbeOutcome returns first success even when another path already failed`() = test {
13631380
val onEvent = startNodeAndCaptureEvents()
13641381
val result = async { sut.waitForProbeOutcome(setOf(probePaymentA, probePaymentB)) }
13651382

1366-
onEvent(Event.ProbeFailed(paymentId = probePaymentA, paymentHash = probeHashA, shortChannelId = 1uL))
1367-
onEvent(Event.ProbeSuccessful(paymentId = probePaymentB, paymentHash = probeHashB))
1383+
onEvent(
1384+
Event.ProbeFailed(
1385+
paymentId = probePaymentA,
1386+
paymentHash = probeHashA,
1387+
shortChannelId = 1uL,
1388+
routeFeeMsat = 123uL,
1389+
)
1390+
)
1391+
onEvent(
1392+
Event.ProbeSuccessful(
1393+
paymentId = probePaymentB,
1394+
paymentHash = probeHashB,
1395+
routeFeeMsat = 456uL,
1396+
)
1397+
)
13681398

13691399
val outcome = result.await().getOrThrow()
13701400
assertIs<ProbeOutcome.Success>(outcome)
13711401
assertEquals(probePaymentB, outcome.paymentId)
13721402
assertEquals(probeHashB, outcome.paymentHash)
1403+
assertEquals(456uL, outcome.routeFeeMsat)
13731404
}
13741405

13751406
@Test
13761407
fun `waitForProbeOutcome does not hang on partial cached failures`() = test {
13771408
val onEvent = startNodeAndCaptureEvents()
1378-
onEvent(Event.ProbeFailed(paymentId = probePaymentA, paymentHash = probeHashA, shortChannelId = 1uL))
1409+
onEvent(
1410+
Event.ProbeFailed(
1411+
paymentId = probePaymentA,
1412+
paymentHash = probeHashA,
1413+
shortChannelId = 1uL,
1414+
routeFeeMsat = 123uL,
1415+
)
1416+
)
13791417

13801418
val result = async { sut.waitForProbeOutcome(setOf(probePaymentA, probePaymentB)) }
1381-
onEvent(Event.ProbeFailed(paymentId = probePaymentB, paymentHash = probeHashB, shortChannelId = 2uL))
1419+
onEvent(
1420+
Event.ProbeFailed(
1421+
paymentId = probePaymentB,
1422+
paymentHash = probeHashB,
1423+
shortChannelId = 2uL,
1424+
routeFeeMsat = 456uL,
1425+
)
1426+
)
13821427

13831428
val outcome = result.await().getOrThrow()
13841429
assertIs<ProbeOutcome.Failure>(outcome)
13851430
assertEquals(probePaymentB, outcome.paymentId)
1386-
assertEquals(2uL, outcome.shortChannelId)
1431+
assertEquals("2", outcome.shortChannelId)
1432+
assertEquals(456uL, outcome.routeFeeMsat)
13871433
}
13881434

13891435
@Test
@@ -1400,7 +1446,7 @@ class LightningRepoTest : BaseUnitTest() {
14001446
fun `stop clears probe cache`() = test {
14011447
val onEvent = startNodeAndCaptureEvents()
14021448
whenever(lightningService.stop()).thenReturn(Unit)
1403-
onEvent(Event.ProbeSuccessful(paymentId = probePaymentA, paymentHash = probeHashA))
1449+
onEvent(Event.ProbeSuccessful(paymentId = probePaymentA, paymentHash = probeHashA, routeFeeMsat = null))
14041450

14051451
sut.stop()
14061452
val result = sut.waitForProbeOutcome(setOf(probePaymentA), timeout = 1.seconds)

app/src/test/java/to/bitkit/viewmodels/ProbingToolViewModelTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,15 @@ class ProbingToolViewModelTest : BaseUnitTest() {
4747
whenever(lightningRepo.sendProbeForNode(nodeId, 42uL))
4848
.thenReturn(Result.success(ProbeDispatch(paymentIds = setOf(paymentId))))
4949
whenever(lightningRepo.waitForProbeOutcome(setOf(paymentId)))
50-
.thenReturn(Result.success(ProbeOutcome.Success(paymentId = paymentId, paymentHash = paymentHash)))
50+
.thenReturn(
51+
Result.success(
52+
ProbeOutcome.Success(
53+
paymentId = paymentId,
54+
paymentHash = paymentHash,
55+
routeFeeMsat = null,
56+
)
57+
)
58+
)
5159

5260
sut.updateInvoice(nodeUri)
5361
sut.updateAmountSats("42")

changelog.d/next/1052.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved Lightning payment route selection reliability.

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
6464
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
6565
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
6666
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
67-
ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.46" }
67+
ldk-node-android = { module = "com.synonym:ldk-node-android", version = "0.7.0-rc.52" }
6868
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
6969
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
7070
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }

0 commit comments

Comments
 (0)