Skip to content

Commit df0de1f

Browse files
committed
Allow non-initiator RBF for dual funding
We previously only allowed the opener to RBF a dual-funded channel. This is not consistent with splicing, where both peers can initiate RBF. There is no technical reason to restrict the channel creation, we can allow the non-initiator to RBF if they wish to do so. The only subtlety is in the case where there is a liquidity purchase. In that case we want the opener to be the only one allowed to RBF to guarantee that we keep the liquidity purchase (since the initiator is the only one that can purchase liquidity).
1 parent 21917f5 commit df0de1f

File tree

4 files changed

+90
-56
lines changed

4 files changed

+90
-56
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ case class InvalidSpliceTxAbortNotAcked (override val channelId: Byte
9494
case class InvalidSpliceNotQuiescent (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: the channel is not quiescent")
9595
case class InvalidSpliceWithUnconfirmedTx (override val channelId: ByteVector32, fundingTx: TxId) extends ChannelException(channelId, s"invalid splice attempt: the current funding transaction is still unconfirmed (txId=$fundingTx), you should use tx_init_rbf instead")
9696
case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed")
97-
case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt")
9897
case class InvalidRbfZeroConf (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're using zero-conf for this interactive-tx attempt")
98+
case class InvalidRbfOverridesLiquidityPurchase (override val channelId: ByteVector32, purchasedAmount: Satoshi) extends ChannelException(channelId, s"cannot initiate rbf attempt: our peer wanted to purchase $purchasedAmount of liquidity that we would override, they must initiate rbf")
9999
case class InvalidRbfMissingLiquidityPurchase (override val channelId: ByteVector32, expectedAmount: Satoshi) extends ChannelException(channelId, s"cannot accept rbf attempt: the previous attempt contained a liquidity purchase of $expectedAmount but this one doesn't contain any liquidity purchase")
100100
case class InvalidRbfAttempt (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt")
101101
case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId
2929
import fr.acinq.eclair.crypto.ShaChain
3030
import fr.acinq.eclair.io.Peer.{LiquidityPurchaseSigned, OpenChannelResponse}
3131
import fr.acinq.eclair.wire.protocol._
32-
import fr.acinq.eclair.{ToMilliSatoshiConversion, UInt64, randomBytes32}
32+
import fr.acinq.eclair.{Features, ToMilliSatoshiConversion, UInt64, randomBytes32}
3333

3434
/**
3535
* Created by t-bast on 19/04/2022.
@@ -495,18 +495,21 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
495495
}
496496

497497
case Event(cmd: CMD_BUMP_FUNDING_FEE, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
498-
val zeroConf = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepth, d.latestFundingTx.sharedTx.tx).isEmpty
499-
if (!d.latestFundingTx.fundingParams.isInitiator) {
500-
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfNonInitiator(d.channelId))
501-
stay()
502-
} else if (zeroConf) {
503-
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfZeroConf(d.channelId))
504-
stay()
505-
} else if (cmd.requestFunding_opt.isEmpty && d.latestFundingTx.liquidityPurchase_opt.nonEmpty) {
506-
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfMissingLiquidityPurchase(d.channelId, d.latestFundingTx.liquidityPurchase_opt.get.amount))
507-
stay()
508-
} else {
509-
d.status match {
498+
d.latestFundingTx.liquidityPurchase_opt match {
499+
case Some(purchase) if !d.latestFundingTx.fundingParams.isInitiator =>
500+
// If we're not the channel initiator and they are purchasing liquidity, they must initiate RBF, otherwise
501+
// the liquidity purchase will be lost (since only the initiator can purchase liquidity).
502+
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfOverridesLiquidityPurchase(d.channelId, purchase.amount))
503+
stay()
504+
case Some(purchase) if cmd.requestFunding_opt.isEmpty =>
505+
// If we were purchasing liquidity, we must keep purchasing liquidity across RBF attempts, otherwise our
506+
// peer will simply reject the RBF attempt.
507+
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfMissingLiquidityPurchase(d.channelId, purchase.amount))
508+
stay()
509+
case _ if d.commitments.params.localParams.initFeatures.hasFeature(Features.ZeroConf) =>
510+
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfZeroConf(d.channelId))
511+
stay()
512+
case _ => d.status match {
510513
case DualFundingStatus.WaitingForConfirmations =>
511514
val minNextFeerate = d.latestFundingTx.fundingParams.minNextFeerate
512515
if (cmd.targetFeerate < minNextFeerate) {
@@ -524,12 +527,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
524527
}
525528

526529
case Event(msg: TxInitRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
527-
val zeroConf = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepth, d.latestFundingTx.sharedTx.tx).isEmpty
528-
if (d.latestFundingTx.fundingParams.isInitiator) {
529-
// Only the initiator is allowed to initiate RBF.
530-
log.info("rejecting tx_init_rbf, we're the initiator, not them!")
531-
stay() sending Error(d.channelId, InvalidRbfNonInitiator(d.channelId).getMessage)
532-
} else if (zeroConf) {
530+
if (d.commitments.params.localParams.initFeatures.hasFeature(Features.ZeroConf)) {
533531
log.info("rejecting tx_init_rbf, we're using zero-conf")
534532
stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfZeroConf(d.channelId).getMessage)
535533
} else {
@@ -567,6 +565,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
567565
val fundingContribution = willFund_opt.map(_.purchase.amount).getOrElse(d.latestFundingTx.fundingParams.localContribution)
568566
log.info("accepting rbf with remote.in.amount={} local.in.amount={}", msg.fundingContribution, fundingContribution)
569567
val fundingParams = d.latestFundingTx.fundingParams.copy(
568+
isInitiator = false,
570569
localContribution = fundingContribution,
571570
remoteContribution = msg.fundingContribution,
572571
lockTime = msg.lockTime,
@@ -607,6 +606,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
607606
stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, error.getMessage)
608607
case DualFundingStatus.RbfRequested(cmd) =>
609608
val fundingParams = d.latestFundingTx.fundingParams.copy(
609+
isInitiator = true,
610610
// we don't change our funding contribution
611611
remoteContribution = msg.fundingContribution,
612612
lockTime = cmd.lockTime,

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ object ChannelStateTestsTags {
9494
val RejectRbfAttempts = "reject_rbf_attempts"
9595
/** If set, the non-initiator will require a 1-block delay between RBF attempts. */
9696
val DelayRbfAttempts = "delay_rbf_attempts"
97+
/** If set, the non-initiator will not enforce any restriction between RBF attempts. */
98+
val UnlimitedRbfAttempts = "unlimited_rbf_attempts"
9799
/** If set, channels will adapt their max HTLC amount to the available balance. */
98100
val AdaptMaxHtlcAmount = "adapt_max_htlc_amount"
99101
/** If set, closing will use option_simple_close. */
@@ -149,6 +151,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
149151
.modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat)
150152
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat)
151153
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat)
154+
.modify(_.channelConf.remoteRbfLimits.maxAttempts).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(100)
155+
.modify(_.channelConf.remoteRbfLimits.attemptDeltaBlocks).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(0)
152156
.modify(_.onChainFeeConf.defaultFeerateTolerance.ratioLow).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(0.000001)
153157
.modify(_.onChainFeeConf.defaultFeerateTolerance.ratioHigh).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(1000000)
154158
.modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false)
@@ -159,7 +163,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
159163
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat)
160164
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat)
161165
.modify(_.channelConf.remoteRbfLimits.maxAttempts).setToIf(tags.contains(ChannelStateTestsTags.RejectRbfAttempts))(0)
166+
.modify(_.channelConf.remoteRbfLimits.maxAttempts).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(100)
162167
.modify(_.channelConf.remoteRbfLimits.attemptDeltaBlocks).setToIf(tags.contains(ChannelStateTestsTags.DelayRbfAttempts))(1)
168+
.modify(_.channelConf.remoteRbfLimits.attemptDeltaBlocks).setToIf(tags.contains(ChannelStateTestsTags.UnlimitedRbfAttempts))(0)
163169
.modify(_.onChainFeeConf.defaultFeerateTolerance.ratioLow).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(0.000001)
164170
.modify(_.onChainFeeConf.defaultFeerateTolerance.ratioHigh).setToIf(tags.contains(ChannelStateTestsTags.HighFeerateMismatchTolerance))(1000000)
165171
.modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false)

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -345,49 +345,61 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
345345
}
346346

