Skip to content

Commit 52b7652

Browse files
authored
Remove non-final transactions from XxxCommitPublished (#3097)
When a channel was force-closed, we previously stored partially created closing transactions for each output of the commitment transaction. In some cases those transactions didn't include any on-chain fee (HTLC txs with 0-fee anchors) and weren't signed. The fee is set in the publisher actors which then sign those transactions. This was an issue because we used those partial transactions as if they were the final transaction that would eventually confirm: this made it look like RBF wasn't an option to callers, and was thus easy to misuse. We now change our data model to only store the outputs of the commit tx that we may spend, without any actual spending transaction. We only store spending transactions once they are confirmed, at which point they can safely be used as closing transactions by callers such as our balance checks. This lets us get rid of some codec migration code related to closing transactions, and is more future-proof because we now don't need to encode closing transactions and can thus change their format easily. This also reduces the size of our channel state during force-close. We add a `max-closing-feerate` config parameter to cap the feerate used for closing transactions which aren't at risk of being double-spent. Node operators should generally use a low value here, and update it when they need to reclaim liquidity if needed.
1 parent 100e174 commit 52b7652

30 files changed

+907
-700
lines changed

docs/FAQ.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FAQ
22

3-
## What does it mean for a channel to be "enabled" or "disabled" ?
3+
## What does it mean for a channel to be "enabled" or "disabled"?
44

55
A channel is disabled if a `channel_update` message has been broadcast for that channel with the `disable` bit set (see [BOLT 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message)). It means that the channel still exists but cannot be used to route payments, until it has been re-enabled.
66

@@ -13,6 +13,16 @@ There are other cases when a channel becomes disabled, for example when its bala
1313

1414
Note that you can have multiple channels between the same nodes, and that some of them can be enabled while others are disabled (i.e. enable/disable is channel-specific, not node-specific).
1515

16-
## How should you stop an Eclair node ?
16+
## How do I make my closing transactions confirm faster?
17+
18+
When channels are unilaterally closed, there is a delay before which closing transactions can be published: you must wait for this delay before you can get your funds back.
19+
20+
Once published, transactions will be automatically RBF-ed by `eclair` based on your configuration values for the [`eclair.on-chain-fees` section](../eclair-core/src/main/resources/reference.conf).
21+
22+
Note that there is an upper bound on the feerate that will be used, configured by the `eclair.on-chain-fees.max-closing-feerate` parameter.
23+
If the current feerate is higher than this value, your transactions will not confirm.
24+
You should update `eclair.on-chain-fees.max-closing-feerate` in your `eclair.conf` and restart your node: your transactions will automatically be RBF-ed using the new feerate.
25+
26+
## How should you stop an Eclair node?
1727

1828
To stop your node you just need to kill its process, there is no API command to do this. The JVM handles the quit signal and notifies the node to perform clean-up. For example, there is a hook to cleanly free DB locks when using Postgres.

docs/release-notes/eclair-vnext.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ It can be enabled by setting `eclair.features.option_attribution_data = optional
3939

4040
### Miscellaneous improvements and bug fixes
4141

42+
#### Add `max-closing-feerate` configuration parameter
43+
44+
We added a new configuration value to `eclair.conf` to limit the feerate used for force-close transactions where funds aren't at risk: `eclair.on-chain-fees.max-closing-feerate`.
45+
This ensures that you won't end up paying a lot of fees during mempool congestion: your node will wait for the feerate to decrease to get your non-urgent transactions confirmed.
46+
If you need those transactions to confirm because you are low on liquidity, you should update `eclair.on-chain-fees.max-closing-feerate` and restart your node: `eclair` will automatically RBF all available transactions.
47+
4248
#### Remove confirmation scaling based on funding amount
4349

4450
We previously scaled the number of confirmations based on the channel funding amount.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,14 @@ eclair {
271271
closing = medium
272272
}
273273

274+
// Maximum feerate that will be used when closing channels for outputs that aren't at risk (main balance and HTLC 3rd-stage transactions).
275+
// Using a low value here ensures that you won't be paying high fees when the mempool is congested and you're not in
276+
// a hurry to get your channel funds back.
277+
// If closing transactions don't confirm and you need to get the funds back quickly, you should increase this value
278+
// and restart your node: closing transactions will automatically be RBF-ed to match the current feerate.
279+
// This value is in satoshis per byte.
280+
max-closing-feerate = 10
281+
274282
feerate-tolerance {
275283
ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate (only enforced when not using anchor outputs)
276284
ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate (for all commitment formats)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ object NodeParams extends Logging {
607607
),
608608
onChainFeeConf = OnChainFeeConf(
609609
feeTargets = feeTargets,
610+
maxClosingFeerate = FeeratePerKw(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.max-closing-feerate")))),
610611
safeUtxosThreshold = config.getInt("on-chain-fees.safe-utxos-threshold"),
611612
spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"),
612613
anchorWithoutHtlcsMaxFee = Satoshi(config.getLong("on-chain-fees.anchor-without-htlcs-max-fee-satoshis")),

eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import fr.acinq.eclair.channel.Helpers.Closing
2323
import fr.acinq.eclair.channel.Helpers.Closing._
2424
import fr.acinq.eclair.channel._
2525
import fr.acinq.eclair.transactions.DirectedHtlc.incoming
26-
import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, HtlcSuccessTx, HtlcTimeoutTx}
2726
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
2827

2928
import scala.concurrent.{ExecutionContext, Future}
@@ -69,31 +68,26 @@ object CheckBalance {
6968
def addLocalClose(lcp: LocalCommitPublished): MainAndHtlcBalance = {
7069
// If our main transaction isn't deeply confirmed yet, we count it in our off-chain balance.
7170
// Once it confirms, it will be included in our on-chain balance, so we ignore it in our off-chain balance.
72-
val additionalToLocal = lcp.claimMainDelayedOutputTx.map(_.input.outPoint) match {
71+
val additionalToLocal = lcp.localOutput_opt match {
7372
case Some(outpoint) if !lcp.irrevocablySpent.contains(outpoint) => lcp.commitTx.txOut(outpoint.index.toInt).amount
7473
case _ => 0 sat
7574
}
76-
val additionalHtlcs = lcp.htlcTxs.map {
77-
case (outpoint, htlcTx_opt) =>
78-
val htlcAmount = lcp.commitTx.txOut(outpoint.index.toInt).amount
79-
lcp.irrevocablySpent.get(outpoint) match {
80-
case Some(spendingTx) =>
81-
// If the HTLC was spent by us, there will be an entry in our 3rd-stage transactions.
82-
// Otherwise it was spent by the remote and we don't have anything to add to our balance.
83-
val delayedHtlcOutpoint = OutPoint(spendingTx.txid, 0)
84-
val htlcSpentByUs = lcp.claimHtlcDelayedTxs.map(_.input.outPoint).contains(delayedHtlcOutpoint)
85-
// If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance.
86-
// Once confirmed, we should ignore it since it will appear in our on-chain balance.
87-
val htlcDelayedPending = !lcp.irrevocablySpent.contains(delayedHtlcOutpoint)
88-
if (htlcSpentByUs && htlcDelayedPending) htlcAmount else 0 sat
89-
case None =>
90-
// We assume that HTLCs will be fulfilled, so we only count incoming HTLCs in our off-chain balance.
91-
htlcTx_opt match {
92-
case Some(_: HtlcSuccessTx) => htlcAmount
93-
case Some(_: HtlcTimeoutTx) => 0 sat
94-
case None => htlcAmount // incoming HTLC for which we don't have the preimage yet
95-
}
96-
}
75+
val additionalHtlcs = lcp.htlcOutputs.map { outpoint =>
76+
val htlcAmount = lcp.commitTx.txOut(outpoint.index.toInt).amount
77+
lcp.irrevocablySpent.get(outpoint) match {
78+
case Some(spendingTx) =>
79+
// If the HTLC was spent by us, there will be an entry in our 3rd-stage transactions.
80+
// Otherwise it was spent by the remote and we don't have anything to add to our balance.
81+
val delayedHtlcOutpoint = OutPoint(spendingTx.txid, 0)
82+
val htlcSpentByUs = lcp.htlcDelayedOutputs.contains(delayedHtlcOutpoint)
83+
// If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance.
84+
// Once confirmed, we should ignore it since it will appear in our on-chain balance.
85+
val htlcDelayedPending = !lcp.irrevocablySpent.contains(delayedHtlcOutpoint)
86+
if (htlcSpentByUs && htlcDelayedPending) htlcAmount else 0 sat
87+
case None =>
88+
// We assume that HTLCs will be fulfilled, so we only count incoming HTLCs in our off-chain balance.
89+
if (lcp.incomingHtlcs.contains(outpoint)) htlcAmount else 0 sat
90+
}
9791
}.sum
9892
MainAndHtlcBalance(toLocal = toLocal + additionalToLocal, htlcs = htlcs + additionalHtlcs)
9993
}
@@ -102,16 +96,15 @@ object CheckBalance {
10296
def addRemoteClose(rcp: RemoteCommitPublished): MainAndHtlcBalance = {
10397
// If our main transaction isn't deeply confirmed yet, we count it in our off-chain balance.
10498
// Once it confirms, it will be included in our on-chain balance, so we ignore it in our off-chain balance.
105-
val additionalToLocal = rcp.claimMainOutputTx.map(_.input.outPoint) match {
99+
val additionalToLocal = rcp.localOutput_opt match {
106100
case Some(outpoint) if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount
107101
case _ => 0 sat
108102
}
109103
// If HTLC transactions are confirmed, they will appear in our on-chain balance if we were the one to claim them.
110104
// We only need to include incoming HTLCs that haven't been claimed yet (since we assume that they will be fulfilled).
111105
// Note that it is their commitment, so incoming/outgoing are inverted.
112-
val additionalHtlcs = rcp.claimHtlcTxs.map {
113-
case (outpoint, Some(_: ClaimHtlcSuccessTx)) if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount
114-
case (outpoint, None) if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount // incoming HTLC for which we don't have the preimage yet
106+
val additionalHtlcs = rcp.incomingHtlcs.keys.map {
107+
case outpoint if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount
115108
case _ => 0 sat
116109
}.sum
117110
MainAndHtlcBalance(toLocal = toLocal + additionalToLocal, htlcs = htlcs + additionalHtlcs)
@@ -122,15 +115,15 @@ object CheckBalance {
122115
// If our main transaction isn't deeply confirmed yet, we count it in our off-chain balance.
123116
// Once it confirms, it will be included in our on-chain balance, so we ignore it in our off-chain balance.
124117
// We do the same thing for our main penalty transaction claiming their main output.
125-
val additionalToLocal = rvk.claimMainOutputTx.map(_.input.outPoint) match {
118+
val additionalToLocal = rvk.localOutput_opt match {
126119
case Some(outpoint) if !rvk.irrevocablySpent.contains(outpoint) => rvk.commitTx.txOut(outpoint.index.toInt).amount
127120
case _ => 0 sat
128121
}
129-
val additionalToRemote = rvk.mainPenaltyTx.map(_.input.outPoint) match {
122+
val additionalToRemote = rvk.remoteOutput_opt match {
130123
case Some(outpoint) if !rvk.irrevocablySpent.contains(outpoint) => rvk.commitTx.txOut(outpoint.index.toInt).amount
131124
case _ => 0 sat
132125
}
133-
val additionalHtlcs = rvk.htlcPenaltyTxs.map(_.input.outPoint).map(htlcOutpoint => {
126+
val additionalHtlcs = rvk.htlcOutputs.map(htlcOutpoint => {
134127
val htlcAmount = rvk.commitTx.txOut(htlcOutpoint.index.toInt).amount
135128
rvk.irrevocablySpent.get(htlcOutpoint) match {
136129
case Some(spendingTx) =>
@@ -139,7 +132,7 @@ object CheckBalance {
139132
case Some(outputIndex) =>
140133
// If they managed to get their HTLC transaction confirmed, we published an HTLC-delayed penalty transaction.
141134
val delayedHtlcOutpoint = OutPoint(spendingTx.txid, outputIndex)
142-
val htlcSpentByThem = rvk.claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).contains(delayedHtlcOutpoint)
135+
val htlcSpentByThem = rvk.htlcDelayedOutputs.contains(delayedHtlcOutpoint)
143136
// If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance.
144137
// Once confirmed, we should ignore it since it will appear in our on-chain balance.
145138
val htlcDelayedPending = !rvk.irrevocablySpent.contains(delayedHtlcOutpoint)
@@ -194,7 +187,7 @@ object CheckBalance {
194187
// In the recovery case, we can only claim our main output, HTLC outputs are lost.
195188
// Once our main transaction confirms, the channel will transition to the CLOSED state and our channel funds
196189
// will appear in our on-chain balance (minus on-chain fees).
197-
case Some(c: RecoveryClose) => c.remoteCommitPublished.claimMainOutputTx.map(_.input.outPoint) match {
190+
case Some(c: RecoveryClose) => c.remoteCommitPublished.localOutput_opt match {
198191
case Some(localOutput) =>
199192
val localBalance = c.remoteCommitPublished.commitTx.txOut(localOutput.index.toInt).amount
200193
this.copy(closing = this.closing.copy(toLocal = this.closing.toLocal + localBalance))

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
9191
}
9292

9393
case class OnChainFeeConf(feeTargets: FeeTargets,
94+
maxClosingFeerate: FeeratePerKw,
9495
safeUtxosThreshold: Int,
9596
spendAnchorWithoutHtlcs: Boolean,
9697
anchorWithoutHtlcsMaxFee: Satoshi,
@@ -128,5 +129,5 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
128129
}
129130
}
130131

131-
def getClosingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.closing.getFeerate(feerates)
132+
def getClosingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.closing.getFeerate(feerates).min(maxClosingFeerate)
132133
}

0 commit comments

Comments
 (0)