From b3080923fb3062674d48aef14149746cacc2a9da Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Fri, 16 May 2025 10:19:58 +0200 Subject: [PATCH 1/6] Add more splice channel_reestablish tests During splice interop testing I added new channel_reestablish tests to splice_tests.md of the splice spec. Some tests correspond to tests in splice_tests.md. I added the corresponding test name from splice_tests.md and test flow diagram to the matching spec tests. --- .../states/e/NormalSplicesStateSpec.scala | 350 +++++++++++++++++- 1 file changed, 342 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 0967000036..17cd105cd5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -73,7 +73,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private val defaultSpliceOutScriptPubKey = hex"0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): TestProbe = { + private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut], sendTxComplete: Boolean): TestProbe = { val sender = TestProbe() val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt, None) s ! cmd @@ -105,14 +105,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } s2r.expectMsgType[TxAddOutput] s2r.forward(r) - r2s.expectMsgType[TxComplete] - r2s.forward(s) - s2r.expectMsgType[TxComplete] - s2r.forward(r) + if (sendTxComplete) { + r2s.expectMsgType[TxComplete] + r2s.forward(s) + s2r.expectMsgType[TxComplete] + s2r.forward(r) + } sender } - private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): TestProbe = initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt) + private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None, sendTxComplete: Boolean = true): TestProbe = initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt, sendTxComplete) private def initiateRbfWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, feerate: FeeratePerKw, sInputsCount: Int, sOutputsCount: Int, rInputsCount: Int, rOutputsCount: Int): TestProbe = { val sender = TestProbe() @@ -213,7 +215,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik private def exchangeSpliceSigs(f: FixtureParam, sender: TestProbe): Transaction = exchangeSpliceSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, sender) private def initiateSplice(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): Transaction = { - val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt) + val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt, sendTxComplete = true) exchangeSpliceSigs(s, r, s2r, r2s, sender) } @@ -1724,6 +1726,40 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik (channelReestablishAlice, channelReestablishBob) } + test("disconnect (tx_complete not received)") { f => + import f._ + // Disconnection with one side sending commit_sig + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --X | + // |------ commit_sig ---X | + // | | + // | | + // | | + // |<------ tx_abort ------| + // |------- tx_abort ----->| + + val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey)), sendTxComplete = false) + bob2alice.expectMsgType[TxComplete] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxComplete] // Bob doesn't receive Alice's tx_complete + alice2bob.expectMsgType[CommitSig] // Bob doesn't receive Alice's commit_sig + sender.expectMsgType[RES_SPLICE] // TODO: we should exchange tx_signatures before returning RES_SPLICE, see issue #3093 + + disconnect(f) + reconnect(f) + + // Bob and Alice will exchange tx_abort because Bob did not receive Alice's tx_complete before the disconnect. + bob2alice.expectMsgType[TxAbort] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAbort] + bob2alice.forward(bob) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + } + test("disconnect (commit_sig not sent)") { f => import f._ @@ -1786,7 +1822,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(channelReestablishBob2.nextFundingTxId_opt.contains(spliceStatus.signingSession.fundingTx.txId)) assert(channelReestablishBob2.nextLocalCommitmentNumber == bobCommitIndex) - // Alice and Bob retransmit commit_sig and tx_signatures. + // Alice retransmits commit_sig and both retransmit tx_signatures. alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) bob2alice.expectMsgType[TxSignatures] @@ -1812,6 +1848,20 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("disconnect (commit_sig received by alice)") { f => import f._ + // Disconnection with both sides sending commit_sig + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---X | + // |<------ commit_sig ----| + // | | + // | | + // | | + // |------ commit_sig ---->| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| val htlcs = setupHtlcs(f) val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex @@ -1954,6 +2004,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("disconnect (tx_signatures received by alice)") { f => import f._ + // Disconnection with one side sending tx_signatures + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures --X| + // | | + // | | + // | | + // |<---- tx_signatures ---| + // |----- tx_signatures -->| val htlcs = setupHtlcs(f) val aliceCommitIndex = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommitIndex @@ -2412,6 +2477,78 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } + test("disconnect before channel update and tx_signatures are received") { f=> + import f._ + // Disconnection with both sides sending tx_signatures and channel updates + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures --X| + // |--- update_add_htlc --X| + // |------ start_batch ---X| batch_size = 2 + // |------ commit_sig ----X| funding_txid = FundingTx + // |------ commit_sig ----X| funding_txid = SpliceFundingTx + // | | + // | | + // | | + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |<--- revoke_and_ack ---| + // |<----- commit_sig -----| + // |<----- commit_sig -----| + // |---- revoke_and_ack -->| + + val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] // Bob doesn't receive Alice's tx_signatures + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) + alice ! cmd.copy(commit = true) + alice2bob.expectMsgType[UpdateAddHtlc] // Bob doesn't receive Alice's update_add_htlc + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sigs + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Bob will not receive Alice's tx_signatures, update_add_htlc or commit_sigs before disconnecting. + disconnect(f) + reconnect(f) + + // Alice must retransmit her tx_signatures, update_add_htlc and commit_sigs first. + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + bob2alice.expectNoMessage(100 millis) + } + test("disconnect and update channel before receiving final splice_locked") { f => import f._ @@ -2483,6 +2620,203 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } + test("disconnection after exchanging tx_signatures and one side sends commit_sig for channel update") { f => + import f._ + + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch ---X| batch_size = 2 + // |------ commit_sig ----X| funding_txid = FundingTx + // |------ commit_sig ----X| funding_txid = SpliceFundingTx + // | | + // | | + // | | + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |<--- revoke_and_ack ---| + // |<----- commit_sig -----| + // |<----- commit_sig -----| + // |---- revoke_and_ack -->| + + val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchConfirmed(f, fundingTx) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) + alice ! cmd.copy(commit = true) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sig + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Bob will not receive Alice's commit_sigs before disconnecting. + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) + assert(channelReestablishBob.nextLocalCommitmentNumber == 1) + + // Alice must retransmit update_add_htlc and commit_sigs first. + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSigBatch] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + bob2alice.expectMsgType[CommitSigBatch] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + + test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update") { f => + import f._ + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |<--- revoke_and_ack ---| + // |X----- start_batch ----| batch_size = 2 + // |X----- commit_sig -----| funding_txid = FundingTx + // |X----- commit_sig -----| funding_txid = SpliceFundingTx + // | | + // | | next_revocation_number = 1 (alice) and 0 (bob) + // | | + // |<----- start_batch ----| batch_size = 2 + // |<----- commit_sig -----| funding_txid = FundingTx + // |<----- commit_sig -----| funding_txid = SpliceFundingTx + // |---- revoke_and_ack -->| + + val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchConfirmed(f, fundingTx) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) + alice ! cmd.copy(commit = true) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Alice will not receive Bob's commit_sigs before disconnecting. + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.nextRemoteRevocationNumber == 1) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) + assert(channelReestablishBob.nextRemoteRevocationNumber == 0) + + // Bob must retransmit his commit_sigs first. + alice2bob.expectNoMessage(100 millis) + bob2alice.expectMsgType[CommitSigBatch] + bob2alice.forward(alice) + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + + test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => + import f._ + // alice bob + // | ... | + // | | + // |<----- tx_complete ----| + // |------ tx_complete --->| + // |------ commit_sig ---->| + // |<------ commit_sig ----| + // |<---- tx_signatures ---| + // |----- tx_signatures -->| + // |--- update_add_htlc -->| + // |------ start_batch --->| batch_size = 2 + // |------ commit_sig ---->| funding_txid = FundingTx + // |------ commit_sig ---->| funding_txid = SpliceFundingTx + // |X--- revoke_and_ack ---| + // |X----- start_batch ----| batch_size = 2 + // |X----- commit_sig -----| funding_txid = FundingTx + // |X----- commit_sig -----| funding_txid = SpliceFundingTx + // | | + // | | + // | | next_revocation_number = 0 (for both alice and bob) + // |<--- revoke_and_ack ---| + // |<----- commit_sig -----| batch_size = 2, funding_txid = FundingTx + // |<----- commit_sig -----| batch_size = 2, funding_txid = SpliceFundingTx + // |---- revoke_and_ack -->| + + val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + checkWatchConfirmed(f, fundingTx) + + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) + alice ! cmd.copy(commit = true) + alice2bob.expectMsgType[UpdateAddHtlc] + alice2bob.forward(bob) + alice2bob.expectMsgType[CommitSigBatch] + alice2bob.forward(bob) + + bob2alice.expectMsgType[RevokeAndAck] // Alice doesn't receive Bob's revoke_and_ack + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig + assert(batch.batchSize == 2) + } + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + + // Alice will not receive Bob's commit_sigs before disconnecting. + disconnect(f) + val (channelReestablishAlice, channelReestablishBob) = reconnect(f) + assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) + assert(channelReestablishAlice.nextRemoteRevocationNumber == 0) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) + assert(channelReestablishBob.nextRemoteRevocationNumber == 0) + + // Bob must retransmit his commit_sigs first. + alice2bob.expectNoMessage(100 millis) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + test("disconnect before receiving announcement_signatures from one peer", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ From 78f717f6b712d58294c2dcd5ea2e1c28d0d2b3fa Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 2 Jun 2025 15:44:35 +0200 Subject: [PATCH 2/6] swap order of last two new tests --- .../states/e/NormalSplicesStateSpec.scala | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 17cd105cd5..dac5a4c7ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -2685,7 +2685,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectNoMessage(100 millis) } - test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update") { f => + test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => import f._ // alice bob // | ... | @@ -2700,16 +2700,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // |------ start_batch --->| batch_size = 2 // |------ commit_sig ---->| funding_txid = FundingTx // |------ commit_sig ---->| funding_txid = SpliceFundingTx - // |<--- revoke_and_ack ---| + // |X--- revoke_and_ack ---| // |X----- start_batch ----| batch_size = 2 // |X----- commit_sig -----| funding_txid = FundingTx // |X----- commit_sig -----| funding_txid = SpliceFundingTx // | | - // | | next_revocation_number = 1 (alice) and 0 (bob) - // | | - // |<----- start_batch ----| batch_size = 2 - // |<----- commit_sig -----| funding_txid = FundingTx - // |<----- commit_sig -----| funding_txid = SpliceFundingTx + // | | + // | | next_revocation_number = 0 (for both alice and bob) + // |<--- revoke_and_ack ---| + // |<----- commit_sig -----| batch_size = 2, funding_txid = FundingTx + // |<----- commit_sig -----| batch_size = 2, funding_txid = SpliceFundingTx // |---- revoke_and_ack -->| val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) @@ -2720,12 +2720,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! cmd.copy(commit = true) alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => - assert(batch.batchSize == 2) - alice2bob.forward(bob) - } - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.forward(alice) + alice2bob.expectMsgType[CommitSigBatch] + alice2bob.forward(bob) + + bob2alice.expectMsgType[RevokeAndAck] // Alice doesn't receive Bob's revoke_and_ack inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig assert(batch.batchSize == 2) } @@ -2736,21 +2734,25 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik disconnect(f) val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) - assert(channelReestablishAlice.nextRemoteRevocationNumber == 1) + assert(channelReestablishAlice.nextRemoteRevocationNumber == 0) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) assert(channelReestablishBob.nextRemoteRevocationNumber == 0) // Bob must retransmit his commit_sigs first. alice2bob.expectNoMessage(100 millis) - bob2alice.expectMsgType[CommitSigBatch] + bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) alice2bob.expectNoMessage(100 millis) bob2alice.expectNoMessage(100 millis) } - test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update; revoke_and_ack not received") { f => + test("Disconnection after exchanging tx_signatures and both sides send commit_sig for channel update") { f => import f._ // alice bob // | ... | @@ -2765,16 +2767,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // |------ start_batch --->| batch_size = 2 // |------ commit_sig ---->| funding_txid = FundingTx // |------ commit_sig ---->| funding_txid = SpliceFundingTx - // |X--- revoke_and_ack ---| + // |<--- revoke_and_ack ---| // |X----- start_batch ----| batch_size = 2 // |X----- commit_sig -----| funding_txid = FundingTx // |X----- commit_sig -----| funding_txid = SpliceFundingTx // | | - // | | - // | | next_revocation_number = 0 (for both alice and bob) - // |<--- revoke_and_ack ---| - // |<----- commit_sig -----| batch_size = 2, funding_txid = FundingTx - // |<----- commit_sig -----| batch_size = 2, funding_txid = SpliceFundingTx + // | | next_revocation_number = 1 (alice) and 0 (bob) + // | | + // |<----- start_batch ----| batch_size = 2 + // |<----- commit_sig -----| funding_txid = FundingTx + // |<----- commit_sig -----| funding_txid = SpliceFundingTx // |---- revoke_and_ack -->| val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) @@ -2785,10 +2787,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! cmd.copy(commit = true) alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - alice2bob.expectMsgType[CommitSigBatch] - alice2bob.forward(bob) - - bob2alice.expectMsgType[RevokeAndAck] // Alice doesn't receive Bob's revoke_and_ack + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig assert(batch.batchSize == 2) } @@ -2799,18 +2803,14 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik disconnect(f) val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) - assert(channelReestablishAlice.nextRemoteRevocationNumber == 0) + assert(channelReestablishAlice.nextRemoteRevocationNumber == 1) assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) assert(channelReestablishBob.nextRemoteRevocationNumber == 0) // Bob must retransmit his commit_sigs first. alice2bob.expectNoMessage(100 millis) - bob2alice.expectMsgType[RevokeAndAck] + bob2alice.expectMsgType[CommitSigBatch] bob2alice.forward(alice) - inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => - assert(batch.batchSize == 2) - bob2alice.forward(alice) - } alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) alice2bob.expectNoMessage(100 millis) From 827b57b122a32a40d5ec337a6a27f49f47ec9df5 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 2 Jun 2025 16:39:41 +0200 Subject: [PATCH 3/6] correct reference to splices-test.md Should reference 'Disconnection with both sides sending tx_signatures' test, not `one side sending` version. --- .../acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index dac5a4c7ee..0e1a392d61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -2004,7 +2004,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik test("disconnect (tx_signatures received by alice)") { f => import f._ - // Disconnection with one side sending tx_signatures + // Disconnection with both sides sending tx_signatures // alice bob // | ... | // | | @@ -2017,7 +2017,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // | | // | | // | | - // |<---- tx_signatures ---| // |----- tx_signatures -->| val htlcs = setupHtlcs(f) From c5605b0f302083af8360f1d40d801ef1588e9deb Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 2 Jun 2025 17:13:34 +0200 Subject: [PATCH 4/6] use addHtlc instead of makeCmdAdd where possible --- .../states/e/NormalSplicesStateSpec.scala | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 0e1a392d61..26c2451ab5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -2505,7 +2505,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // |<----- commit_sig -----| // |---- revoke_and_ack -->| - val sender = initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + initiateSpliceWithoutSigs(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) bob2alice.expectMsgType[CommitSig] @@ -2651,10 +2651,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik checkWatchConfirmed(f, fundingTx) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) - val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) - alice ! cmd.copy(commit = true) - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN(None) inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => // Bob doesn't receive Alice's commit_sig assert(batch.batchSize == 2) } @@ -2715,10 +2713,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik checkWatchConfirmed(f, fundingTx) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) - val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) - alice ! cmd.copy(commit = true) - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN(None) alice2bob.expectMsgType[CommitSigBatch] alice2bob.forward(bob) @@ -2782,10 +2778,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik checkWatchConfirmed(f, fundingTx) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) - val (_, cmd) = makeCmdAdd(25_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight) - alice ! cmd.copy(commit = true) - alice2bob.expectMsgType[UpdateAddHtlc] - alice2bob.forward(bob) + addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) + alice ! CMD_SIGN(None) inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => assert(batch.batchSize == 2) alice2bob.forward(bob) From 3d2e6f414126c0f26ab228c2553a97dd027831ad Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Tue, 3 Jun 2025 14:18:15 +0200 Subject: [PATCH 5/6] check batchSize of each CommitSigBatch --- .../states/e/NormalSplicesStateSpec.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 26c2451ab5..27a8fd87b4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -2670,12 +2670,16 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice must retransmit update_add_htlc and commit_sigs first. alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - alice2bob.expectMsgType[CommitSigBatch] - alice2bob.forward(bob) + inside(alice2bob.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + alice2bob.forward(bob) + } bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) - bob2alice.expectMsgType[CommitSigBatch] - bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) alice2bob.expectNoMessage(100 millis) @@ -2802,8 +2806,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Bob must retransmit his commit_sigs first. alice2bob.expectNoMessage(100 millis) - bob2alice.expectMsgType[CommitSigBatch] - bob2alice.forward(alice) + inside(bob2alice.expectMsgType[CommitSigBatch] ) { batch => + assert(batch.batchSize == 2) + bob2alice.forward(alice) + } alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) alice2bob.expectNoMessage(100 millis) From f4d1881d6cf0cd5b2f8afe233b39e5d4ab48c78a Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Wed, 4 Jun 2025 10:24:40 +0200 Subject: [PATCH 6/6] Fix nits, comment errors and formatting --- .../channel/states/e/NormalSplicesStateSpec.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 27a8fd87b4..a7a28d0b97 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -1755,7 +1755,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2alice.expectMsgType[TxAbort] bob2alice.forward(alice) alice2bob.expectMsgType[TxAbort] - bob2alice.forward(bob) + alice2bob.forward(bob) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) } @@ -2643,6 +2643,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // |------ commit_sig ---->| funding_txid = FundingTx // |------ commit_sig ---->| funding_txid = SpliceFundingTx // |<--- revoke_and_ack ---| + // |<----- start_batch ----| batch_size = 2 // |<----- commit_sig -----| // |<----- commit_sig -----| // |---- revoke_and_ack -->| @@ -2664,7 +2665,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val (channelReestablishAlice, channelReestablishBob) = reconnect(f) assert(channelReestablishAlice.nextFundingTxId_opt.isEmpty) assert(channelReestablishAlice.nextLocalCommitmentNumber == 1) - assert(channelReestablishBob.nextFundingTxId_opt.isEmpty ) + assert(channelReestablishBob.nextFundingTxId_opt.isEmpty) assert(channelReestablishBob.nextLocalCommitmentNumber == 1) // Alice must retransmit update_add_htlc and commit_sigs first. @@ -2709,8 +2710,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // | | // | | next_revocation_number = 0 (for both alice and bob) // |<--- revoke_and_ack ---| - // |<----- commit_sig -----| batch_size = 2, funding_txid = FundingTx - // |<----- commit_sig -----| batch_size = 2, funding_txid = SpliceFundingTx + // |<----- start_batch ----| batch_size = 2 + // |<----- commit_sig -----| funding_txid = FundingTx + // |<----- commit_sig -----| funding_txid = SpliceFundingTx // |---- revoke_and_ack -->| val fundingTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) @@ -2721,7 +2723,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! CMD_SIGN(None) alice2bob.expectMsgType[CommitSigBatch] alice2bob.forward(bob) - bob2alice.expectMsgType[RevokeAndAck] // Alice doesn't receive Bob's revoke_and_ack inside(bob2alice.expectMsgType[CommitSigBatch]) { batch => // Alice doesn't receive Bob's commit_sig assert(batch.batchSize == 2)