Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,12 @@ object PaymentRequest {

sealed trait TaggedField

sealed trait UnknownTaggedField extends TaggedField
sealed trait UnknownTaggedField extends TaggedField {
def data: BitVector
}

// @formatter:off
case class UnknownTag0(data: BitVector) extends UnknownTaggedField
case class UnknownTag1(data: BitVector) extends UnknownTaggedField
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch

case class UnknownTag2(data: BitVector) extends UnknownTaggedField
case class UnknownTag4(data: BitVector) extends UnknownTaggedField
case class UnknownTag7(data: BitVector) extends UnknownTaggedField
Expand All @@ -187,19 +188,27 @@ object PaymentRequest {
case class UnknownTag31(data: BitVector) extends UnknownTaggedField
// @formatter:on

// @formatter:off
sealed trait PaymentHashTag extends TaggedField
/**
* Payment Hash
*
* @param hash payment hash
*/
case class PaymentHash(hash: ByteVector32) extends TaggedField
case class PaymentHash(hash: ByteVector32) extends PaymentHashTag
case class InvalidPaymentHash(data: BitVector) extends PaymentHashTag with UnknownTaggedField
// @formatter:on

// @formatter:off
sealed trait PaymentSecretTag extends TaggedField
/**
* Payment secret. This is currently random bytes used to protect against probing from the next-to-last node.
*
* @param secret payment secret
*/
case class PaymentSecret(secret: ByteVector32) extends TaggedField
case class PaymentSecret(secret: ByteVector32) extends PaymentSecretTag
case class InvalidPaymentSecret(data: BitVector) extends PaymentSecretTag with UnknownTaggedField
// @formatter:on

/**
* Description
Expand All @@ -208,13 +217,17 @@ object PaymentRequest {
*/
case class Description(description: String) extends TaggedField

// @formatter:off
sealed trait DescriptionHashTag extends TaggedField
/**
* Hash
*
* @param hash hash that will be included in the payment request, and can be checked against the hash of a
* long description, an invoice, ...
*/
case class DescriptionHash(hash: ByteVector32) extends TaggedField
case class DescriptionHash(hash: ByteVector32) extends DescriptionHashTag
case class InvalidDescriptionHash(data: BitVector) extends DescriptionHashTag with UnknownTaggedField
// @formatter:on

/**
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
Expand Down Expand Up @@ -377,41 +390,64 @@ object PaymentRequest {

val dataLengthCodec: Codec[Long] = uint(10).xmap(_ * 5, s => (s / 5 + (if (s % 5 == 0) 0 else 1)).toInt)

def dataCodec[A](valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(dataLengthCodec, valueCodec, multipleForPadding = 5)
def dataCodec[A <: TaggedField](valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(dataLengthCodec, valueCodec, multipleForPadding = 5)

def dataCodecLengthDiscriminated[A <: TaggedField](length: Long, valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(provide(length), valueCodec, multipleForPadding = 5)

val paymentHashCodec: Codec[PaymentHashTag] = discriminatorWithDefault(
discriminated.by(dataLengthCodec)
// Bolt11: MUST skip over (...) p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.
// NB: Bech32 encodes 5 bits per character.
.typecase(52 * 5, dataCodecLengthDiscriminated(52 * 5, bytes32.as[PaymentHash])),
bits.as[InvalidPaymentHash].upcast[PaymentHashTag])

val paymentSecretCodec: Codec[PaymentSecretTag] = discriminatorWithDefault(
discriminated.by(dataLengthCodec)
// Bolt11: MUST skip over (...) p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.
// NB: Bech32 encodes 5 bits per character.
.typecase(52 * 5, dataCodecLengthDiscriminated(52 * 5, bytes32.as[PaymentSecret])),
bits.as[InvalidPaymentSecret].upcast[PaymentSecretTag])

val descriptionHashCodec: Codec[DescriptionHashTag] = discriminatorWithDefault(
discriminated.by(dataLengthCodec)
// Bolt11: MUST skip over (...) p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.
// NB: Bech32 encodes 5 bits per character.
.typecase(52 * 5, dataCodecLengthDiscriminated(52 * 5, bytes32.as[DescriptionHash])),
bits.as[InvalidDescriptionHash].upcast[DescriptionHashTag])

Comment thread
t-bast marked this conversation as resolved.
val taggedFieldCodec: Codec[TaggedField] = discriminated[TaggedField].by(ubyte(5))
.typecase(0, dataCodec(bits).as[UnknownTag0])
.typecase(1, dataCodec(bytes32).as[PaymentHash])
.typecase(2, dataCodec(bits).as[UnknownTag2])
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
.typecase(4, dataCodec(bits).as[UnknownTag4])
.typecase(5, dataCodec(bits).as[Features])
.typecase(6, dataCodec(bits).as[Expiry])
.typecase(7, dataCodec(bits).as[UnknownTag7])
.typecase(8, dataCodec(bits).as[UnknownTag8])
.typecase(9, dataCodec(ubyte(5) :: alignedBytesCodec(bytes)).as[FallbackAddress])
.typecase(10, dataCodec(bits).as[UnknownTag10])
.typecase(11, dataCodec(bits).as[UnknownTag11])
.typecase(12, dataCodec(bits).as[UnknownTag12])
.typecase(13, dataCodec(alignedBytesCodec(utf8)).as[Description])
.typecase(14, dataCodec(bits).as[UnknownTag14])
.typecase(15, dataCodec(bits).as[UnknownTag15])
.typecase(16, dataCodec(bytes32).as[PaymentSecret])
.typecase(17, dataCodec(bits).as[UnknownTag17])
.typecase(18, dataCodec(bits).as[UnknownTag18])
.typecase(19, dataCodec(bits).as[UnknownTag19])
.typecase(20, dataCodec(bits).as[UnknownTag20])
.typecase(21, dataCodec(bits).as[UnknownTag21])
.typecase(22, dataCodec(bits).as[UnknownTag22])
.typecase(23, dataCodec(bytes32).as[DescriptionHash])
.typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
.typecase(25, dataCodec(bits).as[UnknownTag25])
.typecase(26, dataCodec(bits).as[UnknownTag26])
.typecase(27, dataCodec(bits).as[UnknownTag27])
.typecase(28, dataCodec(bits).as[UnknownTag28])
.typecase(29, dataCodec(bits).as[UnknownTag29])
.typecase(30, dataCodec(bits).as[UnknownTag30])
.typecase(31, dataCodec(bits).as[UnknownTag31])
.typecase(0, dataCodec(bits.as[UnknownTag0]))
.typecase(1, paymentHashCodec)
.typecase(2, dataCodec(bits.as[UnknownTag2]))
.typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec).as[RoutingInfo]))
.typecase(4, dataCodec(bits.as[UnknownTag4]))
.typecase(5, dataCodec(bits.as[Features]))
.typecase(6, dataCodec(bits.as[Expiry]))
.typecase(7, dataCodec(bits.as[UnknownTag7]))
.typecase(8, dataCodec(bits.as[UnknownTag8]))
.typecase(9, dataCodec((ubyte(5) :: alignedBytesCodec(bytes)).as[FallbackAddress]))
.typecase(10, dataCodec(bits.as[UnknownTag10]))
.typecase(11, dataCodec(bits.as[UnknownTag11]))
.typecase(12, dataCodec(bits.as[UnknownTag12]))
.typecase(13, dataCodec(alignedBytesCodec(utf8).as[Description]))
.typecase(14, dataCodec(bits.as[UnknownTag14]))
.typecase(15, dataCodec(bits.as[UnknownTag15]))
.typecase(16, paymentSecretCodec)
.typecase(17, dataCodec(bits.as[UnknownTag17]))
.typecase(18, dataCodec(bits.as[UnknownTag18]))
.typecase(19, dataCodec(bits.as[UnknownTag19]))
.typecase(20, dataCodec(bits.as[UnknownTag20]))
.typecase(21, dataCodec(bits.as[UnknownTag21]))
.typecase(22, dataCodec(bits.as[UnknownTag22]))
.typecase(23, descriptionHashCodec)
.typecase(24, dataCodec(bits.as[MinFinalCltvExpiry]))
.typecase(25, dataCodec(bits.as[UnknownTag25]))
.typecase(26, dataCodec(bits.as[UnknownTag26]))
.typecase(27, dataCodec(bits.as[UnknownTag27]))
.typecase(28, dataCodec(bits.as[UnknownTag28]))
.typecase(29, dataCodec(bits.as[UnknownTag29]))
.typecase(30, dataCodec(bits.as[UnknownTag30]))
.typecase(31, dataCodec(bits.as[UnknownTag31]))

def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A](
(data: A) => codec.encode(data),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,34 @@ class PaymentRequestSpec extends FunSuite {
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu"
val refs = Seq(
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu",
// All upper-case
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu".toUpperCase,
// With ignored fields
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq2qrqqqfppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqspnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnp5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnpkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2jxxfsnucm4jf4zwtznpaxphce606fvhvje5x7d4gw7n73994hgs7nteqvenq8a4ml8aqtchv5d9pf7l558889hp4yyrqv6a7zpq9fgpskqhza"
)

for (ref <- refs) {
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(2500000000L msat))
assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111")))
assert(pr.timestamp === 1496314658L)
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(!pr.features.allowMultiPart)
assert(!pr.features.requirePaymentSecret)
assert(!pr.features.allowTrampoline)
assert(pr.features.supported)
assert(PaymentRequest.write(pr.sign(priv)) === ref.toLowerCase)
}
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk"
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(2500000000L msat))
Expand All @@ -228,37 +255,55 @@ class PaymentRequestSpec extends FunSuite {
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.fallbackAddress().isEmpty)
assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(!pr.features.allowMultiPart)
assert(!pr.features.requirePaymentSecret)
assert(!pr.features.allowTrampoline)
assert(pr.features.supported)
assert(!pr.features.supported)
assert(PaymentRequest.write(pr.sign(priv)) === ref)
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 9, 15, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk"
test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") {
val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9qn07ytgrxxzad9hc4xt3mawjjt8znfv8xzscs7007v9gh9j569lencxa8xeujzkxs0uamak9aln6ez02uunw6rd2ht2sqe4hz8thcdagpleym0j"
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(2500000000L msat))
assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111")))
assert(pr.timestamp === 1496314658L)
assert(pr.amount === Some(967878534 msat))
assert(pr.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f")
assert(pr.timestamp === 1572468703L)
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items"))
assert(pr.fallbackAddress().isEmpty)
assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001000000000")
assert(!pr.features.allowMultiPart)
assert(!pr.features.requirePaymentSecret)
assert(!pr.features.allowTrampoline)
assert(!pr.features.supported)
assert(pr.expiry === Some(604800L))
assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(10)))
assert(pr.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40)))))
assert(pr.features.supported)
assert(PaymentRequest.write(pr.sign(priv)) === ref)
}

test("reject invalid invoices") {
val refs = Seq(
// Bech32 checksum is invalid.
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrnt",
// Malformed bech32 string (no 1).
"pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
// Malformed bech32 string (mixed case).
"LNBC2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpuyk0sg5g70me25alkluzd2x62aysf2pyy8edtjeevuv4p2d5p76r4zkmneet7uvyakky2zr4cusd45tftc9c5fh0nnqpnl2jfll544esqchsrny",
// Signature is not recoverable.
"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaxtrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspk28uwq",
// String is too short.
"lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6na6hlh",
// Invalid multiplier.
"lnbc2500x1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpujr6jxr9gq9pv6g46y7d20jfkegkg4gljz2ea2a3m9lmvvr95tq2s0kvu70u3axgelz3kyvtp2ywwt0y8hkx2869zq5dll9nelr83zzqqpgl2zg"
)
for (ref <- refs) {
assertThrows[Exception](PaymentRequest.read(ref))
}
}

test("correctly serialize/deserialize variable-length tagged fields") {
val number = 123456

val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits).as[PaymentRequest.Expiry]
val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits.as[PaymentRequest.Expiry])
val field = PaymentRequest.Expiry(number)

assert(field.toLong == number)
Expand Down Expand Up @@ -293,6 +338,26 @@ class PaymentRequestSpec extends FunSuite {
val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u }
}

test("ignore hash tags with invalid length") {
// Bolt11: A reader: MUST skip over p, h, s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.
val inputs = Seq(
"ppnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"pp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"hpnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"hp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"spnqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"sp4qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"np5qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
"npkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
)

for (input <- inputs) {
val data = string2Bits(input)
val decoded = Codecs.taggedFieldCodec.decode(data).require.value
assert(decoded.isInstanceOf[UnknownTaggedField], input)
}
}

test("accept uppercase payment request") {
val input = "lntb1500n1pwxx94fpp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23sjzv0d8794te26xhexuc26eswf9sjpv4t8sma2d9y8dmpgf0qseg8259my8tcs6zte7ex0tz4exm5pjezuxrq9u0vjewa02qhedk9x4gppweupu"

Expand Down