diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 50ba967b44..16fa8094cb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -66,10 +66,10 @@ import scala.util.{Failure, Success} * * Created by PM on 25/01/2016. * - * @param datadir directory where eclair-core will write/read its data. - * @param pluginParams parameters for all configured plugins. - * @param seeds_opt optional seeds, if set eclair will use them instead of generating them and won't create a node_seed.dat and channel_seed.dat files. - * @param db optional databases to use, if not set eclair will create the necessary databases + * @param datadir directory where eclair-core will write/read its data. + * @param pluginParams parameters for all configured plugins. + * @param seeds_opt optional seeds, if set eclair will use them instead of generating them and won't create a node_seed.dat and channel_seed.dat files. + * @param db optional databases to use, if not set eclair will create the necessary databases */ class Setup(datadir: File, pluginParams: Seq[PluginParams], @@ -185,6 +185,8 @@ class Setup(datadir: File, assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") + logger.info(s"current blockchain height=$blocks") + blockCount.set(blocks) Bitcoind(bitcoinClient) case ELECTRUM => val addresses = config.hasPath("electrum") match { 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 21afb2a627..8ab7e8174c 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 @@ -69,8 +69,8 @@ object Channel { // since BOLT 1.1, there is a max value for the refund delay of the main commitment tx val MAX_TO_SELF_DELAY = CltvExpiryDelta(2016) - // as a fundee, we will wait that much time for the funding tx to confirm (funder will rely on the funding tx being double-spent) - val FUNDING_TIMEOUT_FUNDEE = 5 days + // as a fundee, we will wait that many blocks for the funding tx to confirm (funder will rely on the funding tx being double-spent) + val FUNDING_TIMEOUT_FUNDEE = 2016 // pruning occurs if no new update has been received in two weeks (BOLT 7) val REFRESH_CHANNEL_UPDATE_INTERVAL = 10 days @@ -161,7 +161,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, remote, _, channelFlags, channelVersion), Nothing) => + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelVersion), Nothing) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) activeConnection = remote val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey @@ -238,7 +238,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } // no need to go OFFLINE, we can directly switch to CLOSING - goto(CLOSING) using closing + if (closing.waitingSinceBlock > 1500000) { + // we were using timestamps instead of block heights when the channel was created: we reset it *and* we use block heights + goto(CLOSING) using closing.copy(waitingSinceBlock = nodeParams.currentBlockHeight) storing() + } else { + goto(CLOSING) using closing + } case normal: DATA_NORMAL => // TODO: should we wait for an acknowledgment from the watcher? @@ -280,7 +285,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) // we make sure that the funding tx has been published blockchain ! GetTxWithMeta(funding.commitments.commitInput.outPoint.txid) - goto(OFFLINE) using data + if (funding.waitingSinceBlock > 1500000) { + // we were using timestamps instead of block heights when the channel was created: we reset it *and* we use block heights + goto(OFFLINE) using funding.copy(waitingSinceBlock = nodeParams.currentBlockHeight) storing() + } else { + goto(OFFLINE) using funding + } case _ => // TODO: should we wait for an acknowledgment from the watcher? @@ -474,8 +484,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount) blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, fundingMinDepth, BITCOIN_FUNDING_DEPTHOK) - context.system.scheduler.scheduleOnce(FUNDING_TIMEOUT_FUNDEE, self, BITCOIN_FUNDING_TIMEOUT) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, initialRelayFees_opt, now, None, Right(fundingSigned)) storing() sending fundingSigned + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, initialRelayFees_opt, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned } } @@ -592,10 +601,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) stay - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSinceBlock, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) + case Event(c: CurrentBlockCount, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.fundingTx match { + case Some(_) => stay // we are funder, we're still waiting for the funding tx to be confirmed + case None if c.blockCount - d.waitingSinceBlock > FUNDING_TIMEOUT_FUNDEE => + log.warning(s"funding tx hasn't been published in ${c.blockCount - d.waitingSinceBlock} blocks") + self ! BITCOIN_FUNDING_TIMEOUT + stay + case None => stay // let's wait longer + } + case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) @@ -1254,11 +1272,15 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId nextRemoteCommitPublished1.foreach(doPublish) } - stay using d.copy(commitments = commitments1, localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1) storing() calling (republish) + stay using d.copy(commitments = commitments1, localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1) storing() calling republish() case Left(cause) => handleCommandError(cause, c) } - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_CLOSING) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_CLOSING) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => + // NB: waitingSinceBlock contains the block at which closing was initiated, not the block at which funding was initiated. + // That means we're lenient with our peer and give its funding tx more time to confirm, to avoid having to store two distinct + // waitingSinceBlock (e.g. closingWaitingSinceBlock and fundingWaitingSinceBlock). + handleGetFundingTx(getTxResponse, d.waitingSinceBlock, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_CLOSING) => handleFundingPublishFailed(d) @@ -1465,7 +1487,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // 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(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSinceBlock, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) @@ -1635,7 +1657,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CurrentFeerates, d: HasCommitments) => handleOfflineFeerate(c, d) - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSinceBlock, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) @@ -1938,7 +1960,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } - def handleGetFundingTx(getTxResponse: GetTxWithMetaResponse, waitingSince: Long, fundingTx_opt: Option[Transaction]) = { + def handleGetFundingTx(getTxResponse: GetTxWithMetaResponse, waitingSinceBlock: Long, fundingTx_opt: Option[Transaction]) = { import getTxResponse._ tx_opt match { case Some(_) => () // the funding tx exists, nothing to do @@ -1952,14 +1974,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we also check if the funding tx has been double-spent checkDoubleSpent(fundingTx) context.system.scheduler.scheduleOnce(1 day, blockchain, GetTxWithMeta(txid)) - case None if (now.seconds - waitingSince.seconds) > FUNDING_TIMEOUT_FUNDEE && (now.seconds - lastBlockTimestamp.seconds) < 1.hour => + case None if (nodeParams.currentBlockHeight - waitingSinceBlock) > FUNDING_TIMEOUT_FUNDEE => // if we are fundee, we give up after some time - // NB: we want to be sure that the blockchain is in sync to prevent false negatives - log.warning(s"funding tx hasn't been published in ${(now.seconds - waitingSince.seconds).toDays} days and blockchain is fresh from ${(now.seconds - lastBlockTimestamp.seconds).toMinutes} minutes ago") + log.warning(s"funding tx hasn't been published in ${nodeParams.currentBlockHeight - waitingSinceBlock} blocks") self ! BITCOIN_FUNDING_TIMEOUT case None => // let's wait a little longer - log.info(s"funding tx still hasn't been published in ${(now.seconds - waitingSince.seconds).toDays} days, will wait ${(FUNDING_TIMEOUT_FUNDEE - now.seconds + waitingSince.seconds).toDays} more days...") + log.info(s"funding tx still hasn't been published in ${nodeParams.currentBlockHeight - waitingSinceBlock} blocks, will wait ${FUNDING_TIMEOUT_FUNDEE - nodeParams.currentBlockHeight + waitingSinceBlock} more blocks...") context.system.scheduler.scheduleOnce(1 day, blockchain, GetTxWithMeta(txid)) } } @@ -2083,7 +2104,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def handleMutualClose(closingTx: Transaction, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { log.info(s"closing tx published: closingTxId=${closingTx.txid}") val nextData = d match { - case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) + case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) } goto(CLOSING) using nextData storing() calling doPublish(closingTx) @@ -2108,9 +2129,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = now, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) } goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished) } @@ -2170,9 +2191,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = now, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) } goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished) } @@ -2182,12 +2203,12 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId d.commitments.channelVersion match { case v if v.paysDirectlyToWallet => val remoteCommitPublished = RemoteCommitPublished(commitTx, None, List.empty, List.empty, Map.empty) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address case _ => val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished) } } @@ -2201,9 +2222,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) } goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished) } @@ -2236,9 +2257,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val nextData = d match { case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil) // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) } goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error case None => @@ -2426,12 +2447,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } - def send(msg: LightningMessage) = { + def send(msg: LightningMessage): Unit = { peer ! OutgoingMessage(msg, activeConnection) } - def now = System.currentTimeMillis.milliseconds.toSeconds - override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) val id = currentMessage match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 143c3984f7..2e335fb1d3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -335,7 +335,7 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], initialRelayFees_opt: Option[(MilliSatoshi, Int)], - waitingSince: Long, // how long have we been waiting for the funding tx to confirm + waitingSinceBlock: Long, // how long have we been waiting for the funding tx to confirm deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked, initialRelayFees_opt: Option[(MilliSatoshi, Int)]) extends Data with HasCommitments @@ -353,11 +353,11 @@ final case class DATA_NEGOTIATING(commitments: Commitments, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) bestUnpublishedClosingTx_opt: Option[Transaction]) extends Data with HasCommitments { require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation") - require(!commitments.localParams.isFunder || closingTxProposed.forall(_.nonEmpty), "funder must have at least one closing signature for every negotation attempt because it initiates the closing") + require(!commitments.localParams.isFunder || closingTxProposed.forall(_.nonEmpty), "funder must have at least one closing signature for every negotiation attempt because it initiates the closing") } final case class DATA_CLOSING(commitments: Commitments, fundingTx: Option[Transaction], // this will be non-empty if we are funder and we got in closing while waiting for our own tx to be published - waitingSince: Long, // how long since we initiated the closing + waitingSinceBlock: Long, // how long since we initiated the closing mutualCloseProposed: List[Transaction], // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have mutualClosePublished: List[Transaction] = Nil, localCommitPublished: Option[LocalCommitPublished] = None, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 12a940611a..c73a240c18 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -60,7 +60,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = tx1 :: tx2 :: tx3 :: Nil, mutualClosePublished = tx2 :: tx3 :: Nil, localCommitPublished = None, @@ -75,7 +75,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = tx1 :: Nil, mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( @@ -97,7 +97,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = tx1 :: Nil, mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( @@ -121,7 +121,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = Nil, mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( @@ -149,7 +149,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = tx1 :: Nil, mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( @@ -179,7 +179,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments.copy(remoteNextCommitInfo = Left(WaitingForRevocation(commitments.remoteCommit, null, 7L))), fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = tx1 :: Nil, mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( @@ -215,7 +215,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = Nil, mutualClosePublished = Nil, localCommitPublished = None, @@ -236,7 +236,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = Nil, mutualClosePublished = Nil, localCommitPublished = None, @@ -259,7 +259,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = Nil, mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( @@ -306,7 +306,7 @@ class HelpersSpec extends AnyFunSuite { DATA_CLOSING( commitments = commitments, fundingTx = None, - waitingSince = 0, + waitingSinceBlock = 0, mutualCloseProposed = Nil, mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 73823e9ce0..9022f912dc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -106,13 +106,54 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_FUNDING_TIMEOUT") { f => + test("recv BITCOIN_FUNDING_TIMEOUT (funder)") { f => import f._ alice ! BITCOIN_FUNDING_TIMEOUT alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_FUNDING_TIMEOUT (fundee)") { f => + import f._ + bob ! BITCOIN_FUNDING_TIMEOUT + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSED) + } + + test("recv CurrentBlockCount (funder)") { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] + alice ! CurrentBlockCount(initialState.waitingSinceBlock + Channel.FUNDING_TIMEOUT_FUNDEE + 1) + alice2bob.expectNoMsg(100 millis) + } + + test("recv CurrentBlockCount (funding timeout not reached)") { f => + import f._ + val initialState = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] + bob ! CurrentBlockCount(initialState.waitingSinceBlock + Channel.FUNDING_TIMEOUT_FUNDEE - 1) + bob2alice.expectNoMsg(100 millis) + } + + test("recv CurrentBlockCount (funding timeout reached)") { f => + import f._ + val initialState = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] + bob ! CurrentBlockCount(initialState.waitingSinceBlock + Channel.FUNDING_TIMEOUT_FUNDEE + 1) + bob2alice.expectMsgType[Error] + awaitCond(bob.stateName == CLOSED) + } + + test("migrate waitingSince to waitingSinceBlocks") { f => + import f._ + // Before version 0.5.1, eclair used an absolute timestamp instead of a block height for funding timeouts. + val beforeMigration = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].copy(waitingSinceBlock = System.currentTimeMillis().milliseconds.toSeconds) + bob.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + bob ! INPUT_RESTORED(beforeMigration) + awaitCond(bob.stateName == OFFLINE) + // We reset the waiting period to the current block height when starting up after updating eclair. + val currentBlockHeight = bob.underlyingActor.nodeParams.currentBlockHeight + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSinceBlock === currentBlockHeight) + } + test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f => import f._ // bob publishes his commitment tx 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 7d2febd40e..fc7a9cece0 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 @@ -243,32 +243,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchConfirmed] // claim-main-delayed // test starts here - bob.setState(stateData = bob.stateData.asInstanceOf[DATA_CLOSING].copy(waitingSince = System.currentTimeMillis.milliseconds.toSeconds - 15.days.toSeconds)) + bob.setState(stateData = bob.stateData.asInstanceOf[DATA_CLOSING].copy(waitingSinceBlock = bob.underlyingActor.nodeParams.currentBlockHeight - Channel.FUNDING_TIMEOUT_FUNDEE - 1)) bob ! GetTxWithMetaResponse(fundingTx.txid, None, System.currentTimeMillis.milliseconds.toSeconds) bob2alice.expectMsgType[Error] bob2blockchain.expectNoMsg(200 millis) assert(bob.stateName == CLOSED) } - test("recv GetTxResponse (fundee, tx not found, timeout, blockchain lags)", Tag("funding_unconfirmed")) { f => - import f._ - val sender = TestProbe() - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - bob ! CMD_FORCECLOSE(sender.ref) - awaitCond(bob.stateName == CLOSING) - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishAsap] - bob2blockchain.expectMsgType[PublishAsap] // claim-main-delayed - bob2blockchain.expectMsgType[WatchConfirmed] // commitment - bob2blockchain.expectMsgType[WatchConfirmed] // claim-main-delayed - - // test starts here - bob ! GetTxWithMetaResponse(fundingTx.txid, None, System.currentTimeMillis.milliseconds.toSeconds - 3.hours.toSeconds) - bob2alice.expectNoMsg(200 millis) - bob2blockchain.expectNoMsg(200 millis) - assert(bob.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING - } - test("recv CMD_ADD_HTLC") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index f26f08a090..f3d939ff62 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -335,7 +335,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // let's decode the old data (this will use the old codec that provides default values for new fields) val data_new = stateDataCodec.decode(bin_old.toBitVector).require.value assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx === None) - assert(System.currentTimeMillis.milliseconds.toSeconds - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince < 3600) // we just set this timestamp to current time + assert(System.currentTimeMillis.milliseconds.toSeconds - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSinceBlock < 3600) // we just set this timestamp to current time // and re-encode it with the new codec val bin_new = ByteVector(stateDataCodec.encode(data_new).require.toByteVector.toArray) // data should now be encoded under the new format