Skip to content

Commit 2ecfa32

Browse files
Add support for option_payment_metadata (#313)
* 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. We also introduce the option_payment_metadata feature which helps our test cases since it's only allowed in invoices. * Refactor onion to dedicated namespace This commit doesn't contain any logic, it simply prefixes some classes to make it obvious that they are payment-related, rename files and moves some classes. We will update the payment onion, so it was a good time to do this small refactoring which will also be necessary for onion messages. * Add support for option_payment_metadata Add support for lightning/bolts#912 Whenever we find a payment metadata field in an invoice, we send it in the onion payload for the final recipient. We include a payment metadata in every invoice we generate. This lets us see whether our payers support it or not, which is important data to have before we make it mandatory and use it for storage-less invoices.
1 parent 375a9ad commit 2ecfa32

File tree

24 files changed

+785
-587
lines changed

24 files changed

+785
-587
lines changed

lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/TestConstants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ object TestConstants {
5454
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
5555
Feature.AnchorOutputs to FeatureSupport.Mandatory,
5656
Feature.ChannelType to FeatureSupport.Mandatory,
57+
Feature.PaymentMetadata to FeatureSupport.Optional,
5758
Feature.TrampolinePayment to FeatureSupport.Optional,
5859
Feature.WakeUpNotificationProvider to FeatureSupport.Optional,
5960
Feature.PayToOpenProvider to FeatureSupport.Optional,
@@ -129,6 +130,7 @@ object TestConstants {
129130
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
130131
Feature.AnchorOutputs to FeatureSupport.Mandatory,
131132
Feature.ChannelType to FeatureSupport.Mandatory,
133+
Feature.PaymentMetadata to FeatureSupport.Optional,
132134
Feature.TrampolinePayment to FeatureSupport.Optional,
133135
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
134136
Feature.PayToOpenClient to FeatureSupport.Optional,

lightning-kmp-test-fixtures/src/commonMain/kotlin/fr/acinq/lightning/tests/io/peer/builders.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ public suspend fun newPeers(
6565
}
6666

6767
// Initialize Bob with Alice's features
68-
bob.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.first.features.toByteArray().toByteVector()))))
68+
bob.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.first.features.initFeatures().toByteArray().toByteVector()))))
6969
// Initialize Alice with Bob's features
70-
alice.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.second.features.toByteArray().toByteVector()))))
70+
alice.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.second.features.initFeatures().toByteArray().toByteVector()))))
7171

