Skip to content

Commit

Permalink
Add closing test reconnect with 3rd-stage txs (#2820)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
t-bast authored Feb 13, 2024
1 parent 3d8dc88 commit b5e83a6
Showing 1 changed file with 76 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit b5e83a6

Please sign in to comment.