Skip to content

Commit

Permalink
Reestablish partially signed splice with current `remote_commitment_n…
Browse files Browse the repository at this point in the history
…umber`

As pointed out in lightning/bolts#1214, when
reconnecting a partially signed `interactive-tx` session, we should
set `next_commitment_number` to the current commitment number if we
haven't received our peer's `commit_sig`, which tells them they need
to retransmit it.

That's not what we're currently doing: we're currently setting this
value to the next commitment number, regardless of whether or not
we have received our peer's `commit_sig`. And we always retransmit
our `commit_sig` if our peer is setting `next_funding_txid`, even
if they have already received it.

More importantly, if our peer behaves correctly and sends us the
current commitment number, we will think that they're late and will
halt, waiting for them to send `error`. This commit fixes that by
allowing our peers to use the current commitment number when they
set `next_funding_txid`.

Note that this doesn't yet make us spec-compliant, but in order to
guarantee backwards-compatibility, we must first deploy that change
before we can start removing spurious `commit_sig` retransmissions.
  • Loading branch information
t-bast committed Dec 12, 2024
1 parent 61af10a commit 3524e70
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,10 @@ object Helpers {
case Right(_) if remoteChannelReestablish.nextLocalCommitmentNumber == (commitments.remoteCommitIndex + 1) =>
// they have acknowledged the last commit_sig we sent
SyncResult.Success(retransmit = retransmitRevocation_opt.toSeq)
case Right(_) if remoteChannelReestablish.nextLocalCommitmentNumber == commitments.remoteCommitIndex && remoteChannelReestablish.nextFundingTxId_opt.nonEmpty =>
// they haven't received the commit_sig we sent as part of signing a splice transaction
// we will retransmit it before exchanging tx_signatures
SyncResult.Success(retransmit = retransmitRevocation_opt.toSeq)
case Right(_) if remoteChannelReestablish.nextLocalCommitmentNumber < (commitments.remoteCommitIndex + 1) =>
// they are behind
SyncResult.RemoteLate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,23 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
reconnect(f, fundingTxId)
}

test("recv INPUT_DISCONNECTED (commit_sig not received, next_commitment_number = 0)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._

val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId
alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig
bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED)
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED)

alice ! INPUT_DISCONNECTED
awaitCond(alice.stateName == OFFLINE)
bob ! INPUT_DISCONNECTED
awaitCond(bob.stateName == OFFLINE)

reconnect(f, fundingTxId, aliceCommitmentNumber = 0, bobCommitmentNumber = 0)
}

test("recv INPUT_DISCONNECTED (commit_sig partially received)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._

Expand All @@ -397,6 +414,25 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
reconnect(f, fundingTxId)
}

test("recv INPUT_DISCONNECTED (commit_sig partially received, next_commitment_number = 0)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._

val fundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].signingSession.fundingTx.txId
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig
bob2alice.expectMsgType[TxSignatures] // Alice doesn't receive Bob's tx_signatures
awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED)
awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED)

alice ! INPUT_DISCONNECTED
awaitCond(alice.stateName == OFFLINE)
bob ! INPUT_DISCONNECTED
awaitCond(bob.stateName == OFFLINE)

reconnect(f, fundingTxId, aliceCommitmentNumber = 0)
}

test("recv INPUT_DISCONNECTED (commit_sig received)", Tag(ChannelStateTestsTags.DualFunding)) { f =>
import f._

Expand Down Expand Up @@ -454,7 +490,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
assert(listener.expectMsgType[TransactionPublished].tx.txid == fundingTxId)
}

private def reconnect(f: FixtureParam, fundingTxId: TxId): Unit = {
private def reconnect(f: FixtureParam, fundingTxId: TxId, aliceCommitmentNumber: Long = 1, bobCommitmentNumber: Long = 1): Unit = {
import f._

val listener = TestProbe()
Expand All @@ -467,11 +503,11 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny
val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
assert(channelReestablishAlice.nextFundingTxId_opt.contains(fundingTxId))
assert(channelReestablishAlice.nextLocalCommitmentNumber == 1)
alice2bob.forward(bob)
alice2bob.forward(bob, channelReestablishAlice.copy(nextLocalCommitmentNumber = aliceCommitmentNumber))
val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
assert(channelReestablishBob.nextFundingTxId_opt.contains(fundingTxId))
assert(channelReestablishBob.nextLocalCommitmentNumber == 1)
bob2alice.forward(alice)
bob2alice.forward(alice, channelReestablishBob.copy(nextLocalCommitmentNumber = bobCommitmentNumber))
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
alice2bob.expectMsgType[CommitSig]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1560,17 +1560,17 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
awaitCond(bob.stateName == OFFLINE)
}

private def reconnect(f: FixtureParam): (ChannelReestablish, ChannelReestablish) = {
private def reconnect(f: FixtureParam, sendReestablish: Boolean = true): (ChannelReestablish, ChannelReestablish) = {
import f._

val aliceInit = Init(alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures)
val bobInit = Init(bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.localParams.initFeatures)
alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)
bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)
val channelReestablishAlice = alice2bob.expectMsgType[ChannelReestablish]
alice2bob.forward(bob)
if (sendReestablish) alice2bob.forward(bob)
val channelReestablishBob = bob2alice.expectMsgType[ChannelReestablish]
bob2alice.forward(alice)
if (sendReestablish) bob2alice.forward(alice)
(channelReestablishAlice, channelReestablishBob)
}

Expand Down Expand Up @@ -1638,6 +1638,54 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs)
}

test("disconnect (commit_sig not received, reestablish with previous commitment_number)") { f =>
import f._

val htlcs = setupHtlcs(f)
val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex
val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex

val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey)))
alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig
bob2alice.expectMsgType[CommitSig] // Alice doesn't receive Bob's commit_sig
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs])
val spliceStatus = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs]

disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f, sendReestablish = false)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
alice2bob.forward(bob, channelReestablishAlice.copy(nextLocalCommitmentNumber = aliceCommitIndex))
assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
bob2alice.forward(alice, channelReestablishBob.copy(nextLocalCommitmentNumber = bobCommitIndex))

// Alice and Bob retransmit commit_sig and tx_signatures.
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
bob2alice.expectMsgType[TxSignatures]
bob2alice.forward(alice)
alice2bob.expectMsgType[TxSignatures]
alice2bob.forward(bob)
sender.expectMsgType[RES_SPLICE]

val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get
alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
alice2bob.expectMsgType[SpliceLocked]
alice2bob.forward(bob)
bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
bob2alice.expectMsgType[SpliceLocked]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)

resolveHtlcs(f, htlcs)
}

test("disconnect (commit_sig received by alice)") { f =>
import f._

Expand Down Expand Up @@ -1686,6 +1734,56 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
resolveHtlcs(f, htlcs)
}

test("disconnect (commit_sig received by alice, reestablish with previous commitment_number)") { f =>
import f._

val htlcs = setupHtlcs(f)
val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex
val bobCommitIndex = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex
assert(aliceCommitIndex != bobCommitIndex)

val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey)))
alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs])
val spliceStatus = alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs]

disconnect(f)
val (channelReestablishAlice, channelReestablishBob) = reconnect(f, sendReestablish = false)
assert(channelReestablishAlice.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
assert(channelReestablishAlice.nextLocalCommitmentNumber == aliceCommitIndex + 1)
alice2bob.forward(bob, channelReestablishAlice)
assert(channelReestablishBob.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId))
assert(channelReestablishBob.nextLocalCommitmentNumber == bobCommitIndex + 1)
bob2alice.forward(alice, channelReestablishBob.copy(nextLocalCommitmentNumber = bobCommitIndex))

// Alice and Bob retransmit commit_sig and tx_signatures.
alice2bob.expectMsgType[CommitSig]
alice2bob.forward(bob)
bob2alice.expectMsgType[CommitSig]
bob2alice.forward(alice)
bob2alice.expectMsgType[TxSignatures]
bob2alice.forward(alice)
alice2bob.expectMsgType[TxSignatures]
alice2bob.forward(bob)
sender.expectMsgType[RES_SPLICE]

val spliceTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get
alice2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
bob2blockchain.expectWatchFundingConfirmed(spliceTx.txid)
alice ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
alice2bob.expectMsgType[SpliceLocked]
alice2bob.forward(bob)
bob ! WatchFundingConfirmedTriggered(BlockHeight(42), 0, spliceTx)
bob2alice.expectMsgType[SpliceLocked]
bob2alice.forward(alice)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1)

resolveHtlcs(f, htlcs)
}

test("disconnect (tx_signatures sent by bob)") { f =>
import f._

Expand Down

0 comments on commit 3524e70

Please sign in to comment.