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:
parent
3be40a1fab
commit
e17335931b
5 changed files with 66 additions and 20 deletions
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue