From 5f35c9e4bf0e200bd50a073899b1cfe575cdf918 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 25 Aug 2021 15:14:50 +0200 Subject: [PATCH 1/3] send command to itself to update relay fees at restore This is an alternative to #1918 and #1920. It's very close to the latter, except that we do check the conf only once in the `WAIT_FOR_INIT_INTERNAL` state, as opposed to at each reconnection in `SYNCING`. We do not change the `channel_update` in `WAIT_FOR_INIT_INTERNAL`, which allows us to set `previousChannelUpdate_opt=Some(normal.channelUpdate)` in the transition and fix the duplicate bug in the audit db. If there is a change in conf, there will be an additional `LocalChannelUpdate` emitted, but only at reconnection, and following the regular update flow, which should protect us against regressions. We do handle `CMD_UPDATE_RELAY_FEES` in both `OFFLINE` and `SYNCING`, because there may be a race between `CMD_UPDATE_RELAY_FEES` and `ChannelRestablish` if the conf change at restore. And there was no good reason to behave differently in those states anyway. --- .../fr/acinq/eclair/channel/Channel.scala | 66 ++++++++----------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 7109e304dc..ac5d2e3a46 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -284,32 +284,20 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId watchFundingTx(data.commitments) context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId, None)) - // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down + // we check the configuration because the values for channel_update may have changed while eclair was down val fees = getRelayFees(nodeParams, remoteNodeId, data.commitments) - val candidateChannelUpdate = Announcements.makeChannelUpdate( - nodeParams.chainHash, - nodeParams.privateKey, - remoteNodeId, - normal.channelUpdate.shortChannelId, - nodeParams.expiryDelta, - normal.commitments.remoteParams.htlcMinimum, - fees.feeBase, - fees.feeProportionalMillionths, - normal.commitments.capacity.toMilliSatoshi, - enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) - val channelUpdate1 = if (Announcements.areSame(candidateChannelUpdate, normal.channelUpdate)) { - // if there was no configuration change we keep the existing channel update - normal.channelUpdate - } else { - log.info("refreshing channel_update due to configuration changes old={} new={}", normal.channelUpdate, candidateChannelUpdate) - candidateChannelUpdate + if (fees.feeBase != normal.channelUpdate.feeBaseMsat || + fees.feeProportionalMillionths != normal.channelUpdate.feeProportionalMillionths || + nodeParams.expiryDelta != normal.channelUpdate.cltvExpiryDelta) { + log.info("refreshing channel_update due to configuration changes") + self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths) } // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network // we take into account the date of the last update so that we don't send superfluous updates when we restart the app - val periodicRefreshInitialDelay = Helpers.nextChannelUpdateRefresh(channelUpdate1.timestamp) + val periodicRefreshInitialDelay = Helpers.nextChannelUpdateRefresh(normal.channelUpdate.timestamp) context.system.scheduler.scheduleWithFixedDelay(initialDelay = periodicRefreshInitialDelay, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(OFFLINE) using normal.copy(channelUpdate = channelUpdate1) + goto(OFFLINE) case funding: DATA_WAIT_FOR_FUNDING_CONFIRMED => watchFundingTx(funding.commitments) @@ -1542,18 +1530,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway case Event(ProcessCurrentBlockCount(c), d: HasCommitments) => handleNewBlock(c, d) - case Event(c: CurrentFeerates, d: HasCommitments) => - handleOfflineFeerate(c, d) + case Event(c: CurrentFeerates, d: HasCommitments) => handleCurrentFeerateDisconnected(c, d) case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) - case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => - log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) - val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_SUCCESS(c, d.channelId) - // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state - stay() using d.copy(channelUpdate = channelUpdate) storing() + case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSinceBlock, d.fundingTx) @@ -1692,6 +1673,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) + case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) + case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => var sendQueue = Queue.empty[LightningMessage] val (commitments1, sendQueue1) = handleSync(channelReestablish, d) @@ -1734,8 +1717,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(ProcessCurrentBlockCount(c), d: HasCommitments) => handleNewBlock(c, d) - case Event(c: CurrentFeerates, d: HasCommitments) => - handleOfflineFeerate(c, d) + case Event(c: CurrentFeerates, d: HasCommitments) => handleCurrentFeerateDisconnected(c, d) case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSinceBlock, d.fundingTx) @@ -1879,24 +1861,23 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case _ => () } - val previousChannelUpdate_opt = stateData match { - case data: DATA_NORMAL => Some(data.channelUpdate) - case _ => None - } - (state, nextState, stateData, nextStateData) match { // ORDER MATTERS! case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, normal: DATA_NORMAL) => Logs.withMdc(diagLog)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION))) { log.debug("re-emitting channel_update={} enabled={} ", normal.channelUpdate, Announcements.isEnabled(normal.channelUpdate.channelFlags)) } - context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, previousChannelUpdate_opt, normal.commitments)) + context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, Some(normal.channelUpdate), normal.commitments)) case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate == d2.channelUpdate && d1.channelAnnouncement == d2.channelAnnouncement => // don't do anything if neither the channel_update nor the channel_announcement didn't change () case (WAIT_FOR_FUNDING_LOCKED | NORMAL | OFFLINE | SYNCING, NORMAL | OFFLINE, _, normal: DATA_NORMAL) => // when we do WAIT_FOR_FUNDING_LOCKED->NORMAL or NORMAL->NORMAL or SYNCING->NORMAL or NORMAL->OFFLINE, we send out the new channel_update (most of the time it will just be to enable/disable the channel) log.info("emitting channel_update={} enabled={} ", normal.channelUpdate, Announcements.isEnabled(normal.channelUpdate.channelFlags)) + val previousChannelUpdate_opt = stateData match { + case data: DATA_NORMAL => Some(data.channelUpdate) + case _ => None + } context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, previousChannelUpdate_opt, normal.commitments)) case (_, _, _: DATA_NORMAL, _: DATA_NORMAL) => // in any other case (e.g. OFFLINE->SYNCING) we do nothing @@ -1995,7 +1976,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId * @param d the channel commtiments * @return */ - private def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = { + private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: HasCommitments) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelFeatures, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw // if the network fees are too high we risk to not be able to confirm our current commitment @@ -2148,6 +2129,15 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } + private def handleUpdateRelayFeeDisconnected(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) = { + log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo + replyTo ! RES_SUCCESS(c, d.channelId) + // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state + stay() using d.copy(channelUpdate = channelUpdate) storing() + } + private def handleNewBlock(c: CurrentBlockCount, d: HasCommitments) = { val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockCount) val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockCount, nodeParams.fulfillSafetyBeforeTimeout) From a5faae673346e7d3c0c9af6ee1f52c91651809ed Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 25 Aug 2021 15:30:08 +0200 Subject: [PATCH 2/3] update cltv expiry delta along relay fees --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 2 +- .../scala/fr/acinq/eclair/channel/Channel.scala | 14 +++++++------- .../fr/acinq/eclair/channel/ChannelData.scala | 2 +- .../wire/protocol/LightningMessageTypes.scala | 2 ++ .../eclair/channel/states/e/NormalStateSpec.scala | 4 +++- .../eclair/channel/states/e/OfflineStateSpec.scala | 2 +- .../channel/states/f/ShutdownStateSpec.scala | 2 +- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 12fbcaa6a2..d230b76e19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -203,7 +203,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } (appKit.router ? Router.GetLocalChannels).mapTo[Iterable[LocalChannel]] .map(channels => channels.filter(c => nodes.contains(c.remoteNodeId)).map(c => Right(c.shortChannelId))) - .flatMap(channels => sendToChannels[CommandResponse[CMD_UPDATE_RELAY_FEE]](channels.toList, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths))) + .flatMap(channels => sendToChannels[CommandResponse[CMD_UPDATE_RELAY_FEE]](channels.toList, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths, cltvExpiryDelta_opt = None))) } override def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index ac5d2e3a46..8f08f56270 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -290,7 +290,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId fees.feeProportionalMillionths != normal.channelUpdate.feeProportionalMillionths || nodeParams.expiryDelta != normal.channelUpdate.cltvExpiryDelta) { log.info("refreshing channel_update due to configuration changes") - self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths) + self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths, Some(nodeParams.expiryDelta)) } // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network // we take into account the date of the last update so that we don't send superfluous updates when we restart the app @@ -1001,12 +1001,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => - log.info("updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) // we use GOTO instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(channelUpdate = channelUpdate) storing() + goto(NORMAL) using d.copy(channelUpdate = channelUpdate1) storing() case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => val age = System.currentTimeMillis.milliseconds - d.channelUpdate.timestamp.seconds @@ -2130,12 +2130,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } private def handleUpdateRelayFeeDisconnected(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) = { - log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state - stay() using d.copy(channelUpdate = channelUpdate) storing() + stay() using d.copy(channelUpdate = channelUpdate1) storing() } private def handleNewBlock(c: CurrentBlockCount, d: HasCommitments) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 05c9716792..de5ed6ffe0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -173,7 +173,7 @@ final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptio sealed trait CloseCommand extends HasReplyToCommand final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector]) extends CloseCommand final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand -final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends HasReplyToCommand +final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand final case class CMD_GETSTATE(replyTo: ActorRef) extends HasReplyToCommand final case class CMD_GETSTATEDATA(replyTo: ActorRef) extends HasReplyToCommand final case class CMD_GETINFO(replyTo: ActorRef)extends HasReplyToCommand diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 3be02cf3cb..2c687082e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -248,6 +248,8 @@ case class ChannelUpdate(signature: ByteVector64, require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") def isNode1 = Announcements.isNode1(channelFlags) + + def toStringShort: String = s"cltvExpiryDelta=$cltvExpiryDelta,feeBase=$feeBaseMsat,feeProportionalMillionths=$feeProportionalMillionths" } // @formatter:off diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 65bd3d4c2c..4fd9e5c001 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -1782,12 +1782,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val newFeeBaseMsat = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeBase * 2 val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 - sender.send(alice, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, newFeeBaseMsat, newFeeProportionalMillionth)) + val newCltvExpiryDelta = CltvExpiryDelta(145) + sender.send(alice, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, newFeeBaseMsat, newFeeProportionalMillionth, cltvExpiryDelta_opt = Some(newCltvExpiryDelta))) sender.expectMsgType[RES_SUCCESS[CMD_UPDATE_RELAY_FEE]] val localUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat) assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth) + assert(localUpdate.channelUpdate.cltvExpiryDelta == newCltvExpiryDelta) relayerA.expectNoMessage(1 seconds) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 63e9030d0d..8584f6710a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -405,7 +405,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with channelUpdateListener.expectNoMessage(300 millis) // we make alice update here relay fee - alice ! CMD_UPDATE_RELAY_FEE(sender.ref, 4200 msat, 123456) + alice ! CMD_UPDATE_RELAY_FEE(sender.ref, 4200 msat, 123456, cltvExpiryDelta_opt = None) sender.expectMsgType[RES_SUCCESS[CMD_UPDATE_RELAY_FEE]] // alice doesn't broadcast the new channel_update yet 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 ab1d13547d..f1bf5f0f34 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 @@ -598,7 +598,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() val newFeeBaseMsat = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeBase * 2 val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 - alice ! CMD_UPDATE_RELAY_FEE(sender.ref, newFeeBaseMsat, newFeeProportionalMillionth) + alice ! CMD_UPDATE_RELAY_FEE(sender.ref, newFeeBaseMsat, newFeeProportionalMillionth, cltvExpiryDelta_opt = None) sender.expectMsgType[RES_FAILURE[CMD_UPDATE_RELAY_FEE, _]] relayerA.expectNoMessage(1 seconds) } From 2c8db6e4d3f0ae5c20a20495413f7eaa53d76815 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 2 Sep 2021 10:42:01 +0200 Subject: [PATCH 3/3] add test showing issue --- .../fr/acinq/eclair/channel/Channel.scala | 2 +- .../fr/acinq/eclair/db/DbEventHandler.scala | 8 +- .../acinq/eclair/router/Announcements.scala | 3 + .../acinq/eclair/channel/RecoverySpec.scala | 125 ----------- .../fr/acinq/eclair/channel/RestoreSpec.scala | 211 ++++++++++++++++++ 5 files changed, 217 insertions(+), 132 deletions(-) delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 8f08f56270..8ea9ab2bdd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -297,7 +297,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val periodicRefreshInitialDelay = Helpers.nextChannelUpdateRefresh(normal.channelUpdate.timestamp) context.system.scheduler.scheduleWithFixedDelay(initialDelay = periodicRefreshInitialDelay, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(OFFLINE) + goto(OFFLINE) using normal case funding: DATA_WAIT_FOR_FUNDING_CONFIRMED => watchFundingTx(funding.commitments) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index 7b09d84050..696f748e94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.payment.Monitoring.{Metrics => PaymentMetrics, Tags => PaymentTags} import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.Announcements /** * This actor sits at the interface between our event stream and the database. @@ -119,12 +120,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with ActorLogging { case u: LocalChannelUpdate => u.previousChannelUpdate_opt match { - case Some(previous) if - u.channelUpdate.feeBaseMsat == previous.feeBaseMsat && - u.channelUpdate.feeProportionalMillionths == previous.feeProportionalMillionths && - u.channelUpdate.cltvExpiryDelta == previous.cltvExpiryDelta && - u.channelUpdate.htlcMinimumMsat == previous.htlcMinimumMsat && - u.channelUpdate.htlcMaximumMsat == previous.htlcMaximumMsat => () + case Some(previous) if Announcements.areSameIgnoreFlags(previous, u.channelUpdate) => () // channel update hasn't changed => () case _ => auditDb.addChannelUpdate(u) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index d3ee33472c..4d0deae485 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -126,6 +126,9 @@ object Announcements { def areSame(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = u1.copy(signature = ByteVector64.Zeroes, timestamp = 0) == u2.copy(signature = ByteVector64.Zeroes, timestamp = 0) + def areSameIgnoreFlags(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = + u1.copy(signature = ByteVector64.Zeroes, timestamp = 0, messageFlags = 1, channelFlags = 0) == u2.copy(signature = ByteVector64.Zeroes, timestamp = 0, messageFlags = 1, channelFlags = 0) + def makeMessageFlags(hasOptionChannelHtlcMax: Boolean): Byte = BitVector.bits(hasOptionChannelHtlcMax :: Nil).padLeft(8).toByte() def makeChannelFlags(isNode1: Boolean, enable: Boolean): Byte = BitVector.bits(!enable :: !isNode1 :: Nil).padLeft(8).toByte() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala deleted file mode 100644 index abe96276a7..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RecoverySpec.scala +++ /dev/null @@ -1,125 +0,0 @@ -package fr.acinq.eclair.channel - -import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin._ -import fr.acinq.eclair.TestConstants.Alice -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered -import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.crypto.Generators -import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, DefaultCommitmentFormat, InputInfo, TxOwner} -import fr.acinq.eclair.wire.protocol.{ChannelReestablish, CommitSig, Error, Init, RevokeAndAck} -import fr.acinq.eclair.{TestConstants, TestKitBaseClass, _} -import org.scalatest.Outcome -import org.scalatest.funsuite.FixtureAnyFunSuiteLike - -import scala.concurrent.duration._ - -class RecoverySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - - type FixtureParam = SetupFixture - - override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } - import setup._ - within(30 seconds) { - reachNormal(setup) - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - withFixture(test.toNoArgTest(setup)) - } - } - - def aliceInit = Init(TestConstants.Alice.nodeParams.features) - - def bobInit = Init(TestConstants.Bob.nodeParams.features) - - test("use funding pubkeys from publish commitment to spend our output") { f => - import f._ - val sender = TestProbe() - - // we start by storing the current state - val oldStateData = alice.stateData - // then we add an htlc and sign it - addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - sender.send(alice, CMD_SIGN()) - alice2bob.expectMsgType[CommitSig] - alice2bob.forward(bob) - // alice will receive neither the revocation nor the commit sig - bob2alice.expectMsgType[RevokeAndAck] - bob2alice.expectMsgType[CommitSig] - - // we simulate a disconnection - sender.send(alice, INPUT_DISCONNECTED) - sender.send(bob, INPUT_DISCONNECTED) - awaitCond(alice.stateName == OFFLINE) - awaitCond(bob.stateName == OFFLINE) - - // then we manually replace alice's state with an older one - alice.setState(OFFLINE, oldStateData) - - // then we reconnect them - sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) - sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) - - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - val ce = bob2alice.expectMsgType[ChannelReestablish] - - // alice then realizes it has an old state... - bob2alice.forward(alice) - // ... and ask bob to publish its current commitment - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) - - // alice now waits for bob to publish its commitment - awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) - - // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx - - // actual tests starts here: let's see what we can do with Bob's commit tx - sender.send(alice, WatchFundingSpentTriggered(bobCommitTx)) - - // from Bob's commit tx we can extract both funding public keys - val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) - // from Bob's commit tx we can also extract our p2wpkh output - val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get - - val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) - - val keyManager = TestConstants.Alice.nodeParams.channelKeyManager - - // find our funding pub key - val fundingPubKey = Seq(PublicKey(pub1), PublicKey(pub2)).find { - pub => - val channelKeyPath = ChannelKeyManager.keyPath(pub) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint) - localPubkey.hash160 == pubKeyHash - } get - - // compute our to-remote pubkey - val channelKeyPath = ChannelKeyManager.keyPath(fundingPubKey) - val ourToRemotePubKey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint) - - // spend our output - val tx = Transaction(version = 2, - txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, - txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey().publicKey)) :: Nil, - lockTime = 0) - - val sig = keyManager.sign( - ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), - keyManager.paymentPoint(channelKeyPath), - ce.myCurrentPerCommitmentPoint, - TxOwner.Local, - DefaultCommitmentFormat) - val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) - Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala new file mode 100644 index 0000000000..9f8b9e4e95 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -0,0 +1,211 @@ +package fr.acinq.eclair.channel + +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin._ +import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered +import fr.acinq.eclair.channel.states.ChannelStateTestsBase +import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.crypto.Generators +import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager +import fr.acinq.eclair.payment.relay.Relayer.{RelayFees, RelayParams} +import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, DefaultCommitmentFormat, InputInfo, TxOwner} +import fr.acinq.eclair.wire.protocol.{ChannelReestablish, CommitSig, Error, Init, RevokeAndAck} +import fr.acinq.eclair.{TestKitBaseClass, _} +import org.scalatest.Outcome +import org.scalatest.funsuite.FixtureAnyFunSuiteLike + +import scala.concurrent.duration._ + +class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + type FixtureParam = SetupFixture + + override def withFixture(test: OneArgTest): Outcome = { + val setup = test.tags.contains("disable-offline-mismatch") match { + case false => init() + case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) + } + import setup._ + within(30 seconds) { + reachNormal(setup) + awaitCond(alice.stateName == NORMAL) + awaitCond(bob.stateName == NORMAL) + withFixture(test.toNoArgTest(setup)) + } + } + + def aliceInit = Init(Alice.nodeParams.features) + + def bobInit = Init(Bob.nodeParams.features) + + test("use funding pubkeys from publish commitment to spend our output") { f => + import f._ + val sender = TestProbe() + + // we start by storing the current state + val oldStateData = alice.stateData.asInstanceOf[HasCommitments] + // then we add an htlc and sign it + addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) + sender.send(alice, CMD_SIGN()) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + // alice will receive neither the revocation nor the commit sig + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.expectMsgType[CommitSig] + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // we restart Alice + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + newAlice ! INPUT_RESTORED(oldStateData) + + // then we reconnect them + sender.send(newAlice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) + + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + val ce = bob2alice.expectMsgType[ChannelReestablish] + + // alice then realizes it has an old state... + bob2alice.forward(newAlice) + // ... and ask bob to publish its current commitment + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(newAlice)).getMessage) + + // alice now waits for bob to publish its commitment + awaitCond(newAlice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) + + // bob is nice and publishes its commitment + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager).tx + + // actual tests starts here: let's see what we can do with Bob's commit tx + sender.send(newAlice, WatchFundingSpentTriggered(bobCommitTx)) + + // from Bob's commit tx we can extract both funding public keys + val OP_2 :: OP_PUSHDATA(pub1, _) :: OP_PUSHDATA(pub2, _) :: OP_2 :: OP_CHECKMULTISIG :: Nil = Script.parse(bobCommitTx.txIn(0).witness.stack.last) + // from Bob's commit tx we can also extract our p2wpkh output + val ourOutput = bobCommitTx.txOut.find(_.publicKeyScript.length == 22).get + + val OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil = Script.parse(ourOutput.publicKeyScript) + + val keyManager = Alice.nodeParams.channelKeyManager + + // find our funding pub key + val fundingPubKey = Seq(PublicKey(pub1), PublicKey(pub2)).find { + pub => + val channelKeyPath = ChannelKeyManager.keyPath(pub) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint) + localPubkey.hash160 == pubKeyHash + } get + + // compute our to-remote pubkey + val channelKeyPath = ChannelKeyManager.keyPath(fundingPubKey) + val ourToRemotePubKey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, ce.myCurrentPerCommitmentPoint) + + // spend our output + val tx = Transaction(version = 2, + txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, + txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey().publicKey)) :: Nil, + lockTime = 0) + + val sig = keyManager.sign( + ClaimP2WPKHOutputTx(InputInfo(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), ourOutput, Script.pay2pkh(ourToRemotePubKey)), tx), + keyManager.paymentPoint(channelKeyPath), + ce.myCurrentPerCommitmentPoint, + TxOwner.Local, + DefaultCommitmentFormat) + val tx1 = tx.updateWitness(0, ScriptWitness(Scripts.der(sig) :: ourToRemotePubKey.value :: Nil)) + Transaction.correctlySpends(tx1, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + test("restore channel without configuration change") { f => + import f._ + val sender = TestProbe() + + // we start by storing the current state + assert(alice.stateData.isInstanceOf[DATA_NORMAL]) + val oldStateData = alice.stateData.asInstanceOf[DATA_NORMAL] + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // we restart Alice + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + newAlice ! INPUT_RESTORED(oldStateData) + + // first we send out the original channel update + val u1 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(u1.previousChannelUpdate_opt.nonEmpty) + assert(Announcements.areSameIgnoreFlags(u1.previousChannelUpdate_opt.get, u1.channelUpdate)) + assert(Announcements.areSameIgnoreFlags(u1.previousChannelUpdate_opt.get, oldStateData.channelUpdate)) + + newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + alice2bob.expectMsgType[ChannelReestablish] + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob) + bob2alice.forward(newAlice) + awaitCond(newAlice.stateName == NORMAL) + + // no new update + channelUpdateListener.expectNoMessage() + } + + test("restore channel with configuration change") { f => + import f._ + val sender = TestProbe() + + // we start by storing the current state + assert(alice.stateData.isInstanceOf[DATA_NORMAL]) + val oldStateData = alice.stateData.asInstanceOf[DATA_NORMAL] + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // we restart Alice with a different configuration + val newFees = RelayFees(765 msat, 2345) + val newConfig = Alice.nodeParams.copy(relayParams = RelayParams(newFees, newFees, newFees)) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + newAlice ! INPUT_RESTORED(oldStateData) + + // first we send out the original channel update + val u1 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(u1.previousChannelUpdate_opt.nonEmpty) + assert(Announcements.areSameIgnoreFlags(u1.previousChannelUpdate_opt.get, u1.channelUpdate)) + assert(Announcements.areSameIgnoreFlags(u1.previousChannelUpdate_opt.get, oldStateData.channelUpdate)) + + newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + alice2bob.expectMsgType[ChannelReestablish] + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob) + bob2alice.forward(newAlice) + awaitCond(newAlice.stateName == NORMAL) + + // then the new original channel update + val u2 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(u2.previousChannelUpdate_opt.nonEmpty) + assert(!Announcements.areSameIgnoreFlags(u2.previousChannelUpdate_opt.get, u2.channelUpdate)) + assert(Announcements.areSameIgnoreFlags(u2.previousChannelUpdate_opt.get, oldStateData.channelUpdate)) + + // no new update + channelUpdateListener.expectNoMessage() + } + +}