Skip to content

Commit c1a925d

Browse files
authored
Use 0-conf based on local features only (#2460)
If we have activated 0-conf support for a given peer, we send our `channel_ready` early regardless of whether our peer has activated support for 0-conf. If they also immediately send their `channel_ready` it's great, if they don't it's ok, we'll just wait for confirmations, but it was worth trying.
1 parent a0433aa commit c1a925d

File tree

6 files changed

+49
-26
lines changed

6 files changed

+49
-26
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,8 @@ object Helpers {
354354
* wait for one conf, except if the channel has the zero-conf feature (because presumably the peer will send an
355355
* alias in that case).
356356
*/
357-
def minDepthFunder(channelFeatures: ChannelFeatures): Option[Long] = {
358-
if (channelFeatures.hasFeature(Features.ZeroConf)) {
357+
def minDepthFunder(localFeatures: Features[InitFeature]): Option[Long] = {
358+
if (localFeatures.hasFeature(Features.ZeroConf)) {
359359
None
360360
} else {
361361
Some(1)
@@ -369,8 +369,8 @@ object Helpers {
369369
* @param fundingSatoshis funding amount of the channel
370370
* @return number of confirmations needed, if any
371371
*/
372-
def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Option[Long] = fundingSatoshis match {
373-
case _ if channelFeatures.hasFeature(Features.ZeroConf) => None // zero-conf stay zero-conf, whatever the funding amount is
372+
def minDepthFundee(channelConf: ChannelConf, localFeatures: Features[InitFeature], fundingSatoshis: Satoshi): Option[Long] = fundingSatoshis match {
373+
case _ if localFeatures.hasFeature(Features.ZeroConf) => None // zero-conf stay zero-conf, whatever the funding amount is
374374
case funding if funding <= Channel.MAX_FUNDING => Some(channelConf.minDepthBlocks)
375375
case funding =>
376376
val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024
@@ -384,15 +384,15 @@ object Helpers {
384384
* - our peer may also contribute to the funding transaction
385385
* - even if they don't, we may RBF the transaction and don't want to handle reorgs
386386
*/
387-
def minDepthDualFunding(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingParams: InteractiveTxBuilder.InteractiveTxParams): Option[Long] = {
387+
def minDepthDualFunding(channelConf: ChannelConf, localFeatures: Features[InitFeature], fundingParams: InteractiveTxBuilder.InteractiveTxParams): Option[Long] = {
388388
if (fundingParams.isInitiator && fundingParams.remoteAmount == 0.sat) {
389-
if (channelFeatures.hasFeature(Features.ZeroConf)) {
389+
if (localFeatures.hasFeature(Features.ZeroConf)) {
390390
None
391391
} else {
392392
Some(channelConf.minDepthBlocks)
393393
}
394394
} else {
395-
minDepthFundee(channelConf, channelFeatures, fundingParams.fundingAmount)
395+
minDepthFundee(channelConf, localFeatures, fundingParams.fundingAmount)
396396
}
397397
}
398398

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,10 +1364,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
13641364
when(SYNCING)(handleExceptions {
13651365
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
13661366
val minDepth_opt = if (d.commitments.localParams.isInitiator) {
1367-
Helpers.Funding.minDepthFunder(d.commitments.channelFeatures)
1367+
Helpers.Funding.minDepthFunder(d.commitments.localParams.initFeatures)
13681368
} else {
13691369
// when we're not the channel initiator we scale the min_depth confirmations depending on the funding amount
1370-
Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.channelFeatures, d.commitments.commitInput.txOut.amount)
1370+
Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.localParams.initFeatures, d.commitments.commitInput.txOut.amount)
13711371
}
13721372
val minDepth = minDepth_opt.getOrElse {
13731373
val defaultMinDepth = nodeParams.channelConf.minDepthBlocks
@@ -1381,7 +1381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
13811381
goto(WAIT_FOR_FUNDING_CONFIRMED)
13821382

13831383
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
1384-
val minDepth_opt = Helpers.Funding.minDepthDualFunding(nodeParams.channelConf, d.commitments.channelFeatures, d.fundingParams)
1384+
val minDepth_opt = Helpers.Funding.minDepthDualFunding(nodeParams.channelConf, d.commitments.localParams.initFeatures, d.fundingParams)
13851385
val minDepth = minDepth_opt.getOrElse {
13861386
val defaultMinDepth = nodeParams.channelConf.minDepthBlocks
13871387
// If we are in state WAIT_FOR_DUAL_FUNDING_CONFIRMED, then the computed minDepth should be > 0, otherwise we would

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
148148
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
149149
val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig)
150150
val totalFundingAmount = open.fundingAmount + d.init.fundingContribution_opt.getOrElse(0 sat)
151-
val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, channelFeatures, totalFundingAmount)
151+
val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, d.init.localParams.initFeatures, totalFundingAmount)
152152
val upfrontShutdownScript_opt = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) {
153153
Some(ChannelTlv.UpfrontShutdownScriptTlv(localParams.defaultFinalScriptPubKey))
154154
} else {
@@ -316,7 +316,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
316316
case InteractiveTxBuilder.SendMessage(msg) => stay() sending msg
317317
case InteractiveTxBuilder.Succeeded(fundingParams, fundingTx, commitments) =>
318318
d.deferred.foreach(self ! _)
319-
Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams) match {
319+
Funding.minDepthDualFunding(nodeParams.channelConf, commitments.localParams.initFeatures, fundingParams) match {
320320
case Some(fundingMinDepth) =>
321321
blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth)
322322
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, d.localPushAmount, d.remotePushAmount, Nil, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, RbfStatus.NoRbf, None)
@@ -525,7 +525,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
525525
case InteractiveTxBuilder.SendMessage(msg) => stay() sending msg
526526
case InteractiveTxBuilder.Succeeded(fundingParams, fundingTx, commitments) =>
527527
// We now have more than one version of the funding tx, so we cannot use zero-conf.
528-
val fundingMinDepth = Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams).getOrElse(nodeParams.channelConf.minDepthBlocks.toLong)
528+
val fundingMinDepth = Funding.minDepthDualFunding(nodeParams.channelConf, commitments.localParams.initFeatures, fundingParams).getOrElse(nodeParams.channelConf.minDepthBlocks.toLong)
529529
blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth)
530530
val previousFundingTxs = DualFundingTx(d.fundingTx, d.commitments) +: d.previousFundingTxs
531531
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, d.localPushAmount, d.remotePushAmount, previousFundingTxs, d.waitingSince, d.lastChecked, RbfStatus.NoRbf, d.deferred)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
115115
context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.feeratePerKw, None))
116116
val fundingPubkey = keyManager.fundingPublicKey(d.initFundee.localParams.fundingKeyPath).publicKey
117117
val channelKeyPath = keyManager.keyPath(d.initFundee.localParams, d.initFundee.channelConfig)
118-
val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, channelFeatures, open.fundingSatoshis)
118+
val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, d.initFundee.localParams.initFeatures, open.fundingSatoshis)
119119
log.info("will use fundingMinDepth={}", minimumDepth)
120120
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used.
121121
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
@@ -291,7 +291,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
291291
// NB: we don't send a ChannelSignatureSent for the first commit
292292
log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitments.fundingTxId}")
293293
watchFundingTx(commitments)
294-
Funding.minDepthFundee(nodeParams.channelConf, commitments.channelFeatures, fundingAmount) match {
294+
Funding.minDepthFundee(nodeParams.channelConf, commitments.localParams.initFeatures, fundingAmount) match {
295295
case Some(fundingMinDepth) =>
296296
blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth)
297297
goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned
@@ -338,7 +338,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
338338
watchFundingTx(commitments)
339339
// we will publish the funding tx only after the channel state has been written to disk because we want to
340340
// make sure we first persist the commitment that returns back the funds to us in case of problem
341-
Funding.minDepthFunder(commitments.channelFeatures) match {
341+
Funding.minDepthFunder(commitments.localParams.initFeatures) match {
342342
case Some(fundingMinDepth) =>
343343
blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth)
344344
goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx(commitments, fundingTx, fundingTxFee)

eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import fr.acinq.eclair.channel.fsm.Channel
2727
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
2828
import fr.acinq.eclair.transactions.Transactions._
2929
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
30-
import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong}
30+
import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong}
3131
import org.scalatest.Tag
3232
import org.scalatest.funsuite.AnyFunSuiteLike
3333
import scodec.bits.HexStringSyntax
@@ -40,14 +40,14 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat
4040
implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging
4141

4242
test("compute the funding tx min depth according to funding amount") {
43-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(1)).contains(4))
44-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf.copy(minDepthBlocks = 6), ChannelFeatures(), Btc(1)).contains(6)) // 4 conf would be enough but we use min-depth=6
45-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(6.25)).contains(16)) // we use scaling_factor=15 and a fixed block reward of 6.25BTC
46-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.50)).contains(31))
47-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.60)).contains(32))
48-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(30)).contains(73))
49-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(50)).contains(121))
50-
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(Features.ZeroConf), Btc(50)).isEmpty)
43+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(), Btc(1)).contains(4))
44+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf.copy(minDepthBlocks = 6), Features(), Btc(1)).contains(6)) // 4 conf would be enough but we use min-depth=6
45+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(), Btc(6.25)).contains(16)) // we use scaling_factor=15 and a fixed block reward of 6.25BTC
46+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(), Btc(12.50)).contains(31))
47+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(), Btc(12.60)).contains(32))
48+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(), Btc(30)).contains(73))
49+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(), Btc(50)).contains(121))
50+
assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, Features(Features.ZeroConf -> FeatureSupport.Optional), Btc(50)).isEmpty)
5151
}
5252

