Skip to content

Commit

Permalink
Handle mutual close published from the outside (#2046)
Browse files Browse the repository at this point in the history
If a _local_ mutual close transaction is published from outside of the actor state machine, the channel will fail to recognize it, and will move to the `ERR_INFORMATION_LEAK` state. We could instead log a warning and handle it gracefully, since no harm has been done.

This is different from a local force close, because we do not keep the fully-signed local commit tx anymore, so an unexpected published tx would indeed be very fishy in that case. But we do store the best fully-signed, read-to-publish mutual close tx in the channel data so we must be ready to handle the case where the operator manually publishes it for whatever reason.
  • Loading branch information
pm47 committed Nov 3, 2021
1 parent 9f65f3a commit 1f613ec
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
// they can publish a closing tx with any sig we sent them, even if we are not done negotiating
handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d))

case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.bestUnpublishedClosingTx_opt.exists(_.tx.txid == tx.txid) =>
log.warning(s"looks like a mutual close tx has been published from the outside of the channel: closingTxId=${tx.txid}")
// if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that
handleMutualClose(d.bestUnpublishedClosingTx_opt.get, Left(d))

case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)

case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,25 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(alice.stateName === CLOSING)
}

test("recv WatchFundingSpentTriggered (self mutual close)") { f =>
import f._
bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)))
bobClose(f)
// alice starts with a very low proposal
val (aliceClosing1, _) = makeLegacyClosingSigned(f, 500 sat)
alice2bob.send(bob, aliceClosing1)
val bobClosing1 = bob2alice.expectMsgType[ClosingSigned]
// at this point bob has received a mutual close signature from alice, but doesn't yet agree on the fee
// bob's mutual close is published from the outside of the actor
assert(bob.stateName === NEGOTIATING)
val mutualCloseTx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.get.tx
bob ! WatchFundingSpentTriggered(mutualCloseTx)
assert(bob2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx)
assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid)
bob2blockchain.expectNoMessage(100 millis)
assert(bob.stateName == CLOSING)
}

test("recv CMD_CLOSE") { f =>
import f._
bobClose(f)
Expand Down

0 comments on commit 1f613ec

Please sign in to comment.