diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 5942c8dbe7..44cb81c11b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -2367,14 +2367,19 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with emitEvent_opt.foreach { case EmitLocalChannelUpdate(reason, d, sendToPeer) => log.debug(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=$sendToPeer realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no")) - val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, d.commitments.params.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments) + val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, remoteNodeId, d.channelAnnouncement, d.channelUpdate, d.commitments) context.system.eventStream.publish(lcu) if (sendToPeer) { send(Helpers.channelUpdateForDirectPeer(nodeParams, d.channelUpdate, d.shortIds)) } case EmitLocalChannelDown(d) => - log.debug(s"emitting channel down event") - val lcd = LocalChannelDown(self, d.channelId, d.shortIds, d.commitments.params.remoteParams.nodeId) + log.debug("emitting channel down event") + if (d.channelAnnouncement.nonEmpty) { + // We tell the rest of the network that this channel shouldn't be used anymore. + val disabledUpdate = Helpers.makeChannelUpdate(nodeParams, remoteNodeId, Helpers.scidForChannelUpdate(d), d.commitments, d.channelUpdate.relayFees, enable = false) + context.system.eventStream.publish(LocalChannelUpdate(self, d.channelId, d.shortIds, remoteNodeId, d.channelAnnouncement, disabledUpdate, d.commitments)) + } + val lcd = LocalChannelDown(self, d.channelId, d.shortIds, remoteNodeId) context.system.eventStream.publish(lcd) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 890204f3e7..73cf31d99d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.SpontaneousRecipient -import fr.acinq.eclair.wire.protocol.{ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -103,6 +103,42 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit } } + test("emit disabled channel update", Tag(ChannelStateTestsTags.ChannelsPublic)) { () => + val setup = init() + import setup._ + within(30 seconds) { + reachNormal(setup, Set(ChannelStateTestsTags.ChannelsPublic)) + + val aliceListener = TestProbe() + systemA.eventStream.subscribe(aliceListener.ref, classOf[LocalChannelUpdate]) + val bobListener = TestProbe() + systemB.eventStream.subscribe(bobListener.ref, classOf[LocalChannelUpdate]) + + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + alice2bob.expectMsgType[AnnouncementSignatures] + alice2bob.forward(bob) + bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + assert(aliceListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + assert(bobListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + + addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + + alice ! CMD_CLOSE(TestProbe().ref, None, None) + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + awaitCond(alice.stateName == SHUTDOWN) + awaitCond(bob.stateName == SHUTDOWN) + + assert(!aliceListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + assert(!bobListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + } + } + test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 23283f19b4..a6f454a34f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -29,8 +29,8 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat import fr.acinq.eclair.wire.protocol.ClosingSignedTlv.FeeRange -import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown, TlvStream, Warning} -import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, Error, Shutdown, TlvStream, Warning} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -89,6 +89,35 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike def buildFeerates(feerate: FeeratePerKw, minFeerate: FeeratePerKw = FeeratePerKw(250 sat)): FeeratesPerKw = FeeratesPerKw.single(feerate).copy(minimum = minFeerate, slow = minFeerate) + test("emit disabled channel update", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + + val aliceListener = TestProbe() + systemA.eventStream.subscribe(aliceListener.ref, classOf[LocalChannelUpdate]) + val bobListener = TestProbe() + systemB.eventStream.subscribe(bobListener.ref, classOf[LocalChannelUpdate]) + + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + alice2bob.expectMsgType[AnnouncementSignatures] + alice2bob.forward(bob) + bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + assert(aliceListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + assert(bobListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + + alice ! CMD_CLOSE(TestProbe().ref, None, None) + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + awaitCond(alice.stateName == NEGOTIATING) + awaitCond(bob.stateName == NEGOTIATING) + + assert(!aliceListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + assert(!bobListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + } + test("recv CMD_ADD_HTLC") { f => import f._ aliceClose(f) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index fe4ac6bf17..3360ee233b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -346,6 +346,26 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData == initialState) // this was a no-op } + test("recv WatchFundingSpentTriggered (local commit, public channel)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + + val listener = TestProbe() + systemA.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) + + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + alice2bob.expectMsgType[AnnouncementSignatures] + alice2bob.forward(bob) + bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + + // an error occurs and alice publishes her commit tx + localClose(alice, alice2blockchain) + // she notifies the network that the channel shouldn't be used anymore + assert(!listener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + } + test("recv WatchOutputSpentTriggered") { f => import f._ // alice sends an htlc to bob @@ -737,6 +757,27 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit } + test("recv WatchFundingSpentTriggered (remote commit, public channel)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + + val listener = TestProbe() + systemA.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) + + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + alice2bob.expectMsgType[AnnouncementSignatures] + alice2bob.forward(bob) + bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400_000), 42, null) + bob2alice.expectMsgType[AnnouncementSignatures] + bob2alice.forward(alice) + assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + + // bob publishes his commit tx + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + remoteClose(bobCommitTx, alice, alice2blockchain) + // alice notifies the network that the channel shouldn't be used anymore + assert(!listener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) + } + test("recv CMD_BUMP_FORCE_CLOSE_FEE (remote commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._