Skip to content

Commit

Permalink
Disable channel when closing (#2774)
Browse files Browse the repository at this point in the history
As soon as a channel is transitioning to a closing state (mutual or
unilateral close), it cannot be used to relay HTLCs anymore. We should
notify the network by sending a disabled `channel_update`.

Fixes #2766
  • Loading branch information
t-bast committed Nov 10, 2023
1 parent 5fa7d4b commit 0a833a5
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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._

Expand Down

0 comments on commit 0a833a5

Please sign in to comment.