Skip to content

Commit 2530895

Browse files
authored
Activated features should be a map instead of a set (#225)
It doesn't make sense to use a set, because we could end up having a feature activated twice (once as optional and once as mandatory). It should instead be a Map<Feature, FeatureSupport>.
1 parent a17a5eb commit 2530895

File tree

10 files changed

+108
-254
lines changed

10 files changed

+108
-254
lines changed

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

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,17 @@ object TestConstants {
4141
keyManager = keyManager,
4242
alias = "alice",
4343
features = Features(
44-
setOf(
45-
ActivatedFeature(Feature.InitialRoutingSync, FeatureSupport.Optional),
46-
ActivatedFeature(Feature.OptionDataLossProtect, FeatureSupport.Optional),
47-
ActivatedFeature(Feature.ChannelRangeQueries, FeatureSupport.Optional),
48-
ActivatedFeature(Feature.ChannelRangeQueriesExtended, FeatureSupport.Optional),
49-
ActivatedFeature(Feature.VariableLengthOnion, FeatureSupport.Optional),
50-
ActivatedFeature(Feature.PaymentSecret, FeatureSupport.Optional),
51-
ActivatedFeature(Feature.BasicMultiPartPayment, FeatureSupport.Optional),
52-
ActivatedFeature(Feature.Wumbo, FeatureSupport.Optional),
53-
ActivatedFeature(Feature.StaticRemoteKey, FeatureSupport.Mandatory),
54-
ActivatedFeature(Feature.AnchorOutputs, FeatureSupport.Mandatory),
55-
ActivatedFeature(Feature.TrampolinePayment, FeatureSupport.Optional)
56-
)
44+
Feature.InitialRoutingSync to FeatureSupport.Optional,
45+
Feature.OptionDataLossProtect to FeatureSupport.Optional,
46+
Feature.ChannelRangeQueries to FeatureSupport.Optional,
47+
Feature.ChannelRangeQueriesExtended to FeatureSupport.Optional,
48+
Feature.VariableLengthOnion to FeatureSupport.Optional,
49+
Feature.PaymentSecret to FeatureSupport.Optional,
50+
Feature.BasicMultiPartPayment to FeatureSupport.Optional,
51+
Feature.Wumbo to FeatureSupport.Optional,
52+
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
53+
Feature.AnchorOutputs to FeatureSupport.Mandatory,
54+
Feature.TrampolinePayment to FeatureSupport.Optional
5755
),
5856
dustLimit = 1_100.sat,
5957
maxRemoteDustLimit = 1_500.sat,
@@ -110,19 +108,17 @@ object TestConstants {
110108
keyManager = keyManager,
111109
alias = "bob",
112110
features = Features(
113-
setOf(
114-
ActivatedFeature(Feature.InitialRoutingSync, FeatureSupport.Optional),
115-
ActivatedFeature(Feature.OptionDataLossProtect, FeatureSupport.Optional),
116-
ActivatedFeature(Feature.ChannelRangeQueries, FeatureSupport.Optional),
117-
ActivatedFeature(Feature.ChannelRangeQueriesExtended, FeatureSupport.Optional),
118-
ActivatedFeature(Feature.VariableLengthOnion, FeatureSupport.Optional),
119-
ActivatedFeature(Feature.PaymentSecret, FeatureSupport.Optional),
120-
ActivatedFeature(Feature.BasicMultiPartPayment, FeatureSupport.Optional),
121-
ActivatedFeature(Feature.Wumbo, FeatureSupport.Optional),
122-
ActivatedFeature(Feature.StaticRemoteKey, FeatureSupport.Mandatory),
123-
ActivatedFeature(Feature.AnchorOutputs, FeatureSupport.Mandatory),
124-
ActivatedFeature(Feature.TrampolinePayment, FeatureSupport.Optional)
125-
)
111+
Feature.InitialRoutingSync to FeatureSupport.Optional,
112+
Feature.OptionDataLossProtect to FeatureSupport.Optional,
113+
Feature.ChannelRangeQueries to FeatureSupport.Optional,
114+
Feature.ChannelRangeQueriesExtended to FeatureSupport.Optional,
115+
Feature.VariableLengthOnion to FeatureSupport.Optional,
116+
Feature.PaymentSecret to FeatureSupport.Optional,
117+
Feature.BasicMultiPartPayment to FeatureSupport.Optional,
118+
Feature.Wumbo to FeatureSupport.Optional,
119+
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
120+
Feature.AnchorOutputs to FeatureSupport.Mandatory,
121+
Feature.TrampolinePayment to FeatureSupport.Optional
126122
),
127123
dustLimit = 1_000.sat,
128124
maxRemoteDustLimit = 1_500.sat,

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

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -101,53 +101,48 @@ sealed class Feature {
101101
}
102102
}
103103

104-
@Serializable
105-
data class ActivatedFeature(val feature: Feature, val support: FeatureSupport)
106-
107104
@Serializable
108105
data class UnknownFeature(val bitIndex: Int)
109106

110107
@Serializable
111-
data class Features(val activated: Set<ActivatedFeature>, val unknown: Set<UnknownFeature> = emptySet()) {
108+
data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Set<UnknownFeature> = emptySet()) {
112109

113110
fun hasFeature(feature: Feature, support: FeatureSupport? = null): Boolean =
114-
if (support != null) activated.contains(ActivatedFeature(feature, support))
115-
else hasFeature(feature, FeatureSupport.Optional) || hasFeature(feature, FeatureSupport.Mandatory)
111+
if (support != null) activated[feature] == support
112+
else activated.containsKey(feature)
116113

117114
/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
118115
fun areSupported(remoteFeatures: Features): Boolean {
119116
// we allow unknown odd features (it's ok to be odd)
120117
val unknownFeaturesOk = remoteFeatures.unknown.all { it.bitIndex % 2 == 1 }
121118
// we verify that we activated every mandatory feature they require
122-
val knownFeaturesOk = remoteFeatures.activated.all {
123-
when (it.support) {
119+
val knownFeaturesOk = remoteFeatures.activated.all { (feature, support) ->
120+
when (support) {
124121
FeatureSupport.Optional -> true
125-
FeatureSupport.Mandatory -> hasFeature(it.feature)
122+
FeatureSupport.Mandatory -> hasFeature(feature)
126123
}
127124
}
128125
return unknownFeaturesOk && knownFeaturesOk
129126
}
130127

131128
fun toByteArray(): ByteArray {
132-
val activatedFeatureBytes =
133-
activated.mapTo(HashSet()) { it.feature.supportBit(it.support) }.indicesToByteArray()
134-
val unknownFeatureBytes = unknown.mapTo(HashSet()) { it.bitIndex }.indicesToByteArray()
129+
val activatedFeatureBytes = activated.map { (feature, support) -> feature.supportBit(support) }.toHashSet().indicesToByteArray()
130+
val unknownFeatureBytes = unknown.map { it.bitIndex }.toHashSet().indicesToByteArray()
135131
val maxSize = activatedFeatureBytes.size.coerceAtLeast(unknownFeatureBytes.size)
136132
return activatedFeatureBytes.leftPaddedCopyOf(maxSize) or unknownFeatureBytes.leftPaddedCopyOf(maxSize)
137133
}
138134

139135
private fun Set<Int>.indicesToByteArray(): ByteArray {
140136
if (isEmpty()) return ByteArray(0)
141-
// When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting feature bits.
142137
val buf = BitField.forAtMost(maxOrNull()!! + 1)
143138
forEach { buf.setRight(it) }
144139
return buf.bytes
145140
}
146141

147142
companion object {
148-
val empty = Features(emptySet())
143+
val empty = Features(mapOf())
149144

150-
val knownFeatures: Set<Feature> = setOf(
145+
private val knownFeatures: Set<Feature> = setOf(
151146
Feature.OptionDataLossProtect,
152147
Feature.InitialRoutingSync,
153148
Feature.ChannelRangeQueries,
@@ -169,40 +164,19 @@ data class Features(val activated: Set<ActivatedFeature>, val unknown: Set<Unkno
169164
val all = bits.asRightSequence().withIndex()
170165
.filter { it.value }
171166
.map { (idx, _) ->
172-
knownFeatures.find { it.optional == idx }?.let { ActivatedFeature(it, FeatureSupport.Optional) }
173-
?: knownFeatures.find { it.mandatory == idx }
174-
?.let { ActivatedFeature(it, FeatureSupport.Mandatory) }
167+
knownFeatures.find { it.optional == idx }?.let { Pair(it, FeatureSupport.Optional) }
168+
?: knownFeatures.find { it.mandatory == idx }?.let { Pair(it, FeatureSupport.Mandatory) }
175169
?: UnknownFeature(idx)
176170
}
177171
.toList()
178172

179173
return Features(
180-
activated = all.filterIsInstance<ActivatedFeature>().toSet(),
174+
activated = all.filterIsInstance<Pair<Feature, FeatureSupport>>().toMap(),
181175
unknown = all.filterIsInstance<UnknownFeature>().toSet()
182176
)
183177
}
184178

185-
// /** expects to have a top level config block named "features" */
186-
// fun fromConfiguration(config: Config): Features = Features(
187-
// knownFeatures.flatMap {
188-
// feature =>
189-
// getFeature(config, feature.rfcName) match {
190-
// case Some(support) => Some(ActivatedFeature(feature, support))
191-
// case _ => None
192-
// }
193-
// })
194-
//
195-
// /** tries to extract the given feature name from the config, if successful returns its feature support */
196-
// private def getFeature(config: Config, name: String): Option[FeatureSupport] = {
197-
// if (!config.hasPath(s"features.$name")) None
198-
// else {
199-
// config.getString(s"features.$name") match {
200-
// case support if support == Mandatory.toString => Some(Mandatory)
201-
// case support if support == Optional.toString => Some(Optional)
202-
// case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)")
203-
// }
204-
// }
205-
// }
179+
operator fun invoke(vararg pairs: Pair<Feature, FeatureSupport>): Features = Features(mapOf(*pairs))
206180

207181
// Features may depend on other features, as specified in Bolt 9.
208182
private val featuresDependency: Map<Feature, List<Feature>> = mapOf(
@@ -229,7 +203,6 @@ data class Features(val activated: Set<ActivatedFeature>, val unknown: Set<Unkno
229203

230204
fun areCompatible(ours: Features, theirs: Features): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours)
231205

232-
233206
/** returns true if both have at least optional support */
234207
fun canUseFeature(localFeatures: Features, remoteFeatures: Features, feature: Feature): Boolean =
235208
localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature)

src/commonMain/kotlin/fr/acinq/eclair/payment/OutgoingPaymentHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ class OutgoingPaymentHandler(val nodeId: PublicKey, val walletParams: WalletPara
309309
val finalExpiry = finalExpiryDelta.toCltvExpiry(currentBlockHeight.toLong())
310310
val finalPayload = FinalPayload.createSinglePartPayload(request.amount, finalExpiry, request.details.paymentRequest.paymentSecret)
311311

312-
val invoiceFeatures = request.details.paymentRequest.features?.let { Features(it) } ?: Features(setOf())
312+
val invoiceFeatures = request.details.paymentRequest.features?.let { Features(it) } ?: Features(mapOf())
313313
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (invoiceFeatures.hasFeature(Feature.TrampolinePayment)) {
314314
OutgoingPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, OnionRoutingPacket.TrampolinePacketLength)
315315
} else {

src/commonMain/kotlin/fr/acinq/eclair/payment/PaymentRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ data class PaymentRequest(
154154
* This filters out all features unrelated to BOLT 11
155155
*/
156156
fun invoiceFeatures(features: Features): Features {
157-
return Features(activated = features.activated.filter { f -> bolt11Features.contains(f.feature) }.toSet())
157+
return Features(activated = features.activated.filter { (f, _) -> bolt11Features.contains(f) })
158158
}
159159

160160
fun create(

0 commit comments

Comments
 (0)