mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-24 14:50:46 +01:00
Settle HTLCs revoked commit (#1630)
When a revoked commitment is published, we didn't correctly settle pending HTLCs, which could lead to upstream channel closure. This would not cause a loss of funds (especially since we would gain funds from the revoked channel) but it's a temporary liquidity loss that we'd like to avoid.
This commit is contained in:
parent
95b34f270f
commit
810323ca30
6 changed files with 544 additions and 224 deletions
|
@ -1354,7 +1354,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
}
|
||||
// for our outgoing payments, let's send events if we know that they will settle on chain
|
||||
Closing
|
||||
.onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx)
|
||||
.onChainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx)
|
||||
.map(add => (add, d.commitments.originChannels.get(add.id).collect { case o: Origin.Local => o.id })) // we resolve the payment id if this was a local payment
|
||||
.collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = add.amountMsat, add.paymentHash)) }
|
||||
// and we also send events related to fee
|
||||
|
|
|
@ -269,8 +269,24 @@ sealed trait HasCommitments extends Data {
|
|||
|
||||
case class ClosingTxProposed(unsignedTx: Transaction, localClosingSigned: ClosingSigned)
|
||||
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32])
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32])
|
||||
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) {
|
||||
def isConfirmed: Boolean = {
|
||||
// NB: if multiple transactions end up in the same block, the first confirmation we receive may not be the commit tx.
|
||||
// However if the confirmed tx spends from the commit tx, we know that the commit tx is already confirmed and we know
|
||||
// the type of closing.
|
||||
val confirmedTxs = irrevocablySpent.values.toSet
|
||||
(commitTx :: claimMainDelayedOutputTx.toList ::: htlcSuccessTxs ::: htlcTimeoutTxs ::: claimHtlcDelayedTxs).exists(tx => confirmedTxs.contains(tx.txid))
|
||||
}
|
||||
}
|
||||
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], claimHtlcSuccessTxs: List[Transaction], claimHtlcTimeoutTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32]) {
|
||||
def isConfirmed: Boolean = {
|
||||
// NB: if multiple transactions end up in the same block, the first confirmation we receive may not be the commit tx.
|
||||
// However if the confirmed tx spends from the commit tx, we know that the commit tx is already confirmed and we know
|
||||
// the type of closing.
|
||||
val confirmedTxs = irrevocablySpent.values.toSet
|
||||
(commitTx :: claimMainOutputTx.toList ::: claimHtlcSuccessTxs ::: claimHtlcTimeoutTxs).exists(tx => confirmedTxs.contains(tx.txid))
|
||||
}
|
||||
}
|
||||
case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[Transaction], mainPenaltyTx: Option[Transaction], htlcPenaltyTxs: List[Transaction], claimHtlcDelayedPenaltyTxs: List[Transaction], irrevocablySpent: Map[OutPoint, ByteVector32])
|
||||
|
||||
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data {
|
||||
|
|
|
@ -380,27 +380,14 @@ object Helpers {
|
|||
* @return the channel closing type, if applicable
|
||||
*/
|
||||
def isClosingTypeAlreadyKnown(closing: DATA_CLOSING): Option[ClosingType] = {
|
||||
// NB: if multiple transactions end up in the same block, the first confirmation we receive may not be the commit tx.
|
||||
// However if the confirmed tx spends from the commit tx, we know that the commit tx is already confirmed and we know
|
||||
// the type of closing.
|
||||
def isLocalCommitConfirmed(lcp: LocalCommitPublished): Boolean = {
|
||||
val confirmedTxs = lcp.irrevocablySpent.values.toSet
|
||||
(lcp.commitTx :: lcp.claimMainDelayedOutputTx.toList ::: lcp.htlcSuccessTxs ::: lcp.htlcTimeoutTxs ::: lcp.claimHtlcDelayedTxs).exists(tx => confirmedTxs.contains(tx.txid))
|
||||
}
|
||||
|
||||
def isRemoteCommitConfirmed(rcp: RemoteCommitPublished): Boolean = {
|
||||
val confirmedTxs = rcp.irrevocablySpent.values.toSet
|
||||
(rcp.commitTx :: rcp.claimMainOutputTx.toList ::: rcp.claimHtlcSuccessTxs ::: rcp.claimHtlcTimeoutTxs).exists(tx => confirmedTxs.contains(tx.txid))
|
||||
}
|
||||
|
||||
closing match {
|
||||
case _ if closing.localCommitPublished.exists(isLocalCommitConfirmed) =>
|
||||
case _ if closing.localCommitPublished.exists(_.isConfirmed) =>
|
||||
Some(LocalClose(closing.commitments.localCommit, closing.localCommitPublished.get))
|
||||
case _ if closing.remoteCommitPublished.exists(isRemoteCommitConfirmed) =>
|
||||
case _ if closing.remoteCommitPublished.exists(_.isConfirmed) =>
|
||||
Some(CurrentRemoteClose(closing.commitments.remoteCommit, closing.remoteCommitPublished.get))
|
||||
case _ if closing.nextRemoteCommitPublished.exists(isRemoteCommitConfirmed) =>
|
||||
case _ if closing.nextRemoteCommitPublished.exists(_.isConfirmed) =>
|
||||
Some(NextRemoteClose(closing.commitments.remoteNextCommitInfo.left.get.nextRemoteCommit, closing.nextRemoteCommitPublished.get))
|
||||
case _ if closing.futureRemoteCommitPublished.exists(isRemoteCommitConfirmed) =>
|
||||
case _ if closing.futureRemoteCommitPublished.exists(_.isConfirmed) =>
|
||||
Some(RecoveryClose(closing.futureRemoteCommitPublished.get))
|
||||
case _ if closing.revokedCommitPublished.exists(rcp => rcp.irrevocablySpent.values.toSet.contains(rcp.commitTx.txid)) =>
|
||||
Some(RevokedClose(closing.revokedCommitPublished.find(rcp => rcp.irrevocablySpent.values.toSet.contains(rcp.commitTx.txid)).get))
|
||||
|
@ -953,7 +940,7 @@ object Helpers {
|
|||
*
|
||||
* @param tx a transaction that is sufficiently buried in the blockchain
|
||||
*/
|
||||
def onchainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = {
|
||||
def onChainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = {
|
||||
if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) {
|
||||
localCommit.spec.htlcs.collect(outgoing)
|
||||
} else if (remoteCommit.txid == tx.txid) {
|
||||
|
@ -966,10 +953,8 @@ object Helpers {
|
|||
}
|
||||
|
||||
/**
|
||||
* If a local commitment tx reaches min_depth, we need to fail the outgoing htlcs that only us had signed, because
|
||||
* they will never reach the blockchain.
|
||||
*
|
||||
* Those are only present in the remote's commitment.
|
||||
* If a commitment tx reaches min_depth, we need to fail the outgoing htlcs that will never reach the blockchain.
|
||||
* It could be because only us had signed them, or because a revoked commitment got confirmed.
|
||||
*/
|
||||
def overriddenOutgoingHtlcs(d: DATA_CLOSING, tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = {
|
||||
val localCommit = d.commitments.localCommit
|
||||
|
@ -994,6 +979,12 @@ object Helpers {
|
|||
} else if (nextRemoteCommit_opt.map(_.txid).contains(tx.txid)) {
|
||||
// their last commitment got confirmed, so no htlcs will be overridden, they will timeout or be fulfilled on chain
|
||||
Set.empty
|
||||
} else if (d.revokedCommitPublished.map(_.commitTx.txid).contains(tx.txid)) {
|
||||
// a revoked commitment got confirmed: we will claim its outputs, but we also need to fail htlcs that are pending in the latest commitment:
|
||||
// - outgoing htlcs that are in the local commitment but not in remote/nextRemote have already been fulfilled/failed so we don't care about them
|
||||
// - outgoing htlcs that are in the remote/nextRemote commitment may not really be overridden, but since we are going to claim their output as a
|
||||
// punishment we will never get the preimage and may as well consider them failed in the context of relaying htlcs
|
||||
nextRemoteCommit_opt.getOrElse(remoteCommit).spec.htlcs.collect(incoming)
|
||||
} else {
|
||||
Set.empty
|
||||
}
|
||||
|
@ -1061,17 +1052,19 @@ object Helpers {
|
|||
* @param tx a transaction that has been irrevocably confirmed
|
||||
*/
|
||||
def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction): RevokedCommitPublished = {
|
||||
// even if our txes only have one input, maybe our counterparty uses a different scheme so we need to iterate
|
||||
// even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate
|
||||
// over all of them to check if they are relevant
|
||||
val relevantOutpoints = tx.txIn.map(_.outPoint).filter { outPoint =>
|
||||
// is this the commit tx itself ? (we could do this outside of the loop...)
|
||||
val isCommitTx = revokedCommitPublished.commitTx.txid == tx.txid
|
||||
// does the tx spend an output of the remote commitment tx?
|
||||
val spendsTheCommitTx = revokedCommitPublished.commitTx.txid == outPoint.txid
|
||||
// is the tx one of our 3rd stage delayed txes? (a 3rd stage tx is a tx spending the output of an htlc tx, which
|
||||
// is the tx one of our 3rd stage delayed txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which
|
||||
// is itself spending the output of the commitment tx)
|
||||
val is3rdStageDelayedTx = revokedCommitPublished.claimHtlcDelayedPenaltyTxs.map(_.txid).contains(tx.txid)
|
||||
isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx
|
||||
// does the tx spend an output of an htlc tx? (in which case it may invalidate one of our claim-htlc-delayed-penalty)
|
||||
val spendsHtlcOutput = revokedCommitPublished.claimHtlcDelayedPenaltyTxs.flatMap(_.txIn).map(_.outPoint).contains(outPoint)
|
||||
isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx || spendsHtlcOutput
|
||||
}
|
||||
// then we add the relevant outpoints to the map keeping track of which txid spends which outpoint
|
||||
revokedCommitPublished.copy(irrevocablySpent = revokedCommitPublished.irrevocablySpent ++ relevantOutpoints.map(o => o -> tx.txid).toMap)
|
||||
|
@ -1119,11 +1112,13 @@ object Helpers {
|
|||
// are there remaining spendable outputs from the commitment tx?
|
||||
val commitOutputsSpendableByUs = (revokedCommitPublished.claimMainOutputTx.toSeq ++ revokedCommitPublished.mainPenaltyTx ++ revokedCommitPublished.htlcPenaltyTxs)
|
||||
.flatMap(_.txIn.map(_.outPoint)).toSet -- revokedCommitPublished.irrevocablySpent.keys
|
||||
// which htlc delayed txes can we expect to be confirmed?
|
||||
val unconfirmedHtlcDelayedTxes = revokedCommitPublished.claimHtlcDelayedPenaltyTxs
|
||||
.filter(tx => (tx.txIn.map(_.outPoint.txid).toSet -- revokedCommitPublished.irrevocablySpent.values).isEmpty) // only the txes which parents are already confirmed may get confirmed (note that this also eliminates outputs that have been double-spent by a competing tx)
|
||||
.filterNot(tx => revokedCommitPublished.irrevocablySpent.values.toSet.contains(tx.txid)) // has the tx already been confirmed?
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty && unconfirmedHtlcDelayedTxes.isEmpty
|
||||
// which htlc delayed txs can we expect to be confirmed?
|
||||
val unconfirmedHtlcDelayedTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs
|
||||
// only the txs which parents are already confirmed may get confirmed (note that this also eliminates outputs that have been double-spent by a competing tx)
|
||||
.filter(tx => (tx.txIn.map(_.outPoint.txid).toSet -- revokedCommitPublished.irrevocablySpent.values).isEmpty)
|
||||
// if one of the tx inputs has been spent, the tx has already been confirmed or a competing tx has been confirmed
|
||||
.filterNot(tx => tx.txIn.exists(txIn => revokedCommitPublished.irrevocablySpent.contains(txIn.outPoint)))
|
||||
isCommitTxConfirmed && commitOutputsSpendableByUs.isEmpty && unconfirmedHtlcDelayedTxs.isEmpty
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
package fr.acinq.eclair.channel
|
||||
|
||||
import fr.acinq.bitcoin.{OutPoint, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.channel.Helpers.Closing
|
||||
import fr.acinq.eclair.transactions.Transactions
|
||||
import fr.acinq.eclair.{LongToBtcAmount, randomBytes32}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
class ChannelTypesSpec extends AnyFunSuite {
|
||||
|
||||
|
@ -45,4 +49,198 @@ class ChannelTypesSpec extends AnyFunSuite {
|
|||
}
|
||||
}
|
||||
|
||||
test("local commit published") {
|
||||
val (lcp, _, _) = createClosingTransactions()
|
||||
assert(!lcp.isConfirmed)
|
||||
assert(!Closing.isLocalCommitDone(lcp))
|
||||
|
||||
// Commit tx has been confirmed.
|
||||
val lcp1 = Closing.updateLocalCommitPublished(lcp, lcp.commitTx)
|
||||
assert(lcp1.irrevocablySpent.nonEmpty)
|
||||
assert(lcp1.isConfirmed)
|
||||
assert(!Closing.isLocalCommitDone(lcp1))
|
||||
|
||||
// Main output has been confirmed.
|
||||
val lcp2 = Closing.updateLocalCommitPublished(lcp1, lcp.claimMainDelayedOutputTx.get)
|
||||
assert(lcp2.isConfirmed)
|
||||
assert(!Closing.isLocalCommitDone(lcp2))
|
||||
|
||||
// Our htlc-success txs and their 3rd-stage claim txs have been confirmed.
|
||||
val lcp3 = Seq(lcp.htlcSuccessTxs.head, lcp.claimHtlcDelayedTxs.head, lcp.htlcSuccessTxs(1), lcp.claimHtlcDelayedTxs(1)).foldLeft(lcp2) {
|
||||
case (current, tx) => Closing.updateLocalCommitPublished(current, tx)
|
||||
}
|
||||
assert(lcp3.isConfirmed)
|
||||
assert(!Closing.isLocalCommitDone(lcp3))
|
||||
|
||||
// Scenario 1: our htlc-timeout txs and their 3rd-stage claim txs have been confirmed.
|
||||
{
|
||||
val lcp4a = Seq(lcp.htlcTimeoutTxs.head, lcp.claimHtlcDelayedTxs(2), lcp.htlcTimeoutTxs(1)).foldLeft(lcp3) {
|
||||
case (current, tx) => Closing.updateLocalCommitPublished(current, tx)
|
||||
}
|
||||
assert(lcp4a.isConfirmed)
|
||||
assert(!Closing.isLocalCommitDone(lcp4a))
|
||||
|
||||
val lcp4b = Closing.updateLocalCommitPublished(lcp4a, lcp.claimHtlcDelayedTxs(3))
|
||||
assert(lcp4b.isConfirmed)
|
||||
assert(Closing.isLocalCommitDone(lcp4b))
|
||||
}
|
||||
|
||||
// Scenario 2: they claim the htlcs we sent before our htlc-timeout.
|
||||
{
|
||||
val claimHtlcSuccess1 = lcp.htlcTimeoutTxs.head.copy(txOut = Seq(TxOut(3000.sat, ByteVector.empty), TxOut(2500.sat, ByteVector.empty)))
|
||||
val lcp4a = Closing.updateLocalCommitPublished(lcp3, claimHtlcSuccess1)
|
||||
assert(lcp4a.isConfirmed)
|
||||
assert(!Closing.isLocalCommitDone(lcp4a))
|
||||
|
||||
val claimHtlcSuccess2 = lcp.htlcTimeoutTxs(1).copy(txOut = Seq(TxOut(3500.sat, ByteVector.empty), TxOut(3100.sat, ByteVector.empty)))
|
||||
val lcp4b = Closing.updateLocalCommitPublished(lcp4a, claimHtlcSuccess2)
|
||||
assert(lcp4b.isConfirmed)
|
||||
assert(Closing.isLocalCommitDone(lcp4b))
|
||||
}
|
||||
}
|
||||
|
||||
test("remote commit published") {
|
||||
val (_, rcp, _) = createClosingTransactions()
|
||||
assert(!rcp.isConfirmed)
|
||||
assert(!Closing.isRemoteCommitDone(rcp))
|
||||
|
||||
// Commit tx has been confirmed.
|
||||
val rcp1 = Closing.updateRemoteCommitPublished(rcp, rcp.commitTx)
|
||||
assert(rcp1.irrevocablySpent.nonEmpty)
|
||||
assert(rcp1.isConfirmed)
|
||||
assert(!Closing.isRemoteCommitDone(rcp1))
|
||||
|
||||
// Main output has been confirmed.
|
||||
val rcp2 = Closing.updateRemoteCommitPublished(rcp1, rcp.claimMainOutputTx.get)
|
||||
assert(rcp2.isConfirmed)
|
||||
assert(!Closing.isRemoteCommitDone(rcp2))
|
||||
|
||||
// One of our claim-htlc-success and claim-htlc-timeout has been confirmed.
|
||||
val rcp3 = Seq(rcp.claimHtlcSuccessTxs.head, rcp.claimHtlcTimeoutTxs.head).foldLeft(rcp2) {
|
||||
case (current, tx) => Closing.updateRemoteCommitPublished(current, tx)
|
||||
}
|
||||
assert(rcp3.isConfirmed)
|
||||
assert(!Closing.isRemoteCommitDone(rcp3))
|
||||
|
||||
// Scenario 1: our remaining claim-htlc txs have been confirmed.
|
||||
{
|
||||
val rcp4a = Closing.updateRemoteCommitPublished(rcp3, rcp.claimHtlcSuccessTxs(1))
|
||||
assert(rcp4a.isConfirmed)
|
||||
assert(!Closing.isRemoteCommitDone(rcp4a))
|
||||
|
||||
val rcp4b = Closing.updateRemoteCommitPublished(rcp4a, rcp.claimHtlcTimeoutTxs(1))
|
||||
assert(rcp4b.isConfirmed)
|
||||
assert(Closing.isRemoteCommitDone(rcp4b))
|
||||
}
|
||||
|
||||
// Scenario 2: they claim the remaining htlc outputs.
|
||||
{
|
||||
val htlcSuccess = rcp.claimHtlcSuccessTxs(1).copy(txOut = Seq(TxOut(3000.sat, ByteVector.empty), TxOut(2500.sat, ByteVector.empty)))
|
||||
val rcp4a = Closing.updateRemoteCommitPublished(rcp3, htlcSuccess)
|
||||
assert(rcp4a.isConfirmed)
|
||||
assert(!Closing.isRemoteCommitDone(rcp4a))
|
||||
|
||||
val htlcTimeout = rcp.claimHtlcTimeoutTxs(1).copy(txOut = Seq(TxOut(3500.sat, ByteVector.empty), TxOut(3100.sat, ByteVector.empty)))
|
||||
val rcp4b = Closing.updateRemoteCommitPublished(rcp4a, htlcTimeout)
|
||||
assert(rcp4b.isConfirmed)
|
||||
assert(Closing.isRemoteCommitDone(rcp4b))
|
||||
}
|
||||
}
|
||||
|
||||
test("revoked commit published") {
|
||||
val (_, _, rvk) = createClosingTransactions()
|
||||
assert(!Closing.isRevokedCommitDone(rvk))
|
||||
|
||||
// Commit tx has been confirmed.
|
||||
val rvk1 = Closing.updateRevokedCommitPublished(rvk, rvk.commitTx)
|
||||
assert(rvk1.irrevocablySpent.nonEmpty)
|
||||
assert(!Closing.isRevokedCommitDone(rvk1))
|
||||
|
||||
// Main output has been confirmed.
|
||||
val rvk2 = Closing.updateRevokedCommitPublished(rvk1, rvk.claimMainOutputTx.get)
|
||||
assert(!Closing.isRevokedCommitDone(rvk2))
|
||||
|
||||
// Two of our htlc penalty txs have been confirmed.
|
||||
val rvk3 = Seq(rvk.htlcPenaltyTxs.head, rvk.htlcPenaltyTxs(1)).foldLeft(rvk2) {
|
||||
case (current, tx) => Closing.updateRevokedCommitPublished(current, tx)
|
||||
}
|
||||
assert(!Closing.isRevokedCommitDone(rvk3))
|
||||
|
||||
// Scenario 1: the remaining penalty txs have been confirmed.
|
||||
{
|
||||
val rvk4a = Seq(rvk.htlcPenaltyTxs(2), rvk.htlcPenaltyTxs(3)).foldLeft(rvk3) {
|
||||
case (current, tx) => Closing.updateRevokedCommitPublished(current, tx)
|
||||
}
|
||||
assert(!Closing.isRevokedCommitDone(rvk4a))
|
||||
|
||||
val rvk4b = Closing.updateRevokedCommitPublished(rvk4a, rvk.mainPenaltyTx.get)
|
||||
assert(Closing.isRevokedCommitDone(rvk4b))
|
||||
}
|
||||
|
||||
// Scenario 2: they claim the remaining outputs.
|
||||
{
|
||||
val remoteMainOutput = rvk.mainPenaltyTx.get.copy(txOut = Seq(TxOut(35000.sat, ByteVector.empty)))
|
||||
val rvk4a = Closing.updateRevokedCommitPublished(rvk3, remoteMainOutput)
|
||||
assert(!Closing.isRevokedCommitDone(rvk4a))
|
||||
|
||||
val htlcSuccess = rvk.htlcPenaltyTxs(2).copy(txOut = Seq(TxOut(3000.sat, ByteVector.empty), TxOut(2500.sat, ByteVector.empty)))
|
||||
val htlcTimeout = rvk.htlcPenaltyTxs(3).copy(txOut = Seq(TxOut(3500.sat, ByteVector.empty), TxOut(3100.sat, ByteVector.empty)))
|
||||
// When Bob claims these outputs, the channel should call Helpers.claimRevokedHtlcTxOutputs to punish them by claiming the output of their htlc tx.
|
||||
val rvk4b = Seq(htlcSuccess, htlcTimeout).foldLeft(rvk4a) {
|
||||
case (current, tx) => Closing.updateRevokedCommitPublished(current, tx)
|
||||
}.copy(
|
||||
claimHtlcDelayedPenaltyTxs = List(
|
||||
Transaction(2, Seq(TxIn(OutPoint(htlcSuccess, 0), ByteVector.empty, 0)), Seq(TxOut(5000.sat, ByteVector.empty)), 0),
|
||||
Transaction(2, Seq(TxIn(OutPoint(htlcTimeout, 0), ByteVector.empty, 0)), Seq(TxOut(6000.sat, ByteVector.empty)), 0)
|
||||
)
|
||||
)
|
||||
assert(!Closing.isRevokedCommitDone(rvk4b))
|
||||
|
||||
// We claim one of the remaining outputs, they claim the other.
|
||||
val rvk5a = Closing.updateRevokedCommitPublished(rvk4b, rvk4b.claimHtlcDelayedPenaltyTxs.head)
|
||||
assert(!Closing.isRevokedCommitDone(rvk5a))
|
||||
val theyClaimHtlcTimeout = rvk4b.claimHtlcDelayedPenaltyTxs(1).copy(txOut = Seq(TxOut(1500.sat, ByteVector.empty), TxOut(2500.sat, ByteVector.empty)))
|
||||
val rvk5b = Closing.updateRevokedCommitPublished(rvk5a, theyClaimHtlcTimeout)
|
||||
assert(Closing.isRevokedCommitDone(rvk5b))
|
||||
}
|
||||
}
|
||||
|
||||
private def createClosingTransactions(): (LocalCommitPublished, RemoteCommitPublished, RevokedCommitPublished) = {
|
||||
val commitTx = Transaction(
|
||||
2,
|
||||
Seq(TxIn(OutPoint(randomBytes32, 0), ByteVector.empty, 0)),
|
||||
Seq(
|
||||
TxOut(50000.sat, ByteVector.empty), // main output Alice
|
||||
TxOut(40000.sat, ByteVector.empty), // main output Bob
|
||||
TxOut(4000.sat, ByteVector.empty), // htlc received #1
|
||||
TxOut(5000.sat, ByteVector.empty), // htlc received #2
|
||||
TxOut(6000.sat, ByteVector.empty), // htlc sent #1
|
||||
TxOut(7000.sat, ByteVector.empty), // htlc sent #2
|
||||
),
|
||||
0
|
||||
)
|
||||
val claimMainAlice = Transaction(2, Seq(TxIn(OutPoint(commitTx, 0), ByteVector.empty, 144)), Seq(TxOut(49500.sat, ByteVector.empty)), 0)
|
||||
val htlcSuccess1 = Transaction(2, Seq(TxIn(OutPoint(commitTx, 2), ByteVector.empty, 1)), Seq(TxOut(3500.sat, ByteVector.empty)), 0)
|
||||
val htlcSuccess2 = Transaction(2, Seq(TxIn(OutPoint(commitTx, 3), ByteVector.empty, 1)), Seq(TxOut(4500.sat, ByteVector.empty)), 0)
|
||||
val htlcTimeout1 = Transaction(2, Seq(TxIn(OutPoint(commitTx, 4), ByteVector.empty, 1)), Seq(TxOut(5500.sat, ByteVector.empty)), 0)
|
||||
val htlcTimeout2 = Transaction(2, Seq(TxIn(OutPoint(commitTx, 5), ByteVector.empty, 1)), Seq(TxOut(6500.sat, ByteVector.empty)), 0)
|
||||
|
||||
val localCommit = {
|
||||
val claimHtlcDelayedTxs = List(
|
||||
Transaction(2, Seq(TxIn(OutPoint(htlcSuccess1, 0), ByteVector.empty, 1)), Seq(TxOut(3400.sat, ByteVector.empty)), 0),
|
||||
Transaction(2, Seq(TxIn(OutPoint(htlcSuccess2, 0), ByteVector.empty, 1)), Seq(TxOut(4400.sat, ByteVector.empty)), 0),
|
||||
Transaction(2, Seq(TxIn(OutPoint(htlcTimeout1, 0), ByteVector.empty, 1)), Seq(TxOut(5400.sat, ByteVector.empty)), 0),
|
||||
Transaction(2, Seq(TxIn(OutPoint(htlcTimeout2, 0), ByteVector.empty, 1)), Seq(TxOut(6400.sat, ByteVector.empty)), 0),
|
||||
)
|
||||
LocalCommitPublished(commitTx, Some(claimMainAlice), List(htlcSuccess1, htlcSuccess2), List(htlcTimeout1, htlcTimeout2), claimHtlcDelayedTxs, Map.empty)
|
||||
}
|
||||
val remoteCommit = RemoteCommitPublished(commitTx, Some(claimMainAlice), List(htlcSuccess1, htlcSuccess2), List(htlcTimeout1, htlcTimeout2), Map.empty)
|
||||
val revokedCommit = {
|
||||
val mainPenalty = Transaction(2, Seq(TxIn(OutPoint(commitTx, 1), ByteVector.empty, 0)), Seq(TxOut(39500.sat, ByteVector.empty)), 0)
|
||||
RevokedCommitPublished(commitTx, Some(claimMainAlice), Some(mainPenalty), List(htlcSuccess1, htlcSuccess2, htlcTimeout1, htlcTimeout2), Nil, Map.empty)
|
||||
}
|
||||
|
||||
(localCommit, remoteCommit, revokedCommit)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -151,13 +151,20 @@ trait StateTestsHelperMethods extends TestKitBase with FixtureTestSuite with Par
|
|||
htlc
|
||||
}
|
||||
|
||||
def fulfillHtlc(id: Long, R: ByteVector32, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): Unit = {
|
||||
s ! CMD_FULFILL_HTLC(id, R)
|
||||
def fulfillHtlc(id: Long, preimage: ByteVector32, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): Unit = {
|
||||
s ! CMD_FULFILL_HTLC(id, preimage)
|
||||
val fulfill = s2r.expectMsgType[UpdateFulfillHtlc]
|
||||
s2r.forward(r)
|
||||
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fulfill))
|
||||
}
|
||||
|
||||
def failHtlc(id: Long, s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): Unit = {
|
||||
s ! CMD_FAIL_HTLC(id, Right(TemporaryNodeFailure))
|
||||
val fail = s2r.expectMsgType[UpdateFailHtlc]
|
||||
s2r.forward(r)
|
||||
awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fail))
|
||||
}
|
||||
|
||||
def crossSign(s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe): Unit = {
|
||||
val sender = TestProbe()
|
||||
val sCommitIndex = s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index
|
||||
|
|
|
@ -84,7 +84,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
} else {
|
||||
within(30 seconds) {
|
||||
reachNormal(setup, test.tags)
|
||||
val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield {
|
||||
val bobCommitTxs: List[PublishableTxs] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield {
|
||||
val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
relayerB.expectMsgType[RelayForward]
|
||||
|
@ -101,7 +101,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
|
||||
awaitCond(alice.stateName == NORMAL)
|
||||
awaitCond(bob.stateName == NORMAL)
|
||||
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, bobCommitTxes)))
|
||||
withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, bobCommitTxs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -934,235 +934,339 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
|||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
private def testFundingSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): Transaction = {
|
||||
case class RevokedCloseFixture(bobRevokedTxs: Seq[PublishableTxs], htlcsAlice: Seq[UpdateAddHtlc], htlcsBob: Seq[UpdateAddHtlc])
|
||||
|
||||
private def prepareRevokedClose(f: FixtureParam, channelVersion: ChannelVersion): RevokedCloseFixture = {
|
||||
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
|
||||
|
||||
// Bob's first commit tx doesn't contain any htlc
|
||||
val commitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
if (channelVersion.hasAnchorOutputs) {
|
||||
assert(commitTx1.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors
|
||||
} else {
|
||||
assert(commitTx1.commitTx.tx.txOut.size === 2) // 2 main outputs
|
||||
}
|
||||
|
||||
// Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc
|
||||
val (commitTx2, htlcAlice1, htlcBob1) = {
|
||||
val (_, htlcAlice) = addHtlc(35000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
val (_, htlcBob) = addHtlc(20000000 msat, bob, alice, bob2alice, alice2bob)
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
(commitTx, htlcAlice, htlcBob)
|
||||
}
|
||||
|
||||
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size == commitTx2.commitTx.tx.txOut.size)
|
||||
if (channelVersion.hasAnchorOutputs) {
|
||||
assert(commitTx2.commitTx.tx.txOut.size === 6)
|
||||
} else {
|
||||
assert(commitTx2.commitTx.tx.txOut.size === 4)
|
||||
}
|
||||
|
||||
// Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs
|
||||
val (commitTx3, htlcAlice2, htlcBob2) = {
|
||||
val (_, htlcAlice) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
val (_, htlcBob) = addHtlc(18000000 msat, bob, alice, bob2alice, alice2bob)
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
val commitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
(commitTx, htlcAlice, htlcBob)
|
||||
}
|
||||
|
||||
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size == commitTx3.commitTx.tx.txOut.size)
|
||||
if (channelVersion.hasAnchorOutputs) {
|
||||
assert(commitTx3.commitTx.tx.txOut.size === 8)
|
||||
} else {
|
||||
assert(commitTx3.commitTx.tx.txOut.size === 6)
|
||||
}
|
||||
|
||||
// Bob's fourth commit tx doesn't contain any htlc
|
||||
val commitTx4 = {
|
||||
Seq(htlcAlice1, htlcAlice2).foreach(htlcAlice => failHtlc(htlcAlice.id, bob, alice, bob2alice, alice2bob))
|
||||
Seq(htlcBob1, htlcBob2).foreach(htlcBob => failHtlc(htlcBob.id, alice, bob, alice2bob, bob2alice))
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
|
||||
}
|
||||
|
||||
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size == commitTx4.commitTx.tx.txOut.size)
|
||||
if (channelVersion.hasAnchorOutputs) {
|
||||
assert(commitTx4.commitTx.tx.txOut.size === 4)
|
||||
} else {
|
||||
assert(commitTx4.commitTx.tx.txOut.size === 2)
|
||||
}
|
||||
|
||||
RevokedCloseFixture(Seq(commitTx1, commitTx2, commitTx3, commitTx4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2))
|
||||
}
|
||||
|
||||
private def testFundingSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): Unit = {
|
||||
import f._
|
||||
val revokedCloseFixture = prepareRevokedClose(f, channelVersion)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
|
||||
|
||||
// bob publishes one of his revoked txs
|
||||
val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTx.tx
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
|
||||
|
||||
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState)
|
||||
bobRevokedTx
|
||||
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
|
||||
assert(rvk.commitTx === bobRevokedTx)
|
||||
if (!channelVersion.paysDirectlyToWallet) {
|
||||
assert(rvk.claimMainOutputTx.nonEmpty)
|
||||
}
|
||||
assert(rvk.mainPenaltyTx.nonEmpty)
|
||||
assert(rvk.htlcPenaltyTxs.size === 2)
|
||||
assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty)
|
||||
val penaltyTxs = rvk.claimMainOutputTx.toList ++ rvk.mainPenaltyTx.toList ++ rvk.htlcPenaltyTxs
|
||||
|
||||
// alice publishes the penalty txs
|
||||
if (!channelVersion.paysDirectlyToWallet) {
|
||||
alice2blockchain.expectMsg(PublishAsap(rvk.claimMainOutputTx.get))
|
||||
}
|
||||
alice2blockchain.expectMsg(PublishAsap(rvk.mainPenaltyTx.get))
|
||||
assert(Set(alice2blockchain.expectMsgType[PublishAsap].tx, alice2blockchain.expectMsgType[PublishAsap].tx) === rvk.htlcPenaltyTxs.toSet)
|
||||
for (penaltyTx <- penaltyTxs) {
|
||||
Transaction.correctlySpends(penaltyTx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
}
|
||||
|
||||
// alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet
|
||||
val spentOutpoints = penaltyTxs.flatMap(_.txIn.map(_.outPoint)).toSet
|
||||
assert(spentOutpoints.forall(_.txid === bobRevokedTx.txid))
|
||||
if (channelVersion.hasAnchorOutputs) {
|
||||
assert(spentOutpoints.size === bobRevokedTx.txOut.size - 2) // we don't claim the anchors
|
||||
}
|
||||
else if (channelVersion.paysDirectlyToWallet) {
|
||||
assert(spentOutpoints.size === bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet
|
||||
} else {
|
||||
assert(spentOutpoints.size === bobRevokedTx.txOut.size)
|
||||
}
|
||||
|
||||
// alice watches confirmation for the outputs only her can claim
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobRevokedTx.txid)
|
||||
if (!channelVersion.paysDirectlyToWallet) {
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === rvk.claimMainOutputTx.get.txid)
|
||||
}
|
||||
|
||||
// alice watches outputs that can be spent by both parties
|
||||
val watchedOutpoints = Seq(alice2blockchain.expectMsgType[WatchSpent], alice2blockchain.expectMsgType[WatchSpent], alice2blockchain.expectMsgType[WatchSpent]).map(_.outputIndex).toSet
|
||||
assert(watchedOutpoints === (rvk.mainPenaltyTx.get :: rvk.htlcPenaltyTxs).map(_.txIn.head.outPoint.index).toSet)
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
// once all txs are confirmed, alice can move to the closed state
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx), 100, 3, bobRevokedTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.mainPenaltyTx.get), 110, 1, rvk.mainPenaltyTx.get)
|
||||
if (!channelVersion.paysDirectlyToWallet) {
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.claimMainOutputTx.get), 110, 2, rvk.claimMainOutputTx.get)
|
||||
}
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.htlcPenaltyTxs.head), 115, 0, rvk.htlcPenaltyTxs.head)
|
||||
assert(alice.stateName === CLOSING)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.htlcPenaltyTxs(1)), 115, 2, rvk.htlcPenaltyTxs(1))
|
||||
awaitCond(alice.stateName === CLOSED)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f =>
|
||||
import f._
|
||||
val revokedTx = testFundingSpentRevokedTx(f, ChannelVersion.STANDARD)
|
||||
assert(revokedTx.txOut.length === 3)
|
||||
// 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) // 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)
|
||||
testFundingSpentRevokedTx(f, ChannelVersion.STANDARD)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_FUNDING_SPENT (one revoked tx, option_static_remotekey)", Tag("static_remotekey")) { f =>
|
||||
import 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)
|
||||
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)
|
||||
testFundingSpentRevokedTx(f, ChannelVersion.STATIC_REMOTEKEY)
|
||||
}
|
||||
|
||||
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)
|
||||
testFundingSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
val revokedCloseFixture = prepareRevokedClose(f, ChannelVersion.STANDARD)
|
||||
assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTx.tx.txid).toSet.size === revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct
|
||||
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(1).commitTx.tx)
|
||||
// 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)
|
||||
def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCommitPublished = {
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx)
|
||||
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == revokedCount)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx === revokedTx)
|
||||
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTxes(2).commitTx.tx)
|
||||
// alice publishes and watches the penalty tx
|
||||
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)
|
||||
// alice publishes penalty txs
|
||||
val claimMain = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
val mainPenalty = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[PublishAsap].tx)
|
||||
(claimMain +: mainPenalty +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
|
||||
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 3)
|
||||
// alice watches confirmation for the outputs only her can claim
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === revokedTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMain.txid)
|
||||
|
||||
// alice watches outputs that can be spent by both parties
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].outputIndex === mainPenalty.txIn.head.outPoint.index)
|
||||
val htlcOutpoints = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[WatchSpent].outputIndex).toSet
|
||||
assert(htlcOutpoints === htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint.index)).toSet)
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last
|
||||
}
|
||||
|
||||
// bob publishes a first revoked tx (no htlc in that commitment)
|
||||
broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTx.tx, 0, 1)
|
||||
// bob publishes a second revoked tx
|
||||
val rvk2 = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTx.tx, 2, 2)
|
||||
// bob publishes a third revoked tx
|
||||
broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTx.tx, 4, 3)
|
||||
|
||||
// bob's second revoked tx confirms: once all penalty txs are confirmed, alice can move to the closed state
|
||||
// NB: if multiple txs confirm in the same block, we may receive the events in any order
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk2.mainPenaltyTx.get), 100, 1, rvk2.mainPenaltyTx.get)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk2.claimMainOutputTx.get), 100, 2, rvk2.claimMainOutputTx.get)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk2.commitTx), 100, 3, rvk2.commitTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk2.htlcPenaltyTxs.head), 115, 0, rvk2.htlcPenaltyTxs.head)
|
||||
assert(alice.stateName === CLOSING)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk2.htlcPenaltyTxs(1)), 115, 2, rvk2.htlcPenaltyTxs(1))
|
||||
awaitCond(alice.stateName === CLOSED)
|
||||
}
|
||||
|
||||
def prepareOutputSpentRevokedTx(f: FixtureParam, channelVersion: ChannelVersion): PublishableTxs = {
|
||||
def testOutputSpentRevokedTx(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
|
||||
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)
|
||||
val revokedCloseFixture = prepareRevokedClose(f, channelVersion)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
|
||||
|
||||
// bob publishes one of his revoked txs
|
||||
val bobRevokedTxs = revokedCloseFixture.bobRevokedTxs(2)
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTxs.commitTx.tx)
|
||||
|
||||
awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING])
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
|
||||
val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head
|
||||
assert(rvk.commitTx === bobRevokedTxs.commitTx.tx)
|
||||
assert(rvk.claimMainOutputTx.nonEmpty)
|
||||
assert(rvk.mainPenaltyTx.nonEmpty)
|
||||
assert(rvk.htlcPenaltyTxs.size === 4)
|
||||
assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty)
|
||||
|
||||
// alice publishes the penalty txs and watches outputs
|
||||
(1 to 6).foreach(_ => alice2blockchain.expectMsgType[PublishAsap]) // 2 main outputs and 4 htlcs
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === rvk.commitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === rvk.claimMainOutputTx.get.txid)
|
||||
(1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchSpent]) // main output penalty and 4 htlc penalties
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.commitTx == bobRevokedTx.commitTx.tx)
|
||||
|
||||
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
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === htlcPenaltyTx.txid)
|
||||
// bob manages to claim 2 htlc outputs before alice can penalize him: 1 htlc-success and 1 htlc-timeout.
|
||||
val bobHtlcSuccessTx1 = bobRevokedTxs.htlcTxsAndSigs.filter(tx => tx.txinfo.input.txOut.amount == revokedCloseFixture.htlcsAlice.head.amountMsat.truncateToSatoshi).head
|
||||
val bobHtlcTimeoutTx = bobRevokedTxs.htlcTxsAndSigs.filter(tx => tx.txinfo.input.txOut.amount == revokedCloseFixture.htlcsBob.last.amountMsat.truncateToSatoshi).head
|
||||
val bobOutpoints = Seq(bobHtlcSuccessTx1, bobHtlcTimeoutTx).map(_.txinfo.input.outPoint).toSet
|
||||
assert(bobOutpoints.size === 2)
|
||||
|
||||
bobRevokedTx
|
||||
// alice reacts by publishing penalty txs that spend bob's htlc transactions
|
||||
alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx1.txinfo.tx)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 1)
|
||||
val claimHtlcSuccessPenalty1 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.head
|
||||
Transaction.correctlySpends(claimHtlcSuccessPenalty1, bobHtlcSuccessTx1.txinfo.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcSuccessTx1.txinfo.tx.txid)
|
||||
assert(alice2blockchain.expectMsgType[PublishAsap].tx === claimHtlcSuccessPenalty1)
|
||||
val watchSpent1 = alice2blockchain.expectMsgType[WatchSpent]
|
||||
assert(watchSpent1.txId === bobHtlcSuccessTx1.txinfo.tx.txid)
|
||||
assert(Set(watchSpent1.outputIndex) === claimHtlcSuccessPenalty1.txIn.map(_.outPoint.index).toSet)
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcTimeoutTx.txinfo.tx)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2)
|
||||
val claimHtlcTimeoutPenalty = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last
|
||||
Transaction.correctlySpends(claimHtlcTimeoutPenalty, bobHtlcTimeoutTx.txinfo.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcTimeoutTx.txinfo.tx.txid)
|
||||
assert(alice2blockchain.expectMsgType[PublishAsap].tx === claimHtlcTimeoutPenalty)
|
||||
val watchSpent2 = alice2blockchain.expectMsgType[WatchSpent]
|
||||
assert(watchSpent2.txId === bobHtlcTimeoutTx.txinfo.tx.txid)
|
||||
assert(Set(watchSpent2.outputIndex) === claimHtlcTimeoutPenalty.txIn.map(_.outPoint.index).toSet)
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
// bob RBFs his htlc-success with a different transaction
|
||||
val bobHtlcSuccessTx2 = bobHtlcSuccessTx1.txinfo.tx.copy(txIn = TxIn(OutPoint(randomBytes32, 0), Nil, 0) +: bobHtlcSuccessTx1.txinfo.tx.txIn)
|
||||
assert(bobHtlcSuccessTx2.txid !== bobHtlcSuccessTx1.txinfo.tx.txid)
|
||||
alice ! WatchEventSpent(BITCOIN_OUTPUT_SPENT, bobHtlcSuccessTx2)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobHtlcSuccessTx2.txid)
|
||||
val claimHtlcSuccessPenalty2 = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
assert(claimHtlcSuccessPenalty1.txid != claimHtlcSuccessPenalty2.txid)
|
||||
Transaction.correctlySpends(claimHtlcSuccessPenalty2, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
val watchSpent3 = alice2blockchain.expectMsgType[WatchSpent]
|
||||
assert(watchSpent3.txId === bobHtlcSuccessTx2.txid)
|
||||
assert(Set(watchSpent3.outputIndex) === claimHtlcSuccessPenalty2.txIn.map(_.outPoint.index).toSet)
|
||||
alice2blockchain.expectNoMsg(1 second)
|
||||
|
||||
// transactions confirm: alice can move to the closed state
|
||||
val remainingHtlcPenaltyTxs = rvk.htlcPenaltyTxs.filterNot(tx => bobOutpoints.contains(tx.txIn.head.outPoint))
|
||||
assert(remainingHtlcPenaltyTxs.size === 2)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.commitTx), 100, 3, rvk.commitTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.mainPenaltyTx.get), 110, 0, rvk.mainPenaltyTx.get)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(rvk.claimMainOutputTx.get), 110, 1, rvk.claimMainOutputTx.get)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(remainingHtlcPenaltyTxs.head), 110, 2, remainingHtlcPenaltyTxs.head)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(remainingHtlcPenaltyTxs.last), 115, 2, remainingHtlcPenaltyTxs.last)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcTimeoutTx.txinfo.tx), 115, 0, bobHtlcTimeoutTx.txinfo.tx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobHtlcSuccessTx2), 115, 1, bobHtlcSuccessTx2)
|
||||
assert(alice.stateName === CLOSING)
|
||||
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcTimeoutPenalty), 120, 0, claimHtlcTimeoutPenalty)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcSuccessPenalty2), 121, 0, claimHtlcSuccessPenalty2)
|
||||
awaitCond(alice.stateName === CLOSED)
|
||||
}
|
||||
|
||||
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 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(claimHtlcDelayedPenaltyTx), 0, 0, claimHtlcDelayedPenaltyTx) // but alice claims the htlc output
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
testOutputSpentRevokedTx(f, ChannelVersion.STANDARD)
|
||||
}
|
||||
|
||||
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)
|
||||
testOutputSpentRevokedTx(f, ChannelVersion.ANCHOR_OUTPUTS)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion)
|
||||
val initOutputCount = if (channelVersion.hasAnchorOutputs) 4 else 2
|
||||
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size === initOutputCount)
|
||||
|
||||
// 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)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcPenaltyTx), 0, 0, htlcPenaltyTx)
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
// bob's second commit tx contains 2 incoming htlcs
|
||||
val (bobRevokedTx, htlcs1) = {
|
||||
val (_, htlc1) = addHtlc(35000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
val (_, htlc2) = addHtlc(20000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
crossSign(alice, bob, alice2bob, bob2alice)
|
||||
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
assert(bobCommitTx.txOut.size === initOutputCount + 2)
|
||||
(bobCommitTx, Seq(htlc1, htlc2))
|
||||
}
|
||||
|
||||
// bob's third commit tx contains 1 of the previous htlcs and 2 new htlcs
|
||||
val htlcs2 = {
|
||||
val (_, htlc3) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
val (_, htlc4) = addHtlc(18000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
failHtlc(htlcs1.head.id, bob, alice, bob2alice, alice2bob)
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.size === initOutputCount + 3)
|
||||
Seq(htlc3, htlc4)
|
||||
}
|
||||
|
||||
// alice's first htlc has been failed
|
||||
assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc === htlcs1.head)
|
||||
relayerA.expectNoMsg(1 second)
|
||||
|
||||
// bob publishes one of his revoked txs which quickly confirms
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobRevokedTx), 100, 1, bobRevokedTx)
|
||||
awaitCond(alice.stateName === CLOSING)
|
||||
|
||||
// alice should fail all pending htlcs
|
||||
val htlcFails = Seq(
|
||||
relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]],
|
||||
relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]],
|
||||
relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]
|
||||
).map(_.htlc).toSet
|
||||
assert(htlcFails === Set(htlcs1(1), htlcs2.head, htlcs2(1)))
|
||||
relayerA.expectNoMsg(1 second)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_TX_CONFIRMED (one revoked tx)") { f =>
|
||||
test("recv BITCOIN_TX_CONFIRMED (one revoked tx, pending htlcs)") { f =>
|
||||
testRevokedTxConfirmed(f, ChannelVersion.STANDARD)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_TX_CONFIRMED (one revoked tx, anchor outputs)", Tag("anchor_outputs")) { f =>
|
||||
test("recv BITCOIN_TX_CONFIRMED (one revoked tx, pending htlcs, anchor outputs)", Tag("anchor_outputs")) { f =>
|
||||
testRevokedTxConfirmed(f, ChannelVersion.ANCHOR_OUTPUTS)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue