Skip to content

Commit c37eb1a

Browse files
authored
Handle aggregated anchor outputs htlc txs (#1738)
An interesting side-effect of anchor outputs is that htlc txs can be merged when they have the same lockTime (thanks to sighash flags). We're not currently doing that, but our peers may do it, so we need to handle it in the revoked commit tx case and correctly claim multiple outputs if necessary.
1 parent f202587 commit c37eb1a

File tree

6 files changed

+155
-53
lines changed

6 files changed

+155
-53
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
134134
// now let's sign the funding tx
135135
SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(fundTxResponse.tx)
136136
// there will probably be a change output, so we need to find which output is ours
137-
outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, amount_opt = None) match {
137+
outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript) match {
138138
case Right(outputIndex) => Future.successful(outputIndex)
139139
case Left(skipped) => Future.failed(new RuntimeException(skipped.toString))
140140
}

eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -1340,9 +1340,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
13401340
}
13411341
}
13421342
val revokedCommitPublished1 = d.revokedCommitPublished.map { rev =>
1343-
val (rev1, tx_opt) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator)
1344-
tx_opt.foreach(claimTx => blockchain ! PublishAsap(claimTx.tx, PublishStrategy.JustPublish))
1345-
tx_opt.foreach(claimTx => blockchain ! WatchSpent(self, tx, claimTx.input.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT, hints = Set(claimTx.tx.txid)))
1343+
val (rev1, penaltyTxs) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator)
1344+
penaltyTxs.foreach(claimTx => blockchain ! PublishAsap(claimTx.tx, PublishStrategy.JustPublish))
1345+
penaltyTxs.foreach(claimTx => blockchain ! WatchSpent(self, tx, claimTx.input.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT, hints = Set(claimTx.tx.txid)))
13461346
rev1
13471347
}
13481348
stay using d.copy(revokedCommitPublished = revokedCommitPublished1) storing()

eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala

+20-19
Original file line numberDiff line numberDiff line change
@@ -850,8 +850,11 @@ object Helpers {
850850
* - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by
851851
* an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand",
852852
* this is the purpose of this method.
853+
*
854+
* NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same
855+
* lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs.
853856
*/
854-
def claimRevokedHtlcTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Option[ClaimHtlcDelayedOutputPenaltyTx]) = {
857+
def claimRevokedHtlcTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = {
855858
val isHtlcTx = htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) &&
856859
htlcTx.txIn.map(_.witness).collect(Scripts.extractPreimageFromHtlcSuccess.orElse(Scripts.extractPaymentHashFromHtlcTimeout)).nonEmpty
857860
if (isHtlcTx) {
@@ -867,32 +870,30 @@ object Helpers {
867870
// now we know what commit number this tx is referring to, we can derive the commitment point from the shachain
868871
remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber)
869872
.map(d => PrivateKey(d))
870-
.flatMap(remotePerCommitmentSecret => {
873+
.map(remotePerCommitmentSecret => {
871874
val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey
872875
val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint)
873876
val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint)
874877

875878
// we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty
876879
val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1)
877880

878-
generateTx("claim-htlc-delayed-penalty") {
879-
Transactions.makeClaimHtlcDelayedOutputPenaltyTx(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcDelayedPenalty => {
880-
val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat)
881-
val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig)
882-
// we need to make sure that the tx is indeed valid
883-
Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
884-
signedTx
885-
})
886-
}
887-
}) match {
888-
case Some(tx) =>
889-
val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs :+ tx)
890-
(revokedCommitPublished1, Some(tx))
891-
case None =>
892-
(revokedCommitPublished, None)
893-
}
881+
val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => {
882+
generateTx("claim-htlc-delayed-penalty") {
883+
claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => {
884+
val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat)
885+
val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig)
886+
// we need to make sure that the tx is indeed valid
887+
Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
888+
signedTx
889+
})
890+
}
891+
})
892+
val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs)
893+
(revokedCommitPublished1, penaltyTxs)
894+
}).getOrElse((revokedCommitPublished, Nil))
894895
} else {
895-
(revokedCommitPublished, None)
896+
(revokedCommitPublished, Nil)
896897
}
897898
}
898899

eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala

+24-14
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ object Transactions {
568568
def makeClaimP2WPKHOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimP2WPKHOutputTx] = {
569569
val redeemScript = Script.pay2pkh(localPaymentPubkey)
570570
val pubkeyScript = write(pay2wpkh(localPaymentPubkey))
571-
findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match {
571+
findPubKeyScriptIndex(commitTx, pubkeyScript) match {
572572
case Left(skip) => Left(skip)
573573
case Right(outputIndex) =>
574574
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
@@ -594,7 +594,7 @@ object Transactions {
594594
def makeClaimRemoteDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimRemoteDelayedOutputTx] = {
595595
val redeemScript = toRemoteDelayed(localPaymentPubkey)
596596
val pubkeyScript = write(pay2wsh(redeemScript))
597-
findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match {
597+
findPubKeyScriptIndex(commitTx, pubkeyScript) match {
598598
case Left(skip) => Left(skip)
599599
case Right(outputIndex) =>
600600
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
@@ -620,7 +620,7 @@ object Transactions {
620620
def makeClaimLocalDelayedOutputTx(commitTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimLocalDelayedOutputTx] = {
621621
val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)
622622
val pubkeyScript = write(pay2wsh(redeemScript))
623-
findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match {
623+
findPubKeyScriptIndex(commitTx, pubkeyScript) match {
624624
case Left(skip) => Left(skip)
625625
case Right(outputIndex) =>
626626
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
@@ -646,7 +646,7 @@ object Transactions {
646646
private def makeClaimAnchorOutputTx(commitTx: Transaction, fundingPubkey: PublicKey): Either[TxGenerationSkipped, (InputInfo, Transaction)] = {
647647
val redeemScript = anchor(fundingPubkey)
648648
val pubkeyScript = write(pay2wsh(redeemScript))
649-
findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match {
649+
findPubKeyScriptIndex(commitTx, pubkeyScript) match {
650650
case Left(skip) => Left(skip)
651651
case Right(outputIndex) =>
652652
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
@@ -668,12 +668,12 @@ object Transactions {
668668
makeClaimAnchorOutputTx(commitTx, remoteFundingPubkey).map { case (input, tx) => ClaimRemoteAnchorOutputTx(input, tx) }
669669
}
670670

671-
def makeClaimHtlcDelayedOutputPenaltyTx(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx] = {
671+
def makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx: Transaction, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = {
672672
val redeemScript = toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)
673673
val pubkeyScript = write(pay2wsh(redeemScript))
674-
findPubKeyScriptIndex(htlcTx, pubkeyScript, amount_opt = None) match {
675-
case Left(skip) => Left(skip)
676-
case Right(outputIndex) =>
674+
findPubKeyScriptIndexes(htlcTx, pubkeyScript) match {
675+
case Left(skip) => Seq(Left(skip))
676+
case Right(outputIndexes) => outputIndexes.map(outputIndex => {
677677
val input = InputInfo(OutPoint(htlcTx, outputIndex), htlcTx.txOut(outputIndex), write(redeemScript))
678678
// unsigned transaction
679679
val tx = Transaction(
@@ -691,13 +691,14 @@ object Transactions {
691691
val tx1 = tx.copy(txOut = tx.txOut.head.copy(amount = amount) :: Nil)
692692
Right(ClaimHtlcDelayedOutputPenaltyTx(input, tx1))
693693
}
694+
})
694695
}
695696
}
696697

697698
def makeMainPenaltyTx(commitTx: Transaction, localDustLimit: Satoshi, remoteRevocationPubkey: PublicKey, localFinalScriptPubKey: ByteVector, toRemoteDelay: CltvExpiryDelta, remoteDelayedPaymentPubkey: PublicKey, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, MainPenaltyTx] = {
698699
val redeemScript = toLocalDelayed(remoteRevocationPubkey, toRemoteDelay, remoteDelayedPaymentPubkey)
699700
val pubkeyScript = write(pay2wsh(redeemScript))
700-
findPubKeyScriptIndex(commitTx, pubkeyScript, amount_opt = None) match {
701+
findPubKeyScriptIndex(commitTx, pubkeyScript) match {
701702
case Left(skip) => Left(skip)
702703
case Right(outputIndex) =>
703704
val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), write(redeemScript))
@@ -760,21 +761,30 @@ object Transactions {
760761
txIn = TxIn(commitTxInput.outPoint, ByteVector.empty, sequence = 0xffffffffL) :: Nil,
761762
txOut = toLocalOutput_opt.toSeq ++ toRemoteOutput_opt.toSeq ++ Nil,
762763
lockTime = 0))
763-
val toLocalOutput = findPubKeyScriptIndex(tx, localScriptPubKey, None).map(index => OutputInfo(index, toLocalAmount, localScriptPubKey)).toOption
764+
val toLocalOutput = findPubKeyScriptIndex(tx, localScriptPubKey).map(index => OutputInfo(index, toLocalAmount, localScriptPubKey)).toOption
764765
ClosingTx(commitTxInput, tx, toLocalOutput)
765766
}
766767

767-
def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector, amount_opt: Option[Satoshi]): Either[TxGenerationSkipped, Int] = {
768-
val outputIndex = tx.txOut
769-
.zipWithIndex
770-
.indexWhere { case (txOut, _) => amount_opt.forall(_ == txOut.amount) && txOut.publicKeyScript == pubkeyScript }
768+
def findPubKeyScriptIndex(tx: Transaction, pubkeyScript: ByteVector): Either[TxGenerationSkipped, Int] = {
769+
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == pubkeyScript)
771770
if (outputIndex >= 0) {
772771
Right(outputIndex)
773772
} else {
774773
Left(OutputNotFound)
775774
}
776775
}
777776

777+
def findPubKeyScriptIndexes(tx: Transaction, pubkeyScript: ByteVector): Either[TxGenerationSkipped, Seq[Int]] = {
778+
val outputIndexes = tx.txOut.zipWithIndex.collect {
779+
case (txOut, index) if txOut.publicKeyScript == pubkeyScript => index
780+
}
781+
if (outputIndexes.nonEmpty) {
782+
Right(outputIndexes)
783+
} else {
784+
Left(OutputNotFound)
785+
}
786+
}
787+
778788
/**
779789
* Default public key used for fee estimation
780790
*/

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala

+80-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.h
1818

1919
import akka.testkit.{TestFSMRef, TestProbe}
2020
import fr.acinq.bitcoin.Crypto.PrivateKey
21-
import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, ScriptFlags, Transaction, TxIn}
21+
import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction, TxIn, TxOut}
2222
import fr.acinq.eclair.TestConstants.{Alice, Bob}
2323
import fr.acinq.eclair.blockchain._
2424
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw}
@@ -27,10 +27,10 @@ import fr.acinq.eclair.channel._
2727
import fr.acinq.eclair.channel.states.{StateTestsBase, StateTestsTags}
2828
import fr.acinq.eclair.payment._
2929
import fr.acinq.eclair.payment.relay.Relayer._
30-
import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx}
30+
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, HtlcSuccessTx, HtlcTimeoutTx}
3131
import fr.acinq.eclair.transactions.{Scripts, Transactions}
3232
import fr.acinq.eclair.wire.protocol._
33-
import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32}
33+
import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey}
3434
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
3535
import org.scalatest.{Outcome, Tag}
3636
import scodec.bits.ByteVector
@@ -1442,6 +1442,83 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
14421442
testOutputSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS)
14431443
}
14441444

1445+
test("recv BITCOIN_OUTPUT_SPENT (one revoked tx, counterparty published aggregated htlc tx)", Tag(StateTestsTags.AnchorOutputs)) { f =>
1446+
import f._
1447+
1448+
// bob publishes one of his revoked txs
1449+
val revokedCloseFixture = prepareRevokedClose(f, ChannelVersion.ANCHOR_OUTPUTS)
1450+
val bobRevokedTxs = revokedCloseFixture.bobRevokedTxs(2)
1451+
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTxs.commitTx.tx)
1452+
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
1453+
assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat === AnchorOutputsCommitmentFormat)
1454+
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
1455+
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
1456+
assert(rvk.commitTx === bobRevokedTxs.commitTx.tx)
1457+
assert(rvk.htlcPenaltyTxs.size === 4)
1458+
assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty)
1459+
1460+
// alice publishes the penalty txs and watches outputs
1461+
(1 to 6).foreach(_ => alice2blockchain.expectMsgType[PublishAsap]) // 2 main outputs and 4 htlcs
1462+
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === rvk.commitTx.txid)
1463+
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid)
1464+
(1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchSpent]) // main output penalty and 4 htlc penalties
1465+
alice2blockchain.expectNoMsg(1 second)
1466+
1467+
// bob claims multiple htlc outputs in a single transaction (this is possible with anchor outputs because signatures
1468+
// use sighash_single | sighash_anyonecanpay)
1469+
val bobHtlcTxs = bobRevokedTxs.htlcTxsAndSigs.collect {
1470+
case HtlcTxAndSigs(txInfo: HtlcSuccessTx, localSig, remoteSig) =>
1471+
val preimage = revokedCloseFixture.htlcsAlice.collectFirst { case (add, preimage) if add.id == txInfo.htlcId => preimage }.get
1472+
assert(Crypto.sha256(preimage) === txInfo.paymentHash)
1473+
Transactions.addSigs(txInfo, localSig, remoteSig, preimage, AnchorOutputsCommitmentFormat)
1474+
case HtlcTxAndSigs(txInfo: HtlcTimeoutTx, localSig, remoteSig) =>
1475+
Transactions.addSigs(txInfo, localSig, remoteSig, AnchorOutputsCommitmentFormat)
1476+
}
1477+
assert(bobHtlcTxs.map(_.input.outPoint).size === 4)
1478+
val bobHtlcTx = Transaction(
1479+
2,
1480+
Seq(
1481+
TxIn(OutPoint(randomBytes32, 4), Nil, 1), // utxo used for fee bumping
1482+
bobHtlcTxs(0).tx.txIn.head,
1483+
bobHtlcTxs(1).tx.txIn.head,
1484+
bobHtlcTxs(2).tx.txIn.head,
1485+
bobHtlcTxs(3).tx.txIn.head
1486+
),
1487+
Seq(
1488+
TxOut(10000 sat, Script.pay2wpkh(randomKey.publicKey)), // change output
1489+
bobHtlcTxs(0).tx.txOut.head,
1490+
bobHtlcTxs(1).tx.txOut.head,
1491+
bobHtlcTxs(2).tx.txOut.head,
1492+
bobHtlcTxs(3).tx.txOut.head
1493+
),
1494+
0
1495+
)
1496+
1497+
// alice reacts by publishing penalty txs that spend bob's htlc transaction
1498+
alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcTx)
1499+
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 4)
1500+
val claimHtlcDelayedPenaltyTxs = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs
1501+
val spentOutpoints = Set(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4))
1502+
assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet === spentOutpoints)
1503+
claimHtlcDelayedPenaltyTxs.foreach(claimHtlcPenalty => Transaction.correctlySpends(claimHtlcPenalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
1504+
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcTx.txid)
1505+
val publishedPenaltyTxs = Set(
1506+
alice2blockchain.expectMsgType[PublishAsap],
1507+
alice2blockchain.expectMsgType[PublishAsap],
1508+
alice2blockchain.expectMsgType[PublishAsap],
1509+
alice2blockchain.expectMsgType[PublishAsap]
1510+
)
1511+
assert(publishedPenaltyTxs.map(_.tx) === claimHtlcDelayedPenaltyTxs.map(_.tx).toSet)
1512+
val watchedOutpoints = Seq(
1513+
alice2blockchain.expectMsgType[WatchSpent],
1514+
alice2blockchain.expectMsgType[WatchSpent],
1515+
alice2blockchain.expectMsgType[WatchSpent],
1516+
alice2blockchain.expectMsgType[WatchSpent]
1517+
).map(w => OutPoint(w.txId.reverse, w.outputIndex)).toSet
1518+
assert(watchedOutpoints === spentOutpoints)
1519+
alice2blockchain.expectNoMsg(1 second)
1520+
}
1521+
14451522
private def testRevokedTxConfirmed(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
14461523
import f._
14471524
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)

0 commit comments

Comments
 (0)