1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 02:27:32 +01:00

Smarter restore logic for closing channels (#1015)

If the closing type is known:
 - there is no need to watch the funding tx because it has already
 been spent and the spending tx has already reached min_depth
 - there is no need to attempt to publish transactions for other
 type of closes.

* differentiate current/next remote close

We can still match on the trait `RemoteClose` if we don't need that
level of precision.
This commit is contained in:
Pierre-Marie Padiou 2019-06-12 12:29:31 +02:00 committed by GitHub
parent 818199e8f4
commit 849b6bd22b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 349 additions and 38 deletions

View File

@ -183,24 +183,35 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
goto(CLOSED) using closing
case closing: DATA_CLOSING =>
// we don't put back the WatchSpent if the commitment tx has already been published and the spending tx already reached mindepth
val commitTxOutpoint = closing.commitments.commitInput.outPoint
if (closing.localCommitPublished.exists(_.irrevocablySpent.contains(commitTxOutpoint)) ||
closing.remoteCommitPublished.exists(_.irrevocablySpent.contains(commitTxOutpoint)) ||
closing.nextRemoteCommitPublished.exists(_.irrevocablySpent.contains(commitTxOutpoint)) ||
closing.revokedCommitPublished.exists(_.irrevocablySpent.contains(commitTxOutpoint)) ||
closing.futureRemoteCommitPublished.exists(_.irrevocablySpent.contains(commitTxOutpoint))) {
log.info(s"funding tx has already been spent and spending tx reached mindepth, no need to put back the watch-spent")
} else {
// TODO: should we wait for an acknowledgment from the watcher?
blockchain ! WatchSpent(self, data.commitments.commitInput.outPoint.txid, data.commitments.commitInput.outPoint.index.toInt, data.commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT)
blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST)
val closingType_opt = Closing.isClosingTypeAlreadyKnown(closing)
log.info(s"channel is closing (closingType=${closingType_opt.getOrElse("UnknownYet")})")
// if the closing type is known:
// - there is no need to watch the funding tx because it has already been spent and the spending tx has
// already reached mindepth
// - there is no need to attempt to publish transactions for other type of closes
closingType_opt match {
case Some(Closing.LocalClose) =>
closing.localCommitPublished.foreach(doPublish)
case Some(Closing.CurrentRemoteClose) =>
closing.remoteCommitPublished.foreach(doPublish)
case Some(Closing.NextRemoteClose) =>
closing.nextRemoteCommitPublished.foreach(doPublish)
case Some(Closing.RecoveryClose) =>
closing.futureRemoteCommitPublished.foreach(doPublish)
case Some(Closing.RevokedClose) =>
closing.revokedCommitPublished.foreach(doPublish)
case _ =>
// in all other cases we need to be ready for any type of closing
// TODO: should we wait for an acknowledgment from the watcher?
blockchain ! WatchSpent(self, data.commitments.commitInput.outPoint.txid, data.commitments.commitInput.outPoint.index.toInt, data.commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT)
blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST)
closing.mutualClosePublished.foreach(doPublish)
closing.localCommitPublished.foreach(doPublish)
closing.remoteCommitPublished.foreach(doPublish)
closing.nextRemoteCommitPublished.foreach(doPublish)
closing.revokedCommitPublished.foreach(doPublish)
closing.futureRemoteCommitPublished.foreach(doPublish)
}
closing.mutualClosePublished.map(doPublish(_))
closing.localCommitPublished.foreach(doPublish(_))
closing.remoteCommitPublished.foreach(doPublish(_))
closing.nextRemoteCommitPublished.foreach(doPublish(_))
closing.revokedCommitPublished.foreach(doPublish(_))
closing.futureRemoteCommitPublished.foreach(doPublish(_))
// no need to go OFFLINE, we can directly switch to CLOSING
goto(CLOSING) using closing

View File

