1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 02:27:32 +01:00

Reimplemented BOLT 11 with scodec (#856)

This commit is contained in:
Pierre-Marie Padiou 2019-02-11 19:45:56 +01:00 committed by GitHub
parent 4291bef88d
commit 884812ade0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 332 additions and 581 deletions

View File

@ -181,7 +181,7 @@
<dependency>
<groupId>org.scodec</groupId>
<artifactId>scodec-core_${scala.version.short}</artifactId>
<version>1.10.3</version>
<version>1.11.2</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>

View File

@ -1,177 +0,0 @@
/*
* Copyright 2018 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.crypto
import org.spongycastle.util.encoders.Hex
import scala.annotation.tailrec
/**
* Bit stream that can be written to and read at both ends (i.e. you can read from the end or the beginning of the stream)
*
* @param bytes bits packed as bytes, the last byte is padded with 0s
* @param offstart offset at which the first bit is in the first byte
* @param offend offset at which the last bit is in the last byte
*/
case class BitStream(bytes: Vector[Byte], offstart: Int, offend: Int) {
// offstart: 0 1 2 3 4 5 6 7
// offend: 7 6 5 4 3 2 1 0
import BitStream._
def bitCount = 8 * bytes.length - offstart - offend
def isEmpty = bitCount == 0
/**
* append a byte to a bitstream
*
* @param input byte to append
* @return an updated bitstream
*/
def writeByte(input: Byte): BitStream = offend match {
case 0 => this.copy(bytes = this.bytes :+ input)
case shift =>
val input1 = input & 0xff
val last = ((bytes.last | (input1 >>> (8 - shift))) & 0xff).toByte
val next = ((input1 << shift) & 0xff).toByte
this.copy(bytes = bytes.dropRight(1) ++ Vector(last, next))
}
/**
* append bytes to a bitstream
*
* @param input bytes to append
* @return an updated bitstream
*/
def writeBytes(input: Seq[Byte]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeByte(b) }
/**
* append a bit to a bitstream
*
* @param bit bit to append
* @return an update bitstream
*/
def writeBit(bit: Bit): BitStream = offend match {
case 0 if bit =>
BitStream(bytes :+ 0x80.toByte, offstart, 7)
case 0 =>
BitStream(bytes :+ 0x00.toByte, offstart, 7)
case n if bit =>
val last = (bytes.last + (1 << (offend - 1))).toByte
BitStream(bytes.updated(bytes.length - 1, last), offstart, offend - 1)
case n =>
BitStream(bytes, offstart, offend - 1)
}
/**
* append bits to a bitstream
*
* @param input bits to append
* @return an update bitstream
*/
def writeBits(input: Seq[Bit]): BitStream = input.foldLeft(this) { case (bs, b) => bs.writeBit(b) }
/**
* read the last bit from a bitstream
*
* @return a (stream, bit) pair where stream is an updated bitstream and bit is the last bit
*/
def popBit: (BitStream, Bit) = offend match {
case 7 => BitStream(bytes.dropRight(1), offstart, 0) -> lastBit
case n =>
val shift = n + 1
val last = (bytes.last >>> shift) << shift
BitStream(bytes.updated(bytes.length - 1, last.toByte), offstart, offend + 1) -> lastBit
}
/**
* read the last byte from a bitstream
*
* @return a (stream, byte) pair where stream is an updated bitstream and byte is the last byte
*/
def popByte: (BitStream, Byte) = offend match {
case 0 => BitStream(bytes.dropRight(1), offstart, offend) -> bytes.last
case shift =>
val a = bytes(bytes.length - 2) & 0xff
val b = bytes(bytes.length - 1) & 0xff
val byte = ((a << (8 - shift)) | (b >>> shift)) & 0xff
val a1 = (a >>> shift) << shift
BitStream(bytes.dropRight(2) :+ a1.toByte, offstart, offend) -> byte.toByte
}
def popBytes(n: Int): (BitStream, Seq[Byte]) = {
@tailrec
def loop(stream: BitStream, acc: Seq[Byte]): (BitStream, Seq[Byte]) =
if (acc.length == n) (stream, acc) else {
val (stream1, value) = stream.popByte
loop(stream1, acc :+ value)
}
loop(this, Nil)
}
/**
* read the first bit from a bitstream
*
* @return
*/
def readBit: (BitStream, Bit) = offstart match {
case 7 => BitStream(bytes.tail, 0, offend) -> firstBit
case _ => BitStream(bytes, offstart + 1, offend) -> firstBit
}
def readBits(count: Int): (BitStream, Seq[Bit]) = {
@tailrec
def loop(stream: BitStream, acc: Seq[Bit]): (BitStream, Seq[Bit]) = if (acc.length == count) (stream, acc) else {
val (stream1, bit) = stream.readBit
loop(stream1, acc :+ bit)
}
loop(this, Nil)
}
/**
* read the first byte from a bitstream
*
* @return
*/
def readByte: (BitStream, Byte) = {
val byte = ((bytes(0) << offstart) | (bytes(1) >>> (7 - offstart))) & 0xff
BitStream(bytes.tail, offstart, offend) -> byte.toByte
}
def isSet(pos: Int): Boolean = {
val pos1 = pos + offstart
(bytes(pos1 / 8) & (1 << (7 - (pos1 % 8)))) != 0
}
def firstBit = (bytes.head & (1 << (7 - offstart))) != 0
def lastBit = (bytes.last & (1 << offend)) != 0
def toBinString: String = "0b" + (for (i <- 0 until bitCount) yield if (isSet(i)) '1' else '0').mkString
def toHexString: String = "0x" + Hex.toHexString(bytes.toArray).toLowerCase
}
object BitStream {
type Bit = Boolean
val Zero = false
val One = true
val empty = BitStream(Vector.empty[Byte], 0, 0)
}

View File

@ -182,7 +182,7 @@ object PaymentLifecycle {
def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register)
// @formatter:off
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: Seq[Seq[ExtraHop]] = Nil)
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil)
/**
* @param maxFeePct set by default to 3% as a safety measure (even if a route is found, if fee is higher than that payment won't be attempted)
*/

