1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 14:22:39 +01:00

Add closing test reconnect with 3rd-stage txs (#2820)

We had some cases where channels were staying stuck in the CLOSING state
without going to the CLOSED state in previous versions of eclair where
we published different versions of our 3rd-stage transactions after a
restart and a feerate increase.

This happened in two cases:

- when we published a different version of our claim-main, since we only
  store the latest (it's an `Option`, not a `Seq`) and put a watch on
  this specific `txid` instead of watching the corresponding output
- when we re-published 3rd-stage txs after receiving `CMD_FULFILL_HTLC`
  because we replaced the previous `claim-htlc-delayed` transactions

This was on my backlog for a while, and I wanted to take another look at
it after the splicing implementation. Fortunately, this was fixed as
part of the splicing changes, but it's worth having a test to ensure
that there's no regression from future changes.
This commit is contained in:
Bastien Teinturier 2024-02-13 17:31:13 +01:00 committed by GitHub
parent 3d8dc88089
commit b5e83a6092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -646,9 +646,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val closingState = localClose(alice, alice2blockchain)
val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head
// simulate a node restart
// simulate a node restart after a feerate increase
val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING]
alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing)
alice.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(FeeratePerKw(15_000 sat)))
alice ! INPUT_RESTORED(beforeRestart)
alice2blockchain.expectMsgType[SetChannelId]
awaitCond(alice.stateName == CLOSING)
@ -685,6 +686,80 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid)
}
test("recv INPUT_RESTORED (local commit with htlc-delayed transactions)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f =>
import f._
// Alice has one incoming and one outgoing HTLC.
addHtlc(75_000_000 msat, alice, bob, alice2bob, bob2alice)
val (preimage, incomingHtlc) = addHtlc(80_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(alice, bob, alice2bob, bob2alice)
// Alice force-closes.
val closingState1 = localClose(alice, alice2blockchain)
assert(closingState1.claimMainDelayedOutputTx.nonEmpty)
val claimMainTx = closingState1.claimMainDelayedOutputTx.get.tx
assert(getHtlcSuccessTxs(closingState1).isEmpty)
assert(getHtlcTimeoutTxs(closingState1).length == 1)
val htlcTimeoutTx = getHtlcTimeoutTxs(closingState1).head.tx
// The commit tx confirms.
alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState1.commitTx)
alice2blockchain.expectNoMessage(100 millis)
// Alice receives the preimage for the incoming HTLC.
alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, commit = true)
assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx])
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimMainTx.txid)
assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[HtlcTimeoutTx])
assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[HtlcSuccessTx])
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid)
alice2blockchain.expectMsgType[WatchOutputSpent]
alice2blockchain.expectMsgType[WatchOutputSpent]
alice2blockchain.expectMsgType[WatchOutputSpent]
alice2blockchain.expectNoMessage(100 millis)
val closingState2 = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get
assert(getHtlcSuccessTxs(closingState2).length == 1)
val htlcSuccessTx = getHtlcSuccessTxs(closingState2).head.tx
// The HTLC txs confirms, so we publish 3rd-stage txs.
alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx)
val claimHtlcTimeoutDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutDelayedTx.txid)
Transaction.correctlySpends(claimHtlcTimeoutDelayedTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcSuccessTx)
val claimHtlcSuccessDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcSuccessDelayedTx.txid)
Transaction.correctlySpends(claimHtlcSuccessDelayedTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// We simulate a node restart after a feerate increase.
val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING]
alice.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(FeeratePerKw(15_000 sat)))
alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing)
alice ! INPUT_RESTORED(beforeRestart)
alice2blockchain.expectMsgType[SetChannelId]
awaitCond(alice.stateName == CLOSING)
// We re-publish closing transactions.
assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.isInstanceOf[ClaimLocalAnchorOutputTx])
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimMainTx.txid)
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimHtlcTimeoutDelayedTx.txid)
assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimHtlcSuccessDelayedTx.txid)
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid)
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutDelayedTx.txid)
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcSuccessDelayedTx.txid)
alice2blockchain.expectMsgType[WatchOutputSpent]
// We replay the HTLC fulfillment: nothing happens since we already published a 3rd-stage transaction.
alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, commit = true)
alice2blockchain.expectNoMessage(100 millis)
// The remaining transactions confirm.
alice ! WatchTxConfirmedTriggered(BlockHeight(43), 0, claimMainTx)
alice ! WatchTxConfirmedTriggered(BlockHeight(43), 1, claimHtlcTimeoutDelayedTx)
alice ! WatchTxConfirmedTriggered(BlockHeight(43), 2, claimHtlcSuccessDelayedTx)
awaitCond(alice.stateName == CLOSED)
}
test("recv WatchTxConfirmedTriggered (remote commit with htlcs only signed by local in next remote commit)") { f =>
import f._
val listener = TestProbe()