5353
test("compute refresh delay") {

eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/zeroconf/ZeroConfActivationSpec.scala

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong}
55
import fr.acinq.eclair.FeatureSupport.Optional
66
import fr.acinq.eclair.Features.ZeroConf
77
import fr.acinq.eclair.channel.ChannelTypes.AnchorOutputsZeroFeeHtlcTx
8-
import fr.acinq.eclair.channel.PersistentChannelData
8+
import fr.acinq.eclair.channel.{NORMAL, PersistentChannelData}
99
import fr.acinq.eclair.integration.basic.fixtures.composite.TwoNodesFixture
1010
import fr.acinq.eclair.testutils.FixtureSpec
1111
import org.scalatest.concurrent.IntegrationPatience
1212
import org.scalatest.{Tag, TestData}
1313
import scodec.bits.HexStringSyntax
1414

15+
import scala.concurrent.duration.DurationInt
16+
1517
/**
1618
* Test the activation of zero-conf option, via features or channel type.
1719
*/
1820
class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience {
1921

22+
implicit val config: PatienceConfig = PatienceConfig(5 second, 50 milliseconds)
23+
2024
type FixtureParam = TwoNodesFixture
2125

2226
val ZeroConfAlice = "zero_conf_alice"
@@ -80,6 +84,25 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience {
8084
assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf))
8185
}
8286

87+
test("open a channel alice-bob (zero-conf enabled on bob, not requested via channel type by alice)", Tag(ZeroConfBob)) { f =>
88+
import f._
89+
90+
assert(!alice.nodeParams.features.activated.contains(ZeroConf))
91+
assert(bob.nodeParams.features.activated.contains(ZeroConf))
92+
93+
connect(alice, bob)
94+
val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)
95+
val channelId = openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId
96+
97+
// Bob has activated support for 0-conf with Alice, so he doesn't wait for the funding tx to confirm regardless of
98+
// the channel type and activated feature bits. Since Alice has full control over the funding tx, she accepts Bob's
99+
// early channel_ready and completes the channel opening flow without waiting for confirmations.
100+
eventually {
101+
assert(getChannelState(alice, channelId) == NORMAL)
102+
assert(getChannelState(bob, channelId) == NORMAL)
103+
}
104+
}
105+
83106
test("open a channel alice-bob (zero-conf enabled on alice and bob, but not requested via channel type by alice)", Tag(ZeroConfAlice), Tag(ZeroConfBob)) { f =>
84107
import f._
85108

0 commit comments

Comments
 (0)