View File

@ -17,17 +17,15 @@
package fr.acinq.eclair.payment
import java.math.BigInteger
import java.nio.ByteOrder
import fr.acinq.bitcoin.Bech32.Int5
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, _}
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.crypto.BitStream
import fr.acinq.eclair.crypto.BitStream.Bit
import fr.acinq.eclair.payment.PaymentRequest.{Amount, ExtraHop, RoutingInfoTag, Timestamp}
import fr.acinq.eclair.payment.PaymentRequest._
import scodec.Codec
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte}
import scala.annotation.tailrec
import scala.util.Try
/**
@ -41,25 +39,25 @@ import scala.util.Try
* @param tags payment tags; must include a single PaymentHash tag
* @param signature request signature that will be checked against node id
*/
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.Tag], signature: BinaryData) {
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: BinaryData) {
amount.map(a => require(a.amount > 0 && a.amount <= PaymentRequest.MAX_AMOUNT.amount, s"amount is not valid"))
require(tags.collect { case _: PaymentRequest.PaymentHashTag => {} }.size == 1, "there must be exactly one payment hash tag")
require(tags.collect { case PaymentRequest.DescriptionTag(_) | PaymentRequest.DescriptionHashTag(_) => {} }.size == 1, "there must be exactly one description tag or one description hash tag")
require(tags.collect { case _: PaymentRequest.PaymentHash => {} }.size == 1, "there must be exactly one payment hash tag")
require(tags.collect { case PaymentRequest.Description(_) | PaymentRequest.DescriptionHash(_) => {} }.size == 1, "there must be exactly one description tag or one description hash tag")
/**
*
* @return the payment hash
*/
lazy val paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHashTag => p }.get.hash
lazy val paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHash => p }.get.hash
/**
*
* @return the description of the payment, or its hash
*/
lazy val description: Either[String, BinaryData] = tags.collectFirst {
case PaymentRequest.DescriptionTag(d) => Left(d)
case PaymentRequest.DescriptionHashTag(h) => Right(h)
case PaymentRequest.Description(d) => Left(d)
case PaymentRequest.DescriptionHash(h) => Right(h)
}.get
/**
@ -67,40 +65,30 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
* @return the fallback address if any. It could be a script address, pubkey address, ..
*/
def fallbackAddress(): Option[String] = tags.collectFirst {
case PaymentRequest.FallbackAddressTag(17, hash) if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, hash)
case PaymentRequest.FallbackAddressTag(18, hash) if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, hash)
case PaymentRequest.FallbackAddressTag(17, hash) if prefix == "lntb" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, hash)
case PaymentRequest.FallbackAddressTag(18, hash) if prefix == "lntb" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash)
case PaymentRequest.FallbackAddressTag(version, hash) if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, hash)
case PaymentRequest.FallbackAddressTag(version, hash) if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, hash)
case f: PaymentRequest.FallbackAddress => PaymentRequest.FallbackAddress.toAddress(f, prefix)
}
lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfoTag => t.path }
lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path }
lazy val expiry: Option[Long] = tags.collectFirst {
case PaymentRequest.ExpiryTag(seconds) => seconds
case expiry: PaymentRequest.Expiry => expiry.toLong
}
lazy val minFinalCltvExpiry: Option[Long] = tags.collectFirst {
case PaymentRequest.MinFinalCltvExpiryTag(expiry) => expiry
}
/**
*
* @return a representation of this payment request, without its signature, as a bit stream. This is what will be signed.
*/
def stream: BitStream = {
val stream = BitStream.empty
val int5s = Timestamp.encode(timestamp) ++ (tags.map(_.toInt5s).flatten)
val stream1 = int5s.foldLeft(stream)(PaymentRequest.write5)
stream1
case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toLong
}
/**
*
* @return the hash of this payment request
*/
def hash: BinaryData = Crypto.sha256(s"${prefix}${Amount.encode(amount)}".getBytes("UTF-8") ++ stream.bytes)
def hash: BinaryData = {
val hrp = s"${prefix}${Amount.encode(amount)}".getBytes("UTF-8")
val data = Bolt11Data(timestamp, tags, "00" * 65) // fake sig that we are going to strip next
val bin = Codecs.bolt11DataCodec.encode(data).require
val message: BinaryData = hrp ++ bin.dropRight(520).toByteArray
Crypto.sha256(message)
}
/**
*
@ -111,7 +99,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
val (r, s) = Crypto.sign(hash, priv)
val (pub1, pub2) = Crypto.recoverPublicKey((r, s), hash)
val recid = if (nodeId == pub1) 0.toByte else 1.toByte
val signature = PaymentRequest.Signature.encode(r, s, recid)
val signature = Crypto.fixSize(r.toByteArray.dropWhile(_ == 0.toByte)) ++ Crypto.fixSize(s.toByteArray.dropWhile(_ == 0.toByte)) :+ recid
this.copy(signature = signature)
}
}
@ -128,7 +116,7 @@ object PaymentRequest {
def apply(chainHash: BinaryData, amount: Option[MilliSatoshi], paymentHash: BinaryData, privateKey: PrivateKey,
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
extraHops: Seq[Seq[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = {
extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = {
val prefix = prefixes(chainHash)
@ -138,97 +126,129 @@ object PaymentRequest {
timestamp = timestamp,
nodeId = privateKey.publicKey,
tags = List(
Some(PaymentHashTag(paymentHash)),
Some(DescriptionTag(description)),
fallbackAddress.map(FallbackAddressTag(_)),
expirySeconds.map(ExpiryTag)
).flatten ++ extraHops.map(RoutingInfoTag(_)),
Some(PaymentHash(paymentHash)),
Some(Description(description)),
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_))
).flatten ++ extraHops.map(RoutingInfo(_)),
signature = BinaryData.empty)
.sign(privateKey)
}
sealed trait Tag {
def toInt5s: Seq[Int5]
}
case class Bolt11Data(timestamp: Long, taggedFields: List[TaggedField], signature: BinaryData)
sealed trait TaggedField
sealed trait UnknownTaggedField extends TaggedField
// @formatter:off
case class UnknownTag0(data: BitVector) extends UnknownTaggedField
case class UnknownTag1(data: BitVector) extends UnknownTaggedField
case class UnknownTag2(data: BitVector) extends UnknownTaggedField
case class UnknownTag4(data: BitVector) extends UnknownTaggedField
case class UnknownTag5(data: BitVector) extends UnknownTaggedField
case class UnknownTag7(data: BitVector) extends UnknownTaggedField
case class UnknownTag8(data: BitVector) extends UnknownTaggedField
case class UnknownTag10(data: BitVector) extends UnknownTaggedField
case class UnknownTag11(data: BitVector) extends UnknownTaggedField
case class UnknownTag12(data: BitVector) extends UnknownTaggedField
case class UnknownTag14(data: BitVector) extends UnknownTaggedField
case class UnknownTag15(data: BitVector) extends UnknownTaggedField
case class UnknownTag16(data: BitVector) extends UnknownTaggedField
case class UnknownTag17(data: BitVector) extends UnknownTaggedField
case class UnknownTag18(data: BitVector) extends UnknownTaggedField
case class UnknownTag19(data: BitVector) extends UnknownTaggedField
case class UnknownTag20(data: BitVector) extends UnknownTaggedField
case class UnknownTag21(data: BitVector) extends UnknownTaggedField
case class UnknownTag22(data: BitVector) extends UnknownTaggedField
case class UnknownTag25(data: BitVector) extends UnknownTaggedField
case class UnknownTag26(data: BitVector) extends UnknownTaggedField
case class UnknownTag27(data: BitVector) extends UnknownTaggedField
case class UnknownTag28(data: BitVector) extends UnknownTaggedField
case class UnknownTag29(data: BitVector) extends UnknownTaggedField
case class UnknownTag30(data: BitVector) extends UnknownTaggedField
case class UnknownTag31(data: BitVector) extends UnknownTaggedField
// @formatter:on
/**
* Payment Hash Tag
* Payment Hash
*
* @param hash payment hash
*/
case class PaymentHashTag(hash: BinaryData) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(hash)
Seq(Bech32.map('p'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
case class PaymentHash(hash: BinaryData) extends TaggedField
/**
* Description Tag
* Description
*
* @param description a free-format string that will be included in the payment request
*/
case class DescriptionTag(description: String) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(description.getBytes("UTF-8"))
Seq(Bech32.map('d'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
case class Description(description: String) extends TaggedField
/**
* Hash Tag
* 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 DescriptionHashTag(hash: BinaryData) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(hash)
Seq(Bech32.map('h'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
case class DescriptionHash(hash: BinaryData) extends TaggedField
/**
* Fallback Payment Tag that specifies a fallback payment address to be used if LN payment cannot be processed
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
*
* @param version address version; valid values are
* - 17 (pubkey hash)
* - 18 (script hash)
* - 0 (segwit hash: p2wpkh (20 bytes) or p2wsh (32 bytes))
* @param hash address hash
*/
case class FallbackAddressTag(version: Byte, hash: BinaryData) extends Tag {
override def toInt5s = {
val ints = version +: Bech32.eight2five(hash)
Seq(Bech32.map('f'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
case class FallbackAddress(version: Byte, data: ByteVector) extends TaggedField
object FallbackAddressTag {
object FallbackAddress {
/**
*
* @param address valid base58 or bech32 address
* @return a FallbackAddressTag instance
*/
def apply(address: String): FallbackAddressTag = {
def apply(address: String): FallbackAddress = {
Try(fromBase58Address(address)).orElse(Try(fromBech32Address(address))).get
}
def fromBase58Address(address: String): FallbackAddressTag = {
def apply(version: Byte, data: BinaryData): FallbackAddress = FallbackAddress(version, ByteVector(data.toArray))
def fromBase58Address(address: String): FallbackAddress = {
val (prefix, hash) = Base58Check.decode(address)
prefix match {
case Base58.Prefix.PubkeyAddress => FallbackAddressTag(17, hash)
case Base58.Prefix.PubkeyAddressTestnet => FallbackAddressTag(17, hash)
case Base58.Prefix.ScriptAddress => FallbackAddressTag(18, hash)
case Base58.Prefix.ScriptAddressTestnet => FallbackAddressTag(18, hash)
case Base58.Prefix.PubkeyAddress => FallbackAddress(17.toByte, hash)
case Base58.Prefix.PubkeyAddressTestnet => FallbackAddress(17.toByte, hash)
case Base58.Prefix.ScriptAddress => FallbackAddress(18.toByte, hash)
case Base58.Prefix.ScriptAddressTestnet => FallbackAddress(18.toByte, hash)
}
}
def fromBech32Address(address: String): FallbackAddressTag = {
def fromBech32Address(address: String): FallbackAddress = {
val (_, version, hash) = Bech32.decodeWitnessAddress(address)
FallbackAddressTag(version, hash)
FallbackAddress(version, hash)
}
def toAddress(f: FallbackAddress, prefix: String): String = {
val data = BinaryData(f.data.toArray)
f.version match {
case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data)
case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data)
case 17 if prefix == "lntb" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data)
case 18 if prefix == "lntb" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data)
case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data)
case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data)
}
}
}
/**
* This returns a bitvector with the minimum size necessary to encode the long
* @param l
*/
def long2bits(l: Long) = {
val bin = BitVector.fromLong(l)
var highest = -1
for (i <- 0 until bin.size.toInt) {
if (highest == -1 && bin(i)) highest = i
}
if (highest == -1) BitVector.empty else bin.drop(highest)
}
/**
@ -240,65 +260,122 @@ object PaymentRequest {
* @param feeProportionalMillionths node proportional fee
* @param cltvExpiryDelta node cltv expiry delta
*/
case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBaseMsat: Long, feeProportionalMillionths: Long, cltvExpiryDelta: Int) {
def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId.toLong, ByteOrder.BIG_ENDIAN) ++
Protocol.writeUInt32(feeBaseMsat, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt32(feeProportionalMillionths, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
}
case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBaseMsat: Long, feeProportionalMillionths: Long, cltvExpiryDelta: Int)
/**
* Routing Info Tag
* Routing Info
*
* @param path one or more entries containing extra routing information for a private route
*/
case class RoutingInfoTag(path: Seq[ExtraHop]) extends Tag {
override def toInt5s = {
val ints = Bech32.eight2five(path.flatMap(_.pack))
Seq(Bech32.map('r'), (ints.length / 32).toByte, (ints.length % 32).toByte) ++ ints
}
}
object RoutingInfoTag {
def parse(data: Seq[Byte]) = {
val pubkey = data.slice(0, 33)
val shortChannelId = Protocol.uint64(data.slice(33, 33 + 8), ByteOrder.BIG_ENDIAN)
val fee_base_msat = Protocol.uint32(data.slice(33 + 8, 33 + 8 + 4), ByteOrder.BIG_ENDIAN)
val fee_proportional_millionths = Protocol.uint32(data.slice(33 + 8 + 4, 33 + 8 + 8), ByteOrder.BIG_ENDIAN)
val cltv = Protocol.uint16(data.slice(33 + 8 + 8, chunkLength), ByteOrder.BIG_ENDIAN)
ExtraHop(PublicKey(pubkey), ShortChannelId(shortChannelId), fee_base_msat, fee_proportional_millionths, cltv)
}
def parseAll(data: Seq[Byte]): Seq[ExtraHop] =
data.grouped(chunkLength).map(parse).toList
val chunkLength: Int = 33 + 8 + 4 + 4 + 2
}
case class RoutingInfo(path: List[ExtraHop]) extends TaggedField
/**
* Expiry Date
*
*/
case class Expiry(bin: BitVector) extends TaggedField {
def toLong: Long = bin.toLong(signed = false)
}
object Expiry {
/**
* @param seconds expiry data for this payment request
*/
case class ExpiryTag(seconds: Long) extends Tag {
override def toInt5s = {
val ints = writeUnsignedLong(seconds)
Bech32.map('x') +: (writeSize(ints.size) ++ ints)
}
def apply(seconds: Long): Expiry = Expiry(long2bits(seconds))
}
/**
* Min final CLTV expiry
*
* @param blocks min final cltv expiry, in blocks
*/
case class MinFinalCltvExpiryTag(blocks: Long) extends Tag {
override def toInt5s = {
val ints = writeUnsignedLong(blocks)
Bech32.map('c') +: (writeSize(ints.size) ++ ints)
}
case class MinFinalCltvExpiry(bin: BitVector) extends TaggedField {
def toLong: Long = bin.toLong(signed = false)
}
case class UnknownTag(tag: Int5, int5s: Seq[Int5]) extends Tag {
override def toInt5s = tag +: (writeSize(int5s.size) ++ int5s)
object MinFinalCltvExpiry {
/**
* Min final CLTV expiry
*
* @param blocks min final cltv expiry, in blocks
*/
def apply(blocks: Long): MinFinalCltvExpiry = MinFinalCltvExpiry(long2bits(blocks))
}
object Codecs {
import fr.acinq.eclair.wire.LightningMessageCodecs._
import scodec.bits.BitVector
import scodec.codecs._
import scodec.{Attempt, Codec, DecodeResult}
val extraHopCodec: Codec[ExtraHop] = (
("nodeId" | publicKey) ::
("shortChannelId" | shortchannelid) ::
("fee_base_msat" | uint32) ::
("fee_proportional_millionth" | uint32) ::
("cltv_expiry_delta" | uint16)
).as[ExtraHop]
val extraHopsLengthCodec = Codec[Int](
(_: Int) => Attempt.successful(BitVector.empty), // we don't encode the length
(wire: BitVector) => Attempt.successful(DecodeResult(wire.size.toInt / 408, wire)) // we infer the number of items by the size of the data
)
def alignedBytesCodec[A](valueCodec: Codec[A]): Codec[A] = Codec[A](
(value: A) => valueCodec.encode(value),
(wire: BitVector) => (limitedSizeBits(wire.size - wire.size % 8, valueCodec) ~ constant(BitVector.fill(wire.size % 8)(false))).map(_._1).decode(wire) // the 'constant' codec ensures that padding is zero
)
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)
val taggedFieldCodec: Codec[TaggedField] = discriminated[TaggedField].by(ubyte(5))
.typecase(0, dataCodec(bits).as[UnknownTag0])
.typecase(1, dataCodec(binarydata(32)).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[UnknownTag5])
.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(bits).as[UnknownTag16])
.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(binarydata(32)).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])
def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A](
(data: A) => codec.encode(data),
(wire: BitVector) => {
val (head, tail) = wire.splitAt(wire.size - size)
codec.decode(head).map(result => result.copy(remainder = tail))
}
)
val bolt11DataCodec: Codec[Bolt11Data] = (
("timestamp" | ulong(35)) ::
("taggedFields" | fixedSizeTrailingCodec(list(taggedFieldCodec), 520)) ::
("signature" | binarydata(65))
).as[Bolt11Data]
}
object Amount {
@ -334,152 +411,13 @@ object PaymentRequest {
}
}
object Tag {
def parse(input: Seq[Int5]): Tag = {
val tag = input(0)
val len = input(1) * 32 + input(2)
tag match {
case p if p == Bech32.map('p') =>
val hash = Bech32.five2eight(input.drop(3).take(52))
PaymentHashTag(hash)
case d if d == Bech32.map('d') =>
val description = new String(Bech32.five2eight(input.drop(3).take(len)).toArray, "UTF-8")
DescriptionTag(description)
case h if h == Bech32.map('h') =>
val hash: BinaryData = Bech32.five2eight(input.drop(3).take(len))
DescriptionHashTag(hash)
case f if f == Bech32.map('f') =>
val version = input(3)
val prog = Bech32.five2eight(input.drop(4).take(len - 1))
version match {
case v if v >= 0 && v <= 16 =>
FallbackAddressTag(version, prog)
case 17 | 18 =>
FallbackAddressTag(version, prog)
}
case r if r == Bech32.map('r') =>
val data = Bech32.five2eight(input.drop(3).take(len))
val path = RoutingInfoTag.parseAll(data)
RoutingInfoTag(path)
case x if x == Bech32.map('x') =>
val expiry = readUnsignedLong(len, input.drop(3).take(len))
ExpiryTag(expiry)
case c if c == Bech32.map('c') =>
val expiry = readUnsignedLong(len, input.drop(3).take(len))
MinFinalCltvExpiryTag(expiry)
case _ =>
UnknownTag(tag, input.drop(3).take(len))
}
}
}
// char -> 5 bits value
val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian))
object Timestamp {
def decode(data: Seq[Int5]): Long = data.take(7).foldLeft(0L)((a, b) => a * 32 + b)
// TODO: could be optimized by preallocating the resulting buffer
def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)
def encode(timestamp: Long, acc: Seq[Int5] = Nil): Seq[Int5] = if (acc.length == 7) acc else {
encode(timestamp / 32, (timestamp % 32).toByte +: acc)
}
}
object Signature {
/**
*
* @param signature 65-bytes signature: r (32 bytes) | s (32 bytes) | recid (1 bytes)
* @return a (r, s, recoveryId)
*/
def decode(signature: BinaryData): (BigInteger, BigInteger, Byte) = {
require(signature.length == 65)
val r = new BigInteger(1, signature.take(32).toArray)
val s = new BigInteger(1, signature.drop(32).take(32).toArray)
val recid = signature.last
(r, s, recid)
}
/**
*
* @return a 65 bytes representation of (r, s, recid)
*/
def encode(r: BigInteger, s: BigInteger, recid: Byte): BinaryData = {
Crypto.fixSize(r.toByteArray.dropWhile(_ == 0.toByte)) ++ Crypto.fixSize(s.toByteArray.dropWhile(_ == 0.toByte)) :+ recid
}
}
def toBits(value: Int5): Seq[Bit] = Seq((value & 16) != 0, (value & 8) != 0, (value & 4) != 0, (value & 2) != 0, (value & 1) != 0)
/**
* write a 5bits integer to a stream
*
* @param stream stream to write to
* @param value a 5bits value
* @return an updated stream
*/
def write5(stream: BitStream, value: Int5): BitStream = stream.writeBits(toBits(value))
/**
* read a 5bits value from a stream
*
* @param stream stream to read from
* @return a (stream, value) pair
*/
def read5(stream: BitStream): (BitStream, Int5) = {
val (stream1, bits) = stream.readBits(5)
val value = (if (bits(0)) 1 << 4 else 0) + (if (bits(1)) 1 << 3 else 0) + (if (bits(2)) 1 << 2 else 0) + (if (bits(3)) 1 << 1 else 0) + (if (bits(4)) 1 << 0 else 0)
(stream1, (value & 0xff).toByte)
}
/**
* splits a bit stream into 5bits values
*
* @param stream
* @param acc
* @return a sequence of 5bits values
*/
@tailrec
def toInt5s(stream: BitStream, acc: Seq[Int5] = Nil): Seq[Int5] = if (stream.bitCount == 0) acc else {
val (stream1, value) = read5(stream)
toInt5s(stream1, acc :+ value)
}
/**
* prepend an unsigned long value to a sequence of Int5s
*
* @param value input value
* @param acc sequence of Int5 values
* @return an update sequence of Int5s
*/
@tailrec
def writeUnsignedLong(value: Long, acc: Seq[Int5] = Nil): Seq[Int5] = {
require(value >= 0)
if (value == 0) acc
else writeUnsignedLong(value / 32, (value % 32).toByte +: acc)
}
/**
* convert a tag data size to a sequence of Int5s. It * must * fit on a sequence
* of 2 Int5 values
*
* @param size data size
* @return size as a sequence of exactly 2 Int5 values
*/
def writeSize(size: Long): Seq[Int5] = {
val output = writeUnsignedLong(size)
// make sure that size is encoded on 2 int5 values
output.length match {
case 0 => Seq(0.toByte, 0.toByte)
case 1 => 0.toByte +: output
case 2 => output
case n => throw new IllegalArgumentException("tag data length field must be encoded on 2 5-bits integers")
}
}
/**
* reads an unsigned long value from a sequence of Int5s
*
* @param length length of the sequence
* @param ints sequence of Int5s
* @return an unsigned long value
*/
def readUnsignedLong(length: Int, ints: Seq[Int5]): Long = ints.take(length).foldLeft(0L) { case (acc, i) => acc * 32 + i }
val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5))
/**
*
@ -487,37 +425,31 @@ object PaymentRequest {
* @return a payment request
*/
def read(input: String): PaymentRequest = {
val (hrp, data) = Bech32.decode(input)
val stream = data.foldLeft(BitStream.empty)(write5)
require(stream.bitCount >= 65 * 8, "data is too short to contain a 65 bytes signature")
val (stream1, sig) = stream.popBytes(65)
val data0 = toInt5s(stream1)
val timestamp = Timestamp.decode(data0)
val data1 = data0.drop(7)
@tailrec
def loop(data: Seq[Int5], tags: Seq[Seq[Int5]] = Nil): Seq[Seq[Int5]] = if (data.isEmpty) tags else {
// 104 is the size of a signature
val len = 1 + 2 + 32 * data(1) + data(2)
loop(data.drop(len), tags :+ data.take(len))
}
val rawtags = loop(data1)
val tags = rawtags.map(Tag.parse)
val signature = sig.reverse
// used only for data validation
Bech32.decode(input)
val separatorIndex = input.lastIndexOf('1')
val hrp = input.take(separatorIndex)
val prefix: String = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix"))
val data = string2Bits(input.slice(separatorIndex + 1, input.size - 6)) // 6 == checksum size
val bolt11Data = Codecs.bolt11DataCodec.decode(data).require.value
val signature = bolt11Data.signature
val r = new BigInteger(1, signature.take(32).toArray)
val s = new BigInteger(1, signature.drop(32).take(32).toArray)
val recid = signature.last
val message: BinaryData = hrp.getBytes ++ stream1.bytes
val message: BinaryData = hrp.getBytes ++ data.dropRight(520).toByteArray // we drop the sig bytes
val (pub1, pub2) = Crypto.recoverPublicKey((r, s), Crypto.sha256(message))
val recid = signature.last
val pub = if (recid % 2 != 0) pub2 else pub1
val prefix = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix"))
val amount_opt = Amount.decode(hrp.drop(prefix.length))
val pr = PaymentRequest(prefix, amount_opt, timestamp, pub, tags.toList, signature)
val validSig = Crypto.verifySignature(Crypto.sha256(message), (r, s), pub)
require(validSig, "invalid signature")
pr
PaymentRequest(
prefix = prefix,
amount = amount_opt,
timestamp = bolt11Data.timestamp,
nodeId = pub,
tags = bolt11Data.taggedFields,
signature = signature
)
}
/**
@ -529,9 +461,10 @@ object PaymentRequest {
// currency unit is Satoshi, but we compute amounts in Millisatoshis
val hramount = Amount.encode(pr.amount)
val hrp = s"${pr.prefix}$hramount"
val stream = pr.stream.writeBytes(pr.signature)
val checksum = Bech32.checksum(hrp, toInt5s(stream))
hrp + "1" + new String((toInt5s(stream) ++ checksum).map(i => Bech32.pam(i)).toArray)
val data = Codecs.bolt11DataCodec.encode(Bolt11Data(pr.timestamp, pr.tags, pr.signature)).require
val int5s = eight2fiveCodec.decode(data).require.value
val checksum = Bech32.checksum(hrp, int5s)
hrp + "1" + (int5s ++ checksum).map(Bech32.pam).mkString
}
}

View File

@ -1,89 +0,0 @@
/*
* Copyright 2018 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.crypto
import org.scalatest.FunSuite
import org.spongycastle.util.encoders.Hex
/**
* Created by fabrice on 22/06/17.
*/
class BitStreamSpec extends FunSuite {
import BitStream._
test("add bits") {
val bits = BitStream.empty
val bits1 = bits.writeBit(One)
assert(bits1.bitCount == 1)
assert(bits1.isSet(0))
assert(Hex.toHexString(bits1.bytes.toArray) == "80")
val bits2 = bits1.writeBit(Zero)
assert(bits2.bitCount == 2)
assert(bits2.isSet(0))
assert(!bits2.isSet(1))
assert(Hex.toHexString(bits2.bytes.toArray) == "80")
val bits3 = bits2.writeBit(One)
assert(bits3.bitCount == 3)
assert(bits3.isSet(0))
assert(!bits3.isSet(1))
assert(bits3.isSet(2))
assert(bits3.toHexString == "0xa0")
assert(bits3.toBinString == "0b101")
val (bits4, One) = bits3.popBit
assert(bits4 == bits2)
val (bits5, Zero) = bits4.popBit
assert(bits5 == bits1)
val (bits6, One) = bits5.popBit
assert(bits6 == bits)
val (bits7, One) = bits3.readBit
val (bits8, Zero) = bits7.readBit
val (bits9, One) = bits8.readBit
assert(bits9.isEmpty)
}
test("add bytes") {
val bits = BitStream.empty
val bits1 = bits.writeByte(0xb5.toByte)
assert(bits1.bitCount == 8)
assert(bits1.toHexString == "0xb5")
assert(bits1.toBinString == "0b10110101")
// b5 = 1100 0101
val bits2 = bits1.writeBit(Zero)
assert(bits2.bitCount == 9)
// 1100 0101 0
assert(bits2.toHexString == "0xb500")
assert(bits2.toBinString == "0b101101010")
val bits3 = bits2.writeBit(One)
assert(bits3.bitCount == 10)
// 1100 0101 01
assert(bits3.toHexString == "0xb540")
assert(bits3.toBinString == "0b1011010101")
// 1011 0101 01xx xxxx
// 10xx xxxx and 1101 0101
val (bits4, check) = bits3.popByte
assert(check == 0xd5.toByte)
assert(bits4.toBinString == "0b10")
val (bits5, check5) = bits3.readByte
assert(check5 == 0xb5.toByte)
}
}

