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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,16 @@ import scala.util.Try
*/
case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) {

amount.map(a => require(a > 0.msat, s"amount is not valid"))
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")
amount.foreach(a => require(a > 0.msat, s"amount is not valid"))
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.PaymentHash => p }.get.hash

/**
*
* @return the description of the payment, or its hash
*/
lazy val description: Either[String, ByteVector32] = tags.collectFirst {
Expand All @@ -61,7 +59,6 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
}.get

/**
*
* @return the fallback address if any. It could be a script address, pubkey address, ..
*/
def fallbackAddress(): Option[String] = tags.collectFirst {
Expand All @@ -78,25 +75,25 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta
}

lazy val features: Features = tags.collectFirst { case f: Features => f }.getOrElse(Features(BitVector.empty))

def isExpired: Boolean = expiry match {
case Some(expiryTime) => timestamp + expiryTime <= Platform.currentTime.milliseconds.toSeconds
case None => timestamp + DEFAULT_EXPIRY_SECONDS <= Platform.currentTime.milliseconds.toSeconds
}

/**
*
* @return the hash of this payment request
*/
def hash: ByteVector32 = {
val hrp = s"${prefix}${Amount.encode(amount)}".getBytes("UTF-8")
val hrp = s"$prefix${Amount.encode(amount)}".getBytes("UTF-8")
val data = Bolt11Data(timestamp, tags, ByteVector.fill(65)(0)) // fake sig that we are going to strip next
val bin = Codecs.bolt11DataCodec.encode(data).require
val message = ByteVector.view(hrp) ++ bin.dropRight(520).toByteVector
Crypto.sha256(message)
}

