Skip to content

Commit

Permalink
Add support for option_payment_metadata (#313)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
t-bast committed Feb 8, 2022
1 parent ec96ce2 commit 363ed06
Show file tree
Hide file tree
Showing 24 changed files with 785 additions and 587 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ object TestConstants {
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
Feature.AnchorOutputs to FeatureSupport.Mandatory,
Feature.ChannelType to FeatureSupport.Mandatory,
Feature.PaymentMetadata to FeatureSupport.Optional,
Feature.TrampolinePayment to FeatureSupport.Optional,
Feature.WakeUpNotificationProvider to FeatureSupport.Optional,
Feature.PayToOpenProvider to FeatureSupport.Optional,
Expand Down Expand Up @@ -129,6 +130,7 @@ object TestConstants {
Feature.StaticRemoteKey to FeatureSupport.Mandatory,
Feature.AnchorOutputs to FeatureSupport.Mandatory,
Feature.ChannelType to FeatureSupport.Mandatory,
Feature.PaymentMetadata to FeatureSupport.Optional,
Feature.TrampolinePayment to FeatureSupport.Optional,
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
Feature.PayToOpenClient to FeatureSupport.Optional,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ public suspend fun newPeers(
}

// Initialize Bob with Alice's features
bob.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.first.features.toByteArray().toByteVector()))))
bob.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.first.features.initFeatures().toByteArray().toByteVector()))))
// Initialize Alice with Bob's features
alice.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.second.features.toByteArray().toByteVector()))))
alice.send(BytesReceived(LightningMessage.encode(Init(features = nodeParams.second.features.initFeatures().toByteArray().toByteVector()))))

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

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

val initMsg = LightningMessage.encode(theirInit)
peer.send(BytesReceived(initMsg))
Expand Down
48 changes: 45 additions & 3 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import fr.acinq.lightning.utils.leftPaddedCopyOf
import fr.acinq.lightning.utils.or
import kotlinx.serialization.Serializable

/** Feature scope as defined in Bolt 9. */
enum class FeatureScope { Init, Node, Invoice }

enum class FeatureSupport {
Mandatory {
override fun toString() = "mandatory"
Expand All @@ -20,6 +23,7 @@ sealed class Feature {

abstract val rfcName: String
abstract val mandatory: Int
abstract val scopes: Set<FeatureScope>
val optional: Int get() = mandatory + 1

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

@Serializable
Expand All @@ -41,66 +46,84 @@ sealed class Feature {

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

@Serializable
object ChannelRangeQueries : Feature() {
override val rfcName get() = "gossip_queries"
override val mandatory get() = 6
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object VariableLengthOnion : Feature() {
override val rfcName get() = "var_onion_optin"
override val mandatory get() = 8
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
}

@Serializable
object ChannelRangeQueriesExtended : Feature() {
override val rfcName get() = "gossip_queries_ex"
override val mandatory get() = 10
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object StaticRemoteKey : Feature() {
override val rfcName get() = "option_static_remotekey"
override val mandatory get() = 12
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object PaymentSecret : Feature() {
override val rfcName get() = "payment_secret"
override val mandatory get() = 14
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
}

@Serializable
object BasicMultiPartPayment : Feature() {
override val rfcName get() = "basic_mpp"
override val mandatory get() = 16
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
}

@Serializable
object Wumbo : Feature() {
override val rfcName get() = "option_support_large_channel"
override val mandatory get() = 18
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object AnchorOutputs : Feature() {
override val rfcName get() = "option_anchor_outputs"
override val mandatory get() = 20
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object ShutdownAnySegwit : Feature() {
override val rfcName get() = "option_shutdown_anysegwit"
override val mandatory get() = 26
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object ChannelType : Feature() {
override val rfcName get() = "option_channel_type"
override val mandatory get() = 44
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object PaymentMetadata : Feature() {
override val rfcName get() = "option_payment_metadata"
override val mandatory get() = 48
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Invoice)
}

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

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

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

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

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

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

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

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

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

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

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

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

fun hasFeature(feature: Feature, support: FeatureSupport? = null): Boolean =
if (support != null) activated[feature] == support
else activated.containsKey(feature)
fun hasFeature(feature: Feature, support: FeatureSupport? = null): Boolean = when (support) {
null -> activated.containsKey(feature)
else -> activated[feature] == support
}

fun initFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Init) }, unknown)

fun nodeAnnouncementFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Node) }, unknown)

fun invoiceFeatures(): Features = Features(activated.filter { it.key.scopes.contains(FeatureScope.Invoice) }, unknown)

/** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */
fun areSupported(remoteFeatures: Features): Boolean {
Expand Down Expand Up @@ -236,6 +277,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.AnchorOutputs,
Feature.ShutdownAnySegwit,
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.TrampolinePayment,
Feature.ZeroReserveChannels,
Feature.ZeroConfChannels,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import fr.acinq.lightning.blockchain.fee.FeerateTolerance
import fr.acinq.lightning.crypto.Generators
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.crypto.ShaChain
import fr.acinq.lightning.payment.OutgoingPacket
import fr.acinq.lightning.payment.OutgoingPaymentPacket
import fr.acinq.lightning.transactions.CommitmentSpec
import fr.acinq.lightning.transactions.Transactions
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.CommitTx
Expand Down Expand Up @@ -370,7 +370,7 @@ data class Commitments(
// we have already sent a fail/fulfill for this htlc
alreadyProposed(localChanges.proposed, htlc.id) -> Either.Left(UnknownHtlcId(channelId, cmd.id))
else -> {
when (val result = OutgoingPacket.buildHtlcFailure(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket, cmd.reason)) {
when (val result = OutgoingPaymentPacket.buildHtlcFailure(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket, cmd.reason)) {
is Either.Right -> {
val fail = UpdateFailHtlc(channelId, cmd.id, result.value)
val commitments1 = addLocalProposal(fail)
Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class Peer(

private val features = nodeParams.features

private val ourInit = Init(features.toByteArray().toByteVector())
private val ourInit = Init(features.initFeatures().toByteArray().toByteVector())
private var theirInit: Init? = null

public val currentTipFlow = MutableStateFlow<Pair<Int, BlockHeader>?>(null)
Expand Down
Loading

0 comments on commit 363ed06

Please sign in to comment.