mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 22:46:44 +01:00
Support building payments with extra hops (#198)
* Support building of outgoing payments with optional extra hops from payment requests * Add test for route calculation with extra hops * Simplify pattern matching in `buildExtra` * `buildPayment` now uses a reverse Seq[Hop] to create a Seq[ExtraHop] Since `Router` currently stores `ChannelUpdate`'s for non-public channels, it is possible to use it not only to get a route from payer to payee but also to get a "reverse" assisted route from payee when creating a `PaymentRequest`. In principle this could be used to even generate a full reverse path to a payer which does not have an access to routing table for some reason. * Can create `PaymentRequest`s with `RoutingInfoTag`s * Bugfix and update test with data from live payment testing * Move ExtraHop to PaymentRequest.scala
This commit is contained in:
parent
e17335931b
commit
d0e33f23e9
9 changed files with 110 additions and 28 deletions
|
@ -0,0 +1,41 @@
|
|||
package fr.acinq.eclair.payment
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.wire.ChannelUpdate
|
||||
|
||||
|
||||
object PaymentHop {
|
||||
/**
|
||||
*
|
||||
* @param baseMsat fixed fee
|
||||
* @param proportional proportional fee
|
||||
* @param msat amount in millisatoshi
|
||||
* @return the fee (in msat) that a node should be paid to forward an HTLC of 'amount' millisatoshis
|
||||
*/
|
||||
def nodeFee(baseMsat: Long, proportional: Long, msat: Long): Long = baseMsat + (proportional * msat) / 1000000
|
||||
|
||||
/**
|
||||
*
|
||||
* @param reversePath sequence of Hops from recipient to a start of assisted path
|
||||
* @param msat an amount to send to a payment recipient
|
||||
* @return a sequence of extra hops with a pre-calculated fee for a given msat amount
|
||||
*/
|
||||
def buildExtra(reversePath: Seq[Hop], msat: Long): Seq[ExtraHop] = (List.empty[ExtraHop] /: reversePath) {
|
||||
case (Nil, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat), hop.cltvExpiryDelta) :: Nil
|
||||
case (head :: rest, hop) => ExtraHop(hop.nodeId, hop.shortChannelId, hop.nextFee(msat + head.fee), hop.cltvExpiryDelta) :: head :: rest
|
||||
}
|
||||
}
|
||||
|
||||
trait PaymentHop {
|
||||
def nextFee(msat: Long): Long
|
||||
def shortChannelId: Long
|
||||
def cltvExpiryDelta: Int
|
||||
def nodeId: PublicKey
|
||||
}
|
||||
|
||||
case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends PaymentHop {
|
||||
def nextFee(msat: Long): Long = PaymentHop.nodeFee(lastUpdate.feeBaseMsat, lastUpdate.feeProportionalMillionths, msat)
|
||||
def cltvExpiryDelta: Int = lastUpdate.cltvExpiryDelta
|
||||
def shortChannelId: Long = lastUpdate.shortChannelId
|
||||
}
|
|
@ -159,15 +159,6 @@ object PaymentLifecycle {
|
|||
|
||||
def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param baseMsat fixed fee
|
||||
* @param proportional proportional fee
|
||||
* @param msat amount in millisatoshi
|
||||
* @return the fee (in msat) that a node should be paid to forward an HTLC of 'amount' millisatoshis
|
||||
*/
|
||||
def nodeFee(baseMsat: Long, proportional: Long, msat: Long): Long = baseMsat + (proportional * msat) / 1000000
|
||||
|
||||
def buildOnion(nodes: Seq[PublicKey], payloads: Seq[PerHopPayload], associatedData: BinaryData): Sphinx.PacketAndSecrets = {
|
||||
require(nodes.size == payloads.size)
|
||||
val sessionKey = randomKey
|
||||
|
@ -184,18 +175,16 @@ object PaymentLifecycle {
|
|||
*
|
||||
* @param finalAmountMsat the final htlc amount in millisatoshis
|
||||
* @param finalExpiry the final htlc expiry in number of blocks
|
||||
* @param hops the hops as computed by the router
|
||||
* @param hops the hops as computed by the router + extra routes from payment request
|
||||
* @return a (firstAmountMsat, firstExpiry, payloads) tuple where:
|
||||
* - firstAmountMsat is the amount for the first htlc in the route
|
||||
* - firstExpiry is the cltv expiry for the first htlc in the route
|
||||
* - a sequence of payloads that will be used to build the onion
|
||||
*/
|
||||
def buildPayloads(finalAmountMsat: Long, finalExpiry: Int, hops: Seq[Hop]): (Long, Int, Seq[PerHopPayload]) =
|
||||
def buildPayloads(finalAmountMsat: Long, finalExpiry: Int, hops: Seq[PaymentHop]): (Long, Int, Seq[PerHopPayload]) =
|
||||
hops.reverse.foldLeft((finalAmountMsat, finalExpiry, PerHopPayload(0L, finalAmountMsat, finalExpiry) :: Nil)) {
|
||||
case ((msat, expiry, payloads), hop) =>
|
||||
val feeMsat = nodeFee(hop.lastUpdate.feeBaseMsat, hop.lastUpdate.feeProportionalMillionths, msat)
|
||||
val expiryDelta = hop.lastUpdate.cltvExpiryDelta
|
||||
(msat + feeMsat, expiry + expiryDelta, PerHopPayload(hop.lastUpdate.shortChannelId, msat, expiry) +: payloads)
|
||||
(msat + hop.nextFee(msat), expiry + hop.cltvExpiryDelta, PerHopPayload(hop.shortChannelId, msat, expiry) +: payloads)
|
||||
}
|
||||
|
||||
// this is defined in BOLT 11
|
||||
|
|
|
@ -104,12 +104,16 @@ object PaymentRequest {
|
|||
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#adding-an-htlc-update_add_htlc
|
||||
val maxAmount = MilliSatoshi(4294967296L)
|
||||
|
||||
def apply(chainHash: BinaryData, amount: Option[MilliSatoshi], paymentHash: BinaryData, privateKey: PrivateKey, description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None, timestamp: Long = System.currentTimeMillis() / 1000L): 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 = {
|
||||
|
||||
val prefix = chainHash match {
|
||||
case Block.RegtestGenesisBlock.hash => "lntb"
|
||||
case Block.TestnetGenesisBlock.hash => "lntb"
|
||||
case Block.LivenetGenesisBlock.hash => "lnbc"
|
||||
}
|
||||
|
||||
PaymentRequest(
|
||||
prefix = prefix,
|
||||
amount = amount,
|
||||
|
@ -118,7 +122,8 @@ object PaymentRequest {
|
|||
tags = List(
|
||||
Some(PaymentHashTag(paymentHash)),
|
||||
Some(DescriptionTag(description)),
|
||||
expirySeconds.map(ExpiryTag(_))).flatten,
|
||||
expirySeconds.map(ExpiryTag(_))
|
||||
).flatten ++ extraHops.map(RoutingInfoTag(_)),
|
||||
signature = BinaryData.empty)
|
||||
.sign(privateKey)
|
||||
}
|
||||
|
@ -208,16 +213,19 @@ object PaymentRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* Hidden hop
|
||||
* Extra hop contained in RoutingInfoTag
|
||||
*
|
||||
* @param pubkey node id
|
||||
* @param nodeId node id
|
||||
* @param shortChannelId channel id
|
||||
* @param fee node fee
|
||||
* @param cltvExpiryDelta node cltv expiry delta
|
||||
*/
|
||||
case class ExtraHop(pubkey: PublicKey, shortChannelId: Long, fee: Long, cltvExpiryDelta: Int) {
|
||||
def pack: Seq[Byte] = pubkey.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++
|
||||
case class ExtraHop(nodeId: PublicKey, shortChannelId: Long, fee: Long, cltvExpiryDelta: Int) extends PaymentHop {
|
||||
def pack: Seq[Byte] = nodeId.toBin ++ Protocol.writeUInt64(shortChannelId, ByteOrder.BIG_ENDIAN) ++
|
||||
Protocol.writeUInt64(fee, ByteOrder.BIG_ENDIAN) ++ Protocol.writeUInt16(cltvExpiryDelta, ByteOrder.BIG_ENDIAN)
|
||||
|
||||
// Fee is already pre-calculated for extra hops
|
||||
def nextFee(msat: Long): Long = fee
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@ import fr.acinq.eclair.channel._
|
|||
import fr.acinq.eclair.io.Peer
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.payment.Hop
|
||||
import org.jgrapht.alg.shortestpath.DijkstraShortestPath
|
||||
import org.jgrapht.ext._
|
||||
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge, SimpleGraph}
|
||||
|
@ -25,7 +26,6 @@ import scala.util.{Random, Success, Try}
|
|||
// @formatter:off
|
||||
|
||||
case class ChannelDesc(id: Long, a: PublicKey, b: PublicKey)
|
||||
case class Hop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate)
|
||||
case class RouteRequest(source: PublicKey, target: PublicKey, ignoreNodes: Set[PublicKey] = Set.empty, ignoreChannels: Set[Long] = Set.empty)
|
||||
case class RouteResponse(hops: Seq[Hop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[Long]) { require(hops.size > 0, "route cannot be empty") }
|
||||
case class ExcludeChannel(desc: ChannelDesc) // this is used when we get a TemporaryChannelFailure, to give time for the channel to recover (note that exclusions are directed)
|
||||
|
|
|
@ -11,7 +11,6 @@ import fr.acinq.eclair._
|
|||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.Hop
|
||||
import fr.acinq.eclair.wire._
|
||||
import grizzled.slf4j.Logging
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
@ -7,7 +7,7 @@ import fr.acinq.eclair.blockchain._
|
|||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle
|
||||
import fr.acinq.eclair.router.Hop
|
||||
import fr.acinq.eclair.payment.Hop
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, TestConstants}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import fr.acinq.eclair.blockchain._
|
|||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.channel.{Data, State, _}
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle
|
||||
import fr.acinq.eclair.router.Hop
|
||||
import fr.acinq.eclair.payment.Hop
|
||||
import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
|
||||
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass}
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
@ -5,7 +5,7 @@ import fr.acinq.eclair.crypto.Sphinx
|
|||
import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket}
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle._
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.router.Hop
|
||||
import fr.acinq.eclair.payment.PaymentHop.nodeFee
|
||||
import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package fr.acinq.eclair.router
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto}
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.wire.ChannelUpdate
|
||||
import fr.acinq.eclair.wire.{ChannelUpdate, PerHopPayload}
|
||||
import fr.acinq.eclair.payment._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
@ -174,4 +176,47 @@ class RouteCalculationSpec extends FunSuite {
|
|||
assert(hops === Hop(a, b, uab) :: Hop(b, c, ubc) :: Hop(c, d, ucd) :: Hop(d, e, ude) :: Nil)
|
||||
}
|
||||
|
||||
test("calculate route with extra hops") {
|
||||
// E (sender) -> D - public -> C - private -> B - private -> A (receiver)
|
||||
|
||||
val amount = MilliSatoshi(100000000L)
|
||||
val paymentPreimage = BinaryData("0" * 32)
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val privateKey = PrivateKey("bb77027e3b6ef55f3b16eb6973d124f68e0c2afc16accc00a44ec6b3d1e58cc601")
|
||||
|
||||
// Ask router for a route from 02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8 (node C)
|
||||
// to 0299439d988cbf31388d59e3d6f9e184e7a0739b8b8fcdc298957216833935f9d3 (node A)
|
||||
val hopCB = Hop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"),
|
||||
PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"),
|
||||
ChannelUpdate("3044022075bc283539935b1bc126035ef98d0f9bcd5dd7b0832b0a6175dc14a5ee12d47102203d141a4da4f83fca9d65bddfb9ee6ea5cdfcdb364de062d1370500f511b8370701",
|
||||
"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", 24412456671576064L, 1509366313, BinaryData("0000"), 144, 1000, 546000, 10))
|
||||
|
||||
val hopBA = Hop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac"),
|
||||
PublicKey("0299439d988cbf31388d59e3d6f9e184e7a0739b8b8fcdc298957216833935f9d3"),
|
||||
ChannelUpdate("304402205e9b28e26add5417ad97f6eb161229dd7db0d7848e146a1856a8841238bc627902203cc59996ca490375fd76a3327adfb7c5150ee3288ad1663b8c4fbe8908eb489a01",
|
||||
"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", 23366821113626624L, 1509455356, BinaryData("0001"), 144, 1000, 546000, 10))
|
||||
|
||||
val reverseRoute = List(hopBA, hopCB)
|
||||
val extraRoute = PaymentHop.buildExtra(reverseRoute, amount.amount)
|
||||
|
||||
assert(extraRoute === List(ExtraHop(PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"), 24412456671576064L, 547005, 144),
|
||||
ExtraHop(PublicKey("032b4af42b5e8089a7a06005ead9ac4667527390ee39c998b7b0307f0d81d7f4ac") ,23366821113626624L, 547000, 144)))
|
||||
|
||||
// Sender side
|
||||
|
||||
// Ask router for a route D -> C
|
||||
val hopDC = Hop(PublicKey("03c1b07dbe10e178216150b49646ded556466ed15368857fa721cf1acd9d9a6f24"),
|
||||
PublicKey("02f0b230e53723ccc331db140edc518be1ee5ab29a508104a4be2f5be922c928e8"),
|
||||
ChannelUpdate("3044022060c1034092d4e41d75271eb619ef0a0f00d0b5a61c4245e0f14eeac91a3c823202200da9c8b8067e73c32aea41cb9eec050ce49cb944877d9abb3b08be2dea92497301",
|
||||
"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", 24403660578553856L, 1509456040, BinaryData("0001"), 144, 1000, 546000, 10))
|
||||
|
||||
val (amt, expiry, payloads) = PaymentLifecycle.buildPayloads(amount.amount, 10, Seq(hopDC) ++ extraRoute)
|
||||
|
||||
assert(payloads === List(PerHopPayload(24403660578553856L, 101094005L, 298),
|
||||
PerHopPayload(24412456671576064L, 100547000L, 154), PerHopPayload(23366821113626624L, 100000000L, 10), PerHopPayload(0L, 100000000L, 10)))
|
||||
|
||||
assert(amt == 101641015L)
|
||||
assert(expiry == 442)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue