Skip to content

Commit

Permalink
Add anchor outputs zero fee htlcs feature bit
Browse files Browse the repository at this point in the history
Create the corresponding feature bit and channel type.
Ensure it can be correctly negotiated and propagated
to the channel commitments.
  • Loading branch information
t-bast committed Sep 1, 2021
1 parent bca2a83 commit 1ca266d
Show file tree
Hide file tree
Showing 15 changed files with 248 additions and 110 deletions.
1 change: 1 addition & 0 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ eclair {
basic_mpp = optional
option_support_large_channel = optional
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 AnchorOutputsZeroFeeHtlcTxs 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,
AnchorOutputsZeroFeeHtlcTxs,
ShutdownAnySegwit,
KeySend
)
Expand All @@ -236,6 +242,7 @@ object Features {
// PaymentSecret -> (VariableLengthOnion :: Nil),
BasicMultiPartPayment -> (PaymentSecret :: Nil),
AnchorOutputs -> (StaticRemoteKey :: Nil),
AnchorOutputsZeroFeeHtlcTxs -> (StaticRemoteKey :: Nil),
TrampolinePayment -> (PaymentSecret :: Nil),
KeySend -> (VariableLengthOnion :: Nil)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
case ChannelTypes.AnchorOutputs =>
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
case ChannelTypes.AnchorOutputsZeroFeeHtlcTxs =>
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
}
}
}
Expand All @@ -71,7 +73,7 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
}
if (channelType == ChannelTypes.AnchorOutputs) {
if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTxs) {
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
} else {
networkFeerate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

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.{Feature, FeatureSupport, Features}

Expand All @@ -33,17 +32,19 @@ case class ChannelFeatures(activated: Set[Feature]) {

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

val channelType: SupportedChannelType = {
if (hasFeature(AnchorOutputs)) {
if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTxs)) {
ChannelTypes.AnchorOutputsZeroFeeHtlcTxs
} else if (hasFeature(Features.AnchorOutputs)) {
ChannelTypes.AnchorOutputs
} else if (hasFeature(StaticRemoteKey)) {
} else if (hasFeature(Features.StaticRemoteKey)) {
ChannelTypes.StaticRemoteKey
} else {
ChannelTypes.Standard
Expand All @@ -66,7 +67,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 Down Expand Up @@ -102,6 +103,11 @@ object ChannelTypes {
override def paysDirectlyToWallet: Boolean = false
override def toString: String = "anchor_outputs"
}
case object AnchorOutputsZeroFeeHtlcTxs extends SupportedChannelType {
override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTxs)
override def paysDirectlyToWallet: Boolean = false
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 +116,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.AnchorOutputsZeroFeeHtlcTxs -> FeatureSupport.Mandatory) => AnchorOutputsZeroFeeHtlcTxs
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 +125,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.AnchorOutputsZeroFeeHtlcTxs)) {
AnchorOutputsZeroFeeHtlcTxs
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
AnchorOutputs
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
StaticRemoteKey
Expand Down
15 changes: 11 additions & 4 deletions eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ class FeaturesSpec extends AnyFunSuite {
bin"001000000010000000000000" -> true,
bin"001000000001000000000000" -> true,
bin"000100000010000000000000" -> true,
bin"000100000001000000000000" -> true
bin"000100000001000000000000" -> true,
// option_anchors_zero_fee_htlc_tx depends on option_static_remotekey
bin"100000000000000000000000" -> false,
bin"010000000000000000000000" -> false,
bin"100000000010000000000000" -> true,
bin"100000000001000000000000" -> true,
bin"010000000010000000000000" -> true,
bin"010000000001000000000000" -> true,
)

for ((testCase, valid) <- testCases) {
Expand Down Expand Up @@ -191,10 +198,10 @@ class FeaturesSpec extends AnyFunSuite {
compatible = false
),
// nonreg testing of future features (needs to be updated with every new supported mandatory bit)
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(22))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(23))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true)
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false),
TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true),
)

