Skip to content

Commit 79e6aef

Browse files
committed
Filter init, node and invoice features
We should explicitly filter features based on where they can be included (`init`, `node_announcement` or `invoice`) as specified in Bolt 9.
1 parent 6cc37cb commit 79e6aef

File tree

10 files changed

+168
-20
lines changed

10 files changed

+168
-20
lines changed

eclair-core/src/main/resources/reference.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ eclair {
5757
option_anchors_zero_fee_htlc_tx = disabled
5858
option_shutdown_anysegwit = optional
5959
option_onion_messages = disabled
60+
option_payment_metadata = optional
6061
trampoline_payment = disabled
6162
keysend = disabled
6263
}

eclair-core/src/main/scala/fr/acinq/eclair/Features.scala

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ trait Feature {
3838
def mandatory: Int
3939
def optional: Int = mandatory + 1
4040

41+
/** If true, feature should be sent in init messages. */
42+
def init: Boolean
43+
/** If true, feature should be sent in node announcements. */
44+
def nodeAnnouncement: Boolean
45+
/** If true, feature should be set in invoices. */
46+
def invoice: Boolean
47+
4148
def supportBit(support: FeatureSupport): Int = support match {
4249
case Mandatory => mandatory
4350
case Optional => optional
@@ -71,6 +78,12 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow
7178
unknownFeaturesOk && knownFeaturesOk
7279
}
7380

81+
def initFeatures(): Features = Features(activated.filter { case (f, _) => f.init }, unknown)
82+
83+
def nodeAnnouncementFeatures(): Features = Features(activated.filter { case (f, _) => f.nodeAnnouncement }, unknown)
84+
85+
def invoiceFeatures(): Features = Features(activated.filter { case (f, _) => f.invoice }, unknown)
86+
7487
def toByteVector: ByteVector = {
7588
val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet)
7689
val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex))
@@ -140,72 +153,122 @@ object Features {
140153
case object OptionDataLossProtect extends Feature {
141154
val rfcName = "option_data_loss_protect"
142155
val mandatory = 0
156+
val init = true
157+
val nodeAnnouncement = true
158+
val invoice = false
143159
}
144160

145161
case object InitialRoutingSync extends Feature {
146162
val rfcName = "initial_routing_sync"
147163
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
148164
val mandatory = 2
165+
val init = true
166+
val nodeAnnouncement = false
167+
val invoice = false
149168
}
150169

151170
case object OptionUpfrontShutdownScript extends Feature {
152171
val rfcName = "option_upfront_shutdown_script"
153172
val mandatory = 4
173+
val init = true
174+
val nodeAnnouncement = true
175+
val invoice = false
154176
}
155177

156178
case object ChannelRangeQueries extends Feature {
157179
val rfcName = "gossip_queries"
158180
val mandatory = 6
181+
val init = true
182+
val nodeAnnouncement = true
183+
val invoice = false
159184
}
160185

161186
case object VariableLengthOnion extends Feature {
162187
val rfcName = "var_onion_optin"
163188
val mandatory = 8
189+
val init = true
190+
val nodeAnnouncement = true
191+
val invoice = true
164192
}
165193

166194
case object ChannelRangeQueriesExtended extends Feature {
167195
val rfcName = "gossip_queries_ex"
168196
val mandatory = 10
197+
val init = true
198+
val nodeAnnouncement = true
199+
val invoice = false
169200
}
170201

171202
case object StaticRemoteKey extends Feature {
172203
val rfcName = "option_static_remotekey"
173204
val mandatory = 12
205+
val init = true
206+
val nodeAnnouncement = true
207+
val invoice = false
174208
}
175209

176210
case object PaymentSecret extends Feature {
177211
val rfcName = "payment_secret"
178212
val mandatory = 14
213+
val init = true
214+
val nodeAnnouncement = true
215+
val invoice = true
179216
}
180217

181218
case object BasicMultiPartPayment extends Feature {
182219
val rfcName = "basic_mpp"
183220
val mandatory = 16
221+
val init = true
222+
val nodeAnnouncement = true
223+
val invoice = true
184224
}
185225

186226
case object Wumbo extends Feature {
187227
val rfcName = "option_support_large_channel"
188228
val mandatory = 18
229+
val init = true
230+
val nodeAnnouncement = true
231+
val invoice = false
189232
}
190233

191234
case object AnchorOutputs extends Feature {
192235
val rfcName = "option_anchor_outputs"
193236
val mandatory = 20
237+
val init = true
238+
val nodeAnnouncement = true
239+
val invoice = false
194240
}
195241

196242
case object AnchorOutputsZeroFeeHtlcTx extends Feature {
197243
val rfcName = "option_anchors_zero_fee_htlc_tx"
198244
val mandatory = 22
245+
val init = true
246+
val nodeAnnouncement = true
247+
val invoice = false
199248
}
200249

201250
case object ShutdownAnySegwit extends Feature {
202251
val rfcName = "option_shutdown_anysegwit"
203252
val mandatory = 26
253+
val init = true
254+
val nodeAnnouncement = true
255+
val invoice = false
204256
}
205257

206258
case object OnionMessages extends Feature {
207259
val rfcName = "option_onion_messages"
208260
val mandatory = 38
261+
val init = true
262+
val nodeAnnouncement = true
263+
val invoice = true
264+
}
265+
266+
case object PaymentMetadata extends Feature {
267+
val rfcName = "option_payment_metadata"
268+
val mandatory = 48
269+
val init = false
270+
val nodeAnnouncement = false
271+
val invoice = true
209272
}
210273

211274
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
@@ -214,11 +277,17 @@ object Features {
214277
case object TrampolinePayment extends Feature {
215278
val rfcName = "trampoline_payment"
216279
val mandatory = 50
280+
val init = true
281+
val nodeAnnouncement = true
282+
val invoice = true
217283
}
218284

219285
case object KeySend extends Feature {
220286
val rfcName = "keysend"
221287
val mandatory = 54
288+
val init = true
289+
val nodeAnnouncement = true
290+
val invoice = false
222291
}
223292

224293
val knownFeatures: Set[Feature] = Set(
@@ -237,6 +306,7 @@ object Features {
237306
AnchorOutputsZeroFeeHtlcTx,
238307
ShutdownAnySegwit,
239308
OnionMessages,
309+
PaymentMetadata,
240310
KeySend
241311
)
242312

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
107107

108108
def currentBlockHeight: Long = blockCount.get
109109

110-
def featuresFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features)
110+
/** Returns the features that should be used in our init message with the given peer. */
111+
def featuresFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features).initFeatures()
111112
}
112113