7272
// TODO update to depend on the initChannels size
7373
if (initChannels.isNotEmpty()) {
@@ -124,7 +124,7 @@ public suspend fun CoroutineScope.newPeer(
124124

125125
remotedNodeChannelState?.let { state ->
126126
// send Init from remote node
127-
val theirInit = Init(features = state.staticParams.nodeParams.features.toByteArray().toByteVector())
127+
val theirInit = Init(features = state.staticParams.nodeParams.features.initFeatures().toByteArray().toByteVector())
128128

129129
val initMsg = LightningMessage.encode(theirInit)
130130
peer.send(BytesReceived(initMsg))

src/commonMain/kotlin/fr/acinq/lightning/Features.kt

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import fr.acinq.lightning.utils.leftPaddedCopyOf
66
import fr.acinq.lightning.utils.or
77
import kotlinx.serialization.Serializable
88

9+
/** Feature scope as defined in Bolt 9. */
10+
enum class FeatureScope { Init, Node, Invoice }
11+
912
enum class FeatureSupport {
1013
Mandatory {
1114
override fun toString() = "mandatory"
@@ -20,6 +23,7 @@ sealed class Feature {
2023

2124
abstract val rfcName: String
2225
abstract val mandatory: Int
26+
abstract val scopes: Set<FeatureScope>
2327
val optional: Int get() = mandatory + 1
2428

2529
fun supportBit(support: FeatureSupport): Int = when (support) {
@@ -33,6 +37,7 @@ sealed class Feature {
3337
object OptionDataLossProtect : Feature() {
3438
override val rfcName get() = "option_data_loss_protect"
3539
override val mandatory get() = 0
40+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
3641
}
3742

3843
@Serializable
@@ -41,66 +46,84 @@ sealed class Feature {
4146

4247
// reserved but not used as per lightningnetwork/lightning-rfc/pull/178
4348
override val mandatory get() = 2
49+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
4450
}
4551

4652
@Serializable
4753
object ChannelRangeQueries : Feature() {
4854
override val rfcName get() = "gossip_queries"
4955
override val mandatory get() = 6
56+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
5057
}
5158

5259
@Serializable
5360
object VariableLengthOnion : Feature() {
5461
override val rfcName get() = "var_onion_optin"
5562
override val mandatory get() = 8
63+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
5664
}
5765

5866
@Serializable
5967
object ChannelRangeQueriesExtended : Feature() {
6068
override val rfcName get() = "gossip_queries_ex"
6169
override val mandatory get() = 10
70+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
6271
}
6372

6473
@Serializable
6574
object StaticRemoteKey : Feature() {
6675
override val rfcName get() = "option_static_remotekey"
6776
override val mandatory get() = 12
77+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
6878
}
6979

7080
@Serializable
7181
object PaymentSecret : Feature() {
7282
override val rfcName get() = "payment_secret"
7383
override val mandatory get() = 14
84+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
7485
}
7586

7687
@Serializable
7788
object BasicMultiPartPayment : Feature() {
7889
override val rfcName get() = "basic_mpp"
7990
override val mandatory get() = 16
91+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
8092
}
8193

8294
@Serializable
8395
object Wumbo : Feature() {
8496
override val rfcName get() = "option_support_large_channel"
8597
override val mandatory get() = 18
98+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
8699
}
87100

88101
@Serializable
89102
object AnchorOutputs : Feature() {
90103
override val rfcName get() = "option_anchor_outputs"
91104
override val mandatory get() = 20
105+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
92106
}
93107

94108
@Serializable
95109
object ShutdownAnySegwit : Feature() {
96110
override val rfcName get() = "option_shutdown_anysegwit"
97111
override val mandatory get() = 26
112+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
98113
}
99114

100115
@Serializable
101116
object ChannelType : Feature() {
102117
override val rfcName get() = "option_channel_type"
103118
override val mandatory get() = 44
119+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
120+
}
121+
122+
@Serializable
123+
object PaymentMetadata : Feature() {
124+
override val rfcName get() = "option_payment_metadata"
125+
override val mandatory get() = 48
126+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Invoice)
104127
}
105128

106129
// The following features have not been standardised, hence the high feature bits to avoid conflicts.
@@ -109,76 +132,87 @@ sealed class Feature {
109132
object TrampolinePayment : Feature() {
110133
override val rfcName get() = "trampoline_payment"
111134
override val mandatory get() = 50
135+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
112136
}
113137

114138
/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
115139
@Serializable
116140
object ZeroReserveChannels : Feature() {
117141
override val rfcName get() = "zero_reserve_channels"
118142
override val mandatory get() = 128
143+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
119144
}
120145

121146
/** This feature bit should be activated when a node accepts unconfirmed channels (will set min_depth to 0 in accept_channel). */
122147
@Serializable
123148
object ZeroConfChannels : Feature() {
124149
override val rfcName get() = "zero_conf_channels"
125150
override val mandatory get() = 130
151+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
126152
}
127153

128154
/** This feature bit should be activated when a mobile node supports waking up via push notifications. */
129155
@Serializable
130156
object WakeUpNotificationClient : Feature() {
131157
override val rfcName get() = "wake_up_notification_client"
132158
override val mandatory get() = 132
159+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
133160
}
134161

135162
/** This feature bit should be activated when a node supports waking up their peers via push notifications. */
136163
@Serializable
137164
object WakeUpNotificationProvider : Feature() {
138165
override val rfcName get() = "wake_up_notification_provider"
139166
override val mandatory get() = 134
167+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
140168
}
141169

142170
/** This feature bit should be activated when a node accepts on-the-fly channel creation. */
143171
@Serializable
144172
object PayToOpenClient : Feature() {
145173
override val rfcName get() = "pay_to_open_client"
146174
override val mandatory get() = 136
175+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
147176
}
148177

149178
/** This feature bit should be activated when a node supports opening channels on-the-fly when liquidity is missing to receive a payment. */
150179
@Serializable
151180
object PayToOpenProvider : Feature() {
152181
override val rfcName get() = "pay_to_open_provider"
153182
override val mandatory get() = 138
183+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
154184
}
155185

156186
/** This feature bit should be activated when a node accepts channel creation via trusted swaps-in. */
157187
@Serializable
158188
object TrustedSwapInClient : Feature() {
159189
override val rfcName get() = "trusted_swap_in_client"
160190
override val mandatory get() = 140
191+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
161192
}
162193

163194
/** This feature bit should be activated when a node supports opening channels in exchange for on-chain funds (swap-in). */
164195
@Serializable
165196
object TrustedSwapInProvider : Feature() {
166197
override val rfcName get() = "trusted_swap_in_provider"
167198
override val mandatory get() = 142
199+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
168200
}
169201

170202
/** This feature bit should be activated when a node wants to send channel backups to their peers. */
171203
@Serializable
172204
object ChannelBackupClient : Feature() {
173205
override val rfcName get() = "channel_backup_client"
174206
override val mandatory get() = 144
207+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
175208
}
176209

177210
/** This feature bit should be activated when a node stores channel backups for their peers. */
178211
@Serializable
179212
object ChannelBackupProvider : Feature() {
180213
override val rfcName get() = "channel_backup_provider"
181214
override val mandatory get() = 146
215+
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
182216
}
183217
}
184218

@@ -188,9 +222,16 @@ data class UnknownFeature(val bitIndex: Int)
188222
@Serializable
189223
data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Set<UnknownFeature> = emptySet()) {
190224

191-
fun hasFeature(feature: Feature, support: FeatureSupport? = null): Boolean =
192-
if (support != null) activated[feature] == support
193-
else activated.containsKey(feature)
225+
fun hasFeature(feature: Feature, support: FeatureSupport? = null): Boolean = when (support) {
226+
null -> activated.containsKey(feature)
227+
else -> activated[feature] == support
228+
}
229+
230+
fun initFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Init) }, unknown)
231+
232+
fun nodeAnnouncementFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Node) }, unknown)
233+
234+
fun invoiceFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Invoice) }, unknown)
194235

