Skip to content

Commit 96183a9

Browse files
authored
Increase min-depth for funding transactions (#2973)
We already use a minimum depth of 6 before announcing channels to protect against reorgs. However, we allowed using the channel for payments after only 3 confirmations (for small channels). A reorg of 3 blocks that invalidates the funding transaction would allow our peer to potentially steal funds. It's more consistent to use the same depth for announcing the channel and actually using it. Note that for wumbo channels, we already scaled the number of confirmations based on the size of the channel. For closing transaction, we don't need the same reorg safety, since we keep watching the funding output for any transaction that spends it, and concurrently spend any commitment transaction that we detect. We thus keep a minimum depth of 3 for closing transactions. We also update our confirmation scaling factor post-halving. We were still using values from before the halving. We update those values and change the scaling factor to a reasonable scaling. This protects channels against attackers with significant mining power.
1 parent ef1a029 commit 96183a9

19 files changed

+73
-77
lines changed

eclair-core/src/main/resources/reference.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ eclair {
130130

131131
to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days)
132132
max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks)
133-
mindepth-blocks = 3
133+
min-depth-funding-blocks = 6 // minimum number of confirmations for funding transactions
134+
min-depth-closing-blocks = 3 // minimum number of confirmations for closing transactions
134135
expiry-delta-blocks = 144
135136
max-expiry-delta-blocks = 2016 // we won't forward HTLCs with timeouts greater than this delta
136137
// When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,11 @@ object NodeParams extends Logging {
314314
"channel.min-funding-satoshis" -> "channel.min-public-funding-satoshis, channel.min-private-funding-satoshis",
315315
// v0.8.0
316316
"bitcoind.batch-requests" -> "bitcoind.batch-watcher-requests",
317-
// vx.x.x
317+
// v0.9.0
318318
"on-chain-fees.target-blocks.safe-utxos-threshold" -> "on-chain-fees.safe-utxos-threshold",
319-
"on-chain-fees.target-blocks" -> "on-chain-fees.confirmation-priority"
319+
"on-chain-fees.target-blocks" -> "on-chain-fees.confirmation-priority",
320+
// v0.12.0
321+
"channel.mindepth-blocks" -> "channel.min-depth-funding-blocks",
320322
)
321323
deprecatedKeyPaths.foreach {
322324
case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'")
@@ -573,7 +575,8 @@ object NodeParams extends Logging {
573575
minFundingPrivateSatoshis = Satoshi(config.getLong("channel.min-private-funding-satoshis")),
574576
toRemoteDelay = offeredCLTV,
575577
maxToLocalDelay = maxToLocalCLTV,
576-
minDepthBlocks = config.getInt("channel.mindepth-blocks"),
578+
minDepthFunding = config.getInt("channel.min-depth-funding-blocks"),
579+
minDepthClosing = config.getInt("channel.min-depth-closing-blocks"),
577580
expiryDelta = expiryDelta,
578581
maxExpiryDelta = maxExpiryDelta,
579582
fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ object ChannelParams {
141141
// small amount: not scaled
142142
defaultMinDepth
143143
} else {
144-
val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024
145-
val scalingFactor = 15
144+
val blockReward = 3.125 // this will be too large after the halving in 2028
145+
val scalingFactor = 10
146146
val blocksToReachFunding = (((scalingFactor * amount.toBtc.toDouble) / blockReward).ceil + 1).toInt
147147
defaultMinDepth.max(blocksToReachFunding)
148148
}

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ object Channel {
8686
minFundingPrivateSatoshis: Satoshi,
8787
toRemoteDelay: CltvExpiryDelta,
8888
maxToLocalDelay: CltvExpiryDelta,
89-
minDepthBlocks: Int,
89+
minDepthFunding: Int,
90+
minDepthClosing: Int,
9091
expiryDelta: CltvExpiryDelta,
9192
maxExpiryDelta: CltvExpiryDelta,
9293
fulfillSafetyBeforeTimeout: CltvExpiryDelta,
@@ -294,11 +295,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
294295
watchFundingConfirmed(commitment.fundingTxId, Some(singleFundingMinDepth(data)), herdDelay_opt)
295296
case fundingTx: LocalFundingStatus.DualFundedUnconfirmedFundingTx =>
296297
publishFundingTx(fundingTx)
297-
val minDepth_opt = data.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, fundingTx.sharedTx.tx)
298+
val minDepth_opt = data.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, fundingTx.sharedTx.tx)
298299
watchFundingConfirmed(fundingTx.sharedTx.txId, minDepth_opt, herdDelay_opt)
299300
case fundingTx: LocalFundingStatus.ZeroconfPublishedFundingTx =>
300301
// those are zero-conf channels, the min-depth isn't critical, we use the default
301-
watchFundingConfirmed(fundingTx.tx.txid, Some(nodeParams.channelConf.minDepthBlocks.toLong), herdDelay_opt)
302+
watchFundingConfirmed(fundingTx.tx.txid, Some(nodeParams.channelConf.minDepthFunding.toLong), herdDelay_opt)
302303
case _: LocalFundingStatus.ConfirmedFundingTx =>
303304
data match {
304305
case closing: DATA_CLOSING if Closing.nothingAtStake(closing) || Closing.isClosingTypeAlreadyKnown(closing).isDefined =>
@@ -581,7 +582,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
581582
// We don't have their tx_sigs, but they have ours, and could publish the funding tx without telling us.
582583
// That's why we move on immediately to the next step, and will update our unsigned funding tx when we
583584
// receive their tx_sigs.
584-
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession1.fundingTx.sharedTx.tx)
585+
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, signingSession1.fundingTx.sharedTx.tx)
585586
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
586587
val commitments1 = d.commitments.add(signingSession1.commitment)
587588
val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice)
@@ -1316,7 +1317,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13161317
rollbackFundingAttempt(signingSession.fundingTx.tx, previousTxs = Seq.empty) // no splice rbf yet
13171318
stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, f.getMessage)
13181319
case Right(signingSession1) =>
1319-
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession1.fundingTx.sharedTx.tx)
1320+
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, signingSession1.fundingTx.sharedTx.tx)
13201321
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
13211322
val commitments1 = d.commitments.add(signingSession1.commitment)
13221323
val d1 = d.copy(commitments = commitments1, spliceStatus = SpliceStatus.NoSplice)
@@ -1335,7 +1336,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
13351336
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
13361337
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
13371338
case Right((commitments1, _)) =>
1338-
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks), delay_opt = None)
1339+
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthFunding), delay_opt = None)
13391340
maybeEmitEventsPostSplice(d.shortIds, d.commitments, commitments1)
13401341
maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
13411342
stay() using d.copy(commitments = commitments1) storing() sending SpliceLocked(d.channelId, w.tx.txid)
@@ -1802,8 +1803,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
18021803
} else {
18031804
d.commitments.resolveCommitment(tx) match {
18041805
case Some(commitment) =>
1805-
log.warning(s"a commit tx for an older commitment has been published fundingTxId=${tx.txid} fundingTxIndex=${commitment.fundingTxIndex}")
1806-
blockchain ! WatchAlternativeCommitTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)
1806+
log.warning("a commit tx for an older commitment has been published fundingTxId={} fundingTxIndex={}", tx.txid, commitment.fundingTxIndex)
1807+
blockchain ! WatchAlternativeCommitTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthClosing)
18071808
stay()
18081809
case None =>
18091810
// This must be a former funding tx that has already been pruned, because watches are unordered.
@@ -1872,7 +1873,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
18721873
case Event(WatchOutputSpentTriggered(tx), d: DATA_CLOSING) =>
18731874
// one of the outputs of the local/remote/revoked commit was spent
18741875
// we just put a watch to be notified when it is confirmed
1875-
blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)
1876+
blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthClosing)
18761877
// when a remote or local commitment tx containing outgoing htlcs is published on the network,
18771878
// we watch it in order to extract payment preimage if funds are pulled by the counterparty
18781879
// we can then use these preimages to fulfill origin htlcs
@@ -1907,7 +1908,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
19071908
val (localCommitPublished1, claimHtlcTx_opt) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, keyManager, d.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.finalScriptPubKey)
19081909
claimHtlcTx_opt.foreach(claimHtlcTx => {
19091910
txPublisher ! PublishFinalTx(claimHtlcTx, claimHtlcTx.fee, None)
1910-
blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepthBlocks, Some(RelativeDelay(tx.txid, d.commitments.params.remoteParams.toSelfDelay.toInt.toLong)))
1911+
blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepthClosing, Some(RelativeDelay(tx.txid, d.commitments.params.remoteParams.toSelfDelay.toInt.toLong)))
19111912
})
19121913
Closing.updateLocalCommitPublished(localCommitPublished1, tx)
19131914
}),
@@ -2490,8 +2491,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
24902491
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
24912492
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
24922493
case Right((commitments1, _)) =>
2493-
log.info(s"zero-conf funding txid=${w.tx.txid} has been published")
2494-
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks), delay_opt = None)
2494+
log.info("zero-conf funding txid={} has been published", w.tx.txid)
2495+
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthFunding), delay_opt = None)
24952496
val d1 = d match {
24962497
// NB: we discard remote's stashed channel_ready, they will send it back at reconnection
24972498
case d: DATA_WAIT_FOR_FUNDING_CONFIRMED =>
@@ -2566,9 +2567,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
25662567
} else {
25672568
d.commitments.resolveCommitment(tx) match {
25682569
case Some(commitment) =>
2569-
log.warning(s"a commit tx for an older commitment has been published fundingTxId=${tx.txid} fundingTxIndex=${commitment.fundingTxIndex}")
2570+
log.warning("a commit tx for an older commitment has been published fundingTxId={} fundingTxIndex={}", tx.txid, commitment.fundingTxIndex)
25702571
// we watch the commitment tx, in the meantime we force close using the latest commitment
2571-
blockchain ! WatchAlternativeCommitTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)
2572+
blockchain ! WatchAlternativeCommitTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthClosing)
25722573
spendLocalCurrent(d)
25732574
case None =>
25742575
// This must be a former funding tx that has already been pruned, because watches are unordered.

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
173173
// At this point, the min_depth is an estimate and may change after we know exactly how our peer contributes
174174
// to the funding transaction. Maybe they will contribute 0 satoshis to the shared output, but still add inputs
175175
// and outputs.
176-
val minDepth_opt = channelParams.minDepthFundee(nodeParams.channelConf.minDepthBlocks, localAmount + remoteAmount)
176+
val minDepth_opt = channelParams.minDepthFundee(nodeParams.channelConf.minDepthFunding, localAmount + remoteAmount)
177177
val upfrontShutdownScript_opt = localParams.upfrontShutdownScript_opt.map(scriptPubKey => ChannelTlv.UpfrontShutdownScriptTlv(scriptPubKey))
178178
val tlvs: Set[AcceptDualFundedChannelTlv] = Set(
179179
upfrontShutdownScript_opt,
@@ -390,7 +390,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
390390
// We don't have their tx_sigs, but they have ours, and could publish the funding tx without telling us.
391391
// That's why we move on immediately to the next step, and will update our unsigned funding tx when we
392392
// receive their tx_sigs.
393-
val minDepth_opt = d.channelParams.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession1.fundingTx.sharedTx.tx)
393+
val minDepth_opt = d.channelParams.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, signingSession1.fundingTx.sharedTx.tx)
394394
watchFundingConfirmed(d.signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
395395
val commitments = Commitments(
396396
params = d.channelParams,
@@ -413,7 +413,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
413413
rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil)
414414
goto(CLOSED) sending Error(d.channelId, f.getMessage)
415415
case Right(signingSession) =>
416-
val minDepth_opt = d.channelParams.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession.fundingTx.sharedTx.tx)
416+
val minDepth_opt = d.channelParams.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, signingSession.fundingTx.sharedTx.tx)
417417
watchFundingConfirmed(d.signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
418418
val commitments = Commitments(
419419
params = d.channelParams,
@@ -478,7 +478,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
478478
rollbackRbfAttempt(signingSession, d)
479479
stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, f.getMessage)
480480
case Right(signingSession1) =>
481-
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession1.fundingTx.sharedTx.tx)
481+
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, signingSession1.fundingTx.sharedTx.tx)
482482
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
483483
val commitments1 = d.commitments.add(signingSession1.commitment)
484484
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments1, d.localPushAmount, d.remotePushAmount, d.waitingSince, d.lastChecked, DualFundingStatus.WaitingForConfirmations, d.deferred)
@@ -495,7 +495,7 @@ 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.minDepthBlocks, d.latestFundingTx.sharedTx.tx).isEmpty
498+
val zeroConf = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, d.latestFundingTx.sharedTx.tx).isEmpty
499499
if (!d.latestFundingTx.fundingParams.isInitiator) {
500500
cmd.replyTo ! RES_FAILURE(cmd, InvalidRbfNonInitiator(d.channelId))
501501
stay()
@@ -524,7 +524,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
524524
}
525525

