Skip to content

Commit

Permalink
Simplify on-chain fee management (#2696)
Browse files Browse the repository at this point in the history
Move away from the "block target" approach.

Get rid of the `FeeEstimator` abstraction and use an `AtomicReference` to store and update the current feerates, similar to the block count.
  • Loading branch information
pm47 committed Jun 20, 2023
1 parent 194f5dd commit da98e19
Show file tree
Hide file tree
Showing 52 changed files with 807 additions and 826 deletions.
18 changes: 17 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@

## Major changes

<insert changes>
### Use priority instead of block target for feerates

Eclair now uses a `slow`/`medium`/`fast` notation for feerates (in the style of mempool.space),
instead of block targets. Only the funding and closing priorities can be configured, the feerate
for commitment transactions is managed by eclair, so is the fee bumping for htlcs in force close
scenarii. Note that even in a force close scenario, when an output is only spendable by eclair, then
the normal closing priority is used.

Default setting is `medium` for both funding and closing. Node operators may configure their values like so:
```eclair.conf
eclair.on-chain-fees.confirmation-priority {
funding = fast
closing = slow
}
```

This configuration section replaces the previous `eclair.on-chain-fees.target-blocks` section.

### API changes

Expand Down
36 changes: 13 additions & 23 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -201,31 +201,18 @@ eclair {
min-feerate = 1 // minimum feerate in satoshis per byte
smoothing-window = 6 // 1 = no smoothing

default-feerates { // those are per target block, in satoshis per kilobyte
1 = 210000
2 = 180000
6 = 150000
12 = 110000
36 = 50000
72 = 20000
144 = 15000
1008 = 5000
default-feerates { // the following values are in satoshis per byte
minimum = 5
slow = 5
medium = 10
fast = 20
fastest = 30
}

// number of blocks to target when computing fees for each transaction type
target-blocks {
// 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
// confirmation priority for each transaction type, can be slow/medium/fast
confirmation-priority {
funding = medium
closing = medium
}

feerate-tolerance {
Expand Down Expand Up @@ -260,6 +247,9 @@ eclair {
# }
]

// when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios
safe-utxos-threshold = 10

// 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
Expand Down
33 changes: 23 additions & 10 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import java.net.InetSocketAddress
import java.nio.file.Files
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.{AtomicLong, AtomicReference}
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._

Expand All @@ -56,6 +56,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
channelKeyManager: ChannelKeyManager,
instanceId: UUID, // a unique instance ID regenerated after each restart
private val blockHeight: AtomicLong,
private val feerates: AtomicReference[FeeratesPerKw],
alias: String,
color: Color,
publicAddresses: List[NodeAddress],
Expand Down Expand Up @@ -97,6 +98,11 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,

def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get)

def currentFeerates: FeeratesPerKw = feerates.get()

/** Only to be used in tests. */
def setFeerates(value: FeeratesPerKw) = feerates.set(value)

/** Returns the features that should be used in our init message with the given peer. */
def initFeaturesFor(nodeId: PublicKey): Features[InitFeature] = overrideInitFeatures.getOrElse(nodeId, features).initFeatures()
}
Expand Down Expand Up @@ -206,7 +212,7 @@ object NodeParams extends Logging {
}

def makeNodeParams(config: Config, instanceId: UUID, nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager,
torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feeEstimator: FeeEstimator,
torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feerates: AtomicReference[FeeratesPerKw],
pluginParams: Seq[PluginParams] = Nil): NodeParams = {
// check configuration for keys that have been renamed
val deprecatedKeyPaths = Map(
Expand Down Expand Up @@ -265,7 +271,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",
"bitcoind.batch-requests" -> "bitcoind.batch-watcher-requests"
// v0.8.0
"bitcoind.batch-requests" -> "bitcoind.batch-watcher-requests",
// 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 @@ -370,13 +380,15 @@ 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(
fundingBlockTarget = config.getInt("on-chain-fees.target-blocks.funding"),
commitmentBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment"),
commitmentWithoutHtlcsBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment-without-htlcs"),
mutualCloseBlockTarget = config.getInt("on-chain-fees.target-blocks.mutual-close"),
claimMainBlockTarget = config.getInt("on-chain-fees.target-blocks.claim-main"),
safeUtxosThreshold = config.getInt("on-chain-fees.target-blocks.safe-utxos-threshold"),
funding = getConfirmationPriority("on-chain-fees.confirmation-priority.funding"),
closing = getConfirmationPriority("on-chain-fees.confirmation-priority.closing"),
)

def getRelayFees(relayFeesConfig: Config): RelayFees = {
Expand Down Expand Up @@ -465,6 +477,7 @@ object NodeParams extends Logging {
channelKeyManager = channelKeyManager,
instanceId = instanceId,
blockHeight = blockHeight,
feerates = feerates,
alias = nodeAlias,
color = Color(color(0), color(1), color(2)),
publicAddresses = addresses,
Expand Down Expand Up @@ -504,7 +517,7 @@ object NodeParams extends Logging {
),
onChainFeeConf = OnChainFeeConf(
feeTargets = feeTargets,
feeEstimator = feeEstimator,
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"),
updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio"),
Expand Down
45 changes: 14 additions & 31 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,8 @@ class Setup(val datadir: File,
* This holds the current feerates, in satoshi-per-kilobytes.
* The value is read by all actors, hence it needs to be thread-safe.
*/
val feeratesPerKB = new AtomicReference[FeeratesPerKB](null)

/**
* This holds the current feerates, in satoshi-per-kw.
* The value is read by all actors, hence it needs to be thread-safe.
*/
val feeratesPerKw = new AtomicReference[FeeratesPerKw](null)

val feeEstimator = new FeeEstimator {
// @formatter:off
override def getFeeratePerKb(target: Int): FeeratePerKB = feeratesPerKB.get().feePerBlock(target)
override def getFeeratePerKw(target: Int): FeeratePerKw = feeratesPerKw.get().feePerBlock(target)
override def getMempoolMinFeeratePerKw(): FeeratePerKw = feeratesPerKw.get().mempoolMinFee
// @formatter:on
}

val serverBindingAddress = new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port"))

// early checks
Expand Down Expand Up @@ -198,7 +184,7 @@ class Setup(val datadir: File,
logger.info(s"connecting to database with instanceId=$instanceId")
val databases = Databases.init(config.getConfig("db"), instanceId, chaindir, db)

val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, initTor(), databases, blockHeight, feeEstimator, pluginParams)
val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, initTor(), databases, blockHeight, feeratesPerKw, pluginParams)

logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}")
assert(bitcoinChainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$bitcoinChainHash)")
Expand All @@ -221,17 +207,12 @@ class Setup(val datadir: File,

defaultFeerates = {
val confDefaultFeerates = FeeratesPerKB(
mempoolMinFee = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.1008"))),
block_1 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.1"))),
blocks_2 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.2"))),
blocks_6 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.6"))),
blocks_12 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.12"))),
blocks_36 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.36"))),
blocks_72 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.72"))),
blocks_144 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.144"))),
blocks_1008 = FeeratePerKB(Satoshi(config.getLong("on-chain-fees.default-feerates.1008"))),
minimum = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.minimum")))),
slow = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.slow")))),
medium = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.medium")))),
fast = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.fast")))),
fastest = FeeratePerKB(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.default-feerates.fastest")))),
)
feeratesPerKB.set(confDefaultFeerates)
feeratesPerKw.set(FeeratesPerKw(confDefaultFeerates))
confDefaultFeerates
}
Expand All @@ -244,13 +225,15 @@ class Setup(val datadir: File,
FallbackFeeProvider(SmoothFeeProvider(BitcoinCoreFeeProvider(bitcoin, defaultFeerates), smoothFeerateWindow) :: Nil, minFeeratePerByte)
}
_ = system.scheduler.scheduleWithFixedDelay(0 seconds, 10 minutes)(() => feeProvider.getFeerates.onComplete {
case Success(feerates) =>
feeratesPerKB.set(feerates)
feeratesPerKw.set(FeeratesPerKw(feerates))
channel.Monitoring.Metrics.LocalFeeratePerKw.withoutTags().update(feeratesPerKw.get.feePerBlock(nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget).toLong.toDouble)
blockchain.Monitoring.Metrics.MempoolMinFeeratePerKw.withoutTags().update(feeratesPerKw.get.mempoolMinFee.toLong.toDouble)
case Success(feeratesPerKB) =>
feeratesPerKw.set(FeeratesPerKw(feeratesPerKB))
blockchain.Monitoring.Metrics.FeeratesPerByte.withTag(blockchain.Monitoring.Tags.Priority, blockchain.Monitoring.Tags.Priorities.Minimum).update(feeratesPerKw.get.minimum.toLong.toDouble)
blockchain.Monitoring.Metrics.FeeratesPerByte.withTag(blockchain.Monitoring.Tags.Priority, blockchain.Monitoring.Tags.Priorities.Slow).update(feeratesPerKw.get.slow.toLong.toDouble)
blockchain.Monitoring.Metrics.FeeratesPerByte.withTag(blockchain.Monitoring.Tags.Priority, blockchain.Monitoring.Tags.Priorities.Medium).update(feeratesPerKw.get.medium.toLong.toDouble)
blockchain.Monitoring.Metrics.FeeratesPerByte.withTag(blockchain.Monitoring.Tags.Priority, blockchain.Monitoring.Tags.Priorities.Fast).update(feeratesPerKw.get.fast.toLong.toDouble)
blockchain.Monitoring.Metrics.FeeratesPerByte.withTag(blockchain.Monitoring.Tags.Priority, blockchain.Monitoring.Tags.Priorities.Fastest).update(feeratesPerKw.get.fastest.toLong.toDouble)
system.eventStream.publish(CurrentFeerates(feeratesPerKw.get))
logger.info(s"current feeratesPerKB=${feeratesPerKB.get} feeratesPerKw=${feeratesPerKw.get}")
logger.info(s"current feeratesPerKB=${feeratesPerKB} feeratesPerKw=${feeratesPerKw.get}")
feeratesRetrieved.trySuccess(Done)
case Failure(exception) =>
logger.warn(s"cannot retrieve feerates: ${exception.getMessage}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ object Monitoring {
val RpcBasicInvokeCount: Metric.Counter = Kamon.counter("bitcoin.rpc.basic.invoke.count")
val RpcBasicInvokeDuration: Metric.Timer = Kamon.timer("bitcoin.rpc.basic.invoke.duration")
val RpcBatchInvokeDuration: Metric.Timer = Kamon.timer("bitcoin.rpc.batch.invoke.duration")
val MempoolMinFeeratePerKw: Metric.Gauge = Kamon.gauge("bitcoin.mempool.min-feerate-per-kw", "Minimum feerate (sat/kw) for a tx to be accepted in our mempool")
val FeeratesPerByte: Metric.Gauge = Kamon.gauge("bitcoin.feerates-per-byte", "Current feerates in sat/byte")
val CannotRetrieveFeeratesCount: Metric.Counter = Kamon.counter("bitcoin.rpc.feerates.error", "Number of failures to retrieve on-chain feerates")
}

object Tags {
val Method = "method"
val Priority = "priority"

object Priorities {
val Minimum = "0-minimum"
val Slow = "1-slow"
val Medium = "2-medium"
val Fast = "3-fast"
val Fastest = "4-fastest"
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,14 @@ case class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeerat
mempoolMinFee <- mempoolMinFee()
block_1 <- estimateSmartFee(1)
blocks_2 <- estimateSmartFee(2)
blocks_6 <- estimateSmartFee(6)
blocks_12 <- estimateSmartFee(12)
blocks_36 <- estimateSmartFee(36)
blocks_72 <- estimateSmartFee(72)
blocks_144 <- estimateSmartFee(144)
blocks_1008 <- estimateSmartFee(1008)
} yield FeeratesPerKB(
mempoolMinFee = if (mempoolMinFee.feerate > 0.sat) mempoolMinFee else defaultFeerates.mempoolMinFee,
block_1 = if (block_1.feerate > 0.sat) block_1 else defaultFeerates.block_1,
blocks_2 = if (blocks_2.feerate > 0.sat) blocks_2 else defaultFeerates.blocks_2,
blocks_6 = if (blocks_6.feerate > 0.sat) blocks_6 else defaultFeerates.blocks_6,
blocks_12 = if (blocks_12.feerate > 0.sat) blocks_12 else defaultFeerates.blocks_12,
blocks_36 = if (blocks_36.feerate > 0.sat) blocks_36 else defaultFeerates.blocks_36,
blocks_72 = if (blocks_72.feerate > 0.sat) blocks_72 else defaultFeerates.blocks_72,
blocks_144 = if (blocks_144.feerate > 0.sat) blocks_144 else defaultFeerates.blocks_144,
blocks_1008 = if (blocks_1008.feerate > 0.sat) blocks_1008 else defaultFeerates.blocks_1008)
minimum = if (mempoolMinFee.feerate > 0.sat) mempoolMinFee else defaultFeerates.minimum,
fastest = if (block_1.feerate > 0.sat) block_1 else defaultFeerates.fastest,
fast = if (blocks_2.feerate > 0.sat) blocks_2 else defaultFeerates.fast,
medium = if (blocks_12.feerate > 0.sat) blocks_12 else defaultFeerates.medium,
slow = if (blocks_1008.feerate > 0.sat) blocks_1008 else defaultFeerates.slow)
}

object BitcoinCoreFeeProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,11 @@ case class FallbackFeeProvider(providers: Seq[FeeProvider], minFeeratePerByte: F
object FallbackFeeProvider {

private def enforceMinimumFeerate(feeratesPerKB: FeeratesPerKB, minFeeratePerKB: FeeratePerKB): FeeratesPerKB = FeeratesPerKB(
mempoolMinFee = feeratesPerKB.mempoolMinFee.max(minFeeratePerKB),
block_1 = feeratesPerKB.block_1.max(minFeeratePerKB),
blocks_2 = feeratesPerKB.blocks_2.max(minFeeratePerKB),
blocks_6 = feeratesPerKB.blocks_6.max(minFeeratePerKB),
blocks_12 = feeratesPerKB.blocks_12.max(minFeeratePerKB),
blocks_36 = feeratesPerKB.blocks_36.max(minFeeratePerKB),
blocks_72 = feeratesPerKB.blocks_72.max(minFeeratePerKB),
blocks_144 = feeratesPerKB.blocks_144.max(minFeeratePerKB),
blocks_1008 = feeratesPerKB.blocks_1008.max(minFeeratePerKB)
minimum = feeratesPerKB.minimum.max(minFeeratePerKB),
fastest = feeratesPerKB.fastest.max(minFeeratePerKB),
fast = feeratesPerKB.fast.max(minFeeratePerKB),
medium = feeratesPerKB.medium.max(minFeeratePerKB),
slow = feeratesPerKB.slow.max(minFeeratePerKB)
)

}
Loading

0 comments on commit da98e19

Please sign in to comment.