1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 02:27:32 +01:00

Deal with channels with fees=0 when computing a route (#905)

* Treat channels with fees=0 as if they had feeBase=1msat while we compute a route

* Add test to ensure we build the onion attaching no fees if they were not required by the channel_update
This commit is contained in:
araspitzu 2019-03-26 14:00:15 +01:00 committed by Fabrice Drouin
parent 57e43cc65d
commit 89ddc52640
3 changed files with 88 additions and 4 deletions

View File

@ -310,13 +310,21 @@ object Graph {
}
/**
* This forces channel_update(s) with fees=0 to have a minimum of 1msat for the baseFee. Note that
* the update is not being modified and the result of the route computation will still have the update
* with fees=0 which is what will be used to build the onion.
*
* @param edge the edge for which we want to compute the weight
* @param amountWithFees the value that this edge will have to carry along
* @return the new amount updated with the necessary fees for this edge
*/
private def edgeFeeCost(edge: GraphEdge, amountWithFees: Long): Long = {
amountWithFees + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountWithFees)
if(edgeHasZeroFee(edge)) amountWithFees + nodeFee(baseMsat = 1, proportional = 0, amountWithFees)
else amountWithFees + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountWithFees)
}
private def edgeHasZeroFee(edge: GraphEdge): Boolean = {
edge.update.feeBaseMsat == 0 && edge.update.feeProportionalMillionths == 0
}
// Calculates the total cost of a path (amount + fees), direct channels with the source will have a cost of 0 (pay no fees)

View File

@ -19,16 +19,20 @@ package fr.acinq.eclair.payment
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
import akka.actor.Status
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi}
import fr.acinq.bitcoin.Script.{pay2wsh, write}
import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi, Satoshi, Transaction, TxOut}
import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic}
import fr.acinq.eclair.channel.Register.ForwardShortId
import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.payment.PaymentLifecycle._
import fr.acinq.eclair.router.Announcements.makeChannelUpdate
import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement}
import fr.acinq.eclair.router._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, randomBytes32}
import fr.acinq.eclair.{Globals, ShortChannelId, randomBytes32}
/**
* Created by PM on 29/08/2016.
@ -320,6 +324,60 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat))
}
test("payment succeeded to a channel with fees=0") { fixture =>
import fixture._
import fr.acinq.eclair.randomKey
// the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) and b -> g has fees=0
// \
// \--(5)--> g
val (priv_g, priv_funding_g) = (randomKey, randomKey)
val (g, funding_g) = (priv_g.publicKey, priv_funding_g.publicKey)
val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil)
val channelId_bg = ShortChannelId(420000, 5, 0)
val chan_bg = channelAnnouncement(channelId_bg, priv_b, priv_g, priv_funding_b, priv_funding_g)
val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 0, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000L)
val channelUpdate_gb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, b, channelId_bg, cltvExpiryDelta = 9, htlcMinimumMsat = 0, feeBaseMsat = 10, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000L)
assert(Router.getDesc(channelUpdate_bg, chan_bg) === ChannelDesc(chan_bg.shortChannelId, priv_b.publicKey, priv_g.publicKey))
router ! PeerRoutingMessage(null, remoteNodeId, chan_bg)
router ! PeerRoutingMessage(null, remoteNodeId, ann_g)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_bg)
router ! PeerRoutingMessage(null, remoteNodeId, channelUpdate_gb)
watcher.expectMsg(ValidateRequest(chan_bg))
watcher.send(router, ValidateResult(chan_bg, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(1000000), write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_g)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))))
watcher.expectMsgType[WatchSpentBasic]
// actual test begins
val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref))
val monitor = TestProbe()
val sender = TestProbe()
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
// we send a payment to G which is just after the
val request = SendPayment(defaultAmountMsat, defaultPaymentHash, g)
sender.send(paymentFSM, request)
// the route will be A -> B -> G where B -> G has a channel_update with fees=0
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash))
val paymentOK = sender.expectMsgType[PaymentSucceeded]
val PaymentSent(MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent]
// during the route computation the fees were treated as if they were 1msat but when sending the onion we actually put zero
// NB: A -> B doesn't pay fees because it's our direct neighbor
// NB: B -> G doesn't asks for fees at all
assert(fee === MilliSatoshi(0))
assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat))
}
test("filter errors properly") { fixture =>
val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(None), None, None)) :: LocalFailure(RouteNotFound) :: Nil
val filtered = PaymentLifecycle.transformForUser(failures)

View File

@ -590,6 +590,24 @@ class RouteCalculationSpec extends FunSuite {
assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil))
}
test("ensure the route calculation terminates correctly when selecting 0-fees edges") {
// the graph contains a possible 0-cost path that goes back on its steps ( e -> f, f -> e )
val updates = List(
makeUpdate(1L, a, b, 10, 10), // a -> b
makeUpdate(2L, b, c, 10, 10),
makeUpdate(4L, c, d, 10, 10),
makeUpdate(3L, b, e, 0, 0), // b -> e
makeUpdate(6L, e, f, 0, 0), // e -> f
makeUpdate(6L, f, e, 0, 0), // e <- f
makeUpdate(5L, e, d, 0, 0) // e -> d
).toMap
val g = makeGraph(updates)
val route1 = Router.findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS)
assert(route1.map(hops2Ids) === Success(1 :: 3 :: 5 :: Nil))
}
/**
*