for (testCase <- testCases) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,26 +48,30 @@ class FeeEstimatorSpec extends AnyFunSuite {

test("get commitment feerate (anchor outputs)") {
val feeEstimator = new TestFeeEstimator()
val channelType = ChannelTypes.AnchorOutputs
val defaultNodeId = randomKey().publicKey
val defaultMaxCommitFeerate = FeeratePerKw(2500 sat)
val overrideNodeId = randomKey().publicKey
val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2
val feeConf = OnChainFeeConf(FeeTargets(1, 2, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, FeerateTolerance(0.5, 2.0, defaultMaxCommitFeerate), Map(overrideNodeId -> FeerateTolerance(0.5, 2.0, overrideMaxCommitFeerate)))

feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, None) === defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === defaultMaxCommitFeerate / 2)

feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, None) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(overrideNodeId, channelType, 100000 sat, None) === overrideMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === overrideMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, None) === overrideMaxCommitFeerate)

val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2)

val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5))
feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2))
assert(feeConf.getCommitmentFeerate(defaultNodeId, channelType, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate)
}

test("fee difference too high") {
Expand All @@ -91,7 +95,6 @@ class FeeEstimatorSpec extends AnyFunSuite {

test("fee difference too high (anchor outputs)") {
val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat))
val channelType = ChannelTypes.AnchorOutputs
val testCases = Seq(
(FeeratePerKw(500 sat), FeeratePerKw(500 sat), false),
(FeeratePerKw(500 sat), FeeratePerKw(2500 sat), false),
Expand All @@ -106,7 +109,8 @@ class FeeEstimatorSpec extends AnyFunSuite {
(FeeratePerKw(1000 sat), FeeratePerKw(499 sat), true),
)
testCases.foreach { case (networkFeerate, proposedFeerate, expected) =>
assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) === expected)
assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputs, networkFeerate, proposedFeerate) === expected)
assert(tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTxs, networkFeerate, proposedFeerate) === expected)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,96 +18,19 @@ package fr.acinq.eclair.channel

import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut}
import fr.acinq.eclair.FeatureSupport._
import fr.acinq.eclair.Features._
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered
import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateAddHtlc}
import fr.acinq.eclair.{Features, MilliSatoshiLong, TestKitBaseClass}
import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass}
import org.scalatest.funsuite.AnyFunSuiteLike
import scodec.bits.ByteVector

class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods {
class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods {

implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging

test("channel features determines commitment format") {
val standardChannel = ChannelFeatures()
val staticRemoteKeyChannel = ChannelFeatures(Features.StaticRemoteKey)
val anchorOutputsChannel = ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)
assert(!standardChannel.hasFeature(Features.StaticRemoteKey))
assert(!standardChannel.hasFeature(Features.AnchorOutputs))
assert(standardChannel.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(!standardChannel.paysDirectlyToWallet)

assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey))
assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs))
assert(staticRemoteKeyChannel.commitmentFormat === Transactions.DefaultCommitmentFormat)
assert(staticRemoteKeyChannel.paysDirectlyToWallet)

assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey))
assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs))
assert(anchorOutputsChannel.commitmentFormat === Transactions.AnchorOutputsCommitmentFormat)
assert(!anchorOutputsChannel.paysDirectlyToWallet)
}

test("pick channel type based on local and remote features") {
case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType)
val testCases = Seq(
TestCase(Features.empty, Features.empty, ChannelTypes.Standard),
TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard),
TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard),
TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard),
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard),
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey),
TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey),
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey),
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey),
TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs)
)

for (testCase <- testCases) {
assert(ChannelTypes.pickChannelType(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelType)
}
}

test("create channel type from features") {
val validChannelTypes = Seq(
Features.empty -> ChannelTypes.Standard,
Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey,
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs,
)
for ((features, expected) <- validChannelTypes) {
assert(ChannelTypes.fromFeatures(features) === expected)
}

val invalidChannelTypes = Seq(
Features(Wumbo -> Optional),
Features(StaticRemoteKey -> Optional),
Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional),
Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional),
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional),
Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory),
Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional),
)
for (features <- invalidChannelTypes) {
assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features))
}
}

test("enrich channel type with other permanent channel features") {
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).activated.isEmpty)
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(Wumbo))
assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).activated === Set(Wumbo))
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).activated === Set(StaticRemoteKey))
assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, Wumbo))
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).activated === Set(StaticRemoteKey, AnchorOutputs))
assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).activated === Set(StaticRemoteKey, AnchorOutputs, Wumbo))
}

case class HtlcWithPreimage(preimage: ByteVector32, htlc: UpdateAddHtlc)

case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, probe: TestProbe)
Expand Down
Loading

0 comments on commit 1ca266d

Please sign in to comment.