diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 3164d6c4ac..46fae00ca0 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -212,6 +212,12 @@ eclair { 1008 = 5000 } + // confirmation priority for each transaction type, can be slow/medium/fast + confirmation-priority { + funding = medium + closing = medium + } + feerate-tolerance { ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate when not using anchor outputs ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate when not using anchor outputs diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 669c5bc130..a8c7ba7c11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -265,8 +265,11 @@ object NodeParams extends Logging { "payment-request-expiry" -> "invoice-expiry", "override-features" -> "override-init-features", "channel.min-funding-satoshis" -> "channel.min-public-funding-satoshis, channel.min-private-funding-satoshis", + // v0.8.0 "bitcoind.batch-requests" -> "bitcoind.batch-watcher-requests", - "on-chain-fees.target-blocks.safe-utxos-threshold" -> "on-chain-fees.safe-utxos-threshold" + // vx.x.x + "on-chain-fees.target-blocks.safe-utxos-threshold" -> "on-chain-fees.safe-utxos-threshold", + "on-chain-fees.target-blocks" -> "on-chain-fees.confirmation-priority" ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -371,6 +374,17 @@ object NodeParams extends Logging { validateAddresses(addresses) + def getConfirmationPriority(path: String): ConfirmationPriority = config.getString(path) match { + case "slow" => ConfirmationPriority.Slow + case "medium" => ConfirmationPriority.Medium + case "fast" => ConfirmationPriority.Fast + } + + val feeTargets = FeeTargets( + funding = getConfirmationPriority("on-chain-fees.confirmation-priority.funding"), + closing = getConfirmationPriority("on-chain-fees.confirmation-priority.closing"), + ) + def getRelayFees(relayFeesConfig: Config): RelayFees = { val feeBase = MilliSatoshi(relayFeesConfig.getInt("fee-base-msat")) // fee base is in msat but is encoded on 32 bits and not 64 in the BOLTs, which is why it has @@ -487,6 +501,7 @@ object NodeParams extends Logging { ), onChainFeeConf = OnChainFeeConf( feerates = feerates, + feeTargets = feeTargets, safeUtxosThreshold = config.getInt("on-chain-fees.safe-utxos-threshold"), spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"), closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala index 0f09b03093..6c03c8ccf6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala @@ -23,6 +23,23 @@ import fr.acinq.eclair.transactions.Transactions import java.util.concurrent.atomic.AtomicReference +// @formatter:off +sealed trait ConfirmationPriority { + def getFeerate(feerates: FeeratesPerKw): FeeratePerKw = this match { + case ConfirmationPriority.Slow => feerates.blocks_1008 + case ConfirmationPriority.Medium => feerates.blocks_12 + case ConfirmationPriority.Fast => feerates.blocks_2 + } +} +object ConfirmationPriority { + case object Slow extends ConfirmationPriority + case object Medium extends ConfirmationPriority + case object Fast extends ConfirmationPriority +} +// @formatter:on + +case class FeeTargets(funding: ConfirmationPriority, closing: ConfirmationPriority) + /** * @param maxExposure maximum exposure to pending dust htlcs we tolerate: we will automatically fail HTLCs when going above this threshold. * @param closeOnUpdateFeeOverflow force-close channels when an update_fee forces us to go above our max exposure. @@ -47,7 +64,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax } } -case class OnChainFeeConf(feerates: AtomicReference[FeeratesPerKw], safeUtxosThreshold: Int, spendAnchorWithoutHtlcs: Boolean, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { +case class OnChainFeeConf(feerates: AtomicReference[FeeratesPerKw], feeTargets: FeeTargets, safeUtxosThreshold: Int, spendAnchorWithoutHtlcs: Boolean, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { def currentFeerates: FeeratesPerKw = feerates.get() @@ -57,7 +74,7 @@ case class OnChainFeeConf(feerates: AtomicReference[FeeratesPerKw], safeUtxosThr def shouldUpdateFee(currentFeeratePerKw: FeeratePerKw, nextFeeratePerKw: FeeratePerKw): Boolean = currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio - def getFundingFeerate(): FeeratePerKw = currentFeerates.blocks_6 + def getFundingFeerate(): FeeratePerKw = feeTargets.funding.getFeerate(currentFeerates) /** * Get the feerate that should apply to a channel commitment transaction: @@ -81,5 +98,5 @@ case class OnChainFeeConf(feerates: AtomicReference[FeeratesPerKw], safeUtxosThr } } - def getClosingFeerate(): FeeratePerKw = currentFeerates.blocks_12 + def getClosingFeerate(): FeeratePerKw = feeTargets.closing.getFeerate(currentFeerates) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index d1a89686ec..ea498dfb5e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -23,7 +23,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.{FeeratePerKw, OnChainFeeConf} +import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager @@ -634,7 +634,7 @@ object Helpers { } // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. - val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(onChainFeeConf.currentFeerates.mempoolMinFee), preferredFeerate * 2) + val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(ConfirmationPriority.Slow.getFeerate(onChainFeeConf.currentFeerates)), preferredFeerate * 2) firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index ac56a31398..2cf539605d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -134,6 +134,7 @@ object TestConstants { ), onChainFeeConf = OnChainFeeConf( feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)), + feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, @@ -290,6 +291,7 @@ object TestConstants { ), onChainFeeConf = OnChainFeeConf( feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)), + feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala index 8cf4d07487..c663bafd82 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala @@ -25,11 +25,12 @@ import java.util.concurrent.atomic.AtomicReference class OnChainFeeConfSpec extends AnyFunSuite { + private val defaultFeeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium) private val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false)) test("should update fee when diff ratio exceeded") { val feerates = new AtomicReference(FeeratesPerKw.single(TestConstants.feeratePerKw)) - val feeConf = OnChainFeeConf(feerates, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(feerates, defaultFeeTargets, safeUtxosThreshold = 0, 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))) @@ -40,7 +41,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { test("get commitment feerate") { val channelType = ChannelTypes.Standard() val feerates = new AtomicReference(FeeratesPerKw.single(TestConstants.feeratePerKw)) - val feeConf = OnChainFeeConf(feerates, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(feerates, defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) feerates.set(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat))) assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat) == FeeratePerKw(5000 sat)) @@ -55,7 +56,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(feerates, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(feerates, defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) feerates.set(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs(), 100000 sat) == defaultMaxCommitFeerate / 2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 4dbfa5af4d..808b89f608 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -42,6 +42,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val feeConfNoMismatch = OnChainFeeConf( feerates = new AtomicReference(FeeratesPerKw.single(TestConstants.feeratePerKw)), + feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = false,