526526
case Event(msg: TxInitRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
527-
val zeroConf = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, d.latestFundingTx.sharedTx.tx).isEmpty
527+
val zeroConf = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, d.latestFundingTx.sharedTx.tx).isEmpty
528528
if (d.latestFundingTx.fundingParams.isInitiator) {
529529
// Only the initiator is allowed to initiate RBF.
530530
log.info("rejecting tx_init_rbf, we're the initiator, not them!")
@@ -661,7 +661,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
661661
// No need to store their commit_sig, they will re-send it if we disconnect.
662662
stay() using d.copy(status = DualFundingStatus.RbfWaitingForSigs(signingSession1))
663663
case signingSession1: InteractiveTxSigningSession.SendingSigs =>
664-
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession1.fundingTx.sharedTx.tx)
664+
val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthFunding, signingSession1.fundingTx.sharedTx.tx)
665665
watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None)
666666
val commitments1 = d.commitments.add(signingSession1.commitment)
667667
val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments1, d.localPushAmount, d.remotePushAmount, d.waitingSince, d.lastChecked, DualFundingStatus.WaitingForConfirmations, d.deferred)
@@ -727,7 +727,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
727727
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
728728
case Right((commitments1, _)) =>
729729
// we still watch the funding tx for confirmation even if we can use the zero-conf channel right away
730-
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks), delay_opt = None)
730+
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthFunding), delay_opt = None)
731731
val realScidStatus = RealScidStatus.Unknown
732732
val shortIds = createShortIds(d.channelId, realScidStatus)
733733
val channelReady = createChannelReady(shortIds, d.commitments.params)

0 commit comments

Comments
 (0)