113114
object NodeParams extends Logging {

eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,14 +223,12 @@ object MultiPartHandler {
223223
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
224224
val paymentHash = Crypto.sha256(paymentPreimage)
225225
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
226-
val features = {
227-
val f1 = Seq(Features.PaymentSecret.mandatory, Features.VariableLengthOnion.mandatory)
228-
val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment)
229-
val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil
230-
val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil
231-
PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)
226+
val invoiceFeatures = {
227+
val activatedInvoiceFeatures = nodeParams.features.invoiceFeatures().activated.map { case (f, s) => f.supportBit(s) }.toSet
228+
val allInvoiceFeatures = if (nodeParams.enableTrampolinePayment) activatedInvoiceFeatures + Features.TrampolinePayment.optional else activatedInvoiceFeatures
229+
allInvoiceFeatures.toSeq
232230
}
233-
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features)
231+
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = PaymentRequestFeatures(invoiceFeatures: _*))
234232
context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
235233
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
236234
paymentRequest

eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,16 @@ object Announcements {
7676
case address@(_: Tor2) => (3, address)
7777
case address@(_: Tor3) => (4, address)
7878
}.sortBy(_._1).map(_._2)
79-
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, sortedAddresses, TlvStream.empty)
79+
val nodeAnnouncementFeatures = features.nodeAnnouncementFeatures()
80+
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, nodeAnnouncementFeatures, sortedAddresses, TlvStream.empty)
8081
val sig = Crypto.sign(witness, nodeSecret)
8182
NodeAnnouncement(
8283
signature = sig,
8384
timestamp = timestamp,
8485
nodeId = nodeSecret.publicKey,
8586
rgbColor = color,
8687
alias = alias,
87-
features = features,
88+
features = nodeAnnouncementFeatures,
8889
addresses = sortedAddresses
8990
)
9091
}

eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,25 @@ class FeaturesSpec extends AnyFunSuite {
211211
}
212212
}
213213

214+
test("filter features based on their usage") {
215+
val features = Features(
216+
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
217+
Set(UnknownFeature(753), UnknownFeature(852))
218+
)
219+
assert(features.initFeatures() === Features(
220+
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory),
221+
Set(UnknownFeature(753), UnknownFeature(852))
222+
))
223+
assert(features.nodeAnnouncementFeatures() === Features(
224+
Map[Feature, FeatureSupport](OptionDataLossProtect -> Optional, VariableLengthOnion -> Mandatory),
225+
Set(UnknownFeature(753), UnknownFeature(852))
226+
))
227+
assert(features.invoiceFeatures() === Features(
228+
Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional),
229+
Set(UnknownFeature(753), UnknownFeature(852))
230+
))
231+
}
232+
214233
test("features to bytes") {
215234
val testCases = Map(
216235
hex"" -> Features.empty,

eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,41 @@ class StartupSpec extends AnyFunSuite {
164164
assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory))
165165
}
166166