@ -359,7 +359,9 @@ object Helpers {
sealed trait ClosingType
case object MutualClose extends ClosingType
case object LocalClose extends ClosingType
case object RemoteClose extends ClosingType
sealed trait RemoteClose extends ClosingType
case object CurrentRemoteClose extends RemoteClose
case object NextRemoteClose extends RemoteClose
case object RecoveryClose extends ClosingType
case object RevokedClose extends ClosingType
// @formatter:on
@ -377,6 +379,30 @@ object Helpers {
data.commitments.remoteCommit.spec.toRemoteMsat == 0 &&
data.commitments.remoteNextCommitInfo.isRight
/**
* As soon as a tx spending the funding tx has reached min_depth, we know what the closing type will be, before
* the whole closing process finishes(e.g. there may still be delayed or unconfirmed child transactions). It can
* save us from attempting to publish some transactions.
*
* Note that we can't tell for mutual close before it is already final, because only one tx needs to be confirmed.
*
* @param closing channel state data
* @return the channel closing type, if applicable
*/
def isClosingTypeAlreadyKnown(closing: DATA_CLOSING): Option[ClosingType] = closing match {
case _ if closing.localCommitPublished.exists(lcp => lcp.irrevocablySpent.values.toSet.contains(lcp.commitTx.txid)) =>
Some(LocalClose)
case _ if closing.remoteCommitPublished.exists(rcp => rcp.irrevocablySpent.values.toSet.contains(rcp.commitTx.txid)) =>
Some(CurrentRemoteClose)
case _ if closing.nextRemoteCommitPublished.exists(rcp => rcp.irrevocablySpent.values.toSet.contains(rcp.commitTx.txid)) =>
Some(NextRemoteClose)
case _ if closing.futureRemoteCommitPublished.exists(rcp => rcp.irrevocablySpent.values.toSet.contains(rcp.commitTx.txid)) =>
Some(RecoveryClose)
case _ if closing.revokedCommitPublished.exists(rcp => rcp.irrevocablySpent.values.toSet.contains(rcp.commitTx.txid)) =>
Some(RevokedClose)
case _ => None // we don't know yet what the closing type will be
}
/**
* Checks if a channel is closed (i.e. its closing tx has been confirmed)
*
@ -391,9 +417,9 @@ object Helpers {
case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone) =>
Some(LocalClose)
case closing: DATA_CLOSING if closing.remoteCommitPublished.exists(Closing.isRemoteCommitDone) =>
Some(RemoteClose)
Some(CurrentRemoteClose)
case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(Closing.isRemoteCommitDone) =>
Some(RemoteClose)
Some(NextRemoteClose)
case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(Closing.isRemoteCommitDone) =>
Some(RecoveryClose)
case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(Closing.isRevokedCommitDone) =>
@ -854,18 +880,6 @@ object Helpers {
}).toSet.flatten
}
/**
* Tells if we were the origin of this outgoing htlc
*
* @param htlcId
* @param originChannels
* @return
*/
def isSentByLocal(htlcId: Long, originChannels: Map[Long, Origin]) = originChannels.get(htlcId) match {
case Some(Local(_, _)) => true
case _ => false
}
/**
* As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether
* or not they actually have an output in the commitment tx).

View File

@ -65,7 +65,7 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
val event = e.closingType match {
case MutualClose => "mutual"
case LocalClose => "local"
case RemoteClose => "remote"
case _: RemoteClose => "remote" // can be current or next
case RecoveryClose => "recovery"
case RevokedClose => "revoked"
}

View File

@ -16,7 +16,9 @@
package fr.acinq.eclair.channel
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose, MutualClose, NextRemoteClose, RecoveryClose, RevokedClose}
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import org.scalatest.FunSuite
import scala.compat.Platform
@ -50,10 +52,294 @@ class HelpersSpec extends FunSuite {
test("compute refresh delay") {
import org.scalatest.Matchers._
implicit val log = akka.event.NoLogging
Helpers.nextChannelUpdateRefresh(1544400000).toSeconds should equal (0)
Helpers.nextChannelUpdateRefresh((Platform.currentTime.milliseconds - 9.days).toSeconds).toSeconds should equal (24 * 3600L +- 100)
Helpers.nextChannelUpdateRefresh((Platform.currentTime.milliseconds - 3.days).toSeconds).toSeconds should equal (7 * 24 * 3600L +- 100)
Helpers.nextChannelUpdateRefresh(Platform.currentTime.milliseconds.toSeconds).toSeconds should equal (10 * 24 * 3600L +- 100)
Helpers.nextChannelUpdateRefresh(1544400000).toSeconds should equal(0)
Helpers.nextChannelUpdateRefresh((Platform.currentTime.milliseconds - 9.days).toSeconds).toSeconds should equal(24 * 3600L +- 100)
Helpers.nextChannelUpdateRefresh((Platform.currentTime.milliseconds - 3.days).toSeconds).toSeconds should equal(7 * 24 * 3600L +- 100)
Helpers.nextChannelUpdateRefresh(Platform.currentTime.milliseconds.toSeconds).toSeconds should equal(10 * 24 * 3600L +- 100)
}
test("tell closing type") {
val tx1 = Transaction.read("010000000110f01d4a4228ef959681feb1465c2010d0135be88fd598135b2e09d5413bf6f1000000006a473044022074658623424cebdac8290488b76f893cfb17765b7a3805e773e6770b7b17200102202892cfa9dda662d5eac394ba36fcfd1ea6c0b8bb3230ab96220731967bbdb90101210372d437866d9e4ead3d362b01b615d24cc0d5152c740d51e3c55fb53f6d335d82ffffffff01408b0700000000001976a914678db9a7caa2aca887af1177eda6f3d0f702df0d88ac00000000")
val tx2 = Transaction.read("0100000001be43e9788523ed4de0b24a007a90009bc25e667ddac0e9ee83049be03e220138000000006b483045022100f74dd6ad3e6a00201d266a0ed860a6379c6e68b473970423f3fc8a15caa1ea0f022065b4852c9da230d9e036df743cb743601ca5229e1cb610efdd99769513f2a2260121020636de7755830fb4a3f136e97ecc6c58941611957ba0364f01beae164b945b2fffffffff0150f80c000000000017a9146809053148799a10480eada3d56d15edf4a648c88700000000")
val tx3 = Transaction.read("0100000002b8682539550b3182966ecaca3d1fd5b2a96d0966a0fded143aaf771cbaf4222b000000006b483045022100c4484511ea7d9cf989797ca98e403c93372ded754ce30737af4914a222c84e8e022011648b42f8756ef4b83aa4f49e6b77f86ce7c54e1b70f25a16a94c4551c99cff012102506d400d2168a4a272b026d8b95ecb822cccd60277fb7268a6873fef0a85fe96ffffffff5a68052b6c23f6f718e09ffe56378ee90ad438b94c99b398c8d9e581a3c049d0300000006b483045022100d1b0eebc8250ebbb2d692c1e293260387b748115cf4cf892891ca4a1e81029cf02202fb5daa7647355e2c86d3f8bcfde7691a163f0dd99a002aca97f0a17cc72c5da012102fe1ec7be2f1e974c7e75932c0187f61667fa2825c4f79ccb964a83f48cce442cffffffff02ba100000000000001976a914a14c305babbd3d6984a20899f078980f078d433288acc8370800000000001976a914e0b4609a38d1a4dd1196f8c66e879b4923f9ea7388ac00000000")
val tx4 = Transaction.read("0200000001c8a8934fb38a44b969528252bc37be66ee166c7897c57384d1e561449e110c93010000006b483045022100dc6c50f445ed53d2fb41067fdcb25686fe79492d90e6e5db43235726ace247210220773d35228af0800c257970bee9cf75175d75217de09a8ecd83521befd040c4ca012102082b751372fe7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247ba2300000000001976a914f97a7641228e6b17d4b0b08252ae75bd62a95fe788ace3de24000000000017a914a9fefd4b9a9282a1d7a17d2f14ac7d1eb88141d287f7d50800")
val tx5 = Transaction.read("010000000235a2f5c4fd48672534cce1ac063047edc38683f43c5a883f815d6026cb5f8321020000006a47304402206be5fd61b1702599acf51941560f0a1e1965aa086634b004967747f79788bd6e022002f7f719a45b8b5e89129c40a9d15e4a8ee1e33be3a891cf32e859823ecb7a510121024756c5adfbc0827478b0db042ce09d9b98e21ad80d036e73bd8e7f0ecbc254a2ffffffffb2387d3125bb8c84a2da83f4192385ce329283661dfc70191f4112c67ce7b4d0000000006b483045022100a2c737eab1c039f79238767ccb9bb3e81160e965ef0fc2ea79e8360c61b7c9f702202348b0f2c0ea2a757e25d375d9be183200ce0a79ec81d6a4ebb2ae4dc31bc3c9012102db16a822e2ec3706c58fc880c08a3617c61d8ef706cc8830cfe4561d9a5d52f0ffffffff01808d5b00000000001976a9141210c32def6b64d0d77ba8d99adeb7e9f91158b988ac00000000")
val tx6 = Transaction.read("0100000001b14ba6952c83f6f8c382befbf4e44270f13e479d5a5ff3862ac3a112f103ff2a010000006b4830450221008b097fd69bfa3715fc5e119a891933c091c55eabd3d1ddae63a1c2cc36dc9a3e02205666d5299fa403a393bcbbf4b05f9c0984480384796cdebcf69171674d00809c01210335b592484a59a44f40998d65a94f9e2eecca47e8d1799342112a59fc96252830ffffffff024bf308000000000017a914440668d018e5e0ba550d6e042abcf726694f515c8798dd1801000000001976a91453a503fe151dd32e0503bd9a2fbdbf4f9a3af1da88ac00000000")
// only mutual close
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = tx1 :: tx2 :: tx3 :: Nil,
mutualClosePublished = tx2 :: tx3 :: Nil,
localCommitPublished = None,
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished = Nil)
).isEmpty)
// mutual + local close, but local commit tx isn't confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = tx1 :: Nil,
mutualClosePublished = tx1 :: Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx2,
claimMainDelayedOutputTx = Some(tx3),
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map.empty
)),
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished = Nil)
).isEmpty)
// mutual + local close, local commit tx confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = tx1 :: Nil,
mutualClosePublished = tx1 :: Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx2,
claimMainDelayedOutputTx = Some(tx3),
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map(
tx2.txIn.head.outPoint -> tx2.txid
)
)),
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished = Nil)
).contains(LocalClose))
// local close + remote close, none is confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = Nil,
mutualClosePublished = Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx2,
claimMainDelayedOutputTx = None,
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map.empty
)),
remoteCommitPublished = Some(RemoteCommitPublished(
commitTx = tx3,
claimMainOutputTx = None,
claimHtlcSuccessTxs = Nil,
claimHtlcTimeoutTxs = Nil,
irrevocablySpent = Map.empty
)),
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished = Nil)
).isEmpty)
// mutual + local + remote close, remote commit tx confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = tx1 :: Nil,
mutualClosePublished = tx1 :: Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx2,
claimMainDelayedOutputTx = None,
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map.empty
)),
remoteCommitPublished = Some(RemoteCommitPublished(
commitTx = tx3,
claimMainOutputTx = None,
claimHtlcSuccessTxs = Nil,
claimHtlcTimeoutTxs = Nil,
irrevocablySpent = Map(
tx3.txIn.head.outPoint -> tx3.txid
)
)),
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished = Nil)
).contains(CurrentRemoteClose))
// mutual + local + remote + next remote close, next remote commit tx confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = tx1 :: Nil,
mutualClosePublished = tx1 :: Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx2,
claimMainDelayedOutputTx = None,
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map.empty
)),
remoteCommitPublished = Some(RemoteCommitPublished(
commitTx = tx3,
claimMainOutputTx = None,
claimHtlcSuccessTxs = Nil,
claimHtlcTimeoutTxs = Nil,
irrevocablySpent = Map.empty
)),
nextRemoteCommitPublished = Some(RemoteCommitPublished(
commitTx = tx4,
claimMainOutputTx = Some(tx5),
claimHtlcSuccessTxs = Nil,
claimHtlcTimeoutTxs = Nil,
irrevocablySpent = Map(
tx4.txIn.head.outPoint -> tx4.txid
)
)),
futureRemoteCommitPublished = None,
revokedCommitPublished = Nil)
).contains(NextRemoteClose))
// future remote close, not confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = Nil,
mutualClosePublished = Nil,
localCommitPublished = None,
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = Some(RemoteCommitPublished(
commitTx = tx4,
claimMainOutputTx = Some(tx5),
claimHtlcSuccessTxs = Nil,
claimHtlcTimeoutTxs = Nil,
irrevocablySpent = Map.empty
)),
revokedCommitPublished = Nil)
).isEmpty)
// future remote close, confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = Nil,
mutualClosePublished = Nil,
localCommitPublished = None,
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = Some(RemoteCommitPublished(
commitTx = tx4,
claimMainOutputTx = Some(tx5),
claimHtlcSuccessTxs = Nil,
claimHtlcTimeoutTxs = Nil,
irrevocablySpent = Map(
tx4.txIn.head.outPoint -> tx4.txid
)
)),
revokedCommitPublished = Nil)
).contains(RecoveryClose))
// local close + revoked close, none confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = Nil,
mutualClosePublished = Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx1,
claimMainDelayedOutputTx = None,
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map.empty
)),
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished =
RevokedCommitPublished(
commitTx = tx2,
claimMainOutputTx = Some(tx3),
mainPenaltyTx = None,
htlcPenaltyTxs = Nil,
claimHtlcDelayedPenaltyTxs = Nil,
irrevocablySpent = Map.empty
) ::
RevokedCommitPublished(
commitTx = tx4,
claimMainOutputTx = Some(tx5),
mainPenaltyTx = None,
htlcPenaltyTxs = Nil,
claimHtlcDelayedPenaltyTxs = Nil,
irrevocablySpent = Map.empty
) ::
RevokedCommitPublished(
commitTx = tx6,
claimMainOutputTx = None,
mainPenaltyTx = None,
htlcPenaltyTxs = Nil,
claimHtlcDelayedPenaltyTxs = Nil,
irrevocablySpent = Map.empty
) :: Nil
)
).isEmpty)
// local close + revoked close, one revoked confirmed
assert(Closing.isClosingTypeAlreadyKnown(
DATA_CLOSING(
commitments = null,
mutualCloseProposed = Nil,
mutualClosePublished = Nil,
localCommitPublished = Some(LocalCommitPublished(
commitTx = tx1,
claimMainDelayedOutputTx = None,
htlcSuccessTxs = Nil,
htlcTimeoutTxs = Nil,
claimHtlcDelayedTxs = Nil,
irrevocablySpent = Map.empty
)),
remoteCommitPublished = None,
nextRemoteCommitPublished = None,
futureRemoteCommitPublished = None,
revokedCommitPublished =
RevokedCommitPublished(
commitTx = tx2,
claimMainOutputTx = Some(tx3),
mainPenaltyTx = None,
htlcPenaltyTxs = Nil,
claimHtlcDelayedPenaltyTxs = Nil,
irrevocablySpent = Map.empty
) ::
RevokedCommitPublished(
commitTx = tx4,
claimMainOutputTx = Some(tx5),
mainPenaltyTx = None,
htlcPenaltyTxs = Nil,
claimHtlcDelayedPenaltyTxs = Nil,
irrevocablySpent = Map(
tx4.txIn.head.outPoint -> tx4.txid
)
) ::
RevokedCommitPublished(
commitTx = tx6,
claimMainOutputTx = None,
mainPenaltyTx = None,
htlcPenaltyTxs = Nil,
claimHtlcDelayedPenaltyTxs = Nil,
irrevocablySpent = Map.empty
) :: Nil
)
).contains(RevokedClose))
}