347347
def testBumpFundingFees(f: FixtureParam, feerate_opt: Option[FeeratePerKw] = None, requestFunding_opt: Option[LiquidityAds.RequestFunding] = None): FullySignedSharedTransaction = {
348+
testBumpFundingFees(f, f.alice, f.bob, f.alice2bob, f.bob2alice, feerate_opt, requestFunding_opt)
349+
}
350+
351+
def testBumpFundingFees(f: FixtureParam, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, feerate_opt: Option[FeeratePerKw], requestFunding_opt: Option[LiquidityAds.RequestFunding]): FullySignedSharedTransaction = {
348352
import f._
349353

350354
val probe = TestProbe()
351-
val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
352-
val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs
353-
alice ! CMD_BUMP_FUNDING_FEE(probe.ref, feerate_opt.getOrElse(currentFundingTx.feerate * 1.1), fundingFeeBudget = 100_000.sat, 0, requestFunding_opt)
354-
assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis)
355-
alice2bob.forward(bob)
356-
val txAckRbf = bob2alice.expectMsgType[TxAckRbf]
357-
assert(txAckRbf.fundingContribution == requestFunding_opt.map(_.requestedAmount).getOrElse(TestConstants.nonInitiatorFundingSatoshis))
355+
val currentFundingParams = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.fundingParams
356+
val currentFundingTx = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
357+
val previousFundingTxs = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs
358+
s ! CMD_BUMP_FUNDING_FEE(probe.ref, feerate_opt.getOrElse(currentFundingTx.feerate * 1.1), fundingFeeBudget = 100_000.sat, 0, requestFunding_opt)
359+
assert(s2r.expectMsgType[TxInitRbf].fundingContribution == currentFundingParams.localContribution)
360+
s2r.forward(r)
361+
val txAckRbf = r2s.expectMsgType[TxAckRbf]
362+
assert(txAckRbf.fundingContribution == requestFunding_opt.map(_.requestedAmount).getOrElse(currentFundingParams.remoteContribution))
358363
requestFunding_opt.foreach(_ => assert(txAckRbf.willFund_opt.nonEmpty))
359-
bob2alice.forward(alice)
364+
r2s.forward(s)
360365

