Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Minor) Add a feerate method for funding/closing #3001

Merged
merged 1 commit into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,

def currentBitcoinCoreFeerates: FeeratesPerKw = bitcoinCoreFeerates.get()

def currentFeeratesForFundingClosing: FeeratesPerKw = currentBitcoinCoreFeerates

/** Only to be used in tests. */
def setBitcoinCoreFeerates(value: FeeratesPerKw): Unit = bitcoinCoreFeerates.set(value)

Expand All @@ -118,7 +120,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
// Independently of target and tolerance ratios, our transactions must be publishable in our local mempool
val minimumFeerate = currentBitcoinCoreFeerates.minimum
val feerateTolerance = onChainFeeConf.feerateToleranceFor(remoteNodeId)
val fundingFeerate = onChainFeeConf.getFundingFeerate(currentBitcoinCoreFeerates)
val fundingFeerate = onChainFeeConf.getFundingFeerate(currentFeeratesForFundingClosing)
val fundingRange = RecommendedFeeratesTlv.FundingFeerateRange(
min = (fundingFeerate * feerateTolerance.ratioLow).max(minimumFeerate),
max = (fundingFeerate * feerateTolerance.ratioHigh).max(minimumFeerate),
Expand Down Expand Up @@ -159,7 +161,7 @@ case class PaymentFinalExpiryConf(min: CltvExpiryDelta, max: CltvExpiryDelta) {
/**
* @param writeDelay delay before writing the peer's data to disk, which avoids doing multiple writes during bursts of storage updates.
* @param removalDelay we keep our peer's data in our DB even after closing all of our channels with them, up to this duration.
* @param cleanUpFrequency frequency at which we go through the DB to remove unused storage.
* @param cleanUpFrequency frequency at which we go through the DB to remove unused storage.
*/
case class PeerStorageConfig(writeDelay: FiniteDuration, removalDelay: FiniteDuration, cleanUpFrequency: FiniteDuration)

Expand Down
11 changes: 4 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -240,23 +240,19 @@ class Setup(val datadir: File,
confDefaultFeerates
}
minFeeratePerByte = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.min-feerate")))
smoothFeerateWindow = config.getInt("on-chain-fees.smoothing-window")
feeProvider = nodeParams.chainHash match {
case Block.RegtestGenesisBlock.hash | Block.SignetGenesisBlock.hash =>
FallbackFeeProvider(ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte)
case _ =>
val smoothFeerateWindow = config.getInt("on-chain-fees.smoothing-window")
FallbackFeeProvider(SmoothFeeProvider(BitcoinCoreFeeProvider(bitcoin, defaultFeerates), smoothFeerateWindow) :: Nil, minFeeratePerByte)
}
_ = system.scheduler.scheduleWithFixedDelay(0 seconds, 10 minutes)(() => feeProvider.getFeerates.onComplete {
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)
blockchain.Monitoring.recordFeerates(feeratesPerKw.get(), provider = blockchain.Monitoring.Tags.Providers.BitcoinCore)
system.eventStream.publish(CurrentFeerates.BitcoinCore(feeratesPerKw.get))
logger.info(s"current feeratesPerKB=$feeratesPerKB feeratesPerKw=${feeratesPerKw.get}")
logger.info(s"current bitcoin core feerates: min=${feeratesPerKB.minimum.perByte} slow=${feeratesPerKB.slow.perByte} medium=${feeratesPerKB.medium.perByte} fast=${feeratesPerKB.fast.perByte} fastest=${feeratesPerKB.fastest.perByte}")
feeratesRetrieved.trySuccess(Done)
case Failure(exception) =>
logger.warn(s"cannot retrieve feerates: ${exception.getMessage}")
Expand Down Expand Up @@ -381,6 +377,7 @@ class Setup(val datadir: File,
clientSpawner = system.actorOf(SimpleSupervisor.props(ClientSpawner.props(nodeParams.keyPair, nodeParams.socksProxy_opt, nodeParams.peerConnectionConf, switchboard, router), "client-spawner", SupervisorStrategy.Restart))
server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams.keyPair, nodeParams.peerConnectionConf, switchboard, router, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart))
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, PaymentInitiator.SimplePaymentFactory(nodeParams, router, register)), "payment-initiator", SupervisorStrategy.Restart))

_ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart))

balanceActor = system.spawn(BalanceActor(nodeParams.db, bitcoinClient, channelsListener, nodeParams.balanceCheckInterval), name = "balance-actor")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package fr.acinq.eclair.blockchain

import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import kamon.Kamon
import kamon.metric.Metric

Expand All @@ -30,10 +31,20 @@ object Monitoring {
val CannotRetrieveFeeratesCount: Metric.Counter = Kamon.counter("bitcoin.rpc.feerates.error", "Number of failures to retrieve on-chain feerates")
}

def recordFeerates(feerates: FeeratesPerKw, provider: String) = {
val metric = Metrics.FeeratesPerByte.withTag(Tags.Provider, provider)
metric.withTag(Tags.Priority, Tags.Priorities.Minimum).update(feerates.minimum.perByte.feerate.toLong.toDouble)
metric.withTag(Tags.Priority, Tags.Priorities.Slow).update(feerates.slow.perByte.feerate.toLong.toDouble)
metric.withTag(Tags.Priority, Tags.Priorities.Medium).update(feerates.medium.perByte.feerate.toLong.toDouble)
metric.withTag(Tags.Priority, Tags.Priorities.Fast).update(feerates.fast.perByte.feerate.toLong.toDouble)
metric.withTag(Tags.Priority, Tags.Priorities.Fastest).update(feerates.fastest.perByte.feerate.toLong.toDouble)
}

