Skip to content

Commit

Permalink
introduce confirmation priority
Browse files Browse the repository at this point in the history
  • Loading branch information
pm47 committed Jun 16, 2023
1 parent cf2d975 commit 1332647
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 9 deletions.
6 changes: 6 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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_'")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()

Expand All @@ -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:
Expand All @@ -81,5 +98,5 @@ case class OnChainFeeConf(feerates: AtomicReference[FeeratesPerKw], safeUtxosThr
}
}

def getClosingFeerate(): FeeratePerKw = currentFeerates.blocks_12
def getClosingFeerate(): FeeratePerKw = feeTargets.closing.getFeerate(currentFeerates)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand All @@ -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))
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 1332647

Please sign in to comment.