195236
/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
196237
fun areSupported(remoteFeatures: Features): Boolean {
@@ -236,6 +277,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
236277
Feature.AnchorOutputs,
237278
Feature.ShutdownAnySegwit,
238279
Feature.ChannelType,
280+
Feature.PaymentMetadata,
239281
Feature.TrampolinePayment,
240282
Feature.ZeroReserveChannels,
241283
Feature.ZeroConfChannels,

src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import fr.acinq.lightning.blockchain.fee.FeerateTolerance
1111
import fr.acinq.lightning.crypto.Generators
1212
import fr.acinq.lightning.crypto.KeyManager
1313
import fr.acinq.lightning.crypto.ShaChain
14-
import fr.acinq.lightning.payment.OutgoingPacket
14+
import fr.acinq.lightning.payment.OutgoingPaymentPacket
1515
import fr.acinq.lightning.transactions.CommitmentSpec
1616
import fr.acinq.lightning.transactions.Transactions
1717
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.CommitTx
@@ -370,7 +370,7 @@ data class Commitments(
370370
// we have already sent a fail/fulfill for this htlc
371371
alreadyProposed(localChanges.proposed, htlc.id) -> Either.Left(UnknownHtlcId(channelId, cmd.id))
372372
else -> {
373-
when (val result = OutgoingPacket.buildHtlcFailure(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket, cmd.reason)) {
373+
when (val result = OutgoingPaymentPacket.buildHtlcFailure(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket, cmd.reason)) {
374374
is Either.Right -> {
375375
val fail = UpdateFailHtlc(channelId, cmd.id, result.value)
376376
val commitments1 = addLocalProposal(fail)

src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class Peer(
115115

116116
private val features = nodeParams.features
117117

118-
private val ourInit = Init(features.toByteArray().toByteVector())
118+
private val ourInit = Init(features.initFeatures().toByteArray().toByteVector())
119119
private var theirInit: Init? = null
120120

121121
public val currentTipFlow = MutableStateFlow<Pair<Int, BlockHeader>?>(null)

0 commit comments

Comments
 (0)