Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package fr.acinq.eclair.blockchain.fee

import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Satoshi
import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong}
import fr.acinq.eclair.BlockHeight
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions._
Expand Down Expand Up @@ -65,8 +65,8 @@ case class DustTolerance(maxExposure: Satoshi, closeOnUpdateFeeOverflow: Boolean

case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw, dustTolerance: DustTolerance) {
/**
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
* @return true if the difference between proposed and reference fee rates is too high.
*/
def isProposedCommitFeerateTooHigh(networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = networkFeerate * ratioHigh < proposedFeerate
Expand All @@ -85,15 +85,24 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
def feerateToleranceFor(nodeId: PublicKey): FeerateTolerance = perNodeFeerateTolerance.getOrElse(nodeId, defaultFeerateTolerance)

/** To avoid spamming our peers with fee updates every time there's a small variation, we only update the fee when the difference exceeds a given ratio. */
def shouldUpdateFee(currentFeeratePerKw: FeeratePerKw, nextFeeratePerKw: FeeratePerKw): Boolean =
currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio
def shouldUpdateFee(currentFeeratePerKw: FeeratePerKw, nextFeeratePerKw: FeeratePerKw, commitmentFormat: CommitmentFormat): Boolean = {
commitmentFormat match {
case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat =>
// If we're not already using 1 sat/byte, we update the fee.
FeeratePerKw(FeeratePerByte(1 sat)) < currentFeeratePerKw
case Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat =>
// If the fee has a large enough change, we update the fee.
currentFeeratePerKw.toLong == 0 || Math.abs((currentFeeratePerKw.toLong - nextFeeratePerKw.toLong).toDouble / currentFeeratePerKw.toLong) > updateFeeMinDiffRatio
}
}

def getFundingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.funding.getFeerate(feerates)

/**
* Get the feerate that should apply to a channel commitment transaction:
* - if we're using anchor outputs, we use a feerate that allows network propagation of the commit tx: we will use CPFP to speed up confirmation if needed
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
* - if the remote peer is a mobile wallet that supports anchor outputs, we use 1 sat/byte
* - otherwise, we use a feerate that should allow network propagation of the commit tx on its own: we will use CPFP
* on the anchor output to speed up confirmation if needed or to help propagation
*
* @param remoteNodeId nodeId of our channel peer
* @param commitmentFormat commitment format
Expand All @@ -102,7 +111,11 @@ case class OnChainFeeConf(feeTargets: FeeTargets,
val networkFeerate = feerates.fast
val networkMinFee = feerates.minimum
commitmentFormat match {
case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat =>
case Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat | Transactions.PhoenixSimpleTaprootChannelCommitmentFormat =>
// Since Bitcoin Core v28, 1-parent-1-child package relay has been deployed: it should be ok if the commit tx
// doesn't propagate on its own.
FeeratePerKw(FeeratePerByte(1 sat))
case Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat | Transactions.ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat =>
val targetFeerate = networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
// We make sure the feerate is always greater than the propagation threshold.
targetFeerate.max(networkMinFee * 1.25)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2736,7 +2736,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
if (d.commitments.localChannelParams.paysCommitTxFees && !shutdownInProgress) {
val currentFeeratePerKw = d.commitments.latest.localCommit.spec.commitTxFeerate
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat)
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, d.commitments.latest.commitmentFormat)) {
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
}
}
Expand Down Expand Up @@ -3219,7 +3219,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val commitments = d.commitments.latest
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentBitcoinCoreFeerates, remoteNodeId, d.commitments.latest.commitmentFormat)
val currentFeeratePerKw = commitments.localCommit.spec.commitTxFeerate
val shouldUpdateFee = d.commitments.localChannelParams.paysCommitTxFees && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
val shouldUpdateFee = d.commitments.localChannelParams.paysCommitTxFees && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, d.commitments.latest.commitmentFormat)
if (shouldUpdateFee) {
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ object TestConstants {
val nonInitiatorPushAmount: MilliSatoshi = 100_000_000L msat
val feeratePerKw: FeeratePerKw = FeeratePerKw(10_000 sat)
val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2_500 sat)
val phoenixCommitFeeratePerKw: FeeratePerKw = FeeratePerByte(1 sat).perKw
val defaultLiquidityRates: LiquidityAds.WillFundRates = LiquidityAds.WillFundRates(
fundingRates = LiquidityAds.FundingRate(100_000 sat, 10_000_000 sat, 500, 100, 100 sat, 1000 sat) :: Nil,
paymentTypes = Set(LiquidityAds.PaymentType.FromChannelBalance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.fee

import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.transactions.Transactions.{UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
import fr.acinq.eclair.transactions.Transactions.{PhoenixSimpleTaprootChannelCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
import org.scalatest.funsuite.AnyFunSuite

class OnChainFeeConfSpec extends AnyFunSuite {
Expand All @@ -29,56 +29,59 @@ class OnChainFeeConfSpec extends AnyFunSuite {

test("should update fee when diff ratio exceeded") {
val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, 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)))
assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(899 sat)))
assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1101 sat)))
assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat))
assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat), ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat))
assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat))
assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(899 sat), ZeroFeeHtlcTxAnchorOutputsCommitmentFormat))
assert(feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1101 sat), ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat))
}

test("get commitment feerate") {
test("should update fee to set to 1 sat/byte") {
val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty)
val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = FeeratePerKw(5000 sat))
assert(feeConf.getCommitmentFeerate(feerates1, randomKey().publicKey, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(2500 sat))
val feerates2 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = FeeratePerKw(2000 sat))
assert(feeConf.getCommitmentFeerate(feerates2, randomKey().publicKey, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(2000 sat))
// We always use 1 sat/byte for mobile wallet commitment formats, regardless of the current feerate.
val feerates = FeeratesPerKw.single(FeeratePerKw(FeeratePerByte(20 sat)))
assert(feeConf.getCommitmentFeerate(feerates, randomKey().publicKey, UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(FeeratePerByte(1 sat)))
assert(feeConf.getCommitmentFeerate(feerates, randomKey().publicKey, PhoenixSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(FeeratePerByte(1 sat)))
// If we're not already using 1 sat/byte, we update the feerate.
assert(feeConf.shouldUpdateFee(FeeratePerKw(300 sat), FeeratePerKw(FeeratePerByte(1 sat)), UnsafeLegacyAnchorOutputsCommitmentFormat))
assert(feeConf.shouldUpdateFee(FeeratePerKw(300 sat), FeeratePerKw(FeeratePerByte(1 sat)), PhoenixSimpleTaprootChannelCommitmentFormat))
}

test("get commitment feerate (anchor outputs)") {
test("get commitment feerate") {
val defaultNodeId = randomKey().publicKey
val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate
val overrideNodeId = randomKey().publicKey
val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2
val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate)))

val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate / 2, minimum = FeeratePerKw(250 sat))
assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate / 2)

val feerates2 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate * 2, minimum = FeeratePerKw(250 sat))
assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates2, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == overrideMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == overrideMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates2, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == overrideMaxCommitFeerate)

val feerates3 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate / 2, minimum = FeeratePerKw(250 sat))
assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(feerates3, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate / 2)

val feerates4 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate * 1.5, minimum = FeeratePerKw(250 sat))
assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(feerates4, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == defaultMaxCommitFeerate)

val feerates5 = FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(minimum = FeeratePerKw(10000 sat))
assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates5, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates5, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)

val feerates6 = FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(minimum = FeeratePerKw(10000 sat))
assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates6, defaultNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(10000 sat) * 1.25)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
temporaryChannelId = ByteVector32.Zeroes,
fundingAmount = fundingAmount,
dualFunded = dualFunded,
commitTxFeerate = TestConstants.anchorOutputsFeeratePerKw,
commitTxFeerate = channelType match {
case _: ChannelTypes.AnchorOutputs | ChannelTypes.SimpleTaprootChannelsPhoenix => TestConstants.phoenixCommitFeeratePerKw
case _ => TestConstants.anchorOutputsFeeratePerKw
},
fundingTxFeerate = TestConstants.feeratePerKw,
fundingTxFeeBudget_opt = None,
pushAmount_opt = pushAmount_opt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ class PeerSpec extends FixtureSpec {
assert(init.channelType == ChannelTypes.AnchorOutputs())
assert(!init.dualFunded)
assert(init.fundingAmount == 15000.sat)
assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw)
assert(init.commitTxFeerate == TestConstants.phoenixCommitFeeratePerKw)
assert(init.fundingTxFeerate == nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing))
}

Expand Down