object Tags {
val Method = "method"
val Wallet = "wallet"
val Priority = "priority"
val Provider = "provider"

object Priorities {
val Minimum = "0-minimum"
Expand All @@ -42,6 +53,10 @@ object Monitoring {
val Fast = "3-fast"
val Fastest = "4-fastest"
}

object Providers {
val BitcoinCore = "bitcoin-core"
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ case object CannotRetrieveFeerates extends RuntimeException("cannot retrieve fee

/** Fee rate in satoshi-per-bytes. */
case class FeeratePerByte(feerate: Satoshi) {
def perKB: FeeratePerKB = FeeratePerKB(this)
override def toString: String = s"$feerate/byte"
}

object FeeratePerByte {
def apply(feeratePerKw: FeeratePerKw): FeeratePerByte = FeeratePerByte(FeeratePerKB(feeratePerKw).feerate / 1000)
def apply(feeratePerKB: FeeratePerKB): FeeratePerByte = FeeratePerByte(feeratePerKB.feerate / 1000)
def apply(feeratePerKw: FeeratePerKw): FeeratePerByte = FeeratePerByte(FeeratePerKB(feeratePerKw))
}

/** Fee rate in satoshi-per-kilo-bytes (1 kB = 1000 bytes). */
case class FeeratePerKB(feerate: Satoshi) extends Ordered[FeeratePerKB] {
// @formatter:off
def perByte: FeeratePerByte = FeeratePerByte(this)
override def compare(that: FeeratePerKB): Int = feerate.compare(that.feerate)
def max(other: FeeratePerKB): FeeratePerKB = if (this > other) this else other
def min(other: FeeratePerKB): FeeratePerKB = if (this < other) this else other
Expand All @@ -60,6 +63,7 @@ object FeeratePerKB {
/** Fee rate in satoshi-per-kilo-weight. */
case class FeeratePerKw(feerate: Satoshi) extends Ordered[FeeratePerKw] {
// @formatter:off
def perByte: FeeratePerByte = FeeratePerByte(this)
override def compare(that: FeeratePerKw): Int = feerate.compare(that.feerate)
def max(other: FeeratePerKw): FeeratePerKw = if (this > other) this else other
def min(other: FeeratePerKw): FeeratePerKw = if (this < other) this else other
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
// there are no pending signed changes, let's go directly to NEGOTIATING
if (d.commitments.params.localParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.closingFeerates)
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, d.closingFeerates)
goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned
} else {
// we are not the channel initiator, will wait for their closing_signed
Expand Down Expand Up @@ -1532,7 +1532,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
if (commitments1.hasNoPendingHtlcsOrFeeUpdate) {
if (d.commitments.params.localParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates)
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closingFeerates)
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil
} else {
// we are not the channel initiator, will wait for their closing_signed
Expand Down Expand Up @@ -1574,7 +1574,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
log.debug("switching to NEGOTIATING spec:\n{}", commitments1.latest.specs2String)
if (d.commitments.params.localParams.paysClosingFees) {
// we pay the closing fees, so we initiate the negotiation by sending the first closing_signed
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closingFeerates)
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1.latest, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, closingFeerates)
goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned
} else {
// we are not the channel initiator, will wait for their closing_signed
Expand Down Expand Up @@ -1647,7 +1647,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.params.localParams.paysClosingFees =>
// if we are not paying the closing fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation
// we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation
val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)
val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf)
if (maxFee < localClosingFees.min) {
log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min)
stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}")
Expand All @@ -1674,7 +1674,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis)
val (closingTx, closingSigned) = {
// if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee
val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf)
val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf)
val nextPreferredFee = Closing.MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee)
Closing.MutualClose.makeClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee))
}
Expand Down Expand Up @@ -1705,7 +1705,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
} else {
log.info("updating our closing feerates: {}", feerates)
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, Some(feerates))
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, Some(feerates))
val closingTxProposed1 = d.closingTxProposed match {
case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned))
case previousNegotiations => previousNegotiations :+ List(ClosingTxProposed(closingTx, closingSigned))
Expand Down Expand Up @@ -2395,7 +2395,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
// note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them
if (d.commitments.params.localParams.paysClosingFees) {
// we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, None)
val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf, None)
val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx, closingSigned))
goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) storing() sending d.localShutdown :: closingSigned :: Nil
} else {
Expand Down Expand Up @@ -2998,7 +2998,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with

private def initiateSplice(cmd: CMD_SPLICE, d: DATA_NORMAL): Either[ChannelException, SpliceInit] = {
val parentCommitment = d.commitments.latest.commitment
val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentBitcoinCoreFeerates)
val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing)
val fundingContribution = InteractiveTxFunder.computeSpliceContribution(
isInitiator = true,
sharedInput = Multisig2of2Input(parentCommitment),
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class Peer(val nodeParams: NodeParams,
} else {
randomBytes32()
}
val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentBitcoinCoreFeerates))
val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing))
val commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelType.commitmentFormat, c.fundingAmount)
log.info(s"requesting a new channel with type=$channelType fundingAmount=${c.fundingAmount} dualFunded=$dualFunded pushAmount=${c.pushAmount_opt} fundingFeerate=$fundingTxFeerate temporaryChannelId=$temporaryChannelId localParams=$localParams")
channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, dualFunded, commitTxFeerate, fundingTxFeerate, c.fundingTxFeeBudget_opt, c.pushAmount_opt, requireConfirmedInputs, c.requestFunding_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType, c.channelOrigin, replyTo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,9 @@ object ChannelStateTestsBase {

val nodeParams: NodeParams = channel.underlyingActor.nodeParams

def setFeerates(feerates: FeeratesPerKw): Unit = channel.underlyingActor.nodeParams.setBitcoinCoreFeerates(feerates)
def setBitcoinCoreFeerates(feerates: FeeratesPerKw): Unit = channel.underlyingActor.nodeParams.setBitcoinCoreFeerates(feerates)

def setFeerate(feerate: FeeratePerKw): Unit = setFeerates(FeeratesPerKw.single(feerate))
def setBitcoinCoreFeerate(feerate: FeeratePerKw): Unit = setBitcoinCoreFeerates(FeeratesPerKw.single(feerate))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
exchangeStfu(f)
// we tweak the feerate
val spliceInit = alice2bob.expectMsgType[SpliceInit].copy(feerate = FeeratePerKw(100.sat))
bob.setFeerates(alice.nodeParams.currentBitcoinCoreFeerates.copy(minimum = FeeratePerKw(101.sat)))
bob.setBitcoinCoreFeerates(alice.nodeParams.currentBitcoinCoreFeerates.copy(minimum = FeeratePerKw(101.sat)))
alice2bob.forward(bob, spliceInit)
val txAbortBob = bob2alice.expectMsgType[TxAbort]
bob2alice.forward(alice, txAbortBob)
Expand Down
Loading