Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't send splice_locked before tx_signatures #528

Merged
merged 1 commit into from
Sep 13, 2023
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
29 changes: 15 additions & 14 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,26 +155,13 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
// normal case, our data is up-to-date
val actions = ArrayList<ChannelAction>()

// re-send channel_ready or splice_locked
// re-send channel_ready if necessary
if (state.commitments.latest.fundingTxIndex == 0L && cmd.message.nextLocalCommitmentNumber == 1L && state.commitments.localCommitIndex == 0L) {
// If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT
logger.debug { "re-sending channel_ready" }
val nextPerCommitmentPoint = channelKeys().commitmentPoint(1)
val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint)
actions.add(ChannelAction.Message.Send(channelReady))
} else {
// NB: there is a key difference between channel_ready and splice_locked:
// - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
// - splice_locked: the commitment index can be updated as long as it is compatible with all splices, so
// we must keep sending our most recent splice_locked at each reconnection
state.commitments.active
.filter { it.fundingTxIndex > 0L } // only consider splice txs
.firstOrNull { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
?.let {
logger.debug { "re-sending splice_locked for fundingTxId=${it.fundingTxId}" }
val spliceLocked = SpliceLocked(channelId, it.fundingTxId.reversed())
actions.add(ChannelAction.Message.Send(spliceLocked))
}
}

// resume splice signing session if any
Expand Down Expand Up @@ -216,6 +203,20 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent:
state.spliceStatus
}

// Re-send splice_locked (must come *after* potentially retransmitting tx_signatures).
// NB: there is a key difference between channel_ready and splice_locked:
// - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
// - splice_locked: the commitment index can be updated as long as it is compatible with all splices, so
// we must keep sending our most recent splice_locked at each reconnection
state.commitments.active
.filter { it.fundingTxIndex > 0L } // only consider splice txs
.firstOrNull { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
?.let {
logger.debug { "re-sending splice_locked for fundingTxId=${it.fundingTxId}" }
val spliceLocked = SpliceLocked(channelId, it.fundingTxId.reversed())
actions.add(ChannelAction.Message.Send(spliceLocked))
}

try {
val (commitments1, sendQueue1) = handleSync(cmd.message, state)
actions.addAll(sendQueue1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,67 @@ class SpliceTestsCommon : LightningTestSuite() {
actionsBob6.has<ChannelAction.Storage.StoreState>()
}

@Test
fun `disconnect -- tx_signatures sent by bob -- zero-conf`() {
val (alice, bob) = reachNormalWithConfirmedFundingTx(zeroConf = true)
val (alice1, commitSigAlice1, bob1, _) = spliceOutWithoutSigs(alice, bob, 20_000.sat)
val (bob2, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(commitSigAlice1))
assertIs<LNChannel<Normal>>(bob2)
val spliceTxId = actionsBob2.hasOutgoingMessage<TxSignatures>().txId
actionsBob2.hasOutgoingMessage<SpliceLocked>()
assertEquals(bob2.state.spliceStatus, SpliceStatus.None)

val (alice2, bob3, channelReestablishAlice) = disconnect(alice1, bob2)
assertEquals(channelReestablishAlice.nextFundingTxId, spliceTxId)
val (bob4, actionsBob4) = bob3.process(ChannelCommand.MessageReceived(channelReestablishAlice))
assertEquals(actionsBob4.size, 4)
val channelReestablishBob = actionsBob4.findOutgoingMessage<ChannelReestablish>()
val commitSigBob2 = actionsBob4.findOutgoingMessage<CommitSig>()
val txSigsBob = actionsBob4.findOutgoingMessage<TxSignatures>()
// splice_locked must always be sent *after* tx_signatures
assertIs<SpliceLocked>(actionsBob4.filterIsInstance<ChannelAction.Message.Send>().last().message)
val spliceLockedBob = actionsBob4.findOutgoingMessage<SpliceLocked>()
assertEquals(channelReestablishBob.nextFundingTxId, spliceTxId)
val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(channelReestablishBob))
assertEquals(actionsAlice3.size, 1)
val commitSigAlice2 = actionsAlice3.findOutgoingMessage<CommitSig>()

val (alice4, actionsAlice4) = alice3.process(ChannelCommand.MessageReceived(commitSigBob2))
assertTrue(actionsAlice4.isEmpty())
val (alice5, actionsAlice5) = alice4.process(ChannelCommand.MessageReceived(txSigsBob))
assertIs<LNChannel<Normal>>(alice5)
assertEquals(alice5.state.commitments.active.size, 2)
assertEquals(actionsAlice5.size, 6)
assertEquals(actionsAlice5.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId)
actionsAlice5.hasWatchConfirmed(spliceTxId)
actionsAlice5.has<ChannelAction.Storage.StoreState>()
actionsAlice5.has<ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceOut>()
val txSigsAlice = actionsAlice5.findOutgoingMessage<TxSignatures>()
assertIs<SpliceLocked>(actionsAlice5.filterIsInstance<ChannelAction.Message.Send>().last().message)
val spliceLockedAlice = actionsAlice5.findOutgoingMessage<SpliceLocked>()
val (alice6, actionsAlice6) = alice5.process(ChannelCommand.MessageReceived(spliceLockedBob))
assertIs<LNChannel<Normal>>(alice6)
assertEquals(alice6.state.commitments.active.size, 1)
assertEquals(actionsAlice6.size, 2)
actionsAlice6.find<ChannelAction.Storage.SetLocked>().also { assertEquals(it.txId, spliceTxId) }
actionsAlice6.has<ChannelAction.Storage.StoreState>()

val (bob5, actionsBob5) = bob4.process(ChannelCommand.MessageReceived(commitSigAlice2))
assertTrue(actionsBob5.isEmpty())
val (bob6, actionsBob6) = bob5.process(ChannelCommand.MessageReceived(txSigsAlice))
assertIs<LNChannel<Normal>>(bob6)
assertEquals(bob6.state.commitments.active.size, 2)
assertEquals(actionsBob6.size, 2)
assertEquals(actionsBob6.hasPublishTx(ChannelAction.Blockchain.PublishTx.Type.FundingTx).txid, spliceTxId)
actionsBob6.has<ChannelAction.Storage.StoreState>()
val (bob7, actionsBob7) = bob6.process(ChannelCommand.MessageReceived(spliceLockedAlice))
assertIs<LNChannel<Normal>>(bob7)
assertEquals(bob7.state.commitments.active.size, 1)
assertEquals(actionsBob7.size, 2)
actionsBob7.find<ChannelAction.Storage.SetLocked>().also { assertEquals(it.txId, spliceTxId) }
actionsBob7.has<ChannelAction.Storage.StoreState>()
}

@Test
fun `disconnect -- tx_signatures received by alice`() {
val (alice, bob) = reachNormalWithConfirmedFundingTx()
Expand Down
Loading