diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 5f85fdd40..cfd42a735 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -1040,88 +1040,94 @@ object Helpers { */ def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, commitTx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ - require(commitTx.txIn.size == 1, "commitment tx should have 1 input") + // a valid tx will always have at least one input, but this ensures we don't throw in tests + val sequence = commitTx.txIn.headOption.map(_.sequence).getOrElse(0L) + val obscuredTxNumber = Transactions.decodeTxNumber(sequence, commitTx.lockTime) val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) // this tx has been published by remote, so we need to invert local/remote params val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isInitiator, remoteParams.paymentBasepoint, localPaymentPoint) - require(txNumber <= 0xffffffffffffL, "txNumber must be lesser than 48 bits long") - log.warning(s"a revoked commit has been published with txnumber=$txNumber") - // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain - remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) - .map(d => PrivateKey(d)) - .map(remotePerCommitmentSecret => { - val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey - val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + if (txNumber > 0xffffffffffffL) { + // txNumber must be lesser than 48 bits long + None + } else { + // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain + remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) + .map(d => PrivateKey(d)) + .map(remotePerCommitmentSecret => { + log.warning(s"a revoked commit has been published with txnumber=$txNumber") - val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) + val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey + val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - // first we will claim our main output right away - val mainTx = channelFeatures match { - case ct if ct.paysDirectlyToWallet => - log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") - None - case ct => ct.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("claim-p2wpkh-output") { - Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, localPaymentPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, sig) - }) + val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) + + // first we will claim our main output right away + val mainTx = channelFeatures match { + case ct if ct.paysDirectlyToWallet => + log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") + None + case ct => ct.commitmentFormat match { + case DefaultCommitmentFormat => withTxGenerationLog("claim-p2wpkh-output") { + Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, localPaymentPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } } } - } - // then we punish them by stealing their main output - val mainPenaltyTx = withTxGenerationLog("main-penalty") { - Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - Transactions.addSigs(txinfo, sig) - }) - } - - // we retrieve the information needed to rebuild htlc scripts - val htlcInfos = db.listHtlcInfos(commitments.channelId, txNumber) - log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txNumber") - val htlcsRedeemScripts = ( - htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ - htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } - ) - .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) - .toMap - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => - val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) - withTxGenerationLog("htlc-penalty") { - Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + // then we punish them by stealing their main output + val mainPenaltyTx = withTxGenerationLog("main-penalty") { + Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).map(txinfo => { + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + Transactions.addSigs(txinfo, sig) }) } - }.toList.flatten - RevokedCommitPublished( - commitTx = commitTx, - claimMainOutputTx = mainTx, - mainPenaltyTx = mainPenaltyTx, - htlcPenaltyTxs = htlcPenaltyTxs, - claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx - irrevocablySpent = Map.empty - ) - }) + // we retrieve the information needed to rebuild htlc scripts + val htlcInfos = db.listHtlcInfos(commitments.channelId, txNumber) + log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txNumber") + val htlcsRedeemScripts = ( + htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ + htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } + ) + .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) + .toMap + + // and finally we steal the htlc outputs + val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => + val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) + withTxGenerationLog("htlc-penalty") { + Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcPenalty => { + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + }) + } + }.toList.flatten + + RevokedCommitPublished( + commitTx = commitTx, + claimMainOutputTx = mainTx, + mainPenaltyTx = mainPenaltyTx, + htlcPenaltyTxs = htlcPenaltyTxs, + claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx + irrevocablySpent = Map.empty + ) + }) + } } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 91c7ed09d..2a4e161d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -701,12 +701,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val goto(NORMAL) using d.copy(channelUpdate = channelUpdate1) storing() } - case Event(WatchFundingSpentTriggered(tx), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NORMAL) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NORMAL) => handleRemoteSpentOther(tx, d) - case Event(INPUT_DISCONNECTED, d: DATA_NORMAL) => // we cancel the timer that would have made us send the enabled update after reconnection (flappy channel protection) cancelTimer(Reconnected.toString) @@ -908,12 +902,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CurrentFeerates, d: DATA_SHUTDOWN) => handleCurrentFeerate(c, d) - case Event(WatchFundingSpentTriggered(tx), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_SHUTDOWN) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d) - case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) => c.feerates match { case Some(feerates) if c.feerates != d.closingFeerates => @@ -1016,21 +1004,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Left(cause) => handleLocalError(cause, d, Some(c)) } - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => - // they can publish a closing tx with any sig we sent them, even if we are not done negotiating - handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.bestUnpublishedClosingTx_opt.exists(_.tx.txid == tx.txid) => - log.warning(s"looks like a mutual close tx has been published from the outside of the channel: closingTxId=${tx.txid}") - // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that - handleMutualClose(d.bestUnpublishedClosingTx_opt.get, Left(d)) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d) - case Event(c: CMD_CLOSE, d: DATA_NEGOTIATING) => c.feerates match { case Some(feerates) => @@ -1337,18 +1310,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(_: WatchFundingConfirmedTriggered, _) => stay() case Event(_: WatchFundingDeeplyBuriedTriggered, _) => stay() - - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => - handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) => handleRemoteSpentFuture(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d) - }) when(SYNCING)(handleExceptions { @@ -1540,14 +1501,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(_: WatchFundingDeeplyBuriedTriggered, _) => stay() - case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d) - case Event(e: Error, d: PersistentChannelData) => handleRemoteError(e, d) }) @@ -1634,9 +1587,30 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // peer doesn't cancel the timer case Event(TickChannelOpenTimeout, _) => stay() - case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid => - log.warning(s"processing local commit spent in catch-all handler") - spendLocalCurrent(d) + // we declare WatchFundingSpentTriggered handlers here because they apply to variants of each state in OFFLINE/SYNCING + + case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => + // they can publish a closing tx with any sig we sent them, even if we are not done negotiating + handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.bestUnpublishedClosingTx_opt.exists(_.tx.txid == tx.txid) => + log.warning(s"looks like a mutual close tx has been published from the outside of the channel: closingTxId=${tx.txid}") + // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that + handleMutualClose(d.bestUnpublishedClosingTx_opt.get, Left(d)) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) => handleRemoteSpentFuture(tx, d) + + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => + if (tx.txid == d.commitments.remoteCommit.txid) { + handleRemoteSpentCurrent(tx, d) + } else if (d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid)) { + handleRemoteSpentNext(tx, d) + } else if (tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) { + log.warning(s"processing local commit spent from the outside") + spendLocalCurrent(d) + } else { + handleRemoteSpentOther(tx, d) + } } onTransition { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 88db1571e..7b522b695 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -641,10 +641,6 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { delayEarlyAnnouncementSigs(remoteAnnSigs) stay() - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_READY) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => handleInformationLeak(tx, d) - case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) => handleRemoteError(e, d) }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index a445428b0..61b6ef6b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -421,10 +421,6 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) }) @@ -437,10 +433,6 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { delayEarlyAnnouncementSigs(remoteAnnSigs) stay() - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) => handleInformationLeak(tx, d) - case Event(e: Error, d: DATA_WAIT_FOR_CHANNEL_READY) => handleRemoteError(e, d) }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 0c9a2c02a..e419e74ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -326,20 +326,6 @@ trait ErrorHandlers extends CommonHandlers { watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) } - def handleInformationLeak(tx: Transaction, d: PersistentChannelData) = { - // this is never supposed to happen !! - log.error(s"our funding tx ${d.commitments.fundingTxId} was spent by txid=${tx.txid}!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.fundingTxId} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) - val exc = FundingTxSpent(d.channelId, tx.txid) - val error = Error(d.channelId, exc.getMessage) - - // let's try to spend our current local tx - val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) - - goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error - } - def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: PersistentChannelData) = { val exc = PleasePublishYourCommitment(d.channelId) val error = Error(d.channelId, exc.getMessage) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 9327f841e..34f2002bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -245,11 +245,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv WatchFundingSpentTriggered (other commit)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index 3ceb9e8a8..fe3fc2517 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.states.c import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.{CurrentBlockHeight, SingleKeyOnChainWallet} import fr.acinq.eclair.channel.InteractiveTxBuilder.FullySignedSharedTransaction @@ -574,6 +574,12 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob2.stateName == CLOSING) } + test("recv WatchFundingSpentTriggered (other commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala index 66834ac69..b82726708 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala @@ -184,10 +184,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF test("recv WatchFundingSpentTriggered (other commit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ alice2bob.expectMsgType[ChannelReady] - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == commitTx.txid) awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } 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 1b35b2079..494be1ce9 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 @@ -236,10 +236,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv WatchFundingSpentTriggered (other commit)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } 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 b075e1693..6fe2c7e27 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 @@ -3256,6 +3256,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(addSettled.htlc == htlc3) } + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + test("recv Error") { f => import f._ val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) 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 30cbba120..4bdaf159d 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 @@ -799,6 +799,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with channelUpdateListener.expectMsgType[LocalChannelUpdate] } + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + def disconnect(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel]): Unit = { alice ! INPUT_DISCONNECTED bob ! INPUT_DISCONNECTED 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 7103c6df0..b82cd4f5c 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 @@ -900,6 +900,12 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2blockchain.expectNoMessage(1 second) } + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + test("recv Error") { f => import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index b49bf12f1..d6f3d62b9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.states.g import akka.testkit.TestProbe -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing @@ -547,6 +547,12 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike awaitCond(bob.stateName == CLOSING) } + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + test("recv Error") { f => import f._ bobClose(f) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 77424552b..e797220ea 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 @@ -1629,6 +1629,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(new String(error.data.toArray) == FundingTxSpent(channelId(alice), initialState.spendingTxs.head.txid).getMessage) } + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + test("recv CMD_CLOSE") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)