361366
// Alice and Bob build a new version of the funding transaction, with one new input every time.
362367
val inputCount = previousFundingTxs.length + 2
363368
(1 to inputCount).foreach(_ => {
364-
alice2bob.expectMsgType[TxAddInput]
365-
alice2bob.forward(bob)
366-
bob2alice.expectMsgType[TxAddInput]
367-
bob2alice.forward(alice)
369+
s2r.expectMsgType[TxAddInput]
370+
s2r.forward(r)
371+
r2s.expectMsgType[TxAddInput]
372+
r2s.forward(s)
368373
})
369-
alice2bob.expectMsgType[TxAddOutput]
370-
alice2bob.forward(bob)
371-
bob2alice.expectMsgType[TxAddOutput]
372-
bob2alice.forward(alice)
373-
alice2bob.expectMsgType[TxAddOutput]
374-
alice2bob.forward(bob)
375-
bob2alice.expectMsgType[TxComplete]
376-
bob2alice.forward(alice)
377-
alice2bob.expectMsgType[TxComplete]
378-
alice2bob.forward(bob)
379-
bob2alice.expectMsgType[CommitSig]
380-
bob2alice.forward(alice)
381-
alice2bob.expectMsgType[CommitSig]
382-
alice2bob.forward(bob)
383-
bob2alice.expectMsgType[TxSignatures]
384-
bob2alice.forward(alice)
385-
alice2bob.expectMsgType[TxSignatures]
386-
alice2bob.forward(bob)
374+
s2r.expectMsgType[TxAddOutput]
375+
s2r.forward(r)
376+
r2s.expectMsgType[TxAddOutput]
377+
r2s.forward(s)
378+
s2r.expectMsgType[TxAddOutput]
379+
s2r.forward(r)
380+
r2s.expectMsgType[TxComplete]
381+
r2s.forward(s)
382+
s2r.expectMsgType[TxComplete]
383+
s2r.forward(r)
384+
r2s.expectMsgType[CommitSig]
385+
r2s.forward(s)
386+
s2r.expectMsgType[CommitSig]
387+
s2r.forward(r)
388+
if (currentFundingParams.localContribution < currentFundingParams.remoteContribution) {
389+
s2r.expectMsgType[TxSignatures]
390+
s2r.forward(r)
391+
r2s.expectMsgType[TxSignatures]
392+
r2s.forward(s)
393+
} else {
394+
r2s.expectMsgType[TxSignatures]
395+
r2s.forward(s)
396+
s2r.expectMsgType[TxSignatures]
397+
s2r.forward(r)
398+
}
387399

