From 62b922133edc719b7b28700c5c31c02bc9eacd8f Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Fri, 28 Jul 2023 10:25:06 +0200 Subject: [PATCH] Update tests to include pending htlcs --- .../channel/fund/InteractiveTxBuilder.scala | 2 +- .../src/test/resources/logback-test.xml | 4 +- .../channel/InteractiveTxBuilderSpec.scala | 24 +- .../states/e/NormalSplicesStateSpec.scala | 264 +++++++++++++++++- .../channel/version4/ChannelCodecs4Spec.scala | 2 +- 5 files changed, 281 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 8506161de3..99503a686b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -251,7 +251,7 @@ object InteractiveTxBuilder { case class Remote(serialId: UInt64, amount: Satoshi, pubkeyScript: ByteVector) extends Output with Incoming /** The shared output can be added by us or by our peer, depending on who initiated the protocol. */ - case class Shared(serialId: UInt64, pubkeyScript: ByteVector, localAmount: MilliSatoshi, remoteAmount: MilliSatoshi, htlcsAmount: MilliSatoshi = 0 msat) extends Output with Incoming with Outgoing { + case class Shared(serialId: UInt64, pubkeyScript: ByteVector, localAmount: MilliSatoshi, remoteAmount: MilliSatoshi, htlcsAmount: MilliSatoshi) extends Output with Incoming with Outgoing { // Note that the truncation is a no-op: the sum of balances in a channel must be a satoshi amount. override val amount: Satoshi = (localAmount + remoteAmount + htlcsAmount).truncateToSatoshi } diff --git a/eclair-core/src/test/resources/logback-test.xml b/eclair-core/src/test/resources/logback-test.xml index 15bb987b0c..bd8c336f20 100644 --- a/eclair-core/src/test/resources/logback-test.xml +++ b/eclair-core/src/test/resources/logback-test.xml @@ -59,11 +59,11 @@ - + - - + \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 9bcc9e7c35..3f8b7b538f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -2510,13 +2510,19 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit test("reference test vector") { val channelId = ByteVector32.Zeroes val parentTx = Transaction.read("02000000000101f86fd1d0db3ac5a72df968622f31e6b5e6566a09e29206d7c7a55df90e181de800000000171600141fb9623ffd0d422eacc450fd1e967efc477b83ccffffffff0580b2e60e00000000220020fd89acf65485df89797d9ba7ba7a33624ac4452f00db08107f34257d33e5b94680b2e60e0000000017a9146a235d064786b49e7043e4a042d4cc429f7eb6948780b2e60e00000000160014fbb4db9d85fba5e301f4399e3038928e44e37d3280b2e60e0000000017a9147ecd1b519326bc13b0ec716e469b58ed02b112a087f0006bee0000000017a914f856a70093da3a5b5c4302ade033d4c2171705d387024730440220696f6cee2929f1feb3fd6adf024ca0f9aa2f4920ed6d35fb9ec5b78c8408475302201641afae11242160101c6f9932aeb4fcd1f13a9c6df5d1386def000ea259a35001210381d7d5b1bc0d7600565d827242576d9cb793bfe0754334af82289ee8b65d137600000000") - val sharedOutput = Output.Shared(UInt64(44), hex"0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5", 200_000_000_000L msat, 200_000_000_000L msat) + val localAmountIn = 1_749_990_000_000L msat + val remoteAmountIn = 2_000_000_000_000L msat + val localAmountOut = localAmountIn + (200_000_000_000L msat) + val remoteAmountOut = remoteAmountIn + (200_000_000_000L msat) + val htlcsAmount = parentTx.txOut(4).amount - (localAmountIn + remoteAmountIn) + val sharedOutput = Output.Shared(UInt64(44), hex"0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5", localAmountOut, remoteAmountOut, htlcsAmount) + val sharedInput = Input.Shared(UInt64(22), OutPoint(parentTx, 4), 4294967293L, localAmountIn, remoteAmountIn) val initiatorTx = { - val initiatorInput = Input.Local(UInt64(20), parentTx, 0, 4294967293L) + val initiatorInput = Input.Local(UInt64(20), parentTx, 0, 4294967293L) // 2_500_000_000 sat val initiatorOutput = Output.Local.Change(UInt64(30), 49_999_845 sat, hex"00141ca1cca8855bad6bc1ea5436edd8cff10b7e448b") - val nonInitiatorInput = Input.Remote(UInt64(11), OutPoint(parentTx, 2), parentTx.txOut(2), 4294967293L) + val nonInitiatorInput = Input.Remote(UInt64(11), OutPoint(parentTx, 2), parentTx.txOut(2), 4294967293L) // 2_500_000_000 sat val nonInitiatorOutput = Output.Remote(UInt64(33), 49_999_900 sat, hex"001444cb0c39f93ecc372b5851725bd29d865d333b10") - SharedTransaction(None, sharedOutput, List(initiatorInput), List(nonInitiatorInput), List(initiatorOutput), List(nonInitiatorOutput), lockTime = 120) + SharedTransaction(Some(sharedInput), sharedOutput, List(initiatorInput), List(nonInitiatorInput), List(initiatorOutput), List(nonInitiatorOutput), lockTime = 120) } assert(initiatorTx.localFees == 155_000.msat) assert(initiatorTx.remoteFees == 100_000.msat) @@ -2527,23 +2533,23 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val initiatorOutput = Output.Remote(UInt64(30), 49_999_845 sat, hex"00141ca1cca8855bad6bc1ea5436edd8cff10b7e448b") val nonInitiatorInput = Input.Local(UInt64(11), parentTx, 2, 4294967293L) val nonInitiatorOutput = Output.Local.Change(UInt64(33), 49_999_900 sat, hex"001444cb0c39f93ecc372b5851725bd29d865d333b10") - SharedTransaction(None, sharedOutput, List(nonInitiatorInput), List(initiatorInput), List(nonInitiatorOutput), List(initiatorOutput), lockTime = 120) + SharedTransaction(Some(sharedInput), sharedOutput, List(nonInitiatorInput), List(initiatorInput), List(nonInitiatorOutput), List(initiatorOutput), lockTime = 120) } assert(nonInitiatorTx.localFees == 100_000.msat) assert(nonInitiatorTx.remoteFees == 155_000.msat) assert(nonInitiatorTx.fees == 255.sat) - val unsignedTx = Transaction.read("0200000002b932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430000000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000000016001444cb0c39f93ecc372b5851725bd29d865d333b100084d71700000000220020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec578000000") + val unsignedTx = Transaction.read("0200000003b932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430000000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430400000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000000016001444cb0c39f93ecc372b5851725bd29d865d333b10f084420601000000220020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec578000000") assert(initiatorTx.buildUnsignedTx().txid == unsignedTx.txid) assert(nonInitiatorTx.buildUnsignedTx().txid == unsignedTx.txid) val initiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87"))), None) val nonInitiatorSigs = TxSignatures(channelId, unsignedTx, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484"))), None) val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs, None) - assert(initiatorSignedTx.feerate == FeeratePerKw(262 sat)) + assert(initiatorSignedTx.feerate == FeeratePerKw(224 sat)) val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs, None) - assert(nonInitiatorSignedTx.feerate == FeeratePerKw(262 sat)) - val signedTx = Transaction.read("02000000000102b932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430000000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000000016001444cb0c39f93ecc372b5851725bd29d865d333b100084d71700000000220020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec50247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff8778000000") + assert(nonInitiatorSignedTx.feerate == FeeratePerKw(224 sat)) + val signedTx = Transaction.read("02000000000103b932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430000000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430400000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000000016001444cb0c39f93ecc372b5851725bd29d865d333b10f084420601000000220020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec50247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff870078000000") assert(initiatorSignedTx.signedTx == signedTx) assert(initiatorSignedTx.signedTx == nonInitiatorSignedTx.signedTx) } 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 98c27c5d00..5ea146a5d6 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 @@ -19,7 +19,8 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.NumericSatoshi.abs +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw @@ -33,11 +34,12 @@ import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFact import fr.acinq.eclair.channel.states.ChannelStateTestsTags.{AnchorOutputsZeroFeeHtlcTxs, NoMaxHtlcValueInFlight, ZeroConf} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.testutils.PimpTestProbe.convert +import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.time.SpanSugar.convertIntToGrainOfTime -import org.scalatest.{Outcome, Tag} +import org.scalatest.{Assertion, Outcome, Tag} import scodec.bits.HexStringSyntax /** @@ -70,6 +72,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val sender = TestProbe() val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt) alice ! cmd + if (alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.useQuiescence) { + alice2bob.expectMsgType[Stfu] + alice2bob.forward(bob) + bob2alice.expectMsgType[Stfu] + bob2alice.forward(alice) + } alice2bob.expectMsgType[SpliceInit] alice2bob.forward(bob) bob2alice.expectMsgType[SpliceAck] @@ -1494,4 +1502,256 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik bob2blockchain.expectNoMessage(100 millis) } + private def setupHtlcs(f: FixtureParam): Seq[(ByteVector32, UpdateAddHtlc)] = { + import f._ + + // add htlcs in both directions + val (preimage1a, htlc1a) = addHtlc(5_000 msat, alice, bob, alice2bob, bob2alice) + val (preimage2a, htlc2a) = addHtlc(5_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + val (preimage1b, htlc1b) = addHtlc(5_000 msat, bob, alice, bob2alice, alice2bob) + val (preimage2b, htlc2b) = addHtlc(5_000 msat, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + assert(initialState.commitments.latest.capacity == 1_500_000.sat) + assert(initialState.commitments.latest.localCommit.spec.toLocal == 799_990_000.msat) + assert(initialState.commitments.latest.localCommit.spec.toRemote == 699_990_000.msat) + + Seq((preimage1a, htlc1a), (preimage2a, htlc2a), (preimage1b, htlc1b), (preimage2b, htlc2b)) + } + + def spliceOutFee(f: FixtureParam): Satoshi = { + import f._ + + val fundingTx2 = alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.localFundingStatus.signedTx_opt.get + val feerate = alice.nodeParams.onChainFeeConf.getFundingFeerate(alice.nodeParams.currentFeerates) + val expectedMiningFee = Transactions.weight2fee(feerate, fundingTx2.weight()) + val actualMiningFee = 1_900_000.sat - alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.capacity + // fee computation is approximate + assert(actualMiningFee >= 0.sat && abs(actualMiningFee - expectedMiningFee) < 100.sat) + actualMiningFee + } + + def checkPostSpliceState(f: FixtureParam, fee: Satoshi): Assertion = { + import f._ + + // if the swap includes a splice-in, swap-out fees will be paid from bitcoind so final capacity is predictable + val postSpliceState = alice.stateData.asInstanceOf[ChannelDataWithCommitments] + assert(postSpliceState.commitments.latest.capacity == 1_900_000.sat - fee) + assert(postSpliceState.commitments.latest.localCommit.spec.toLocal == 1_199_990_000.msat - fee) + assert(postSpliceState.commitments.latest.localCommit.spec.toRemote == 699_990_000.msat) + assert(postSpliceState.commitments.latest.localCommit.spec.htlcs.collect(incoming).toSeq.map(_.amountMsat).sum == 10_000.msat) + assert(postSpliceState.commitments.latest.localCommit.spec.htlcs.collect(outgoing).toSeq.map(_.amountMsat).sum == 10_000.msat) + } + + def resolveHtlcs(f: FixtureParam, htlcs: Seq[(ByteVector32, UpdateAddHtlc)], paySpliceOutFee: Boolean): Assertion = { + import f._ + + val fee = if (paySpliceOutFee) spliceOutFee(f) else 0 sat + val Seq((preimage1a, htlc1a), (preimage2a, htlc2a), (preimage1b, htlc1b), (preimage2b, htlc2b)) = htlcs + + checkPostSpliceState(f, fee) + + // resolve pre-splice HTLCs after splice + fulfillHtlc(htlc1a.id, preimage1a, bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlc2a.id, preimage2a, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlc1b.id, preimage1b, alice, bob, alice2bob, bob2alice) + fulfillHtlc(htlc2b.id, preimage2b, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.spec.htlcs.collect(outgoing).isEmpty) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteCommit.spec.htlcs.collect(outgoing).isEmpty) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.spec.htlcs.collect(outgoing).isEmpty) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.remoteCommit.spec.htlcs.collect(outgoing).isEmpty) + + val finalState = alice.stateData.asInstanceOf[DATA_NORMAL] + assert(finalState.commitments.latest.capacity == 1_900_000.sat - fee) + assert(finalState.commitments.latest.localCommit.spec.toLocal == 1_200_000_000.msat - fee) + assert(finalState.commitments.latest.localCommit.spec.toRemote == 700_000_000.msat) + } + + test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + + + val htlcs = setupHtlcs(f) + + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + + resolveHtlcs(f, htlcs, paySpliceOutFee = false) + } + + test("recv multiple CMD_SPLICE (splice-in, splice-out) with pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + + + val htlcs = setupHtlcs(f) + + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) + initiateSplice(f, spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + + resolveHtlcs(f, htlcs, paySpliceOutFee = true) + } + + test("recv CMD_SPLICE (splice-in + splice-out) with pending htlcs, resolved after splice locked", Tag(ChannelStateTestsTags.Quiescence), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + val spliceTx = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + + 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, paySpliceOutFee = false) + } + + test("disconnect (commit_sig not received) with pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + 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]) + alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs] + disconnect(f) + reconnect(f) + exchangeSpliceSigs(f, sender) + + resolveHtlcs(f, htlcs, paySpliceOutFee = false) + } + + test("disconnect (commit_sig received by alice) with pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + 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]) + alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs] + disconnect(f) + reconnect(f) + exchangeSpliceSigs(f, sender) + + resolveHtlcs(f, htlcs, paySpliceOutFee = false) + } + + test("disconnect (tx_signatures sent by bob) with pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + 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].txId // Alice doesn't receive Bob's tx_signatures + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + disconnect(f) + reconnect(f, interceptFundingDeeplyBuried = false) + exchangeSpliceSigs(f, sender) + + resolveHtlcs(f, htlcs, paySpliceOutFee = false) + } + + test("disconnect (tx_signatures received by alice) with pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + val htlcs = setupHtlcs(f) + + 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].txId // Bob doesn't receive Alice's tx_signatures + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice) + disconnect(f) + reconnect(f, interceptFundingDeeplyBuried = false) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + + resolveHtlcs(f, htlcs, paySpliceOutFee = false) + } + + test("force-close with multiple splices (simple) and pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + setupHtlcs(f) + + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + alice2blockchain.expectMsgType[WatchFundingConfirmed] + initiateSplice(f, spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + alice2blockchain.expectMsgType[WatchFundingConfirmed] + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + alice2blockchain.expectNoMessage(100 millis) + // we now have two unconfirmed splices + + alice ! CMD_FORCECLOSE(ActorRef.noSender) + alice2bob.expectMsgType[Error] + + checkPostSpliceState(f, spliceOutFee(f)) + } + + + test("force-close with multiple splices (previous active remote) and pending htlcs", Tag(ChannelStateTestsTags.Quiescence)) { f => + import f._ + + setupHtlcs(f) + + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) + val fundingTx1 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get + val watchConfirmed1 = alice2blockchain.expectMsgType[WatchFundingConfirmed] + initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat))) + alice2blockchain.expectMsgType[WatchFundingConfirmed] + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + alice2blockchain.expectNoMessage(100 millis) + // we now have two unconfirmed splices + + alice ! CMD_FORCECLOSE(ActorRef.noSender) + alice2bob.expectMsgType[Error] + val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") + assertPublished(alice2blockchain, "local-main-delayed") + alice2blockchain.expectMsgType[WatchTxConfirmed] + alice2blockchain.expectMsgType[WatchTxConfirmed] + alice ! WatchFundingSpentTriggered(aliceCommitTx2) + alice2blockchain.expectNoMessage(100 millis) + + // splice 1 confirms + watchConfirmed1.replyTo ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) + alice2bob.forward(bob) + alice2blockchain.expectMsgType[WatchFundingSpent] + alice2blockchain.expectNoMessage(100 millis) + + // oops! remote commit for splice 1 is published + val bobCommitTx1 = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.find(_.fundingTxIndex == 1).get.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(bobCommitTx1) + val watchAlternativeConfirmed = alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed] + alice2blockchain.expectNoMessage(100 millis) + + // remote commit tx confirms + watchAlternativeConfirmed.replyTo ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + + checkPostSpliceState(f, fee = 0.sat) + } + + // TODO: add pending htlcs to force close tests with multiple splices? + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index 4e18d08786..152d7e45ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -123,7 +123,7 @@ class ChannelCodecs4Spec extends AnyFunSuite { val fundingInput = InputInfo(OutPoint(randomBytes32(), 3), TxOut(175_000 sat, Script.pay2wpkh(randomKey().publicKey)), Nil) val fundingTx = SharedTransaction( sharedInput_opt = None, - sharedOutput = InteractiveTxBuilder.Output.Shared(UInt64(8), ByteVector.empty, 100_000_600 msat, 74_000_400 msat), + sharedOutput = InteractiveTxBuilder.Output.Shared(UInt64(8), ByteVector.empty, 100_000_600 msat, 74_000_400 msat, 150_000_000 msat), localInputs = Nil, remoteInputs = Nil, localOutputs = Nil, remoteOutputs = Nil, lockTime = 0