From 2e0d0248d24a3c48919f0a1ca16faf61c84bc459 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 31 Jul 2020 10:33:23 +0200 Subject: [PATCH 1/2] Handle unilateral close for anchor outputs Change the static_remotekey behavior to use a wallet basepoint only when using "standard" commitments; with anchor outputs we need to claim this output (it doesn't directly spend to our wallet) so we must use key-derivation with our lightning keys. Correctly claim all outputs in unilateral cases, and add corresponding test cases. Anchor output commitments should now work end-to-end (but there's no support for fee bumping yet). --- .../fr/acinq/eclair/channel/Channel.scala | 31 +- .../acinq/eclair/channel/ChannelTypes.scala | 4 +- .../fr/acinq/eclair/channel/Commitments.scala | 6 +- .../fr/acinq/eclair/channel/Helpers.scala | 34 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 14 +- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 2 +- .../eclair/wire/LegacyChannelCodecs.scala | 2 +- .../states/StateTestsHelperMethods.scala | 8 +- .../channel/states/e/NormalStateSpec.scala | 3 - .../channel/states/h/ClosingStateSpec.scala | 338 ++++++++++++++---- .../eclair/integration/IntegrationSpec.scala | 60 +++- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 4 +- .../eclair/payment/PaymentPacketSpec.scala | 8 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 8 +- 14 files changed, 389 insertions(+), 133 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 bb2f7301a2..a65c797268 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 @@ -177,7 +177,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubKey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), @@ -307,7 +307,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubkey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), @@ -2048,13 +2048,11 @@ 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 Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) } - - goto(CLOSING) using nextData storing() calling (doPublish(closingTx)) + goto(CLOSING) using nextData storing() calling doPublish(closingTx) } def doPublish(closingTx: Transaction): Unit = { @@ -2063,29 +2061,24 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } def spendLocalCurrent(d: HasCommitments) = { - val outdatedCommitment = d match { case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true case _ => false } - if (outdatedCommitment) { log.warning("we have an outdated commitment: will not publish our local tx") stay } else { val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx - 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)) } - - goto(CLOSING) using nextData storing() calling (doPublish(localCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished) } } @@ -2141,21 +2134,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") 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)) } - - goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished) } def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") d.commitments.channelVersion match { - case v if v.hasStaticRemotekey => + 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)) 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 @@ -2163,7 +2154,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId 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)) - goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished) } } @@ -2174,15 +2165,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(commitTx.txid == remoteCommit.txid, "txid mismatch") 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)) // 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)) } - - goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished) } def doPublish(remoteCommitPublished: RemoteCommitPublished): Unit = { @@ -2217,7 +2206,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // 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) } - goto(CLOSING) using nextData storing() calling (doPublish(revokedCommitPublished)) sending error + goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error case None => // the published tx was neither their current commitment nor a revoked one log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") @@ -2252,7 +2241,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val commitTx = d.commitments.localCommit.publishableTxs.commitTx.tx val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - goto(ERR_INFORMATION_LEAK) calling (doPublish(localCommitPublished)) sending error + goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished) sending error } def handleSync(channelReestablish: ChannelReestablish, d: HasCommitments): (Commitments, Queue[LightningMessage]) = { 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 6f7fbacc62..430604503c 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 @@ -309,7 +309,7 @@ final case class LocalParams(nodeId: PublicKey, maxAcceptedHtlcs: Int, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, - staticPaymentBasepoint: Option[PublicKey], + walletStaticPaymentBasepoint: Option[PublicKey], features: Features) final case class RemoteParams(nodeId: PublicKey, @@ -351,6 +351,8 @@ case class ChannelVersion(bits: BitVector) { def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT) def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT) def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT) + /** True if our main output in the remote commitment is directly sent to one of our wallet addresses. */ + def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs } object ChannelVersion { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 4920044e2b..1e26b2c2fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -65,7 +65,7 @@ case class Commitments(channelVersion: ChannelVersion, commitInput: InputInfo, remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) { - require(!channelVersion.hasStaticRemotekey || (channelVersion.hasStaticRemotekey && localParams.staticPaymentBasepoint.isDefined), s"localParams.localPaymentBasepoint must be defined for commitments with version=$channelVersion") + require(channelVersion.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (version=$channelVersion)") val commitmentFormat: CommitmentFormat = channelVersion.commitmentFormat @@ -617,7 +617,7 @@ object Commitments { val remotePaymentPubkey = if (channelVersion.hasStaticRemotekey) remoteParams.paymentBasepoint else Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localPaymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelVersion.commitmentFormat) val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs, channelVersion.commitmentFormat) @@ -634,7 +634,7 @@ object Commitments { spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val localPaymentBasepoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) val localPaymentPubkey = if (channelVersion.hasStaticRemotekey) localPaymentBasepoint else Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) 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 9220beb5eb..6189b60c98 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 @@ -632,7 +632,7 @@ object Helpers { }.toSeq.flatten channelVersion match { - case v if v.hasStaticRemotekey => + case v if v.paysDirectlyToWallet => RemoteCommitPublished( commitTx = tx, claimMainOutputTx = None, @@ -661,13 +661,22 @@ object Helpers { def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - val mainTx = generateTx("claim-p2wpkh-output") { - Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, localPubkey, sig) - }) + val mainTx = commitments.commitmentFormat match { + case DefaultCommitmentFormat => generateTx("claim-p2wpkh-output") { + Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, localPubkey, sig) + }) + } + case AnchorOutputsCommitmentFormat => generateTx("claim-remote-delayed-output") { + Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } } RemoteCommitPublished( @@ -693,7 +702,7 @@ object Helpers { require(tx.txIn.size == 1, "commitment tx should have 1 input") val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn.head.sequence, tx.lockTime) - val localPaymentPoint = localParams.staticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + 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.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long") @@ -715,9 +724,15 @@ object Helpers { // first we will claim our main output right away val mainTx = channelVersion match { - case v if v.hasStaticRemotekey => - log.info(s"channel uses option_static_remotekey, not claiming our p2wpkh output") + case v if v.paysDirectlyToWallet => + log.info(s"channel uses option_static_remotekey to pay directly to our wallet, not claiming our p2wpkh output") None + case v if v.hasAnchorOutputs => generateTx("claim-remote-delayed-output") { + Transactions.makeClaimRemoteDelayedOutputTx(tx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } case _ => generateTx("claim-p2wpkh-output") { Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) @@ -1071,6 +1086,7 @@ object Helpers { // is the commitment tx buried? (we need to check this because we may not have any outputs) val isCommitTxConfirmed = localCommitPublished.irrevocablySpent.values.toSet.contains(localCommitPublished.commitTx.txid) // are there remaining spendable outputs from the commitment tx? we just subtract all known spent outputs from the ones we control + // NB: we ignore anchors here, claiming them can be batched later val commitOutputsSpendableByUs = (localCommitPublished.claimMainDelayedOutputTx.toSeq ++ localCommitPublished.htlcSuccessTxs ++ localCommitPublished.htlcTimeoutTxs) .flatMap(_.txIn.map(_.outPoint)).toSet -- localCommitPublished.irrevocablySpent.keys // which htlc delayed txes can we expect to be confirmed? diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 1a61b42174..ea0e02afaf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -277,14 +277,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe } def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef], channelVersion: ChannelVersion): (ActorRef, LocalParams) = { - val (finalScript, localPaymentBasepoint) = channelVersion match { - case v if v.hasStaticRemotekey => + val (finalScript, walletStaticPaymentBasepoint) = channelVersion match { + case v if v.paysDirectlyToWallet => val walletKey = Helpers.getWalletPaymentBasepoint(wallet) (Script.write(Script.pay2wpkh(walletKey)), Some(walletKey)) case _ => (Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash), None) } - val localParams = makeChannelParams(nodeParams, finalScript, localPaymentBasepoint, funder, fundingAmount) + val localParams = makeChannelParams(nodeParams, finalScript, walletStaticPaymentBasepoint, funder, fundingAmount) val channel = spawnChannel(nodeParams, origin_opt) (channel, localParams) } @@ -391,13 +391,13 @@ object Peer { // @formatter:on - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubkey: ByteVector, localPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { // we make sure that funder and fundee key path end differently val fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isFunder) - makeChannelParams(nodeParams, defaultFinalScriptPubkey, localPaymentBasepoint, isFunder, fundingAmount, fundingKeyPath) + makeChannelParams(nodeParams, defaultFinalScriptPubkey, walletStaticPaymentBasepoint, isFunder, fundingAmount, fundingKeyPath) } - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubkey: ByteVector, staticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { LocalParams( nodeParams.nodeId, fundingKeyPath, @@ -409,7 +409,7 @@ object Peer { maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, isFunder = isFunder, defaultFinalScriptPubKey = defaultFinalScriptPubkey, - staticPaymentBasepoint = staticPaymentBasepoint, + walletStaticPaymentBasepoint = walletStaticPaymentBasepoint, features = nodeParams.features) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 7b8e257098..9dab2e02f8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -65,7 +65,7 @@ object ChannelCodecs extends Logging { ("maxAcceptedHtlcs" | uint16) :: ("isFunder" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: - ("localPaymentBasepoint" | optional(provide(channelVersion.hasStaticRemotekey), publicKey)) :: + ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] val remoteParamsCodec: Codec[RemoteParams] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala index f47b490a22..18c9533ffb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala @@ -69,7 +69,7 @@ private[wire] object LegacyChannelCodecs extends Logging { ("maxAcceptedHtlcs" | uint16) :: ("isFunder" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: - ("localPaymentBasepoint" | optional(provide(channelVersion.hasStaticRemotekey), publicKey)) :: + ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams].decodeOnly val remoteParamsCodec: Codec[RemoteParams] = ( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index fc3a41eeb0..e45d888147 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -82,13 +82,11 @@ trait StateTestsHelperMethods extends TestKitBase with FixtureTestSuite with Par val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams, channelVersion) = if (tags.contains("anchor_outputs")) { val features = Features(Set(ActivatedFeature(Features.StaticRemoteKey, FeatureSupport.Mandatory), ActivatedFeature(Features.AnchorOutputs, FeatureSupport.Optional))) - val aliceParams = Alice.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) - val bobParams = Bob.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) - (aliceParams, bobParams, ChannelVersion.ANCHOR_OUTPUTS) + (Alice.channelParams.copy(features = features), Bob.channelParams.copy(features = features), ChannelVersion.ANCHOR_OUTPUTS) } else if (tags.contains("static_remotekey")) { val features = Features(Set(ActivatedFeature(Features.StaticRemoteKey, FeatureSupport.Optional))) - val aliceParams = Alice.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) - val bobParams = Bob.channelParams.copy(features = features, staticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) + val aliceParams = Alice.channelParams.copy(features = features, walletStaticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) + val bobParams = Bob.channelParams.copy(features = features, walletStaticPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))) (aliceParams, bobParams, ChannelVersion.STATIC_REMOTEKEY) } else { (Alice.channelParams, Bob.channelParams, ChannelVersion.STANDARD) 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 7f47908208..5b2a534e8a 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 @@ -16,8 +16,6 @@ package fr.acinq.eclair.channel.states.e -import java.util.UUID - import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} @@ -32,7 +30,6 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{ChannelErrorOccurred, _} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.Peer -import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} 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 050b8c186c..c2dc3446bc 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 @@ -287,9 +287,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // NB: nominal case is tested in IntegrationSpec } - test("recv BITCOIN_FUNDING_SPENT (mutual close before converging)") { f => + def testMutualCloseBeforeConverge(f: FixtureParam, channelVersion: ChannelVersion): Unit = { import f._ val sender = TestProbe() + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion) // alice initiates a closing sender.send(alice, CMD_CLOSE(None)) alice2bob.expectMsgType[Shutdown] @@ -308,6 +309,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // let's make alice publish this closing tx alice ! Error(ByteVector32.Zeroes, "") awaitCond(alice.stateName == CLOSING) + alice2blockchain.expectMsg(PublishAsap(mutualCloseTx)) assert(mutualCloseTx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) // actual test starts here @@ -316,6 +318,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_FUNDING_SPENT (mutual close before converging)") { f => + testMutualCloseBeforeConverge(f, ChannelVersion.STANDARD) + } + + test("recv BITCOIN_FUNDING_SPENT (mutual close before converging, anchor outputs)", Tag("anchor_outputs")) { f => + testMutualCloseBeforeConverge(f, ChannelVersion.ANCHOR_OUTPUTS) + } + test("recv BITCOIN_TX_CONFIRMED (mutual close)") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) @@ -370,9 +380,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData == initialState) // this was a no-op } - test("recv BITCOIN_TX_CONFIRMED (local commit)") { f => + def testLocalCommitTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = { import f._ + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion) + val listener = TestProbe() system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) @@ -409,6 +421,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_TX_CONFIRMED (local commit)") { f => + testLocalCommitTxConfirmed(f, ChannelVersion.STANDARD) + } + + test("recv BITCOIN_TX_CONFIRMED (local commit, anchor outputs)", Tag("anchor_outputs")) { f => + testLocalCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS) + } + test("recv BITCOIN_TX_CONFIRMED (local commit with multiple htlcs for the same payment)") { f => import f._ @@ -531,6 +551,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(initialState.commitments.channelVersion === ChannelVersion.STANDARD) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxes.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs @@ -548,6 +569,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv BITCOIN_TX_CONFIRMED (remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelVersion === ChannelVersion.STATIC_REMOTEKEY) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxes.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs @@ -561,8 +583,29 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (remote commit with multiple htlcs for the same payment)") { f => + test("recv BITCOIN_TX_CONFIRMED (remote commit, anchor outputs)", Tag("anchor_outputs")) { f => import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(initialState.commitments.channelVersion === ChannelVersion.ANCHOR_OUTPUTS) + // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state + val bobCommitTx = bobCommitTxes.last.commitTx.tx + assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors + val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + + // actual test starts here + assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingState.claimHtlcSuccessTxs.isEmpty && closingState.claimHtlcTimeoutTxs.isEmpty) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimMainOutputTx.get), 0, 0, closingState.claimMainOutputTx.get) + awaitCond(alice.stateName == CLOSED) + } + + def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = { + import f._ + + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice) @@ -575,7 +618,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + if (channelVersion.hasAnchorOutputs) { + assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs + } else { + assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimHtlcTimeoutTxs.length === 3) @@ -595,6 +642,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_TX_CONFIRMED (remote commit with multiple htlcs for the same payment)") { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ChannelVersion.STANDARD) + } + + test("recv BITCOIN_TX_CONFIRMED (remote commit with multiple htlcs for the same payment, anchor outputs)", Tag("anchor_outputs")) { f => + testRemoteCommitTxWithHtlcsConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS) + } + test("recv BITCOIN_TX_CONFIRMED (remote commit) followed by CMD_FULFILL_HTLC") { f => import f._ // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. @@ -645,9 +700,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with relayerA.expectNoMsg(100 millis) } - private def testNextRemoteCommitTxConfirmed(f: FixtureParam): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { + private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { import f._ + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion) + // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice) // alice sends more htlcs with the same payment_hash @@ -665,7 +722,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + if (channelVersion.hasAnchorOutputs) { + assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs + } else { + assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimHtlcTimeoutTxs.length === 3) (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) @@ -673,7 +734,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv BITCOIN_TX_CONFIRMED (next remote commit)") { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f) + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.STANDARD) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimMainOutputTx.get), 45, 0, closingState.claimMainOutputTx.get) relayerA.expectNoMsg(100 millis) @@ -692,7 +753,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv BITCOIN_TX_CONFIRMED (next remote commit, static_remotekey)", Tag("static_remotekey")) { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f) + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.STATIC_REMOTEKEY) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output relayerA.expectNoMsg(100 millis) @@ -709,6 +770,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_TX_CONFIRMED (next remote commit, anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimMainOutputTx.get), 45, 0, closingState.claimMainOutputTx.get) + relayerA.expectNoMsg(100 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs.head), 201, 0, closingState.claimHtlcTimeoutTxs.head) + val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + relayerA.expectNoMsg(250 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(1)), 202, 0, closingState.claimHtlcTimeoutTxs(1)) + val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + relayerA.expectNoMsg(250 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(2)), 203, 1, closingState.claimHtlcTimeoutTxs(2)) + val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) + relayerA.expectNoMsg(250 millis) + awaitCond(alice.stateName == CLOSED) + } + test("recv BITCOIN_TX_CONFIRMED (next remote commit) followed by CMD_FULFILL_HTLC") { f => import f._ // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. @@ -759,10 +839,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with relayerA.expectNoMsg(100 millis) } - private def testFutureRemoteCommitTxConfirmed(f: FixtureParam): Transaction = { + private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Transaction = { import f._ val sender = TestProbe() val oldStateData = alice.stateData + assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion) // This HTLC will be fulfilled. val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) // These 2 HTLCs should timeout on-chain, but since alice lost data, she won't be able to claim them. @@ -796,14 +877,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + if (channelVersion.hasAnchorOutputs) { + assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs + } else { + assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + } alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) bobCommitTx } test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.STANDARD) // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -820,7 +905,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv BITCOIN_TX_CONFIRMED (future remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.STATIC_REMOTEKEY) // using option_static_remotekey alice doesn't need to sweep her output awaitCond(alice.stateName == CLOSING, 10 seconds) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) @@ -828,128 +913,235 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED, 10 seconds) } - private def testFundingSpentRevokedTx(f: FixtureParam) = { + test("recv BITCOIN_TX_CONFIRMED (future remote commit, anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS) + // alice is able to claim its main output + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx + Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid) + alice2blockchain.expectNoMsg(250 millis) // alice ignores the htlc-timeout + + // actual test starts here + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0, claimMainTx) + awaitCond(alice.stateName == CLOSED) + } + + private def testFundingSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): Transaction = { import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + assert(initialState.commitments.channelVersion === channelVersion) // bob publishes one of his revoked txes val bobRevokedTx = bobCommitTxes.head.commitTx.tx alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) + bobRevokedTx } test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => import f._ - testFundingSpentRevokedTx(f) + val revokedTx = testFundingSpentRevokedTx(f, ChannelVersion.STANDARD) + assert(revokedTx.txOut.length === 3) // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + val claimMain = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenalty = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenalty = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMain, mainPenalty, htlcPenalty)) { + Transaction.correctlySpends(penaltyTx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(Seq(claimMain, mainPenalty, htlcPenalty).map(_.txIn.head.outPoint).toSet.size === revokedTx.txOut.length) // spend all outpoints of the revoked tx + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenalty.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) } test("recv BITCOIN_FUNDING_SPENT (one revoked tx, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ - testFundingSpentRevokedTx(f) + val revokedTx = testFundingSpentRevokedTx(f, ChannelVersion.STATIC_REMOTEKEY) + assert(revokedTx.txOut.length === 3) // alice publishes and watches the penalty tx, but she won't claim her main output (claim-main) - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + val mainPenalty = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenalty = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(mainPenalty, htlcPenalty)) { + Transaction.correctlySpends(penaltyTx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(Seq(mainPenalty, htlcPenalty).map(_.txIn.head.outPoint).toSet.size === revokedTx.txOut.length - 1) // spend all outpoints of the revoked tx except our main output + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenalty.txIn.head.outPoint.index) + alice2blockchain.expectNoMsg(1 second) + } + + test("recv BITCOIN_FUNDING_SPENT (one revoked tx, anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val revokedTx = testFundingSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS) + assert(revokedTx.txOut.length === 5) + // alice publishes and watches the penalty tx + val claimMain = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenalty = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenalty = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMain, mainPenalty, htlcPenalty)) { + Transaction.correctlySpends(penaltyTx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(Seq(claimMain, mainPenalty, htlcPenalty).map(_.txIn.head.outPoint).toSet.size === revokedTx.txOut.length - 2) // spend all outpoints of the revoked tx except anchors + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenalty.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) } test("recv BITCOIN_FUNDING_SPENT (multiple revoked tx)") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + assert(bobCommitTxes.map(_.commitTx.tx.txid).toSet.size === bobCommitTxes.size) // all commit txs are distinct // bob publishes multiple revoked txes (last one isn't revoked) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes.head.commitTx.tx) // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + val claimMain1 = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenalty1 = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenalty1 = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMain1, mainPenalty1, htlcPenalty1)) { + Transaction.correctlySpends(penaltyTx, bobCommitTxes.head.commitTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTxes.head.commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMain1.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty1.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenalty1.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(1).commitTx.tx) - // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty + // alice publishes and watches the penalty tx (no HTLC in that commitment) + val claimMain2 = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenalty2 = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMain2, mainPenalty2)) { + Transaction.correctlySpends(penaltyTx, bobCommitTxes(1).commitTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTxes(1).commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMain2.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty2.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(2).commitTx.tx) // alice publishes and watches the penalty tx - alice2blockchain.expectMsgType[PublishAsap] // claim-main - alice2blockchain.expectMsgType[PublishAsap] // main-penalty - alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + val claimMain3 = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenalty3 = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenalty3 = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMain3, mainPenalty3, htlcPenalty3)) { + Transaction.correctlySpends(penaltyTx, bobCommitTxes(2).commitTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTxes(2).commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMain3.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty3.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenalty3.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 3) } - test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published HtlcSuccess tx)") { f => + def prepareOutputSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): PublishableTxs = { import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelVersion === channelVersion) // bob publishes one of his revoked txes val bobRevokedTx = bobCommitTxes.head alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) // alice publishes and watches the penalty tx - val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMainTx, mainPenaltyTx, htlcPenaltyTx)) { + Transaction.correctlySpends(penaltyTx, bobRevokedTx.commitTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobRevokedTx.commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenaltyTx.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenaltyTx.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx) - // actual test starts here alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx.commitTx.tx), 0, 0, bobRevokedTx.commitTx.tx) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0, claimMainTx) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mainPenaltyTx), 0, 0, mainPenaltyTx) alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, htlcPenaltyTx) // we published this - alice2blockchain.expectMsgType[WatchConfirmed] // htlc-penalty + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === htlcPenaltyTx.txid) + + bobRevokedTx + } + + test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published htlc-success tx)") { f => + import f._ + val bobRevokedTx = prepareOutputSpentRevokedTx(f, ChannelVersion.STANDARD) + assert(bobRevokedTx.commitTx.tx.txOut.size === 3) val bobHtlcSuccessTx = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx - alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx) // bob published his HtlcSuccess tx - alice2blockchain.expectMsgType[WatchConfirmed] // htlc-success - val claimHtlcDelayedPenaltyTxs = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's HtlcSuccess tx + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx) // bob published his htlc-success tx + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcSuccessTx.txid) + val claimHtlcDelayedPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's htlc-success tx + Transaction.correctlySpends(claimHtlcDelayedPenaltyTx, bobHtlcSuccessTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val watchOutput = alice2blockchain.expectMsgType[WatchSpent] + assert(watchOutput.txId === claimHtlcDelayedPenaltyTx.txIn.head.outPoint.txid) + assert(watchOutput.outputIndex === claimHtlcDelayedPenaltyTx.txIn.head.outPoint.index) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx), 0, 0, bobHtlcSuccessTx) // bob won - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTxs), 0, 0, claimHtlcDelayedPenaltyTxs) // bob won + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTx), 0, 0, claimHtlcDelayedPenaltyTx) // but alice claims the htlc output awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (one revoked tx)") { f => + test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published htlc-success tx, anchor outputs)", Tag("anchor_outputs")) { f => + import f._ + val bobRevokedTx = prepareOutputSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS) + assert(bobRevokedTx.commitTx.tx.txOut.size === 5) + + val bobHtlcSuccessTx1 = bobRevokedTx.htlcTxsAndSigs.head.txinfo.tx + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx1) // bob published his htlc-success tx + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcSuccessTx1.txid) + val claimHtlcDelayedPenaltyTx1 = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's htlc-success tx + Transaction.correctlySpends(claimHtlcDelayedPenaltyTx1, bobHtlcSuccessTx1 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val watchOutput1 = alice2blockchain.expectMsgType[WatchSpent] + assert(watchOutput1.txId === claimHtlcDelayedPenaltyTx1.txIn.head.outPoint.txid) + assert(watchOutput1.outputIndex === claimHtlcDelayedPenaltyTx1.txIn.head.outPoint.index) + + // Bob may RBF his htlc-success with a different transaction + val bobHtlcSuccessTx2 = bobHtlcSuccessTx1.copy(txIn = TxIn(OutPoint(randomBytes32, 0), Nil, 0) +: bobHtlcSuccessTx1.txIn) + assert(bobHtlcSuccessTx2.txid !== bobHtlcSuccessTx1.txid) + alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx2) // bob published a new version of his htlc-success tx + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcSuccessTx2.txid) + val claimHtlcDelayedPenaltyTx2 = alice2blockchain.expectMsgType[PublishAsap].tx // we publish a tx spending the output of bob's new htlc-success tx + Transaction.correctlySpends(claimHtlcDelayedPenaltyTx2, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val watchOutput2 = alice2blockchain.expectMsgType[WatchSpent] + assert(watchOutput2.txId === claimHtlcDelayedPenaltyTx2.txIn.head.outPoint.txid) + assert(watchOutput2.outputIndex === claimHtlcDelayedPenaltyTx2.txIn.head.outPoint.index) + + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx2), 0, 0, bobHtlcSuccessTx2) // bob won + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcDelayedPenaltyTx2), 0, 0, claimHtlcDelayedPenaltyTx2) // but alice claims the htlc output + awaitCond(alice.stateName == CLOSED) + } + + private def testRevokedTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = { import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelVersion === channelVersion) // bob publishes one of his revoked txes val bobRevokedTx = bobCommitTxes.head alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx.commitTx.tx) // alice publishes and watches the penalty tx - val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx // claim-main - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // main-penalty - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx // htlc-penalty - alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit - alice2blockchain.expectMsgType[WatchConfirmed] // claim-main - alice2blockchain.expectMsgType[WatchSpent] // main-penalty - alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx + val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx + for (penaltyTx <- Seq(claimMainTx, mainPenaltyTx, htlcPenaltyTx)) { + Transaction.correctlySpends(penaltyTx, bobRevokedTx.commitTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobRevokedTx.commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenaltyTx.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === htlcPenaltyTx.txIn.head.outPoint.index) alice2blockchain.expectNoMsg(1 second) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx) @@ -962,6 +1154,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_TX_CONFIRMED (one revoked tx)") { f => + testRevokedTxConfirmed(f, ChannelVersion.STANDARD) + } + + test("recv BITCOIN_TX_CONFIRMED (one revoked tx, anchor outputs)", Tag("anchor_outputs")) { f => + testRevokedTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS) + } + test("recv ChannelReestablish") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 91670b8587..5abfc5512c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -935,11 +935,16 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS val bitcoinClient = new ExtendedBitcoinClient(bitcoinrpcclient) bitcoinClient.getTransaction(commitTx.txid).pipeTo(sender.ref) val tx = sender.expectMsgType[Transaction](10 seconds) - // the unilateral close contains the static toRemote output assert(tx.txOut.exists(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + // bury the unilateral close in a block, C should claim its main output + generateBlocks(bitcoincli, 2) + awaitCond({ + bitcoinClient.getMempool().pipeTo(sender.ref) + sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint.txid === commitTx.txid) + }, max = 20 seconds, interval = 1 second) - // bury the unilateral close in a block, since there are no outputs to claim the channel can go to CLOSED state + // get the claim-remote-output confirmed, then the channel can go to the CLOSED state generateBlocks(bitcoincli, 2) awaitCond({ nodes("C").register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE) @@ -1086,6 +1091,10 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS testDownstreamFulfillLocalCommit("F1", Transactions.DefaultCommitmentFormat) } + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs)") { + testDownstreamFulfillLocalCommit("F2", Transactions.AnchorOutputsCommitmentFormat) + } + def testDownstreamFulfillRemoteCommit(nodeF: String, commitmentFormat: Transactions.CommitmentFormat): Unit = { val forceCloseFixture = prepareForceCloseCF(nodeF, commitmentFormat) import forceCloseFixture._ @@ -1129,6 +1138,10 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS testDownstreamFulfillRemoteCommit("F1", Transactions.DefaultCommitmentFormat) } + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs)") { + testDownstreamFulfillRemoteCommit("F2", Transactions.AnchorOutputsCommitmentFormat) + } + def testDownstreamTimeoutLocalCommit(nodeF: String, commitmentFormat: Transactions.CommitmentFormat): Unit = { val forceCloseFixture = prepareForceCloseCF(nodeF, commitmentFormat) import forceCloseFixture._ @@ -1183,6 +1196,10 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS testDownstreamTimeoutLocalCommit("F1", Transactions.DefaultCommitmentFormat) } + test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs)") { + testDownstreamTimeoutLocalCommit("F2", Transactions.AnchorOutputsCommitmentFormat) + } + def testDownstreamTimeoutRemoteCommit(nodeF: String, commitmentFormat: Transactions.CommitmentFormat): Unit = { val forceCloseFixture = prepareForceCloseCF(nodeF, commitmentFormat) import forceCloseFixture._ @@ -1241,6 +1258,10 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS testDownstreamTimeoutRemoteCommit("F1", Transactions.DefaultCommitmentFormat) } + test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs)") { + testDownstreamTimeoutRemoteCommit("F2", Transactions.AnchorOutputsCommitmentFormat) + } + case class RevokedCommitFixture(sender: TestProbe, stateListenerC: TestProbe, revokedCommitTx: Transaction, htlcSuccess: Seq[Transaction], htlcTimeout: Seq[Transaction], finalAddressC: String) def testRevokedCommit(nodeF: String, commitmentFormat: Transactions.CommitmentFormat): RevokedCommitFixture = { @@ -1295,9 +1316,12 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments sigListener.expectNoMsg(1 second) assert(commitmentsF.commitmentFormat === commitmentFormat) - // in this commitment, both parties should have a main output, and there are four pending htlcs + // in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable val localCommitF = commitmentsF.localCommit.publishableTxs - assert(localCommitF.commitTx.tx.txOut.size === 6) + commitmentFormat match { + case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTx.tx.txOut.size === 6) + case Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTx.tx.txOut.size === 8) + } val htlcTimeoutTxs = localCommitF.htlcTxsAndSigs.collect { case h@HtlcTxAndSigs(_: Transactions.HtlcTimeoutTx, _, _) => h } val htlcSuccessTxs = localCommitF.htlcTxsAndSigs.collect { case h@HtlcTxAndSigs(_: Transactions.HtlcSuccessTx, _, _) => h } assert(htlcTimeoutTxs.size === 2) @@ -1359,6 +1383,34 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS awaitAnnouncements(nodes.filterKeys(_ == "A").toMap, 5, 7, 16) } + test("punish a node that has published a revoked commit tx (anchor outputs)") { + val revokedCommitFixture = testRevokedCommit("F2", Transactions.AnchorOutputsCommitmentFormat) + import revokedCommitFixture._ + + val bitcoinClient = new ExtendedBitcoinClient(bitcoinrpcclient) + // we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test + val previouslyReceivedByC = listReceivedByAddress(finalAddressC, sender) + // F publishes the revoked commitment: it can't publish the HTLC txs because of the CSV 1 + bitcoinClient.publishTransaction(revokedCommitTx).pipeTo(sender.ref) + sender.expectMsg(revokedCommitTx.txid) + // get the revoked commitment confirmed: now HTLC txs can be published + generateBlocks(bitcoincli, 2) + bitcoinClient.publishTransaction(htlcSuccess.head).pipeTo(sender.ref) + sender.expectMsg(htlcSuccess.head.txid) + bitcoinClient.publishTransaction(htlcTimeout.head).pipeTo(sender.ref) + sender.expectMsg(htlcTimeout.head.txid) + // at this point C should have 6 recv transactions: its previous main output, F's main output and all htlc outputs (taken as punishment) + awaitCond({ + val receivedByC = listReceivedByAddress(finalAddressC, sender) + (receivedByC diff previouslyReceivedByC).size == 6 + }, max = 30 seconds, interval = 1 second) + // we generate blocks to make txs confirm + generateBlocks(bitcoincli, 2) + // and we wait for C's channel to close + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) + awaitAnnouncements(nodes.filterKeys(_ == "A").toMap, 5, 7, 16) + } + test("generate and validate lots of channels") { implicit val bitcoinClient: ExtendedBitcoinClient = new ExtendedBitcoinClient(bitcoinrpcclient) // we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 50c6bafd9e..dc44fde7d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -298,8 +298,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe assert(info.state == WAIT_FOR_ACCEPT_CHANNEL) val inputInit = info.data.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder assert(inputInit.channelVersion.hasStaticRemotekey) - assert(inputInit.localParams.staticPaymentBasepoint.isDefined) - assert(inputInit.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(inputInit.localParams.staticPaymentBasepoint.get))) + assert(inputInit.localParams.walletStaticPaymentBasepoint.isDefined) + assert(inputInit.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(inputInit.localParams.walletStaticPaymentBasepoint.get))) } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index c32d67449d..180d63a98c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features._ -import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPacket._ @@ -396,11 +396,13 @@ object PaymentPacketSpec { packetType.create(sessionKey, nodes, payloadsBin, associatedData).packet } - def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat): Commitments = - new Commitments(ChannelVersion.STANDARD, null, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) { + def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat): Commitments = { + val params = LocalParams(null, null, null, null, null, null, null, 0, isFunder = true, null, None, null) + new Commitments(ChannelVersion.STANDARD, params, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, null, null, channelId) { override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } + } def randomExtendedPrivateKey: ExtendedPrivateKey = DeterministicWallet.generate(randomBytes32) 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 c24530151d..d80e990936 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 @@ -121,14 +121,14 @@ class ChannelCodecsSpec extends AnyFunSuite { toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), - staticPaymentBasepoint = None, + walletStaticPaymentBasepoint = None, isFunder = Random.nextBoolean(), features = Features(randomBytes(256))) - val o1 = o.copy(staticPaymentBasepoint = Some(PrivateKey(randomBytes32).publicKey)) + val o1 = o.copy(walletStaticPaymentBasepoint = Some(PrivateKey(randomBytes32).publicKey)) roundtrip(o, localParamsCodec(ChannelVersion.ZEROES)) roundtrip(o1, localParamsCodec(ChannelVersion.STATIC_REMOTEKEY)) - roundtrip(o1, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS)) + roundtrip(o, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS)) } test("backward compatibility local params with global features") { @@ -452,7 +452,7 @@ object ChannelCodecsSpec { toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, - staticPaymentBasepoint = None, + walletStaticPaymentBasepoint = None, isFunder = true, features = Features.empty) From d140f8bee908e287f5a6cab5888dbc3aae71846e Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 21 Sep 2020 14:33:58 +0200 Subject: [PATCH 2/2] Rename transaction types --- .../acinq/eclair/channel/ChannelTypes.scala | 2 +- .../fr/acinq/eclair/channel/Helpers.scala | 8 ++-- .../eclair/transactions/Transactions.scala | 30 ++++++------- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 2 +- .../eclair/wire/LegacyChannelCodecs.scala | 2 +- .../transactions/TransactionsSpec.scala | 42 +++++++++---------- 6 files changed, 43 insertions(+), 43 deletions(-) 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 430604503c..f53033b4e2 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 @@ -351,7 +351,7 @@ case class ChannelVersion(bits: BitVector) { def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT) def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT) def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT) - /** True if our main output in the remote commitment is directly sent to one of our wallet addresses. */ + /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs } 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 6189b60c98..56de799884 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 @@ -533,7 +533,7 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output") { - Transactions.makeClaimDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).right.map(claimDelayed => { + Transactions.makeClaimLocalDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).right.map(claimDelayed => { val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimDelayed, sig) }) @@ -563,7 +563,7 @@ object Helpers { val htlcDelayedTxes = htlcTxes.flatMap { txinfo: TransactionWithInputInfo => generateTx("claim-htlc-delayed") { - Transactions.makeClaimDelayedOutputTx(txinfo.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).right.map(claimDelayed => { + Transactions.makeClaimLocalDelayedOutputTx(txinfo.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).right.map(claimDelayed => { val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimDelayed, sig) }) @@ -725,7 +725,7 @@ object Helpers { // first we will claim our main output right away val mainTx = channelVersion match { case v if v.paysDirectlyToWallet => - log.info(s"channel uses option_static_remotekey to pay directly to our wallet, not claiming our p2wpkh output") + log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") None case v if v.hasAnchorOutputs => generateTx("claim-remote-delayed-output") { Transactions.makeClaimRemoteDelayedOutputTx(tx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { @@ -814,7 +814,7 @@ object Helpers { val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) generateTx("claim-htlc-delayed-penalty") { - Transactions.makeClaimDelayedOutputPenaltyTx(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).right.map(htlcDelayedPenalty => { + Transactions.makeClaimHtlcDelayedOutputPenaltyTx(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).right.map(htlcDelayedPenalty => { val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // we need to make sure that the tx is indeed valid diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 1d8d764e6c..2a43a48a17 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -109,8 +109,8 @@ object Transactions { case class ClaimAnchorOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class ClaimRemoteDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo - case class ClaimDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo - case class ClaimDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo + case class ClaimLocalDelayedOutputTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo + case class ClaimHtlcDelayedOutputPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class MainPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class HtlcPenaltyTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo case class ClosingTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo @@ -122,12 +122,12 @@ object Transactions { /** * When *local* *current* [[CommitTx]] is published: - * - [[ClaimDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay + * - [[ClaimLocalDelayedOutputTx]] spends to-local output of [[CommitTx]] after a delay * - When using anchor outputs, [[ClaimAnchorOutputTx]] spends to-local anchor of [[CommitTx]] * - [[HtlcSuccessTx]] spends htlc-received outputs of [[CommitTx]] for which we have the preimage - * - [[ClaimDelayedOutputTx]] spends [[HtlcSuccessTx]] after a delay + * - [[ClaimLocalDelayedOutputTx]] spends [[HtlcSuccessTx]] after a delay * - [[HtlcTimeoutTx]] spends htlc-sent outputs of [[CommitTx]] after a timeout - * - [[ClaimDelayedOutputTx]] spends [[HtlcTimeoutTx]] after a delay + * - [[ClaimLocalDelayedOutputTx]] spends [[HtlcTimeoutTx]] after a delay * * When *remote* *current* [[CommitTx]] is published: * - When using the default commitment format, [[ClaimP2WPKHOutputTx]] spends to-local output of [[CommitTx]] @@ -142,9 +142,9 @@ object Transactions { * - When using anchor outputs, [[ClaimAnchorOutputTx]] spends to-local anchor of [[CommitTx]] * - [[MainPenaltyTx]] spends remote main output using the per-commitment secret * - [[HtlcSuccessTx]] spends htlc-sent outputs of [[CommitTx]] for which they have the preimage (published by remote) - * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] using the revocation secret (published by local) + * - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcSuccessTx]] using the revocation secret (published by local) * - [[HtlcTimeoutTx]] spends htlc-received outputs of [[CommitTx]] after a timeout (published by remote) - * - [[ClaimDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) + * - [[ClaimHtlcDelayedOutputPenaltyTx]] spends [[HtlcTimeoutTx]] using the revocation secret (published by local) * - [[HtlcPenaltyTx]] spends competes with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] for the same outputs (published by local) */ @@ -567,7 +567,7 @@ object Transactions { } } - def makeClaimDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimDelayedOutputTx] = { + def makeClaimLocalDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimLocalDelayedOutputTx] = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match { @@ -581,14 +581,14 @@ object Transactions { txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, lockTime = 0) // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() + val weight = addSigs(ClaimLocalDelayedOutputTx(input, tx), PlaceHolderSig).tx.weight() val fee = weight2fee(feeratePerKw, weight) val amount = input.txOut.amount - fee if (amount < localDustLimit) { Left(AmountBelowDustLimit) } else { val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimDelayedOutputTx(input, tx1)) + Right(ClaimLocalDelayedOutputTx(input, tx1)) } } } @@ -610,7 +610,7 @@ object Transactions { } } - def makeClaimDelayedOutputPenaltyTx(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimDelayedOutputPenaltyTx] = { + def makeClaimHtlcDelayedOutputPenaltyTx(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx] = { val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey) val pubkeyScript = write(pay2wsh(redeemScript)) findPubKeyScriptIndex(htlcTx, pubkeyScript, amount_opt = None) match { @@ -624,14 +624,14 @@ object Transactions { txOut = TxOut(Satoshi(0), localFinalScriptPubKey) :: Nil, lockTime = 0) // compute weight with a dummy 73 bytes signature (the largest you can get) - val weight = addSigs(ClaimDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() + val weight = addSigs(ClaimHtlcDelayedOutputPenaltyTx(input, tx), PlaceHolderSig).tx.weight() val fee = weight2fee(feeratePerKw, weight) val amount = input.txOut.amount - fee if (amount < localDustLimit) { Left(AmountBelowDustLimit) } else { val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil) - Right(ClaimDelayedOutputPenaltyTx(input, tx1)) + Right(ClaimHtlcDelayedOutputPenaltyTx(input, tx1)) } } } @@ -786,7 +786,7 @@ object Transactions { claimRemoteDelayedOutputTx.copy(tx = claimRemoteDelayedOutputTx.tx.updateWitness(0, witness)) } - def addSigs(claimDelayedOutputTx: ClaimDelayedOutputTx, localSig: ByteVector64): ClaimDelayedOutputTx = { + def addSigs(claimDelayedOutputTx: ClaimLocalDelayedOutputTx, localSig: ByteVector64): ClaimLocalDelayedOutputTx = { val witness = witnessToLocalDelayedAfterDelay(localSig, claimDelayedOutputTx.input.redeemScript) claimDelayedOutputTx.copy(tx = claimDelayedOutputTx.tx.updateWitness(0, witness)) } @@ -796,7 +796,7 @@ object Transactions { claimAnchorOutputTx.copy(tx = claimAnchorOutputTx.tx.updateWitness(0, witness)) } - def addSigs(claimHtlcDelayedPenalty: ClaimDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimDelayedOutputPenaltyTx = { + def addSigs(claimHtlcDelayedPenalty: ClaimHtlcDelayedOutputPenaltyTx, revocationSig: ByteVector64): ClaimHtlcDelayedOutputPenaltyTx = { val witness = Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, claimHtlcDelayedPenalty.input.redeemScript) claimHtlcDelayedPenalty.copy(tx = claimHtlcDelayedPenalty.tx.updateWitness(0, witness)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 9dab2e02f8..e287460f45 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -111,7 +111,7 @@ object ChannelCodecs extends Logging { .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcSuccessTx]) .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcTimeoutTx]) .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) - .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimDelayedOutputTx]) + .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) .typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx]) .typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx]) .typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClosingTx]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala index 18c9533ffb..8551bf6f1a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LegacyChannelCodecs.scala @@ -120,7 +120,7 @@ private[wire] object LegacyChannelCodecs extends Logging { .typecase(0x04, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcSuccessTx]) .typecase(0x05, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimHtlcTimeoutTx]) .typecase(0x06, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimP2WPKHOutputTx]) - .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimDelayedOutputTx]) + .typecase(0x07, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClaimLocalDelayedOutputTx]) .typecase(0x08, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[MainPenaltyTx]) .typecase(0x09, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[HtlcPenaltyTx]) .typecase(0x10, (("inputInfo" | inputInfoCodec) :: ("tx" | txCodec)).as[ClosingTx]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index d642592f1d..1327abfcda 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -111,7 +111,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the ClaimDelayedOutputTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) val htlcSuccessOrTimeoutTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) - val Right(claimHtlcDelayedTx) = makeClaimDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedTx) = makeClaimLocalDelayedOutputTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcDelayedTx, PlaceHolderSig).tx) assert(claimHtlcDelayedWeight == weight) @@ -285,12 +285,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // local spends delayed output of htlc1 timeout tx - val Right(claimHtlcDelayed) = makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayed) = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayed1 === Left(OutputNotFound)) } { @@ -315,17 +315,17 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // local spends delayed output of htlc2 success tx - val Right(claimHtlcDelayed) = makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayed) = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 success tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayed1 === Left(AmountBelowDustLimit)) } { // local spends main delayed output - val Right(claimMainOutputTx) = makeClaimDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) @@ -353,12 +353,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends htlc1's htlc-timeout tx with revocation key - val Right(claimHtlcDelayedPenaltyTx) = makeClaimDelayedOutputPenaltyTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedPenaltyTx) = makeClaimHtlcDelayedOutputPenaltyTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimDelayedOutputPenaltyTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayedPenaltyTx1 === Left(AmountBelowDustLimit)) } { @@ -375,12 +375,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends htlc2's htlc-success tx with revocation key - val Right(claimHtlcDelayedPenaltyTx) = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedPenaltyTx) = makeClaimHtlcDelayedOutputPenaltyTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayedPenaltyTx1 === Left(AmountBelowDustLimit)) } { @@ -493,7 +493,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output - val Right(claimMainOutputTx) = makeClaimDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimMainOutputTx, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) @@ -551,12 +551,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // local spends delayed output of htlc1 timeout tx - val Right(claimHtlcDelayed) = makeClaimDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayed) = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayed1 === Left(OutputNotFound)) } { @@ -580,15 +580,15 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // local spends delayed output of htlc2a and htlc2b success txs - val Right(claimHtlcDelayedA) = makeClaimDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val Right(claimHtlcDelayedB) = makeClaimDelayedOutputTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedA) = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedB) = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcDelayed <- Seq(claimHtlcDelayedA, claimHtlcDelayedB)) { val localSig = sign(claimHtlcDelayed, localDelayedPaymentPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signedTx = addSigs(claimHtlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) } // local can't claim delayed output of htlc4 success tx because it is below the dust limit - val claimHtlcDelayed1 = makeClaimDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayed1 === Left(AmountBelowDustLimit)) } { @@ -602,12 +602,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends htlc1's htlc-timeout tx with revocation key - val Right(claimHtlcDelayedPenaltyTx) = makeClaimDelayedOutputPenaltyTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedPenaltyTx) = makeClaimHtlcDelayedOutputPenaltyTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val sig = sign(claimHtlcDelayedPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimDelayedOutputPenaltyTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayedPenaltyTx1 === Left(AmountBelowDustLimit)) } { @@ -621,15 +621,15 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { // remote spends htlc2a/htlc2b's htlc-success tx with revocation key - val Right(claimHtlcDelayedPenaltyTxA) = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val Right(claimHtlcDelayedPenaltyTxB) = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedPenaltyTxA) = makeClaimHtlcDelayedOutputPenaltyTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val Right(claimHtlcDelayedPenaltyTxB) = makeClaimHtlcDelayedOutputPenaltyTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { val sig = sign(claimHtlcSuccessPenaltyTx, localRevocationPriv, TxOwner.Local, AnchorOutputsCommitmentFormat) val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit - val claimHtlcDelayedPenaltyTx1 = makeClaimDelayedOutputPenaltyTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) + val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) assert(claimHtlcDelayedPenaltyTx1 === Left(AmountBelowDustLimit)) } {