1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

Add an optional 'minimum htlc expiry' tag (#202)

This commit is contained in:
Fabrice Drouin 2017-11-06 10:56:07 -08:00 committed by Pierre-Marie Padiou
parent 3be40a1fab
commit e17335931b
5 changed files with 66 additions and 20 deletions

View file

@ -13,7 +13,7 @@ import scodec.Attempt
// @formatter:off
case class ReceivePayment(amountMsat: MilliSatoshi, description: String)
case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, maxAttempts: Int = 5)
case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, minFinalCltvExpiry: Long = PaymentLifecycle.defaultMinFinalCltvExpiry, maxAttempts: Int = 5)
sealed trait PaymentResult
case class PaymentSucceeded(route: Seq[Hop], paymentPreimage: BinaryData) extends PaymentResult
@ -54,7 +54,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto
case Event(RouteResponse(hops, ignoreNodes, ignoreChannels), WaitingForRoute(s, c, failures)) =>
log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId.toHexString).mkString("->")}")
val firstHop = hops.head
val finalExpiry = Globals.blockCount.get().toInt + defaultHtlcExpiry
val finalExpiry = Globals.blockCount.get().toInt + c.minFinalCltvExpiry.toInt
val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops)
// TODO: HACK!!!! see Router.scala (we actually store the first node id in the sig)
if (firstHop.lastUpdate.signature.size == 32) {
@ -198,8 +198,8 @@ object PaymentLifecycle {
(msat + feeMsat, expiry + expiryDelta, PerHopPayload(hop.lastUpdate.shortChannelId, msat, expiry) +: payloads)
}
// TODO: set correct initial expiry
val defaultHtlcExpiry = 10
// this is defined in BOLT 11
val defaultMinFinalCltvExpiry = 9
def buildCommand(finalAmountMsat: Long, finalExpiry: Int, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = {
val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1))

View file

@ -60,6 +60,14 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam
def routingInfo(): Seq[RoutingInfoTag] = tags.collect { case t: RoutingInfoTag => t }
def expiry: Option[Long] = tags.collectFirst {
case PaymentRequest.ExpiryTag(seconds) => seconds
}
def 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.
@ -242,20 +250,24 @@ object PaymentRequest {
/**
* Expiry Date
*
* @param seconds expriry data for this payment request
* @param seconds expiry data for this payment request
*/
case class ExpiryTag(seconds: Long) extends Tag {
override def toInt5s = {
val ints = writeUnsignedLong(seconds)
val size = writeUnsignedLong(ints.size)
// make sure that size is encoded on 2 int5 values
val size1 = size.length match {
case 0 => Seq(0.toByte, 0. toByte)
case 1 => 0.toByte +: size
case 2 => size
case n => throw new IllegalArgumentException("tag data length field must be encoded on 2 5-bits integers")
}
Bech32.map('x') +: (size1 ++ ints)
Bech32.map('x') +: (writeSize(ints.size) ++ ints) }
}
/**
* 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)
}
}
@ -322,6 +334,9 @@ object PaymentRequest {
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)
}
}
}
@ -406,9 +421,26 @@ object PaymentRequest {
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 seauence
* @param length length of the sequence
* @param ints sequence of Int5s
* @return an unsigned long value
*/

View file

@ -75,7 +75,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
def buildCmdAdd(paymentHash: BinaryData, dest: PublicKey) = {
// allow overpaying (no more than 2 times the required amount)
val amount = requiredAmount + Random.nextInt(requiredAmount)
val expiry = Globals.blockCount.get().toInt + PaymentLifecycle.defaultHtlcExpiry
val expiry = Globals.blockCount.get().toInt + PaymentLifecycle.defaultMinFinalCltvExpiry
PaymentLifecycle.buildCommand(amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1
}

View file

@ -149,11 +149,11 @@ object HtlcGenerationSpec {
val finalAmountMsat = 42000000L
val currentBlockCount = 420000
val finalExpiry = currentBlockCount + defaultHtlcExpiry
val finalExpiry = currentBlockCount + defaultMinFinalCltvExpiry
val paymentPreimage = BinaryData("42" * 32)
val paymentHash = Crypto.sha256(paymentPreimage)
val expiry_de = currentBlockCount + defaultHtlcExpiry
val expiry_de = currentBlockCount + defaultMinFinalCltvExpiry
val amount_de = finalAmountMsat
val fee_d = nodeFee(channelUpdate_de.feeBaseMsat, channelUpdate_de.feeProportionalMillionths, amount_de)

View file

@ -157,13 +157,27 @@ class PaymentRequestSpec extends FunSuite {
assert(PaymentRequest.write(pr.sign(priv)) == ref)
}
test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") {
val ref = "lnbc20m1pvjluezcqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q90qkf3gd7fcqs0ewr7t3xf72ptmc4n38evg0xhy4p64nlg7hgrmq6g997tkrvezs8afs0x0y8v4vs8thwsk6knkvdfvfa7wmhhpcsxcqw0ny48"
val pr = PaymentRequest.read(ref)
assert(pr.prefix == "lnbc")
assert(pr.amount == Some(MilliSatoshi(2000000000L)))
assert(pr.paymentHash == BinaryData("0001020304050607080900010203040506070809000102030405060708090102"))
assert(pr.timestamp == 1496314658L)
assert(pr.nodeId == PublicKey(BinaryData("03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")))
assert(pr.description == Right(Crypto.sha256("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))
assert(pr.fallbackAddress === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"))
assert(pr.minFinalCltvExpiry === Some(12))
assert(pr.tags.size == 4)
assert(PaymentRequest.write(pr.sign(priv)) == ref)
}
test("expiry is a variable-length unsigned value") {
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(MilliSatoshi(100000L)), BinaryData("0001020304050607080900010203040506070809000102030405060708090102"),
priv, "test", fallbackAddress = None, expirySeconds = Some(21600), timestamp = System.currentTimeMillis() / 1000L)
val serialized = PaymentRequest write pr
val pr1 = PaymentRequest read serialized
val expiry = pr1.tags.collectFirst { case expiry: PaymentRequest.ExpiryTag => expiry.seconds }.get
assert(expiry == 21600)
assert(pr.expiry === Some(21600))
}
}