Skip to content

Commit

Permalink
Implement anchor outputs zero fee htlc txs (#1932)
Browse files Browse the repository at this point in the history
Add support for lightning/bolts#824

When the channel type is anchor outputs with zero fee htlc txs, we set
the fees for the htlc txs to 0.

An important side-effect is that it changes the trimmed to dust calculation,
and outputs that were previously dust can now be included in the commit tx.
  • Loading branch information
t-bast committed Sep 10, 2021
1 parent 24dd613 commit a228bac
Show file tree
Hide file tree
Showing 38 changed files with 788 additions and 400 deletions.
3 changes: 3 additions & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ eclair {
payment_secret = mandatory
basic_mpp = optional
option_support_large_channel = optional
// NB: option_anchors_zero_fee_htlc_tx should always be preferred to option_anchor_outputs (it's safer).
// Do not enable option_anchor_outputs unless you really know what you're doing.
option_anchor_outputs = disabled
option_anchors_zero_fee_htlc_tx = disabled
option_shutdown_anysegwit = optional
trampoline_payment = disabled
keysend = disabled
Expand Down
7 changes: 7 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ object Features {
val mandatory = 20
}

case object AnchorOutputsZeroFeeHtlcTx extends Feature {
val rfcName = "option_anchors_zero_fee_htlc_tx"
val mandatory = 22
}

case object ShutdownAnySegwit extends Feature {
val rfcName = "option_shutdown_anysegwit"
val mandatory = 26
Expand Down Expand Up @@ -224,6 +229,7 @@ object Features {
TrampolinePayment,
StaticRemoteKey,
AnchorOutputs,
AnchorOutputsZeroFeeHtlcTx,
ShutdownAnySegwit,
KeySend
)
Expand All @@ -236,6 +242,7 @@ object Features {
// PaymentSecret -> (VariableLengthOnion :: Nil),
BasicMultiPartPayment -> (PaymentSecret :: Nil),
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil),
TrampolinePayment -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ package fr.acinq.eclair.blockchain.fee
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.blockchain.CurrentFeerates
import fr.acinq.eclair.channel.{ChannelType, ChannelTypes, SupportedChannelType}
import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType}
import fr.acinq.eclair.transactions.Transactions

trait FeeEstimator {
// @formatter:off
Expand All @@ -39,11 +40,9 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
*/
def isFeeDiffTooHigh(channelType: SupportedChannelType, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
channelType match {
case ChannelTypes.Standard =>
case ChannelTypes.Standard | ChannelTypes.StaticRemoteKey =>
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
case ChannelTypes.StaticRemoteKey =>
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
case ChannelTypes.AnchorOutputs =>
case ChannelTypes.AnchorOutputs | ChannelTypes.AnchorOutputsZeroFeeHtlcTx =>
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
}
}
Expand All @@ -66,15 +65,14 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
* @param channelType channel type
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
*/
def getCommitmentFeerate(remoteNodeId: PublicKey, channelType: ChannelType, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
def getCommitmentFeerate(remoteNodeId: PublicKey, channelType: SupportedChannelType, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
val networkFeerate = currentFeerates_opt match {
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
}
if (channelType == ChannelTypes.AnchorOutputs) {
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
} else {
networkFeerate
channelType.commitmentFormat match {
case Transactions.DefaultCommitmentFormat => networkFeerate
case _: Transactions.AnchorOutputsCommitmentFormat => networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
}
}
}
10 changes: 5 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
// we send it (if needed) when reconnected.
val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty
if (d.commitments.localParams.isFunder && !shutdownInProgress) {
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None)
if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) {
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
Expand Down Expand Up @@ -2031,7 +2031,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId

private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = {
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate
val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)
val shouldClose = !d.commitments.localParams.isFunder &&
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
Expand All @@ -2040,7 +2040,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
stay()
} else if (shouldClose) {
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c))
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate), d, Some(c))
} else {
stay()
}
Expand All @@ -2055,7 +2055,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
*/
private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: HasCommitments) = {
val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c))
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate
// if the network fees are too high we risk to not be able to confirm our current commitment
val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) &&
Expand Down Expand Up @@ -2381,7 +2381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Transactions.DefaultCommitmentFormat =>
val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishRawTx(tx, Some(commitTx.txid)))
List(PublishRawTx(commitTx, commitInput, "commit-tx", None)) ++ (claimMainDelayedOutputTx.map(tx => PublishRawTx(tx, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishRawTx(tx, None)))
case Transactions.AnchorOutputsCommitmentFormat =>
case _: Transactions.AnchorOutputsCommitmentFormat =>
val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) }
val redeemableHtlcTxs = htlcTxs.values.collect { case Some(tx) => PublishReplaceableTx(tx, commitments) }
List(PublishRawTx(commitTx, commitInput, "commit-tx", None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishRawTx(tx, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishRawTx(tx, None))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

package fr.acinq.eclair.channel

import fr.acinq.eclair.Features.{AnchorOutputs, OptionUpfrontShutdownScript, StaticRemoteKey, Wumbo}
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat}
import fr.acinq.eclair.{Feature, FeatureSupport, Features}

/**
Expand All @@ -31,26 +30,20 @@ import fr.acinq.eclair.{Feature, FeatureSupport, Features}
*/
case class ChannelFeatures(activated: Set[Feature]) {

/** Format of the channel transactions. */
val commitmentFormat: CommitmentFormat = {
if (hasFeature(AnchorOutputs)) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}
}

val channelType: SupportedChannelType = {
if (hasFeature(AnchorOutputs)) {
if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) {
ChannelTypes.AnchorOutputsZeroFeeHtlcTx
} else if (hasFeature(Features.AnchorOutputs)) {
ChannelTypes.AnchorOutputs
} else if (hasFeature(StaticRemoteKey)) {
} else if (hasFeature(Features.StaticRemoteKey)) {
ChannelTypes.StaticRemoteKey
} else {
ChannelTypes.Standard
}
}

val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet
val commitmentFormat: CommitmentFormat = channelType.commitmentFormat

def hasFeature(feature: Feature): Boolean = activated.contains(feature)

Expand All @@ -66,7 +59,7 @@ object ChannelFeatures {
def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
// such as option_dataloss_protect or option_shutdown_anysegwit.
val availableFeatures: Seq[Feature] = Seq(Wumbo, OptionUpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
val availableFeatures: Seq[Feature] = Seq(Features.Wumbo, Features.OptionUpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
val allFeatures = channelType.features.toSeq ++ availableFeatures
ChannelFeatures(allFeatures: _*)
}
Expand All @@ -82,6 +75,9 @@ sealed trait ChannelType {
sealed trait SupportedChannelType extends ChannelType {
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
def paysDirectlyToWallet: Boolean

/** Format of the channel transactions. */
def commitmentFormat: CommitmentFormat
}

object ChannelTypes {
Expand All @@ -90,18 +86,27 @@ object ChannelTypes {
case object Standard extends SupportedChannelType {
override def features: Set[Feature] = Set.empty
override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
override def toString: String = "standard"
}
case object StaticRemoteKey extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey)
override def paysDirectlyToWallet: Boolean = true
override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat
override def toString: String = "static_remotekey"
}
case object AnchorOutputs extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs)
override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat
override def toString: String = "anchor_outputs"
}
case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)
override def paysDirectlyToWallet: Boolean = false
override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
override def toString: String = "anchor_outputs_zero_fee_htlc_tx"
}
case class UnsupportedChannelType(featureBits: Features) extends ChannelType {
override def features: Set[Feature] = featureBits.activated.keySet
override def toString: String = s"0x${featureBits.toByteVector.toHex}"
Expand All @@ -110,6 +115,7 @@ object ChannelTypes {

// NB: Bolt 2: features must exactly match in order to identify a channel type.
def fromFeatures(features: Features): ChannelType = features match {
case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputsZeroFeeHtlcTx -> FeatureSupport.Mandatory) => AnchorOutputsZeroFeeHtlcTx
case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory, Features.AnchorOutputs -> FeatureSupport.Mandatory) => AnchorOutputs
case f if f == Features(Features.StaticRemoteKey -> FeatureSupport.Mandatory) => StaticRemoteKey
case f if f == Features.empty => Standard
Expand All @@ -118,7 +124,9 @@ object ChannelTypes {

/** Pick the channel type based on local and remote feature bits. */
def pickChannelType(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) {
AnchorOutputsZeroFeeHtlcTx
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
AnchorOutputs
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
StaticRemoteKey
Expand Down
Loading

0 comments on commit a228bac

Please sign in to comment.