Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for quiescence back ported from lightning-kmp #2779

Merged
merged 6 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}

}