mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-19 18:10:42 +01:00
Reimplemented BOLT 11 with scodec (#856)
This commit is contained in:
parent
4291bef88d
commit
884812ade0
@ -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>
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
*/
|
||||
|
@ -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
|
||||
*
|
||||
* @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)
|
||||
}
|
||||
case class Expiry(bin: BitVector) extends TaggedField {
|
||||
def toLong: Long = bin.toLong(signed = false)
|
||||
}
|
||||
|
||||
object Expiry {
|
||||
/**
|
||||
* @param seconds expiry data for this payment request
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"))
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user