Skip to content

Commit

Permalink
Fixes for quiescence back ported from lightning-kmp (#2779)
Browse files Browse the repository at this point in the history
Fixes some issues we found while porting quiescence to lightning-kmp.
  • Loading branch information
remyers authored Jan 10, 2024
1 parent 61f1e1f commit a9b5903
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
stay()
}

case Event(_: Stfu, d: DATA_NORMAL) if d.localShutdown.isDefined =>
log.warning("our peer sent stfu but we sent shutdown first")
// We don't need to do anything, they should accept our shutdown.
stay()

case Event(msg: Stfu, d: DATA_NORMAL) =>
if (d.commitments.params.useQuiescence) {
if (d.commitments.remoteIsQuiescent) {
Expand Down Expand Up @@ -928,6 +933,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with

case Event(_: QuiescenceTimeout, d: DATA_NORMAL) => handleQuiescenceTimeout(d)

case Event(_: SpliceInit, d: DATA_NORMAL) if d.spliceStatus == SpliceStatus.NoSplice && d.commitments.params.useQuiescence =>
log.info("rejecting splice attempt: quiescence not negotiated")
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage)

case Event(msg: SpliceInit, d: DATA_NORMAL) =>
d.spliceStatus match {
case SpliceStatus.NoSplice | SpliceStatus.NonInitiatorQuiescent =>
Expand Down Expand Up @@ -2705,7 +2714,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
spliceInAmount = cmd.additionalLocalFunding,
spliceOut = cmd.spliceOutputs,
targetFeerate = targetFeerate)
val commitTxFees = Transactions.commitTxTotalCost(d.commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, d.commitments.params.commitmentFormat)
val commitTxFees = if (d.commitments.params.localParams.isInitiator) {
Transactions.commitTxTotalCost(d.commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, d.commitments.params.commitmentFormat)
} else 0.sat
if (fundingContribution < 0.sat && parentCommitment.localCommit.spec.toLocal + fundingContribution < parentCommitment.localChannelReserve(d.commitments.params).max(commitTxFees)) {
log.warning(s"cannot do splice: insufficient funds (commitTxFees=$commitTxFees reserve=${parentCommitment.localChannelReserve(d.commitments.params)})")
Left(InvalidSpliceRequest(d.channelId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package fr.acinq.eclair.channel.states.e

import akka.actor.ActorRef
import akka.actor.typed.scaladsl.adapter.actorRefAdapter
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script}
Expand Down Expand Up @@ -115,7 +116,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
test("send stfu after pending local changes have been added") { f =>
import f._
// we have an unsigned htlc in our local changes
addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice)
addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
alice ! CMD_SPLICE(TestProbe().ref, spliceIn_opt = Some(SpliceIn(50_000 sat)), spliceOut_opt = None)
alice2bob.expectNoMessage(100 millis)
crossSign(alice, bob, alice2bob, bob2alice)
Expand All @@ -127,7 +128,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
import f._
initiateQuiescence(f, sendInitialStfu = false)
// we're holding the stfu from alice so that bob can add a pending local change
addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
// bob will not reply to alice's stfu until bob has no pending local commitment changes
alice2bob.forward(bob)
bob2alice.expectNoMessage(100 millis)
Expand Down Expand Up @@ -187,8 +188,8 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
private def receiveSettlementCommand(f: FixtureParam, c: SettlementCommandEnum, sendInitialStfu: Boolean, resetConnection: Boolean = false): Unit = {
import f._

val (preimage, add) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val cmd = c match {
val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
val cmd = c match {
case FulfillHtlc => CMD_FULFILL_HTLC(add.id, preimage)
case FailHtlc => CMD_FAIL_HTLC(add.id, Left(randomBytes32()))
}
Expand Down Expand Up @@ -269,7 +270,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
test("recv second stfu while non-initiator waiting for local commitment to be signed") { f =>
import f._
initiateQuiescence(f, sendInitialStfu = false)
val (_, _) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val (_, _) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
alice2bob.forward(bob)
// second stfu to bob is ignored
bob ! Stfu(channelId(bob), initiator = true)
Expand All @@ -278,12 +279,20 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL

test("recv Shutdown message before initiator receives stfu from remote") { f =>
import f._
// Alice initiates quiescence.
initiateQuiescence(f, sendInitialStfu = false)
val bobData = bob.stateData.asInstanceOf[DATA_NORMAL]
val forbiddenMsg = Shutdown(channelId(bob), bob.underlyingActor.getOrGenerateFinalScriptPubKey(bobData))
bob2alice.forward(alice, forbiddenMsg)
// handle Shutdown normally
alice2bob.expectMsgType[Shutdown]
val stfuAlice = Stfu(channelId(alice), initiator = true)
// But Bob is concurrently initiating a mutual close, which should "win".
bob ! CMD_CLOSE(ActorRef.noSender, None, None)
val shutdownBob = bob2alice.expectMsgType[Shutdown]
bob ! stfuAlice
bob2alice.expectNoMessage(100 millis)
alice ! shutdownBob
val shutdownAlice = alice2bob.expectMsgType[Shutdown]
awaitCond(alice.stateName == NEGOTIATING)
alice2bob.expectMsgType[ClosingSigned]
bob ! shutdownAlice
awaitCond(bob.stateName == NEGOTIATING)
}

test("recv (forbidden) Shutdown message while quiescent") { f =>
Expand All @@ -300,7 +309,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL

test("recv (forbidden) UpdateFulfillHtlc message while quiescent") { f =>
import f._
val (preimage, add) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val (preimage, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
alice2relayer.expectMsg(RelayForward(add))
initiateQuiescence(f, sendInitialStfu = true)
Expand All @@ -315,7 +324,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL

test("recv (forbidden) UpdateFailHtlc message while quiescent") { f =>
import f._
val (_, add) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val (_, add) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
initiateQuiescence(f, sendInitialStfu = true)
val forbiddenMsg = UpdateFailHtlc(channelId(bob), add.id, randomBytes32())
Expand All @@ -328,7 +337,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL

test("recv (forbidden) UpdateFee message while quiescent") { f =>
import f._
val (_, _) = addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val (_, _) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
crossSign(bob, alice, bob2alice, alice2bob)
initiateQuiescence(f, sendInitialStfu = true)
val forbiddenMsg = UpdateFee(channelId(bob), FeeratePerKw(500 sat))
Expand All @@ -353,7 +362,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL

test("recv stfu from splice initiator that is not quiescent") { f =>
import f._
addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice)
addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
alice2bob.forward(bob, Stfu(channelId(alice), initiator = true))
bob2alice.expectMsg(Warning(channelId(bob), InvalidSpliceNotQuiescent(channelId(bob)).getMessage))
// we should disconnect after giving alice time to receive the warning
Expand All @@ -365,7 +374,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL

test("recv stfu from splice non-initiator that is not quiescent") { f =>
import f._
addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
initiateQuiescence(f, sendInitialStfu = false)
alice2bob.forward(bob)
bob2alice.forward(alice, Stfu(channelId(bob), initiator = false))
Expand Down Expand Up @@ -393,10 +402,10 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
sender.expectMsgType[RES_FAILURE[CMD_SPLICE, ConcurrentRemoteSplice]]
}

test("initiate quiescence concurrently (pending changes on initiator side)") { f =>
test("initiate quiescence concurrently (pending changes on one side)") { f =>
import f._

addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice)
addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
alice ! cmd
Expand All @@ -412,26 +421,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
bob2alice.expectMsgType[SpliceInit]
}

test("initiate quiescence concurrently (pending changes on non-initiator side)") { f =>
import f._

addHtlc(10_000 msat, bob, alice, bob2alice, alice2bob)
val sender = TestProbe()
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 0 msat)), spliceOut_opt = None)
alice ! cmd
alice2bob.expectMsgType[Stfu]
bob ! cmd
bob2alice.expectNoMessage(100 millis) // bob isn't quiescent yet
alice2bob.forward(bob)
crossSign(bob, alice, bob2alice, alice2bob)
bob2alice.expectMsgType[Stfu]
bob2alice.forward(alice)
assert(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NonInitiatorQuiescent)
sender.expectMsgType[RES_FAILURE[CMD_SPLICE, ConcurrentRemoteSplice]]
alice2bob.expectMsgType[SpliceInit]
}

test("htlc timeout during quiescence negotiation") { f =>
test("outgoing htlc timeout during quiescence negotiation") { f =>
import f._
val (_, add) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
Expand All @@ -455,7 +445,7 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
channelUpdateListener.expectMsgType[LocalChannelDown]
}

test("htlc timeout during quiescence negotiation (with pending preimage)") { f =>
test("incoming htlc timeout during quiescence negotiation") { f =>
import f._
val (preimage, add) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
Expand All @@ -466,6 +456,10 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
assert(bobCommit.htlcTxsAndRemoteSigs.size == 1)
val htlcSuccessTx = bobCommit.htlcTxsAndRemoteSigs.head.htlcTx.tx

// bob does not force-close unless there is a pending preimage for the incoming htlc
bob ! CurrentBlockHeight(add.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt)
bob2blockchain.expectNoMessage(100 millis)

// bob receives the fulfill for htlc, which is ignored because the channel is quiescent
val fulfillHtlc = CMD_FULFILL_HTLC(add.id, preimage)
safeSend(bob, Seq(fulfillHtlc))
Expand Down Expand Up @@ -522,4 +516,13 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL
bob2alice.expectMsg(Warning(channelId(bob), SpliceAttemptTimedOut(channelId(bob)).getMessage))
}

test("receive SpliceInit when channel is not quiescent") { f =>
import f._
val spliceInit = SpliceInit(channelId(alice), 500_000.sat, FeeratePerKw(253.sat), 0, randomKey().publicKey)
alice ! spliceInit
// quiescence not negotiated
alice2bob.expectMsgType[TxAbort]
assert(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.SpliceAborted)
}

}

0 comments on commit a9b5903

Please sign in to comment.