388400
probe.expectMsgType[RES_BUMP_FUNDING_FEE]
389401

390-
val nextFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
402+
val nextFundingTx = s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction]
391403
assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid)
392404
assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == nextFundingTx.signedTx.txid)
393405
assert(bobListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid)
@@ -396,12 +408,12 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
396408
assert(currentFundingTx.feerate < nextFundingTx.feerate)
397409
// The new transaction double-spends previous inputs.
398410
currentFundingTx.signedTx.txIn.map(_.outPoint).foreach(o => assert(nextFundingTx.signedTx.txIn.exists(_.outPoint == o)))
399-
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == previousFundingTxs.length + 1)
400-
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.sharedTx == currentFundingTx)
411+
assert(s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == previousFundingTxs.length + 1)
412+
assert(s.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.sharedTx == currentFundingTx)
401413
nextFundingTx
402414
}
403415

404-
test("recv CMD_BUMP_FUNDING_FEE", Tag(ChannelStateTestsTags.DualFunding)) { f =>
416+
test("recv CMD_BUMP_FUNDING_FEE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.UnlimitedRbfAttempts)) { f =>
405417
import f._
406418

407419
// Bob contributed to the funding transaction.
@@ -427,9 +439,21 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
427439
assert(FeeratePerKw(15_000 sat) <= fundingTx3.feerate && fundingTx3.feerate < FeeratePerKw(15_500 sat))
428440
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 2)
429441

430-
// The initial funding transaction confirms
442+
// Bob RBFs the funding transaction: Alice keeps contributing the same amount.
443+
val feerate4 = FeeratePerKw(20_000 sat)
444+
testBumpFundingFees(f, bob, alice, bob2alice, alice2bob, Some(feerate4), requestFunding_opt = None)
445+
val balanceBob4 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal
446+
assert(balanceBob4 == TestConstants.nonInitiatorFundingSatoshis.toMilliSatoshi)
447+
val balanceAlice4 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.spec.toLocal
448+
assert(balanceAlice4 == TestConstants.fundingSatoshis.toMilliSatoshi)
449+
val fundingTx4 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx].sharedTx.asInstanceOf[FullySignedSharedTransaction]
450+
assert(FeeratePerKw(20_000 sat) <= fundingTx4.feerate && fundingTx4.feerate < FeeratePerKw(20_500 sat))
451+
assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 3)
452+
assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 3)
453+
454+
// The initial funding transaction confirms: we rollback unused inputs.
431455
alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1.signedTx)
432-
testUnusedInputsUnlocked(wallet, Seq(fundingTx2, fundingTx3))
456+
testUnusedInputsUnlocked(wallet, Seq(fundingTx2, fundingTx3, fundingTx4))
433457
}
434458

435459
test("recv CMD_BUMP_FUNDING_FEE (liquidity ads)", Tag(ChannelStateTestsTags.DualFunding), Tag(liquidityPurchase)) { f =>
@@ -485,6 +509,10 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture
485509
bob2alice.forward(alice)
486510
alice2bob.expectMsgType[TxAbort]
487511
alice2bob.forward(bob)
512+
513+
// Bob tries to RBF: this is disabled because it would override Alice's liquidity purchase.
514+
bob ! CMD_BUMP_FUNDING_FEE(sender.ref, FeeratePerKw(20_000 sat), 100_000 sat, 0, requestFunding_opt = None)
515+
assert(sender.expectMsgType[RES_FAILURE[_, ChannelException]].t.isInstanceOf[InvalidRbfOverridesLiquidityPurchase])
488516
}
489517

490518
test("recv CMD_BUMP_FUNDING_FEE (aborted)", Tag(ChannelStateTestsTags.DualFunding)) { f =>

0 commit comments

Comments
 (0)