Skip to content

Commit

Permalink
Allow plain outgoing_node_id in blinded payment_relay (#2943)
Browse files Browse the repository at this point in the history
When we're the introduction node of a trampoline blinded path, we
previously only allowed our custom `wallet_node_id` format when a
`short_channel_id` was not included. But we actually can allow plain
`node_id`s as well, as we only need to know the public key.

We've rejected some payments in the past months because they included an
`outgoing_node_id` that didn't use the wallet format: by removing that
limitation we ensure that those payments will be correctly relayed in
the future.
  • Loading branch information
t-bast authored Nov 22, 2024
1 parent 47fdfae commit 02abc3a
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ object BlindedRouteData {
// This is usually a channel, unless the next node is a mobile wallet connected to our node.
val outgoing: Either[PublicKey, ShortChannelId] = records.get[RouteBlindingEncryptedDataTlv.OutgoingChannelId] match {
case Some(r) => Right(r.shortChannelId)
case None => Left(records.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].get.nodeId.asInstanceOf[EncodedNodeId.WithPublicKey.Wallet].publicKey)
case None => Left(records.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].get.nodeId.asInstanceOf[EncodedNodeId.WithPublicKey].publicKey)
}
val paymentRelay: PaymentRelay = records.get[RouteBlindingEncryptedDataTlv.PaymentRelay].get
val paymentConstraints: PaymentConstraints = records.get[RouteBlindingEncryptedDataTlv.PaymentConstraints].get
Expand All @@ -114,9 +114,9 @@ object BlindedRouteData {
}

def validatePaymentRelayData(records: TlvStream[RouteBlindingEncryptedDataTlv]): Either[InvalidTlvPayload, PaymentRelayData] = {
// Note that the BOLTs require using an OutgoingChannelId, but we optionally support a wallet node_id.
// Note that the BOLTs require using an OutgoingChannelId, but we optionally support using a node_id.
if (records.get[OutgoingChannelId].isEmpty && records.get[OutgoingNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
if (records.get[OutgoingNodeId].nonEmpty && !records.get[OutgoingNodeId].get.nodeId.isInstanceOf[EncodedNodeId.WithPublicKey.Wallet]) return Left(ForbiddenTlv(UInt64(4)))
if (records.get[OutgoingNodeId].nonEmpty && !records.get[OutgoingNodeId].get.nodeId.isInstanceOf[EncodedNodeId.WithPublicKey]) return Left(ForbiddenTlv(UInt64(4)))
if (records.get[PaymentRelay].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
if (records.get[PaymentConstraints].isEmpty) return Left(MissingRequiredTlv(UInt64(12)))
if (records.get[PathId].nonEmpty) return Left(ForbiddenTlv(UInt64(6)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,20 @@ class PaymentOnionSpec extends AnyFunSuite {
}
}

test("encode/decode channel relay blinded per-hop-payload (with wallet node_id)") {
val walletNodeId = PublicKey(hex"0221cd519eba9c8b840a5e40b65dc2c040e159a766979723ed770efceb97260ec8")
val blindedTlvs = TlvStream[RouteBlindingEncryptedDataTlv](
RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId.WithPublicKey.Wallet(walletNodeId)),
RouteBlindingEncryptedDataTlv.PaymentRelay(CltvExpiryDelta(144), 100, 10 msat),
RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1500), 1 msat),
)
val Right(payload) = IntermediatePayload.ChannelRelay.Blinded.validate(TlvStream(EncryptedRecipientData(hex"deadbeef")), blindedTlvs, randomKey().publicKey)
assert(payload.outgoing == Left(walletNodeId))
assert(payload.amountToForward(10_000 msat) == 9990.msat)
assert(payload.outgoingCltv(CltvExpiry(1000)) == CltvExpiry(856))
assert(payload.paymentRelayData.allowedFeatures.isEmpty)
test("encode/decode channel relay blinded per-hop-payload (with node_id)") {
val nextNodeId = PublicKey(hex"0221cd519eba9c8b840a5e40b65dc2c040e159a766979723ed770efceb97260ec8")
Seq(EncodedNodeId.WithPublicKey.Wallet(nextNodeId), EncodedNodeId.WithPublicKey.Plain(nextNodeId)).foreach(outgoingNodeId => {
val blindedTlvs = TlvStream[RouteBlindingEncryptedDataTlv](
RouteBlindingEncryptedDataTlv.OutgoingNodeId(outgoingNodeId),
RouteBlindingEncryptedDataTlv.PaymentRelay(CltvExpiryDelta(144), 100, 10 msat),
RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1500), 1 msat),
)
val Right(payload) = IntermediatePayload.ChannelRelay.Blinded.validate(TlvStream(EncryptedRecipientData(hex"deadbeef")), blindedTlvs, randomKey().publicKey)
assert(payload.outgoing == Left(nextNodeId))
assert(payload.amountToForward(10_000 msat) == 9990.msat)
assert(payload.outgoingCltv(CltvExpiry(1000)) == CltvExpiry(856))
assert(payload.paymentRelayData.allowedFeatures.isEmpty)
})
}

test("encode/decode node relay per-hop payload") {
Expand Down Expand Up @@ -306,8 +308,6 @@ class PaymentOnionSpec extends AnyFunSuite {
TestCase(MissingRequiredTlv(UInt64(10)), hex"23 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", validBlindedTlvs),
// Missing encrypted outgoing channel.
TestCase(MissingRequiredTlv(UInt64(2)), hex"0a 0a080123456789abcdef", TlvStream(RouteBlindingEncryptedDataTlv.PaymentRelay(CltvExpiryDelta(144), 100, 10 msat), RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1500), 1 msat))),
// Forbidden encrypted outgoing plain node_id.
TestCase(ForbiddenTlv(UInt64(4)), hex"0a 0a080123456789abcdef", TlvStream(RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId.WithPublicKey.Plain(randomKey().publicKey)), RouteBlindingEncryptedDataTlv.PaymentRelay(CltvExpiryDelta(144), 100, 10 msat), RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1500), 1 msat))),
// Missing encrypted payment relay data.
TestCase(MissingRequiredTlv(UInt64(10)), hex"0a 0a080123456789abcdef", TlvStream(RouteBlindingEncryptedDataTlv.OutgoingChannelId(ShortChannelId(42)), RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1500), 1 msat))),
// Missing encrypted payment constraint.
Expand Down

0 comments on commit 02abc3a

Please sign in to comment.