167+
test("filter out non-init features in node override") {
168+
val perNodeConf = ConfigFactory.parseString(
169+
"""
170+
| override-features = [ // optional per-node features
171+
| {
172+
| nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
173+
| features {
174+
| var_onion_optin = mandatory
175+
| payment_secret = mandatory
176+
| option_payment_metadata = disabled
177+
| }
178+
| },
179+
| {
180+
| nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
181+
| features {
182+
| var_onion_optin = mandatory
183+
| payment_secret = mandatory
184+
| option_payment_metadata = mandatory
185+
| }
186+
| }
187+
| ]
188+
""".stripMargin
189+
)
190+
191+
val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))
192+
val perNodeFeaturesA = nodeParams.featuresFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")))
193+
val perNodeFeaturesB = nodeParams.featuresFor(PublicKey(ByteVector.fromValidHex("02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
194+
val defaultNodeFeatures = nodeParams.featuresFor(PublicKey(ByteVector.fromValidHex("02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")))
195+
// Some features should never be sent in init messages.
196+
assert(nodeParams.features.hasFeature(PaymentMetadata))
197+
assert(!perNodeFeaturesA.hasFeature(PaymentMetadata))
198+
assert(!perNodeFeaturesB.hasFeature(PaymentMetadata))
199+
assert(!defaultNodeFeatures.hasFeature(PaymentMetadata))
200+
}
201+
167202
test("override feerate mismatch tolerance") {
168203
val perNodeConf = ConfigFactory.parseString(
169204
"""

eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ object TestConstants {
6565
case object TestFeature extends Feature {
6666
val rfcName = "test_feature"
6767
val mandatory = 50000
68+
val init = true
69+
val nodeAnnouncement = true
70+
val invoice = false
6871
}
6972

7073
val pluginParams: CustomFeaturePlugin = new CustomFeaturePlugin {
@@ -104,7 +107,8 @@ object TestConstants {
104107
ChannelRangeQueriesExtended -> Optional,
105108
VariableLengthOnion -> Mandatory,
106109
PaymentSecret -> Mandatory,
107-
BasicMultiPartPayment -> Optional
110+
BasicMultiPartPayment -> Optional,
111+
PaymentMetadata -> Optional,
108112
),
109113
Set(UnknownFeature(TestFeature.optional))
110114
),
@@ -233,7 +237,8 @@ object TestConstants {
233237
ChannelRangeQueriesExtended -> Optional,
234238
VariableLengthOnion -> Mandatory,
235239
PaymentSecret -> Mandatory,
236-
BasicMultiPartPayment -> Optional
240+
BasicMultiPartPayment -> Optional,
241+
PaymentMetadata -> Optional,
237242
),
238243
pluginParams = Nil,
239244
overrideFeatures = Map.empty,

eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
5858
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
5959
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
6060
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
61-
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = true)
61+
sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = true)
6262
}
6363

6464
test("sync if no whitelist is defined and peer creates a channel") {
@@ -71,30 +71,30 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
7171
// We have a channel with our peer, so we trigger a sync when connecting.
7272
switchboard ! ChannelIdAssigned(TestProbe().ref, remoteNodeId, randomBytes32(), randomBytes32())
7373
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
74-
peerConnection.expectMsg(PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true))
74+
peerConnection.expectMsg(PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true))
7575

7676
// We don't have channels with our peer, so we won't trigger a sync when connecting.
7777
switchboard ! LastChannelClosed(peer.ref, remoteNodeId)
7878
switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId)
79-
peerConnection.expectMsg(PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = false))
79+
peerConnection.expectMsg(PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false))
8080
}
8181

8282
test("don't sync if no whitelist is defined and peer does not have channels") {
8383
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set.empty)
84-
sendFeatures(nodeParams, randomKey().publicKey, nodeParams.features, expectedSync = false)
84+
sendFeatures(nodeParams, randomKey().publicKey, nodeParams.features.initFeatures(), expectedSync = false)
8585
}
8686

8787
test("sync if whitelist contains peer") {
8888
val remoteNodeId = randomKey().publicKey
8989
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(remoteNodeId, randomKey().publicKey, randomKey().publicKey))
90-
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = true)
90+
sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = true)
9191
}
9292

9393
test("don't sync if whitelist doesn't contain peer") {
9494
val nodeParams = Alice.nodeParams.copy(syncWhitelist = Set(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))
9595
val remoteNodeId = ChannelCodecsSpec.normal.commitments.remoteParams.nodeId
9696
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal)
97-
sendFeatures(nodeParams, remoteNodeId, nodeParams.features, expectedSync = false)
97+
sendFeatures(nodeParams, remoteNodeId, nodeParams.features.initFeatures(), expectedSync = false)
9898
}
9999

100100
}

0 commit comments

Comments
 (0)