/**
*
* @param priv private key
* @return a signed payment request
*/
Expand All @@ -120,7 +117,8 @@ object PaymentRequest {

def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], paymentHash: ByteVector32, privateKey: PrivateKey,
description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L): PaymentRequest = {
extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L,
features: Option[Features] = None): PaymentRequest = {

val prefix = prefixes(chainHash)

Expand All @@ -133,8 +131,9 @@ object PaymentRequest {
Some(PaymentHash(paymentHash)),
Some(Description(description)),
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_))
).flatten ++ extraHops.map(RoutingInfo(_)),
expirySeconds.map(Expiry(_)),
features
).flatten ++ extraHops.map(RoutingInfo),
signature = ByteVector.empty)
.sign(privateKey)
}
Expand All @@ -150,7 +149,6 @@ object PaymentRequest {
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
Expand Down Expand Up @@ -198,13 +196,11 @@ object PaymentRequest {

/**
* Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
*
*/
case class FallbackAddress(version: Byte, data: ByteVector) extends TaggedField

object FallbackAddress {
/**
*
* @param address valid base58 or bech32 address
* @return a FallbackAddressTag instance
*/
Expand Down Expand Up @@ -244,8 +240,6 @@ object PaymentRequest {
/**
* This returns a bitvector with the minimum size necessary to encode the long, left padded
* to have a length (in bits) multiples of 5
*
* @param l
*/
def long2bits(l: Long) = {
val bin = BitVector.fromLong(l)
Expand All @@ -256,7 +250,7 @@ object PaymentRequest {
val nonPadded = if (highest == -1) BitVector.empty else bin.drop(highest)
nonPadded.size % 5 match {
case 0 => nonPadded
case remaining => BitVector.fill(5 - remaining)(false) ++ nonPadded
case remaining => BitVector.fill(5 - remaining)(high = false) ++ nonPadded
}
}

Expand Down Expand Up @@ -294,7 +288,6 @@ object PaymentRequest {

/**
* Min final CLTV expiry
*
*/
case class MinFinalCltvExpiry(bin: BitVector) extends TaggedField {
def toCltvExpiryDelta = CltvExpiryDelta(bin.toInt(signed = false))
Expand All @@ -309,6 +302,17 @@ object PaymentRequest {
def apply(blocks: Long): MinFinalCltvExpiry = MinFinalCltvExpiry(long2bits(blocks))
}

/**
* Features supported or required for receiving this payment.
*/
case class Features(bitmask: BitVector) extends TaggedField

object Features {
def apply(features: Int*): Features = Features(long2bits(features.foldLeft(0) {
case (current, feature) => current + (1 << feature)
}))
}

object Codecs {

import fr.acinq.eclair.wire.CommonCodecs._
Expand All @@ -331,7 +335,7 @@ object PaymentRequest {

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
(wire: BitVector) => (limitedSizeBits(wire.size - wire.size % 8, valueCodec) ~ constant(BitVector.fill(wire.size % 8)(high = 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)
Expand All @@ -344,7 +348,7 @@ object PaymentRequest {
.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(5, dataCodec(bits).as[Features])
.typecase(6, dataCodec(bits).as[Expiry])
.typecase(7, dataCodec(bits).as[UnknownTag7])
.typecase(8, dataCodec(bits).as[UnknownTag8])
Expand Down Expand Up @@ -390,7 +394,6 @@ object PaymentRequest {
object Amount {

/**
* @param amount
* @return the unit allowing for the shortest representation possible
*/
def unit(amount: MilliSatoshi): Char = amount.toLong * 10 match { // 1 milli-satoshis == 10 pico-bitcoin
Expand Down Expand Up @@ -430,7 +433,6 @@ object PaymentRequest {
val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5))

/**
*
* @param input bech32-encoded payment request
* @return a payment request
*/
Expand All @@ -441,7 +443,7 @@ object PaymentRequest {
val separatorIndex = lowercaseInput.lastIndexOf('1')
val hrp = lowercaseInput.take(separatorIndex)
val prefix: String = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix"))
val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.size - 6)) // 6 == checksum size
val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size
val bolt11Data = Codecs.bolt11DataCodec.decode(data).require.value
val signature = ByteVector64(bolt11Data.signature.take(64))
val message: ByteVector = ByteVector.view(hrp.getBytes) ++ data.dropRight(520).toByteVector // we drop the sig bytes
Expand All @@ -462,7 +464,6 @@ object PaymentRequest {
}

/**
*
* @param pr payment request
* @return a bech32-encoded payment request
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId, _}
import org.scalatest.FunSuite
import scodec.DecodeResult
import scodec.bits._
import scodec.codecs.bits

/**
* Created by fabrice on 15/05/17.
Expand Down Expand Up @@ -52,19 +53,17 @@ class PaymentRequestSpec extends FunSuite {
}

test("check that we can still decode non-minimal amount encoding") {
assert(Some(100000000 msat) === Amount.decode("1000u"))
assert(Some(100000000 msat) === Amount.decode("1000000n"))
assert(Some(100000000 msat) === Amount.decode("1000000000p"))
assert(Amount.decode("1000u") === Some(100000000 msat))
assert(Amount.decode("1000000n") === Some(100000000 msat))
assert(Amount.decode("1000000000p") === Some(100000000 msat))
}

test("data string -> bitvector") {
import scodec.bits._
assert(string2Bits("p") === bin"00001")
assert(string2Bits("pz") === bin"0000100010")
}

test("minimal length long, left-padded to be multiple of 5") {
import scodec.bits._
assert(long2bits(0) == bin"")
assert(long2bits(1) == bin"00001")
assert(long2bits(42) == bin"0000101010")
Expand All @@ -74,13 +73,10 @@ class PaymentRequestSpec extends FunSuite {
}

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") {
Expand Down Expand Up @@ -187,7 +183,6 @@ class PaymentRequestSpec extends FunSuite {
assert(PaymentRequest.write(pr.sign(priv)) == ref)
}


test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") {
val ref = "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8"
val pr = PaymentRequest.read(ref)
Expand Down Expand Up @@ -217,6 +212,34 @@ class PaymentRequestSpec extends FunSuite {
assert(PaymentRequest.write(pr.sign(priv)) == ref)
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 1 and 9") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl"
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(MilliSatoshi(2500000000L)))
assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.timestamp === 1496314658L)
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.fallbackAddress().isEmpty)
assert(pr.features.bitmask === bin"1000000010")
assert(PaymentRequest.write(pr.sign(priv)) === ref)
}

test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 1, 9 and 100") {
val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7"
val pr = PaymentRequest.read(ref)
assert(pr.prefix === "lnbc")
assert(pr.amount === Some(MilliSatoshi(2500000000L)))
assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102")
assert(pr.timestamp === 1496314658L)
assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"))
assert(pr.description === Left("coffee beans"))
assert(pr.fallbackAddress().isEmpty)
assert(pr.features.bitmask === bin"000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000010")
assert(PaymentRequest.write(pr.sign(priv)) === ref)
}

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

Expand Down Expand Up @@ -264,7 +287,8 @@ class PaymentRequestSpec extends FunSuite {
test("Pay 1 BTC without multiplier") {
val ref = "lnbc11pdkmqhupp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2xvtsv5tc743wgctlza8k3zlpxucl7f3kvjnjptv7xz0nkaww307sdyrvgke2w8kmq7dgz4lkasfn0zvplc9aa4gp8fnhrwfjny0j59sq42x9gp"
val pr = PaymentRequest.read(ref)
assert(pr.amount.contains(100000000000L msat))
assert(pr.amount === Some(100000000000L msat))
assert(pr.features.bitmask === BitVector.empty)
}

test("nonreg") {
Expand Down Expand Up @@ -325,7 +349,9 @@ class PaymentRequestSpec extends FunSuite {
"lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p",
"lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx",
"lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9",
"lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9"
"lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9",
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl",
"lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7"
)

for (req <- requests) {
Expand Down