Skip to content

Commit

Permalink
Add recommended_feerates optional message (#2860)
Browse files Browse the repository at this point in the history
We send to our peers an optional message that tells them the feerates
we'd like to use for funding channels. This lets them know which values
are acceptable to us, in case we reject their funding requests.

This is using an odd type and will be automatically ignored by existing
nodes who don't support that feature.

Co-authored-by: Pierre-Marie Padiou <[email protected]>
  • Loading branch information
t-bast and pm47 authored Sep 24, 2024
1 parent cfdb088 commit db8290f
Show file tree
Hide file tree
Showing 26 changed files with 241 additions and 118 deletions.
6 changes: 3 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
override def sendOnChain(address: String, amount: Satoshi, confirmationTargetOrFeerate: Either[Long, FeeratePerByte]): Future[TxId] = {
val feeRate = confirmationTargetOrFeerate match {
case Left(blocks) =>
if (blocks < 3) appKit.nodeParams.currentFeerates.fast
else if (blocks > 6) appKit.nodeParams.currentFeerates.slow
else appKit.nodeParams.currentFeerates.medium
if (blocks < 3) appKit.nodeParams.currentBitcoinCoreFeerates.fast
else if (blocks > 6) appKit.nodeParams.currentBitcoinCoreFeerates.slow
else appKit.nodeParams.currentBitcoinCoreFeerates.medium
case Right(feeratePerByte) => FeeratePerKw(feeratePerByte)
}
appKit.wallet match {
Expand Down
34 changes: 28 additions & 6 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
import fr.acinq.eclair.db._
Expand All @@ -36,6 +36,7 @@ import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router.{Graph, PathFindingExperimentConf}
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol._
import grizzled.slf4j.Logging
import scodec.bits.ByteVector
Expand All @@ -57,7 +58,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
onChainKeyManager_opt: Option[OnChainKeyManager],
instanceId: UUID, // a unique instance ID regenerated after each restart
private val blockHeight: AtomicLong,
private val feerates: AtomicReference[FeeratesPerKw],
private val bitcoinCoreFeerates: AtomicReference[FeeratesPerKw],
alias: String,
color: Color,
publicAddresses: List[NodeAddress],
Expand Down Expand Up @@ -102,13 +103,34 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,

def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get)

def currentFeerates: FeeratesPerKw = feerates.get()
def currentBitcoinCoreFeerates: FeeratesPerKw = bitcoinCoreFeerates.get()

/** Only to be used in tests. */
def setFeerates(value: FeeratesPerKw): Unit = feerates.set(value)
def setBitcoinCoreFeerates(value: FeeratesPerKw): Unit = bitcoinCoreFeerates.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()

/** Returns the feerates we'd like our peer to use when funding channels. */
def recommendedFeerates(remoteNodeId: PublicKey, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): RecommendedFeerates = {
val feerateTolerance = onChainFeeConf.feerateToleranceFor(remoteNodeId)
val fundingFeerate = onChainFeeConf.getFundingFeerate(currentBitcoinCoreFeerates)
val fundingRange = RecommendedFeeratesTlv.FundingFeerateRange(
min = fundingFeerate * feerateTolerance.ratioLow,
max = fundingFeerate * feerateTolerance.ratioHigh,
)
// We use the most likely commitment format, even though there is no guarantee that this is the one that will be used.
val commitmentFormat = ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = false).commitmentFormat
val commitmentFeerate = onChainFeeConf.getCommitmentFeerate(currentBitcoinCoreFeerates, remoteNodeId, commitmentFormat, channelConf.minFundingPrivateSatoshis)
val commitmentRange = RecommendedFeeratesTlv.CommitmentFeerateRange(
min = commitmentFeerate * feerateTolerance.ratioLow,
max = commitmentFormat match {
case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh
case _: Transactions.AnchorOutputsCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate)
},
)
RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange))
}
}

case class PaymentFinalExpiryConf(min: CltvExpiryDelta, max: CltvExpiryDelta) {
Expand Down Expand Up @@ -219,7 +241,7 @@ object NodeParams extends Logging {

def makeNodeParams(config: Config, instanceId: UUID,
nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, onChainKeyManager_opt: Option[OnChainKeyManager],
torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feerates: AtomicReference[FeeratesPerKw],
torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, bitcoinCoreFeerates: AtomicReference[FeeratesPerKw],
pluginParams: Seq[PluginParams] = Nil): NodeParams = {
// check configuration for keys that have been renamed
val deprecatedKeyPaths = Map(
Expand Down Expand Up @@ -513,7 +535,7 @@ object NodeParams extends Logging {
onChainKeyManager_opt = onChainKeyManager_opt,
instanceId = instanceId,
blockHeight = blockHeight,
feerates = feerates,
bitcoinCoreFeerates = bitcoinCoreFeerates,
alias = nodeAlias,
color = Color(color(0), color(1), color(2)),
publicAddresses = addresses,
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ class Setup(val datadir: File,
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))
system.eventStream.publish(CurrentFeerates.BitcoinCore(feeratesPerKw.get))
logger.info(s"current feeratesPerKB=$feeratesPerKB feeratesPerKw=${feeratesPerKw.get}")
feeratesRetrieved.trySuccess(Done)
case Failure(exception) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,14 @@ case class NewTransaction(tx: Transaction) extends BlockchainEvent

case class CurrentBlockHeight(blockHeight: BlockHeight) extends BlockchainEvent

case class CurrentFeerates(feeratesPerKw: FeeratesPerKw) extends BlockchainEvent
sealed trait CurrentFeerates extends BlockchainEvent {
val feeratesPerKw: FeeratesPerKw
}

object CurrentFeerates {
//@formatter:off
case class BitcoinCore(feeratesPerKw: FeeratesPerKw) extends CurrentFeerates
//@formatter:on
}


Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ object Helpers {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)

// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingSatoshis)
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingSatoshis)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.feeratePerKw)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw))

// we don't check that the funder's amount for the initial commitment transaction is sufficient for full fee payment
Expand Down Expand Up @@ -166,7 +166,7 @@ object Helpers {
val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel)

// BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large.
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingAmount)
val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, channelFeatures.commitmentFormat, open.fundingAmount)
if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelFeatures.commitmentFormat, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate))

for {
Expand Down
Loading

0 comments on commit db8290f

Please sign in to comment.