1
0
Fork 0
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:
Anton Kumaigorodski 2017-11-07 13:08:05 +02:00 committed by Pierre-Marie Padiou
parent e17335931b
commit d0e33f23e9
9 changed files with 110 additions and 28 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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
}
/**

View file

@ -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)

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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

View file

@ -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)
}
}