1
0
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:
Bastien Teinturier 2022-07-01 16:04:41 +02:00 committed by GitHub
parent 2461ef08cb
commit 3b97e446aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 116 additions and 51 deletions

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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)),

View File

@ -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)

View File

@ -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)),

View File

@ -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)

View File

@ -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 =>