Skip to content

Commit d1c4092

Browse files
committed
fix: add a guard for edge case were the LSP batches two channel opens to the same wallet into one funding transaction while two CJIT invoices are pending, so their entries share a txid and differ only by vout
1 parent 2532043 commit d1c4092

2 files changed

Lines changed: 23 additions & 3 deletions

File tree

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,12 @@ class BlocktankRepo @Inject constructor(
129129
}
130130

131131
suspend fun getCjitEntry(channel: ChannelDetails): IcJitEntry? = withContext(bgDispatcher) {
132-
val fundingTxId = channel.fundingTxo?.txid ?: return@withContext null
132+
val fundingTxo = channel.fundingTxo ?: return@withContext null
133133

134-
fun List<IcJitEntry>.matching(): IcJitEntry? =
135-
firstOrNull { it.channel?.fundingTx?.id == fundingTxId }
134+
fun List<IcJitEntry>.matching(): IcJitEntry? = firstOrNull { entry ->
135+
val fundingTx = entry.channel?.fundingTx ?: return@firstOrNull false
136+
fundingTx.id == fundingTxo.txid && fundingTx.vout == fundingTxo.vout.toULong()
137+
}
136138

137139
val cached = _blocktankState.value.cjitEntries
138140
cached.matching()?.let { return@withContext it }

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,24 @@ class BlocktankRepoTest : BaseUnitTest() {
263263
assertNull(sut.getCjitEntry(channelDetails))
264264
}
265265

266+
@Test
267+
fun `getCjitEntry does not match a CJIT entry with the same funding txid but a different vout`() = test {
268+
sut = createSut()
269+
seedCjitEntries(pendingCjitEntry())
270+
// A batched funding tx can hold several channel outputs sharing one txid; only vout distinguishes them.
271+
val sharedTxId = "shared-funding-tx"
272+
val channel = mock<IBtChannel>()
273+
whenever(channel.fundingTx).thenReturn(FundingTx(id = sharedTxId, vout = 0u))
274+
val entry = mock<IcJitEntry>()
275+
whenever(entry.channel).thenReturn(channel)
276+
whenever(coreService.blocktank.cjitEntries(refresh = true)).thenReturn(listOf(entry))
277+
278+
val channelDetails = mock<ChannelDetails>()
279+
whenever(channelDetails.fundingTxo).thenReturn(OutPoint(txid = sharedTxId, vout = 1u))
280+
281+
assertNull(sut.getCjitEntry(channelDetails))
282+
}
283+
266284
@Test
267285
fun `getCjitEntry does not refresh when no cached CJIT entry is awaiting a channel`() = test {
268286
sut = createSut()

0 commit comments

Comments
 (0)