mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-19 01:43:22 +01:00
Allow disabling no-htlc commitment fee-bump (#2246)
When a channel force-close without any pending htlcs, funds are not at risk. We want to eventually get our main output back, but if we are not in a rush we can save on fees by never spending the anchors. This is disabled by default as there is a potential risk: if the commit tx doesn't confirm and the feerate rises, the commit tx may eventually be below the network's min-relay-fee and won't confirm (at least until package relay is available).
This commit is contained in:
parent
2461ef08cb
commit
3b97e446aa
@ -130,6 +130,19 @@ change the search interval with two new settings:
|
||||
- `eclair.purge-expired-invoices.enabled = true
|
||||
- `eclair.purge-expired-invoices.interval = 24 hours`
|
||||
|
||||
#### Skip anchor CPFP for empty commitment
|
||||
|
||||
When using anchor outputs and a channel force-closes without HTLCs in the commitment transaction, funds cannot be stolen by your counterparty.
|
||||
In that case eclair can skip spending the anchor output to save on-chain fees, even if the transaction doesn't confirm.
|
||||
This can be activated by setting the following value in your `eclair.conf`:
|
||||
|
||||
```conf
|
||||
eclair.on-chain-fees.spend-anchor-without-htlcs = false
|
||||
```
|
||||
|
||||
This is disabled by default, because there is still a risk of losing funds until bitcoin adds support for package relay.
|
||||
If the mempool becomes congested and the feerate is too low, the commitment transaction may never reach miners' mempools because it's below the minimum relay feerate.
|
||||
|
||||
## Verifying signatures
|
||||
|
||||
You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it:
|
||||
|
@ -167,12 +167,18 @@ eclair {
|
||||
|
||||
// number of blocks to target when computing fees for each transaction type
|
||||
target-blocks {
|
||||
funding = 6 // target for the funding transaction
|
||||
commitment = 2 // target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing*
|
||||
commitment-without-htlcs = 12 // target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing*
|
||||
mutual-close = 12 // target for the mutual close transaction
|
||||
claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet)
|
||||
safe-utxos-threshold = 10 // when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios
|
||||
// target for the funding transaction
|
||||
funding = 6
|
||||
// target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing*
|
||||
commitment = 2
|
||||
// target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing*
|
||||
commitment-without-htlcs = 12
|
||||
// target for the mutual close transaction
|
||||
mutual-close = 12
|
||||
// target for the claim main transaction (tx that spends main channel output back to wallet)
|
||||
claim-main = 12
|
||||
// when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios
|
||||
safe-utxos-threshold = 10
|
||||
}
|
||||
|
||||
feerate-tolerance {
|
||||
@ -207,6 +213,10 @@ eclair {
|
||||
# }
|
||||
]
|
||||
|
||||
// if false, the commitment transaction will not be fee-bumped when we have no htlcs to claim (used in force-close scenario)
|
||||
// *do not change this unless you know what you are doing*
|
||||
spend-anchor-without-htlcs = true
|
||||
|
||||
close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing
|
||||
|
||||
// the channel initiator will send an UpdateFee message if the difference between current commitment fee and actual
|
||||
|
@ -440,6 +440,7 @@ object NodeParams extends Logging {
|
||||
onChainFeeConf = OnChainFeeConf(
|
||||
feeTargets = feeTargets,
|
||||
feeEstimator = feeEstimator,
|
||||
spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"),
|
||||
closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"),
|
||||
updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio"),
|
||||
defaultFeerateTolerance = FeerateTolerance(
|
||||
|
@ -56,7 +56,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
|
||||
}
|
||||
}
|
||||
|
||||
case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) {
|
||||
case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, spendAnchorWithoutHtlcs: Boolean, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) {
|
||||
|
||||
def feerateToleranceFor(nodeId: PublicKey): FeerateTolerance = perNodeFeerateTolerance.getOrElse(nodeId, defaultFeerateTolerance)
|
||||
|
||||
|
@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256}
|
||||
import fr.acinq.bitcoin.scalacompat.Script._
|
||||
import fr.acinq.bitcoin.scalacompat._
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw}
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw, OnChainFeeConf}
|
||||
import fr.acinq.eclair.channel.fsm.Channel
|
||||
import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL}
|
||||
import fr.acinq.eclair.crypto.Generators
|
||||
@ -668,7 +668,7 @@ object Helpers {
|
||||
* @param commitments our commitment data, which include payment preimages
|
||||
* @return a list of transactions (one per output of the commit tx that we can claim)
|
||||
*/
|
||||
def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = {
|
||||
def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): LocalCommitPublished = {
|
||||
import commitments._
|
||||
require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx")
|
||||
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
|
||||
@ -676,7 +676,7 @@ object Helpers {
|
||||
val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint)
|
||||
val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint)
|
||||
val localFundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey
|
||||
val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget)
|
||||
val feeratePerKwDelayed = onChainFeeConf.feeEstimator.getFeeratePerKw(onChainFeeConf.feeTargets.claimMainBlockTarget)
|
||||
|
||||
// first we will claim our main output as soon as the delay is over
|
||||
val mainDelayedTx = withTxGenerationLog("local-main-delayed") {
|
||||
@ -688,16 +688,21 @@ object Helpers {
|
||||
|
||||
val htlcTxs: Map[OutPoint, Option[HtlcTx]] = claimHtlcOutputs(keyManager, commitments)
|
||||
|
||||
// If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation.
|
||||
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget)
|
||||
val claimAnchorTxs: List[ClaimAnchorOutputTx] = List(
|
||||
withTxGenerationLog("local-anchor") {
|
||||
Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore)
|
||||
},
|
||||
withTxGenerationLog("remote-anchor") {
|
||||
Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey)
|
||||
}
|
||||
).flatten
|
||||
val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs
|
||||
val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) {
|
||||
// If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation.
|
||||
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + onChainFeeConf.feeTargets.commitmentWithoutHtlcsBlockTarget)
|
||||
List(
|
||||
withTxGenerationLog("local-anchor") {
|
||||
Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore)
|
||||
},
|
||||
withTxGenerationLog("remote-anchor") {
|
||||
Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey)
|
||||
}
|
||||
).flatten
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
|
||||
LocalCommitPublished(
|
||||
commitTx = tx,
|
||||
@ -787,26 +792,31 @@ object Helpers {
|
||||
* @param tx the remote commitment transaction that has just been published
|
||||
* @return a list of transactions (one per output of the commit tx that we can claim)
|
||||
*/
|
||||
def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = {
|
||||
def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): RemoteCommitPublished = {
|
||||
require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx")
|
||||
|
||||
val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitments, remoteCommit, feeEstimator)
|
||||
val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitments, remoteCommit, onChainFeeConf.feeEstimator)
|
||||
|
||||
// If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation.
|
||||
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget)
|
||||
val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey
|
||||
val claimAnchorTxs: List[ClaimAnchorOutputTx] = List(
|
||||
withTxGenerationLog("local-anchor") {
|
||||
Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore)
|
||||
},
|
||||
withTxGenerationLog("remote-anchor") {
|
||||
Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey)
|
||||
}
|
||||
).flatten
|
||||
val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs
|
||||
val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) {
|
||||
// If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation.
|
||||
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + onChainFeeConf.feeTargets.commitmentWithoutHtlcsBlockTarget)
|
||||
val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey
|
||||
List(
|
||||
withTxGenerationLog("local-anchor") {
|
||||
Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore)
|
||||
},
|
||||
withTxGenerationLog("remote-anchor") {
|
||||
Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey)
|
||||
}
|
||||
).flatten
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
|
||||
RemoteCommitPublished(
|
||||
commitTx = tx,
|
||||
claimMainOutputTx = claimMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets),
|
||||
claimMainOutputTx = claimMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, onChainFeeConf.feeEstimator, onChainFeeConf.feeTargets),
|
||||
claimHtlcTxs = htlcTxs,
|
||||
claimAnchorTxs = claimAnchorTxs,
|
||||
irrevocablySpent = Map.empty
|
||||
|
@ -177,7 +177,7 @@ trait ErrorHandlers extends CommonHandlers {
|
||||
stay()
|
||||
} else {
|
||||
val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx
|
||||
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
|
||||
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf)
|
||||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished))
|
||||
@ -222,7 +222,7 @@ trait ErrorHandlers extends CommonHandlers {
|
||||
require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch")
|
||||
|
||||
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "remote-commit"))
|
||||
val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
|
||||
val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf)
|
||||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished))
|
||||
@ -254,7 +254,7 @@ trait ErrorHandlers extends CommonHandlers {
|
||||
require(commitTx.txid == remoteCommit.txid, "txid mismatch")
|
||||
|
||||
context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "next-remote-commit"))
|
||||
val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
|
||||
val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf)
|
||||
val nextData = d match {
|
||||
case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished))
|
||||
case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished))
|
||||
@ -332,7 +332,7 @@ trait ErrorHandlers extends CommonHandlers {
|
||||
|
||||
// let's try to spend our current local tx
|
||||
val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx
|
||||
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
|
||||
val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf)
|
||||
|
||||
goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error
|
||||
}
|
||||
|
@ -125,6 +125,7 @@ object TestConstants {
|
||||
onChainFeeConf = OnChainFeeConf(
|
||||
feeTargets = FeeTargets(6, 2, 36, 12, 18, 0),
|
||||
feeEstimator = new TestFeeEstimator,
|
||||
spendAnchorWithoutHtlcs = true,
|
||||
closeOnOfflineMismatch = true,
|
||||
updateFeeMinDiffRatio = 0.1,
|
||||
defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw, DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true)),
|
||||
@ -266,6 +267,7 @@ object TestConstants {
|
||||
onChainFeeConf = OnChainFeeConf(
|
||||
feeTargets = FeeTargets(6, 2, 36, 12, 18, 0),
|
||||
feeEstimator = new TestFeeEstimator,
|
||||
spendAnchorWithoutHtlcs = true,
|
||||
closeOnOfflineMismatch = true,
|
||||
updateFeeMinDiffRatio = 0.1,
|
||||
defaultFeerateTolerance = FeerateTolerance(0.75, 1.5, anchorOutputsFeeratePerKw, DustTolerance(30_000 sat, closeOnUpdateFeeOverflow = true)),
|
||||
|
@ -27,7 +27,7 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
||||
val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false))
|
||||
|
||||
test("should update fee when diff ratio exceeded") {
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty)
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty)
|
||||
assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat)))
|
||||
assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat)))
|
||||
assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat)))
|
||||
@ -38,7 +38,7 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
||||
test("get commitment feerate") {
|
||||
val feeEstimator = new TestFeeEstimator()
|
||||
val channelType = ChannelTypes.Standard
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty)
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty)
|
||||
|
||||
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat)))
|
||||
assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) == FeeratePerKw(5000 sat))
|
||||
@ -53,7 +53,7 @@ class FeeEstimatorSpec extends AnyFunSuite {
|
||||
val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate
|
||||
val overrideNodeId = randomKey().publicKey
|
||||
val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate)))
|
||||
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate)))
|
||||
|
||||
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat)))
|
||||
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate / 2)
|
||||
|
@ -44,6 +44,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
||||
val feeConfNoMismatch = OnChainFeeConf(
|
||||
FeeTargets(6, 2, 12, 2, 6, 1),
|
||||
new TestFeeEstimator(),
|
||||
spendAnchorWithoutHtlcs = true,
|
||||
closeOnOfflineMismatch = false,
|
||||
1.0,
|
||||
FeerateTolerance(0.00001, 100000.0, TestConstants.anchorOutputsFeeratePerKw, DustTolerance(100000 sat, closeOnUpdateFeeOverflow = false)),
|
||||
|
@ -77,6 +77,8 @@ object ChannelStateTestsTags {
|
||||
val ZeroConf = "zeroconf"
|
||||
/** If set, channels will use option_scid_alias. */
|
||||
val ScidAlias = "scid_alias"
|
||||
/** If set, we won't spend anchors to fee-bump commitments without htlcs (no funds at risk). */
|
||||
val DontSpendAnchorWithoutHtlcs = "dont-spend-anchor-without-htlcs"
|
||||
}
|
||||
|
||||
trait ChannelStateTestsBase extends Assertions with Eventually {
|
||||
@ -135,11 +137,13 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
|
||||
.modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat)
|
||||
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat)
|
||||
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat)
|
||||
.modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false)
|
||||
val finalNodeParamsB = nodeParamsB
|
||||
.modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat)
|
||||
.modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat)
|
||||
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat)
|
||||
.modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat)
|
||||
.modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false)
|
||||
val alice: TestFSMRef[ChannelState, ChannelData, Channel] = {
|
||||
implicit val system: ActorSystem = systemA
|
||||
TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref)
|
||||
|
@ -3300,7 +3300,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
||||
alice2blockchain.expectNoMessage(1 second)
|
||||
}
|
||||
|
||||
test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
|
||||
def testErrorAnchorOutputsWithHtlcs(f: FixtureParam): Unit = {
|
||||
import f._
|
||||
|
||||
val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice)
|
||||
@ -3343,7 +3343,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
||||
alice2blockchain.expectNoMessage(1 second)
|
||||
}
|
||||
|
||||
test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
|
||||
test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
|
||||
testErrorAnchorOutputsWithHtlcs(f)
|
||||
}
|
||||
|
||||
test("recv Error (anchor outputs zero fee htlc txs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f =>
|
||||
// We should ignore the disable flag since there are htlcs in the commitment (funds at risk).
|
||||
testErrorAnchorOutputsWithHtlcs(f)
|
||||
}
|
||||
|
||||
def testErrorAnchorOutputsWithoutHtlcs(f: FixtureParam, commitFeeBumpDisabled: Boolean): Unit = {
|
||||
import f._
|
||||
|
||||
// an error occurs and alice publishes her commit tx
|
||||
@ -3355,14 +3364,29 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
|
||||
|
||||
val currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight
|
||||
val blockTargets = alice.underlyingActor.nodeParams.onChainFeeConf.feeTargets
|
||||
val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx]
|
||||
// When there are no pending HTLCs, there is no rush to get the commit tx confirmed
|
||||
assert(localAnchor.txInfo.confirmBefore == currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget)
|
||||
val claimMain = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == localAnchor.input.index)
|
||||
alice2blockchain.expectNoMessage(1 second)
|
||||
if (commitFeeBumpDisabled) {
|
||||
val claimMain = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)
|
||||
alice2blockchain.expectNoMessage(1 second)
|
||||
} else {
|
||||
val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx]
|
||||
// When there are no pending HTLCs, there is no rush to get the commit tx confirmed
|
||||
assert(localAnchor.txInfo.confirmBefore === currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget)
|
||||
val claimMain = alice2blockchain.expectMsgType[PublishFinalTx]
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index)
|
||||
alice2blockchain.expectNoMessage(1 second)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
|
||||
testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false)
|
||||
}
|
||||
|
||||
test("recv Error (anchor outputs zero fee htlc txs without htlcs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f =>
|
||||
testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = true)
|
||||
}
|
||||
|
||||
test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f =>
|
||||
|
Loading…
Reference in New Issue
Block a user