Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
- ALWAYS check existing code patterns before implementing new features
- USE existing extensions and utilities rather than creating new ones
- ALWAYS use or create `Context` extension properties in `ext/Context.kt` instead of raw `context.getSystemService()` casts
- NEVER use `System.currentTimeMillis()`, use time helpers from `ext/DateTime.kt` instead (e.g. `nowMillis()`, `Clock.nowMs()`) — they accept a `Clock` and are unit-testable
- ALWAYS apply the YAGNI (You Ain't Gonna Need It) principle for new code
- ALWAYS reuse existing constants
- ALWAYS ensure a method exist before calling it
Expand Down
20 changes: 20 additions & 0 deletions app/src/debug/java/to/bitkit/dev/DevToolsProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ private sealed interface DevCommand {
ProbeInvoice.METHOD -> ProbeInvoice.parse(arg)
ProbeNode.METHOD -> ProbeNode.parse(arg)
ProbeReadiness.METHOD -> ProbeReadiness
ResetScores.METHOD -> ResetScores
else -> null
}
}
Expand Down Expand Up @@ -172,6 +173,21 @@ private sealed interface DevCommand {
override suspend fun execute(deps: DevToolsProvider.Dependencies): DevResult =
DevResult.ProbeReadiness.from(deps.lightningRepo().probeReadiness())
}

data object ResetScores : DevCommand {
const val METHOD = "resetScores"

override suspend fun execute(deps: DevToolsProvider.Dependencies): DevResult {
Logger.info("Resetting pathfinding scores via devtools", context = TAG)
return deps.lightningRepo().resetPathfindingScores().fold(
onSuccess = { DevResult.Ack(timestamp = it) },
onFailure = {
Logger.error("Failed to reset pathfinding scores", it, context = TAG)
DevResult.Error(it.message)
},
)
}
}
}

@Serializable
Expand All @@ -183,6 +199,8 @@ private sealed interface DevResult {

@Serializable data class Invoice(val bolt11: String) : DevResult

@Serializable data class Ack(val success: Boolean = true, val timestamp: Long? = null) : DevResult

@Serializable
data class ProbeSuccess(
val success: Boolean = true,
Expand Down Expand Up @@ -224,6 +242,7 @@ private sealed interface DevResult {
val graphNodeCount: Int? = null,
val graphChannelCount: Int? = null,
val latestRgsSyncTimestamp: ULong? = null,
val latestPathfindingScoresSyncTimestamp: ULong? = null,
) : DevResult {
companion object {
fun from(readiness: NodeProbeReadiness) = ProbeReadiness(
Expand All @@ -241,6 +260,7 @@ private sealed interface DevResult {
graphNodeCount = readiness.graphNodeCount,
graphChannelCount = readiness.graphChannelCount,
latestRgsSyncTimestamp = readiness.latestRgsSyncTimestamp,
latestPathfindingScoresSyncTimestamp = readiness.latestPathfindingScoresSyncTimestamp,
)
}
}
Expand Down
45 changes: 45 additions & 0 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import to.bitkit.di.BgDispatcher
import to.bitkit.env.Defaults
import to.bitkit.env.Env
import to.bitkit.ext.getSatsPerVByteFor
import to.bitkit.ext.nowMillis
import to.bitkit.ext.nowTimestamp
import to.bitkit.ext.toPeerDetailsList
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
Expand Down Expand Up @@ -99,6 +100,7 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime

@Singleton
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
Expand Down Expand Up @@ -1620,9 +1622,49 @@ class LightningRepo @Inject constructor(
graphNodeCount = graph?.nodeCount,
graphChannelCount = graph?.channelCount,
latestRgsSyncTimestamp = graph?.latestRgsSyncTimestamp,
latestPathfindingScoresSyncTimestamp = state.nodeStatus?.latestPathfindingScoresSyncTimestamp,
syncHealthy = state.isSyncHealthy,
)
}

/**
* Returns the device epoch seconds captured after the VSS deletes and before the node restart,
* so callers can require any scores sync timestamp to be strictly newer to prove a post-reset download.
*/
@OptIn(ExperimentalTime::class)
suspend fun resetPathfindingScores(walletIndex: Int = 0): Result<Long> = withContext(bgDispatcher) {
Logger.info("Resetting pathfinding scores", context = TAG)

waitForNodeToStop().onFailure { return@withContext Result.failure(it) }
stop().onFailure {
Logger.error("Failed to stop node during pathfinding scores reset", it, context = TAG)
return@withContext Result.failure(it)
}

runCatching {
val lifecycleState = _lightningState.value.nodeLifecycleState
check(lifecycleState == NodeLifecycleState.Stopped) {
"Node lifecycle changed to '$lifecycleState' during pathfinding scores reset"
}
vssBackupClientLdk.setup(walletIndex).getOrThrow()
vssBackupClientLdk.deleteObject(VSS_KEY_SCORER).getOrThrow()
vssBackupClientLdk.deleteObject(VSS_KEY_EXTERNAL_SCORES_CACHE).getOrThrow()
}.onFailure {
Logger.error("Failed to delete pathfinding scores from VSS", it, context = TAG)
start(walletIndex = walletIndex, shouldRetry = false).onFailure { startError ->
Logger.error("Failed to restart node after pathfinding scores reset failure", startError, context = TAG)
}
return@withContext Result.failure(it)
}
Comment thread
piotr-iohk marked this conversation as resolved.

val resetAtSecs = nowMillis() / 1000

start(walletIndex = walletIndex, shouldRetry = false)
.map { resetAtSecs }
.onSuccess {
Logger.info("Pathfinding scores reset at '$resetAtSecs'", context = TAG)
}
}
// endregion

suspend fun restartNode(): Result<Unit> = withContext(bgDispatcher) {
Expand All @@ -1642,6 +1684,8 @@ class LightningRepo @Inject constructor(
companion object {
private const val TAG = "LightningRepo"
private const val LENGTH_CHANNEL_ID_PREVIEW = 10
private const val VSS_KEY_SCORER = "scorer"
private const val VSS_KEY_EXTERNAL_SCORES_CACHE = "external_pathfinding_scores_cache"
private const val MS_SYNC_LOOP_DEBOUNCE = 500L
private const val SYNC_RETRY_DELAY_MS = 15_000L
private val CHANNELS_USABLE_TIMEOUT = 15.seconds
Expand Down Expand Up @@ -1702,6 +1746,7 @@ data class ProbeReadiness(
val graphNodeCount: Int?,
val graphChannelCount: Int?,
val latestRgsSyncTimestamp: ULong?,
val latestPathfindingScoresSyncTimestamp: ULong?,
val syncHealthy: Boolean,
) {
val ready: Boolean
Expand Down
Loading