View File

@ -187,7 +187,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
val route_x_z = extraHop_x_y :: extraHop_y_z :: Nil
val route_x_t = extraHop_x_t :: Nil
sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee with additional routing info", extraHops = Seq(route_x_z, route_x_t)))
sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee with additional routing info", extraHops = List(route_x_z, route_x_t)))
assert(sender.expectMsgType[PaymentRequest].routingInfo === Seq(route_x_z, route_x_t))
sender.send(handler, ReceivePayment(Some(MilliSatoshi(42000)), "1 coffee without routing info"))

View File

@ -19,10 +19,12 @@ package fr.acinq.eclair.payment
import java.nio.ByteOrder
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Bech32, BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
import fr.acinq.bitcoin.{BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.payment.PaymentRequest._
import org.scalatest.FunSuite
import scodec.DecodeResult
import scodec.bits.BitVector
/**
* Created by fabrice on 15/05/17.
@ -55,6 +57,32 @@ class PaymentRequestSpec extends FunSuite {
assert(Some(MilliSatoshi(100000000)) == Amount.decode("1000000000p"))
}
test("data string -> bitvector") {
import scodec.bits._
assert(string2Bits("p") === bin"00001")
assert(string2Bits("pz") === bin"0000100010")
}
test("minimal length long") {
import scodec.bits._
assert(long2bits(0) == bin"")
assert(long2bits(1) == bin"1")
assert(long2bits(42) == bin"101010")
assert(long2bits(255) == bin"11111111")
assert(long2bits(256) == bin"100000000")
assert(long2bits(3600) == bin"111000010000")
}
test("verify that padding is zero") {
import scodec.bits._
import scodec.codecs._
val codec = PaymentRequest.Codecs.alignedBytesCodec(bits)
assert(codec.decode(bin"1010101000").require == DecodeResult(bin"10101010", BitVector.empty))
assert(codec.decode(bin"1010101001").isFailure) // non-zero padding
}
test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") {
val ref = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w"
val pr = PaymentRequest.read(ref)
@ -200,28 +228,84 @@ class PaymentRequestSpec extends FunSuite {
}
test("ignore unknown tags") {
// create a new tag that we don't know about
class MyExpiryTag(override val seconds: Long) extends ExpiryTag(seconds) {
// replace the tag with 'j' which is not used yet
override def toInt5s = super.toInt5s.updated(0, Bech32.map('j'))
}
val pr = PaymentRequest(
prefix = "lntb",
amount = Some(MilliSatoshi(100000L)),
timestamp = System.currentTimeMillis() / 1000L,
nodeId = nodeId,
tags = List(
PaymentHashTag(BinaryData("01" * 32)),
DescriptionTag("description"),
new MyExpiryTag(42L)
PaymentHash(BinaryData("01" * 32)),
Description("description"),
UnknownTag21(BitVector("some data we don't understand".getBytes))
),
signature = BinaryData.empty).sign(priv)
val serialized = PaymentRequest write pr
val pr1 = PaymentRequest read serialized
val Some(unknownTag) = pr1.tags.collectFirst { case u: UnknownTag => u }
assert(unknownTag.tag == Bech32.map('j'))
assert(unknownTag.toInt5s == (new MyExpiryTag(42L)).toInt5s)
val Some(unknownTag) = pr1.tags.collectFirst { case u: UnknownTag21 => u }
}
test("nonreg") {
val requests = List(
"lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl",
"lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd",
"lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex",
"lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque",
"lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0",
"lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54",
"lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe",
"lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf",
"lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc",
"lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3",
"lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw",
"lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28",
"lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah",
"lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv",
"lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d",
"lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9",
"lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3",
"lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t",
"lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee",
"lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6",
"lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h",
"lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv",
"lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww",
"lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr",
"lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad",
"lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw",
"lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e",
"lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr",
"lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u",
"lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j",
"lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5",
"lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda",
"lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu",
"lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu",
"lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733",
"lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh",
"lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7",
"lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g",
"lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v",
"lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0",
"lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838",
"lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx",
"lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer",
"lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr",
"lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0",
"lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x",
"lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra",
"lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r",
"lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w",
"lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y",
"lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53",
"lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz",
"lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua",
"lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p",
"lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx",
"lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9",
"lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9"
)
for (req <- requests) { assert(PaymentRequest.write(PaymentRequest.read(req)) == req) }
}
}