1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-15 04:11:33 +01:00

Merge branch 'android' into android-phoenix

This commit is contained in:
pm47 2020-10-09 10:10:16 +02:00
commit ebed70b93c
No known key found for this signature in database
GPG key ID: E434ED292E85643A
49 changed files with 1586 additions and 1287 deletions

View file

@ -33,7 +33,7 @@ and COMMAND is one of the available commands:
- connect - connect
- disconnect - disconnect
- peers - peers
- allnodes - nodes
- audit - audit
=== Channel === === Channel ===

View file

@ -44,6 +44,8 @@ eclair {
gossip_queries = optional gossip_queries = optional
gossip_queries_ex = optional gossip_queries_ex = optional
var_onion_optin = optional var_onion_optin = optional
payment_secret = optional
basic_mpp = optional
} }
override-features = [ // optional per-node features override-features = [ // optional per-node features
# { # {
@ -96,9 +98,10 @@ eclair {
claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet) claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet)
} }
// maximum local vs remote feerate mismatch; 1.0 means 100% feerate-tolerance {
// actual check is abs((local feerate - remote fee rate) / (local fee rate + remote fee rate)/2) > fee rate mismatch ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate
max-feerate-mismatch = 1.56 // will allow remote fee rates up to 8x bigger or smaller than our local fee rate ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate
}
close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing
// funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater // funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater

View file

@ -86,7 +86,9 @@ trait Eclair {
def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[RES_GETINFO] def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[RES_GETINFO]
def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]]
def nodes(nodeIds_opt: Option[Set[PublicKey]] = None)(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]]
def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] def receive(description: String, amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest]
@ -108,7 +110,7 @@ trait Eclair {
def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]] def networkFees(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[NetworkFee]]
def channelStats()(implicit timeout: Timeout): Future[Seq[Stats]] def channelStats(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[Stats]]
def networkStats()(implicit timeout: Timeout): Future[Option[NetworkStats]] def networkStats()(implicit timeout: Timeout): Future[Option[NetworkStats]]
@ -118,8 +120,6 @@ trait Eclair {
def allInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]] def allInvoices(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[PaymentRequest]]
def allNodes()(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]]
def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]]
def allUpdates(nodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[ChannelUpdate]] def allUpdates(nodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[ChannelUpdate]]
@ -174,11 +174,17 @@ class EclairImpl(appKit: Kit) extends Eclair {
sendToChannels[ChannelCommandResponse](channels, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths)) sendToChannels[ChannelCommandResponse](channels, CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths))
} }
override def peersInfo()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for { override def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for {
peers <- (appKit.switchboard ? Symbol("peers")).mapTo[Iterable[ActorRef]] peers <- (appKit.switchboard ? Symbol("peers")).mapTo[Iterable[ActorRef]]
peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo])) peerinfos <- Future.sequence(peers.map(peer => (peer ? GetPeerInfo).mapTo[PeerInfo]))
} yield peerinfos } yield peerinfos
override def nodes(nodeIds_opt: Option[Set[PublicKey]])(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] = {
(appKit.router ? Symbol("nodes"))
.mapTo[Iterable[NodeAnnouncement]]
.map(_.filter(n => nodeIds_opt.forall(_.contains(n.nodeId))))
}
override def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] = toRemoteNode_opt match { override def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] = toRemoteNode_opt match {
case Some(pk) => for { case Some(pk) => for {
channelIds <- (appKit.register ? Symbol("channelsTo")).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys) channelIds <- (appKit.register ? Symbol("channelsTo")).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys)
@ -194,8 +200,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
sendToChannel[RES_GETINFO](channel, CMD_GETINFO) sendToChannel[RES_GETINFO](channel, CMD_GETINFO)
} }
override def allNodes()(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] = (appKit.router ? Symbol("nodes")).mapTo[Iterable[NodeAnnouncement]]
override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = { override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = {
(appKit.router ? Symbol("channels")).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))) (appKit.router ? Symbol("channels")).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)))
} }
@ -312,7 +316,10 @@ class EclairImpl(appKit: Kit) extends Eclair {
Future(appKit.nodeParams.db.audit.listNetworkFees(filter.from, filter.to)) Future(appKit.nodeParams.db.audit.listNetworkFees(filter.from, filter.to))
} }
override def channelStats()(implicit timeout: Timeout): Future[Seq[Stats]] = Future(appKit.nodeParams.db.audit.stats) override def channelStats(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[Seq[Stats]] = {
val filter = getDefaultTimestampFilters(from_opt, to_opt)
Future(appKit.nodeParams.db.audit.stats(filter.from, filter.to))
}
override def networkStats()(implicit timeout: Timeout): Future[Option[NetworkStats]] = (appKit.router ? GetNetworkStats).mapTo[GetNetworkStatsResponse].map(_.stats) override def networkStats()(implicit timeout: Timeout): Future[Option[NetworkStats]] = (appKit.router ? GetNetworkStats).mapTo[GetNetworkStatsResponse].map(_.stats)

View file

@ -27,7 +27,7 @@ import com.google.common.io.Files
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi}
import fr.acinq.eclair.NodeParams.WatcherType import fr.acinq.eclair.NodeParams.WatcherType
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, OnChainFeeConf} import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeerateTolerance, OnChainFeeConf}
import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.crypto.KeyManager
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
@ -143,7 +143,9 @@ object NodeParams {
"update-fee_min-diff-ratio" -> "on-chain-fees.update-fee-min-diff-ratio", "update-fee_min-diff-ratio" -> "on-chain-fees.update-fee-min-diff-ratio",
// v0.3.3 // v0.3.3
"global-features" -> "features", "global-features" -> "features",
"local-features" -> "features" "local-features" -> "features",
// v0.4.1
"on-chain-fees.max-feerate-mismatch" -> "on-chain-fees.feerate-tolerance.ratio-low / on-chain-fees.feerate-tolerance.ratio-high"
) )
deprecatedKeyPaths.foreach { deprecatedKeyPaths.foreach {
case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'")
@ -246,7 +248,7 @@ object NodeParams {
onChainFeeConf = OnChainFeeConf( onChainFeeConf = OnChainFeeConf(
feeTargets = feeTargets, feeTargets = feeTargets,
feeEstimator = feeEstimator, feeEstimator = feeEstimator,
maxFeerateMismatch = config.getDouble("on-chain-fees.max-feerate-mismatch"), maxFeerateMismatch = FeerateTolerance(config.getDouble("on-chain-fees.feerate-tolerance.ratio-low"), config.getDouble("on-chain-fees.feerate-tolerance.ratio-high")),
closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"), closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"),
updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio") updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio")
), ),

View file

@ -252,7 +252,7 @@ class Setup(datadir: File,
_ <- postRestartCleanUpInitialized.future _ <- postRestartCleanUpInitialized.future
switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, relayer, paymentHandler, wallet), "switchboard", SupervisorStrategy.Resume)) switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, relayer, paymentHandler, wallet), "switchboard", SupervisorStrategy.Resume))
clientSpawner = system.actorOf(SimpleSupervisor.props(ClientSpawner.props(nodeParams, switchboard, router), "client-spawner", SupervisorStrategy.Restart)) clientSpawner = system.actorOf(SimpleSupervisor.props(ClientSpawner.props(nodeParams, switchboard, router), "client-spawner", SupervisorStrategy.Restart))
paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, router, relayer, register), "payment-initiator", SupervisorStrategy.Restart)) paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, router, register), "payment-initiator", SupervisorStrategy.Restart))
kit = Kit( kit = Kit(
nodeParams = nodeParams, nodeParams = nodeParams,

View file

@ -18,12 +18,14 @@ package fr.acinq.eclair.blockchain.fee
trait FeeEstimator { trait FeeEstimator {
def getFeeratePerKb(target: Int) : Long def getFeeratePerKb(target: Int): Long
def getFeeratePerKw(target: Int) : Long def getFeeratePerKw(target: Int): Long
} }
case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int) case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int)
case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, maxFeerateMismatch: Double, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double) case class FeerateTolerance(ratioLow: Double, ratioHigh: Double)
case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, maxFeerateMismatch: FeerateTolerance, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double)

View file

@ -35,10 +35,8 @@ import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions._ import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import scodec.bits.ByteVector import scodec.bits.ByteVector
import ChannelVersion._
import scala.collection.immutable.Queue import scala.collection.immutable.Queue
import scala.compat.Platform
import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -653,7 +651,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, error, origin(c), Some(d.channelUpdate), Some(c)), c) handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, error, origin(c), Some(d.channelUpdate), Some(c)), c)
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) =>
Commitments.sendAdd(d.commitments, c, origin(c), nodeParams.currentBlockHeight) match { Commitments.sendAdd(d.commitments, c, origin(c), nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) match {
case Success((commitments1, add)) => case Success((commitments1, add)) =>
if (c.commit) self ! CMD_SIGN if (c.commit) self ! CMD_SIGN
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
@ -662,7 +660,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
} }
case Event(add: UpdateAddHtlc, d: DATA_NORMAL) => case Event(add: UpdateAddHtlc, d: DATA_NORMAL) =>
Commitments.receiveAdd(d.commitments, add) match { Commitments.receiveAdd(d.commitments, add, nodeParams.onChainFeeConf) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1) case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d, Some(add)) case Failure(cause) => handleLocalError(cause, d, Some(add))
} }
@ -734,7 +732,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
} }
case Event(fee: UpdateFee, d: DATA_NORMAL) => case Event(fee: UpdateFee, d: DATA_NORMAL) =>
Commitments.receiveFee(d.commitments, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, fee, nodeParams.onChainFeeConf.maxFeerateMismatch) match { Commitments.receiveFee(d.commitments, fee, nodeParams.onChainFeeConf) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1) case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d, Some(fee)) case Failure(cause) => handleLocalError(cause, d, Some(fee))
} }
@ -822,14 +820,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
case Event(c@CMD_CLOSE(localScriptPubKey_opt), d: DATA_NORMAL) => case Event(c@CMD_CLOSE(localScriptPubKey_opt), d: DATA_NORMAL) =>
val localScriptPubKey = localScriptPubKey_opt.getOrElse(d.commitments.localParams.defaultFinalScriptPubKey) val localScriptPubKey = localScriptPubKey_opt.getOrElse(d.commitments.localParams.defaultFinalScriptPubKey)
if (d.localShutdown.isDefined) if (d.localShutdown.isDefined) {
handleCommandError(ClosingAlreadyInProgress(d.channelId), c) handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) } else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) {
// TODO: simplistic behavior, we could also sign-then-close // TODO: simplistic behavior, we could also sign-then-close
handleCommandError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), c) handleCommandError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), c)
else if (!Closing.isValidFinalScriptPubkey(localScriptPubKey)) } else if (!Closing.isValidFinalScriptPubkey(localScriptPubKey)) {
handleCommandError(InvalidFinalScript(d.channelId), c) handleCommandError(InvalidFinalScript(d.channelId), c)
else { } else {
val shutdown = Shutdown(d.channelId, localScriptPubKey) val shutdown = Shutdown(d.channelId, localScriptPubKey)
handleCommandSuccess(sender, d.copy(localShutdown = Some(shutdown))) storing() sending shutdown handleCommandSuccess(sender, d.copy(localShutdown = Some(shutdown))) storing() sending shutdown
} }
@ -1079,7 +1077,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
} }
case Event(fee: UpdateFee, d: DATA_SHUTDOWN) => case Event(fee: UpdateFee, d: DATA_SHUTDOWN) =>
Commitments.receiveFee(d.commitments, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, fee, nodeParams.onChainFeeConf.maxFeerateMismatch) match { Commitments.receiveFee(d.commitments, fee, nodeParams.onChainFeeConf) match {
case Success(commitments1) => stay using d.copy(commitments = commitments1) case Success(commitments1) => stay using d.copy(commitments = commitments1)
case Failure(cause) => handleLocalError(cause, d, Some(fee)) case Failure(cause) => handleLocalError(cause, d, Some(fee))
} }
@ -1827,13 +1825,18 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = {
val networkFeeratePerKw = c.feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val networkFeeratePerKw = c.feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget)
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
d.commitments.localParams.isFunder match { val shouldUpdateFee = d.commitments.localParams.isFunder &&
case true if Helpers.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio) => Helpers.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.updateFeeMinDiffRatio)
self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true) val shouldClose = !d.commitments.localParams.isFunder &&
stay Helpers.isFeeDiffTooHigh(networkFeeratePerKw, currentFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) &&
case false if Helpers.isFeeDiffTooHigh(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) => d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c)) if (shouldUpdateFee) {
case _ => stay self ! CMD_UPDATE_FEE(networkFeeratePerKw, commit = true)
stay
} else if (shouldClose) {
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = networkFeeratePerKw, remoteFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw), d, Some(c))
} else {
stay
} }
} }
@ -1847,13 +1850,16 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = { def handleOfflineFeerate(c: CurrentFeerates, d: HasCommitments) = {
val networkFeeratePerKw = c.feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val networkFeeratePerKw = c.feeratesPerKw.feePerBlock(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget)
val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw val currentFeeratePerKw = d.commitments.localCommit.spec.feeratePerKw
// if the fees are too high we risk to not be able to confirm our current commitment // if the network fees are too high we risk to not be able to confirm our current commitment
if (networkFeeratePerKw > currentFeeratePerKw && Helpers.isFeeDiffTooHigh(currentFeeratePerKw, networkFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch)) { val shouldClose = networkFeeratePerKw > currentFeeratePerKw &&
Helpers.isFeeDiffTooHigh(networkFeeratePerKw, currentFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch) &&
d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk
if (shouldClose) {
if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) { if (nodeParams.onChainFeeConf.closeOnOfflineMismatch) {
log.warning(s"closing OFFLINE ${d.channelId} due to fee mismatch: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw") log.warning(s"closing OFFLINE channel due to fee mismatch: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw")
handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = currentFeeratePerKw, remoteFeeratePerKw = networkFeeratePerKw), d, Some(c)) handleLocalError(FeerateTooDifferent(d.channelId, localFeeratePerKw = currentFeeratePerKw, remoteFeeratePerKw = networkFeeratePerKw), d, Some(c))
} else { } else {
log.warning(s"channel ${d.channelId} is OFFLINE but its fee mismatch is over the threshold: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw") log.warning(s"channel is OFFLINE but its fee mismatch is over the threshold: currentFeeratePerKw=$currentFeeratePerKw networkFeeratePerKw=$networkFeeratePerKw")
stay stay
} }
} else { } else {
@ -2173,7 +2179,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint
val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished))
goto(CLOSING) using nextData storing() calling(doPublish(remoteCommitPublished)) goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished))
} }
} }

View file

@ -17,10 +17,9 @@
package fr.acinq.eclair.channel package fr.acinq.eclair.channel
import akka.event.LoggingAdapter import akka.event.LoggingAdapter
import fr.acinq.eclair.channel.ChannelVersion._
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256}
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.blockchain.fee.OnChainFeeConf
import fr.acinq.eclair.channel.Monitoring.Metrics import fr.acinq.eclair.channel.Monitoring.Metrics
import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx}
import fr.acinq.eclair.payment.relay.{Origin, Relayer} import fr.acinq.eclair.payment.relay.{Origin, Relayer}
@ -37,7 +36,9 @@ import scala.util.{Failure, Success, Try}
case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) {
def all: List[UpdateMessage] = proposed ++ signed ++ acked def all: List[UpdateMessage] = proposed ++ signed ++ acked
} }
case class RemoteChanges(proposed: List[UpdateMessage], acked: List[UpdateMessage], signed: List[UpdateMessage]) case class RemoteChanges(proposed: List[UpdateMessage], acked: List[UpdateMessage], signed: List[UpdateMessage]) {
def all: List[UpdateMessage] = proposed ++ signed ++ acked
}
case class Changes(ourChanges: LocalChanges, theirChanges: RemoteChanges) case class Changes(ourChanges: LocalChanges, theirChanges: RemoteChanges)
case class HtlcTxAndSigs(txinfo: TransactionWithInputInfo, localSig: ByteVector64, remoteSig: ByteVector64) case class HtlcTxAndSigs(txinfo: TransactionWithInputInfo, localSig: ByteVector64, remoteSig: ByteVector64)
case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs]) case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs])
@ -70,6 +71,10 @@ case class Commitments(channelVersion: ChannelVersion,
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight
def hasPendingOrProposedHtlcs: Boolean = !hasNoPendingHtlcs ||
localChanges.all.exists(_.isInstanceOf[UpdateAddHtlc]) ||
remoteChanges.all.exists(_.isInstanceOf[UpdateAddHtlc])
def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = { def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = {
def expired(add: UpdateAddHtlc) = blockheight >= add.cltvExpiry.toLong def expired(add: UpdateAddHtlc) = blockheight >= add.cltvExpiry.toLong
@ -179,7 +184,7 @@ object Commitments {
* @param cmd add HTLC command * @param cmd add HTLC command
* @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right((new commitments, updateAddHtlc) * @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right((new commitments, updateAddHtlc)
*/ */
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin, blockHeight: Long): Try[(Commitments, UpdateAddHtlc)] = { def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, origin: Origin, blockHeight: Long, feeConf: OnChainFeeConf): Try[(Commitments, UpdateAddHtlc)] = {
// our counterparty needs a reasonable amount of time to pull the funds from downstream before we can get refunded (see BOLT 2 and BOLT 11 for a calculation and rationale) // our counterparty needs a reasonable amount of time to pull the funds from downstream before we can get refunded (see BOLT 2 and BOLT 11 for a calculation and rationale)
val minExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(blockHeight) val minExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(blockHeight)
if (cmd.cltvExpiry < minExpiry) { if (cmd.cltvExpiry < minExpiry) {
@ -197,6 +202,13 @@ object Commitments {
return Failure(HtlcValueTooSmall(commitments.channelId, minimum = htlcMinimum, actual = cmd.amount)) return Failure(HtlcValueTooSmall(commitments.channelId, minimum = htlcMinimum, actual = cmd.amount))
} }
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
// we need to verify that we're not disagreeing on feerates anymore before offering new HTLCs
val localFeeratePerKw = feeConf.feeEstimator.getFeeratePerKw(target = feeConf.feeTargets.commitmentBlockTarget)
if (Helpers.isFeeDiffTooHigh(localFeeratePerKw, commitments.localCommit.spec.feeratePerKw, feeConf.maxFeerateMismatch)) {
return Failure(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw))
}
// let's compute the current commitment *as seen by them* with this change taken into account // let's compute the current commitment *as seen by them* with this change taken into account
val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val add = UpdateAddHtlc(commitments.channelId, commitments.localNextHtlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)
// we increment the local htlc index and add an entry to the origins map // we increment the local htlc index and add an entry to the origins map
@ -238,7 +250,7 @@ object Commitments {
Success(commitments1, add) Success(commitments1, add)
} }
def receiveAdd(commitments: Commitments, add: UpdateAddHtlc): Try[Commitments] = Try { def receiveAdd(commitments: Commitments, add: UpdateAddHtlc, feeConf: OnChainFeeConf): Try[Commitments] = Try {
if (add.id != commitments.remoteNextHtlcId) { if (add.id != commitments.remoteNextHtlcId) {
throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id) throw UnexpectedHtlcId(commitments.channelId, expected = commitments.remoteNextHtlcId, actual = add.id)
} }
@ -249,6 +261,13 @@ object Commitments {
throw HtlcValueTooSmall(commitments.channelId, minimum = htlcMinimum, actual = add.amountMsat) throw HtlcValueTooSmall(commitments.channelId, minimum = htlcMinimum, actual = add.amountMsat)
} }
// we allowed mismatches between our feerates and our remote's as long as commitments didn't contain any HTLC at risk
// we need to verify that we're not disagreeing on feerates anymore before accepting new HTLCs
val localFeeratePerKw = feeConf.feeEstimator.getFeeratePerKw(target = feeConf.feeTargets.commitmentBlockTarget)
if (Helpers.isFeeDiffTooHigh(localFeeratePerKw, commitments.localCommit.spec.feeratePerKw, feeConf.maxFeerateMismatch)) {
throw FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = commitments.localCommit.spec.feeratePerKw)
}
// let's compute the current commitment *as seen by us* including this change // let's compute the current commitment *as seen by us* including this change
val commitments1 = addRemoteProposal(commitments, add).copy(remoteNextHtlcId = commitments.remoteNextHtlcId + 1) val commitments1 = addRemoteProposal(commitments, add).copy(remoteNextHtlcId = commitments.remoteNextHtlcId + 1)
val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed)
@ -398,16 +417,16 @@ object Commitments {
} }
} }
def receiveFee(commitments: Commitments, feeEstimator: FeeEstimator, feeTargets: FeeTargets, fee: UpdateFee, maxFeerateMismatch: Double)(implicit log: LoggingAdapter): Try[Commitments] = { def receiveFee(commitments: Commitments, fee: UpdateFee, feeConf: OnChainFeeConf)(implicit log: LoggingAdapter): Try[Commitments] = {
if (commitments.localParams.isFunder) { if (commitments.localParams.isFunder) {
Failure(FundeeCannotSendUpdateFee(commitments.channelId)) Failure(FundeeCannotSendUpdateFee(commitments.channelId))
} else if (fee.feeratePerKw < fr.acinq.eclair.MinimumFeeratePerKw) { } else if (fee.feeratePerKw < fr.acinq.eclair.MinimumFeeratePerKw) {
Failure(FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw)) Failure(FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw))
} else { } else {
Metrics.RemoteFeeratePerKw.withoutTags().record(fee.feeratePerKw) Metrics.RemoteFeeratePerKw.withoutTags().record(fee.feeratePerKw)
val localFeeratePerKw = feeEstimator.getFeeratePerKw(target = feeTargets.commitmentBlockTarget) val localFeeratePerKw = feeConf.feeEstimator.getFeeratePerKw(target = feeConf.feeTargets.commitmentBlockTarget)
log.info("remote feeratePerKw={}, local feeratePerKw={}, ratio={}", fee.feeratePerKw, localFeeratePerKw, fee.feeratePerKw.toDouble / localFeeratePerKw) log.info("remote feeratePerKw={}, local feeratePerKw={}, ratio={}", fee.feeratePerKw, localFeeratePerKw, fee.feeratePerKw.toDouble / localFeeratePerKw)
if (Helpers.isFeeDiffTooHigh(fee.feeratePerKw, localFeeratePerKw, maxFeerateMismatch)) { if (Helpers.isFeeDiffTooHigh(localFeeratePerKw, fee.feeratePerKw, feeConf.maxFeerateMismatch) && commitments.hasPendingOrProposedHtlcs) {
Failure(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)) Failure(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw))
} else { } else {
// NB: we check that the funder can afford this new fee even if spec allows to do it at next signature // NB: we check that the funder can afford this new fee even if spec allows to do it at next signature
@ -461,6 +480,7 @@ object Commitments {
// NB: IN/OUT htlcs are inverted because this is the remote commit // NB: IN/OUT htlcs are inverted because this is the remote commit
log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx) log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.feeratePerKw} txid=${remoteCommitTx.tx.txid} tx={}", spec.htlcs.collect(outgoing).map(_.id).mkString(","), spec.htlcs.collect(incoming).map(_.id).mkString(","), remoteCommitTx.tx)
Metrics.recordHtlcsInFlight(spec, remoteCommit.spec)
// don't sign if they don't get paid // don't sign if they don't get paid
val commitSig = CommitSig( val commitSig = CommitSig(

View file

@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256}
import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin._ import fr.acinq.bitcoin._
import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeerateTolerance}
import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL
import fr.acinq.eclair.crypto.{ChaCha20Poly1305, Generators, KeyManager} import fr.acinq.eclair.crypto.{ChaCha20Poly1305, Generators, KeyManager}
import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.ChannelsDb
@ -32,7 +32,6 @@ import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire.OpenChannelTlv.ChannelVersionTlv import fr.acinq.eclair.wire.OpenChannelTlv.ChannelVersionTlv
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{NodeParams, ShortChannelId, addressToPublicKeyScript, _} import fr.acinq.eclair.{NodeParams, ShortChannelId, addressToPublicKeyScript, _}
import fr.acinq.eclair.channel.ChannelVersion.USE_STATIC_REMOTEKEY_BIT
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.concurrent.Await import scala.concurrent.Await
@ -143,7 +142,7 @@ object Helpers {
} }
val localFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val localFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget)
if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw) if (isFeeDiffTooHigh(localFeeratePerKw, open.feeratePerKw, nodeParams.onChainFeeConf.maxFeerateMismatch)) throw FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)
// only enforce dust limit check on mainnet // only enforce dust limit check on mainnet
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
if (open.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) throw DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUSTLIMIT) if (open.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) throw DustLimitTooSmall(open.temporaryChannelId, open.dustLimitSatoshis, Channel.MIN_DUSTLIMIT)
@ -207,25 +206,20 @@ object Helpers {
} }
/** /**
* @param referenceFeePerKw reference fee rate per kiloweight * To avoid spamming our peers with fee updates every time there's a small variation, we only update the fee when the
* @param currentFeePerKw current fee rate per kiloweight * difference exceeds a given ratio (updateFeeMinDiffRatio).
* @return the "normalized" difference between i.e local and remote fee rate: |reference - current| / avg(current, reference)
*/ */
def feeRateMismatch(referenceFeePerKw: Long, currentFeePerKw: Long): Double = def shouldUpdateFee(currentFeeratePerKw: Long, nextFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean =
Math.abs((2.0 * (referenceFeePerKw - currentFeePerKw)) / (currentFeePerKw + referenceFeePerKw)) currentFeeratePerKw == 0 || Math.abs((currentFeeratePerKw - nextFeeratePerKw).toDouble / currentFeeratePerKw) > updateFeeMinDiffRatio
def shouldUpdateFee(commitmentFeeratePerKw: Long, networkFeeratePerKw: Long, updateFeeMinDiffRatio: Double): Boolean =
feeRateMismatch(networkFeeratePerKw, commitmentFeeratePerKw) > updateFeeMinDiffRatio
/** /**
* @param referenceFeePerKw reference fee rate per kiloweight * @param referenceFeePerKw reference fee rate per kiloweight
* @param currentFeePerKw current fee rate per kiloweight * @param currentFeePerKw current fee rate per kiloweight
* @param maxFeerateMismatchRatio maximum fee rate mismatch ratio * @param maxFeerateMismatch maximum fee rate mismatch tolerated
* @return true if the difference between current and reference fee rates is too high. * @return true if the difference between proposed and reference fee rates is too high.
* the actual check is |reference - current| / avg(current, reference) > mismatch ratio
*/ */
def isFeeDiffTooHigh(referenceFeePerKw: Long, currentFeePerKw: Long, maxFeerateMismatchRatio: Double): Boolean = def isFeeDiffTooHigh(referenceFeePerKw: Long, currentFeePerKw: Long, maxFeerateMismatch: FeerateTolerance): Boolean =
feeRateMismatch(referenceFeePerKw, currentFeePerKw) > maxFeerateMismatchRatio currentFeePerKw < referenceFeePerKw * maxFeerateMismatch.ratioLow || referenceFeePerKw * maxFeerateMismatch.ratioHigh < currentFeePerKw
/** /**
* @param remoteFeeratePerKw remote fee rate per kiloweight * @param remoteFeeratePerKw remote fee rate per kiloweight
@ -663,9 +657,9 @@ object Helpers {
) )
case _ => case _ =>
claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy( claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy(
claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx },
claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx }
) )
} }
} }

View file

@ -16,6 +16,7 @@
package fr.acinq.eclair.channel package fr.acinq.eclair.channel
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc}
import kamon.Kamon import kamon.Kamon
import kamon.tag.TagSet import kamon.tag.TagSet
@ -25,15 +26,35 @@ object Monitoring {
val ChannelsCount = Kamon.gauge("channels.count") val ChannelsCount = Kamon.gauge("channels.count")
val ChannelErrors = Kamon.counter("channels.errors") val ChannelErrors = Kamon.counter("channels.errors")
val ChannelLifecycleEvents = Kamon.counter("channels.lifecycle") val ChannelLifecycleEvents = Kamon.counter("channels.lifecycle")
val HtlcsInFlight = Kamon.histogram("channels.htlc-in-flight", "Per-channel HTLCs in flight")
val HtlcsInFlightGlobal = Kamon.gauge("channels.htlc-in-flight-global", "Global HTLCs in flight across all channels")
val HtlcValueInFlight = Kamon.histogram("channels.htlc-value-in-flight", "Per-channel HTLC value in flight")
val HtlcValueInFlightGlobal = Kamon.gauge("channels.htlc-value-in-flight-global", "Global HTLC value in flight across all channels")
val LocalFeeratePerKw = Kamon.gauge("channels.local-feerate-per-kw") val LocalFeeratePerKw = Kamon.gauge("channels.local-feerate-per-kw")
val RemoteFeeratePerKw = Kamon.histogram("channels.remote-feerate-per-kw") val RemoteFeeratePerKw = Kamon.histogram("channels.remote-feerate-per-kw")
def recordHtlcsInFlight(remoteSpec: CommitmentSpec, previousRemoteSpec: CommitmentSpec): Unit = {
for (direction <- Tags.Directions.Incoming :: Tags.Directions.Outgoing :: Nil) {
// NB: IN/OUT htlcs are inverted because this is the remote commit
val filter = if (direction == Tags.Directions.Incoming) DirectedHtlc.outgoing else DirectedHtlc.incoming
// NB: we need the `toSeq` because otherwise duplicate amounts would be removed (since htlcs are sets)
val htlcs = remoteSpec.htlcs.collect(filter).toSeq.map(_.amountMsat)
val previousHtlcs = previousRemoteSpec.htlcs.collect(filter).toSeq.map(_.amountMsat)
HtlcsInFlight.withTag(Tags.Direction, direction).record(htlcs.length)
HtlcsInFlightGlobal.withTag(Tags.Direction, direction).increment(htlcs.length - previousHtlcs.length)
val (value, previousValue) = (htlcs.sum.truncateToSatoshi.toLong, previousHtlcs.sum.truncateToSatoshi.toLong)
HtlcValueInFlight.withTag(Tags.Direction, direction).record(value)
HtlcValueInFlightGlobal.withTag(Tags.Direction, direction).increment(value - previousValue)
}
}
} }
object Tags { object Tags {
val Event = TagSet.Empty val Direction = "direction"
val Fatal = TagSet.Empty val Event = "event"
val Origin = TagSet.Empty val Fatal = "fatal"
val State = TagSet.Empty val Origin = "origin"
val State = "state"
object Events { object Events {
val Created = "created" val Created = "created"
@ -46,6 +67,11 @@ object Monitoring {
val Remote = "remote" val Remote = "remote"
} }
object Directions {
val Incoming = "incoming"
val Outgoing = "outgoing"
}
} }
} }

View file

@ -0,0 +1,31 @@
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.crypto
import kamon.Kamon
object Monitoring {
object Metrics {
val OnionPayloadFormat = Kamon.counter("crypto.sphinx.onion-payload-format")
}
object Tags {
val LegacyOnion = "legacy"
}
}

View file

@ -18,6 +18,7 @@ package fr.acinq.eclair.crypto
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.wire import fr.acinq.eclair.wire
import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs, Onion, OnionCodecs} import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs, Onion, OnionCodecs}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
@ -88,11 +89,13 @@ object Sphinx extends Logging {
case 0 => case 0 =>
// The 1.0 BOLT spec used 65-bytes frames inside the onion payload. // The 1.0 BOLT spec used 65-bytes frames inside the onion payload.
// The first byte of the frame (called `realm`) is set to 0x00, followed by 32 bytes of per-hop data, followed by a 32-bytes mac. // The first byte of the frame (called `realm`) is set to 0x00, followed by 32 bytes of per-hop data, followed by a 32-bytes mac.
Metrics.OnionPayloadFormat.withTag(Tags.LegacyOnion, value = true).increment()
65 65
case _ => case _ =>
// The 1.1 BOLT spec changed the frame format to use variable-length per-hop payloads. // The 1.1 BOLT spec changed the frame format to use variable-length per-hop payloads.
// The first bytes contain a varint encoding the length of the payload data (not including the trailing mac). // The first bytes contain a varint encoding the length of the payload data (not including the trailing mac).
// Since messages are always smaller than 65535 bytes, this varint will either be 1 or 3 bytes long. // Since messages are always smaller than 65535 bytes, this varint will either be 1 or 3 bytes long.
Metrics.OnionPayloadFormat.withTag(Tags.LegacyOnion, value = false).increment()
MacLength + OnionCodecs.payloadLengthDecoder.decode(payload.bits).require.value.toInt MacLength + OnionCodecs.payloadLengthDecoder.decode(payload.bits).require.value.toInt
} }
} }

View file

@ -45,7 +45,7 @@ trait AuditDb extends Closeable {
def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] def listNetworkFees(from: Long, to: Long): Seq[NetworkFee]
def stats: Seq[Stats] def stats(from: Long, to: Long): Seq[Stats]
} }
@ -53,4 +53,4 @@ case class ChannelLifecycleEvent(channelId: ByteVector32, remoteNodeId: PublicKe
case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, fee: Satoshi, txType: String, timestamp: Long) case class NetworkFee(remoteNodeId: PublicKey, channelId: ByteVector32, txId: ByteVector32, fee: Satoshi, txType: String, timestamp: Long)
case class Stats(channelId: ByteVector32, avgPaymentAmount: Satoshi, paymentCount: Int, relayFee: Satoshi, networkFee: Satoshi) case class Stats(channelId: ByteVector32, direction: String, avgPaymentAmount: Satoshi, paymentCount: Int, relayFee: Satoshi, networkFee: Satoshi)

View file

@ -298,35 +298,46 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
q q
} }
override def stats: Seq[Stats] = { override def stats(from: Long, to: Long): Seq[Stats] = {
val networkFees = listNetworkFees(0, System.currentTimeMillis + 1).foldLeft(Map.empty[ByteVector32, Satoshi]) { case (feeByChannelId, f) => val networkFees = listNetworkFees(from, to).foldLeft(Map.empty[ByteVector32, Satoshi]) { case (feeByChannelId, f) =>
feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee)) feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee))
} }
val relayed = listRelayed(0, System.currentTimeMillis + 1).foldLeft(Map.empty[ByteVector32, Seq[PaymentRelayed]]) { case (relayedByChannelId, e) => case class Relayed(amount: MilliSatoshi, fee: MilliSatoshi, direction: String)
val relayedTo = e match { val relayed = listRelayed(from, to).foldLeft(Map.empty[ByteVector32, Seq[Relayed]]) { case (previous, e) =>
case c: ChannelPaymentRelayed => Set(c.toChannelId) // NB: we must avoid counting the fee twice: we associate it to the outgoing channels rather than the incoming ones.
case t: TrampolinePaymentRelayed => t.outgoing.map(_.channelId).toSet val current = e match {
case c: ChannelPaymentRelayed => Map(
c.fromChannelId -> (Relayed(c.amountIn, 0 msat, "IN") +: previous.getOrElse(c.fromChannelId, Nil)),
c.toChannelId -> (Relayed(c.amountOut, c.amountIn - c.amountOut, "OUT") +: previous.getOrElse(c.toChannelId, Nil))
)
case t: TrampolinePaymentRelayed =>
// We ensure a trampoline payment is counted only once per channel and per direction (if multiple HTLCs were
// sent from/to the same channel, we group them).
val in = t.incoming.groupBy(_.channelId).map { case (channelId, parts) => (channelId, Relayed(parts.map(_.amount).sum, 0 msat, "IN")) }.toSeq
val out = t.outgoing.groupBy(_.channelId).map { case (channelId, parts) =>
val fee = (t.amountIn - t.amountOut) * parts.length / t.outgoing.length // we split the fee among outgoing channels
(channelId, Relayed(parts.map(_.amount).sum, fee, "OUT"))
}.toSeq
(in ++ out).groupBy(_._1).map { case (channelId, payments) => (channelId, payments.map(_._2) ++ previous.getOrElse(channelId, Nil)) }
} }
val updated = relayedTo.map(channelId => (channelId, relayedByChannelId.getOrElse(channelId, Nil) :+ e)).toMap previous ++ current
relayedByChannelId ++ updated
} }
// Channels opened by our peers won't have any entry in the network_fees table, but we still want to compute stats for them. // Channels opened by our peers won't have any entry in the network_fees table, but we still want to compute stats for them.
val allChannels = networkFees.keySet ++ relayed.keySet val allChannels = networkFees.keySet ++ relayed.keySet
allChannels.map(channelId => { allChannels.toSeq.flatMap(channelId => {
val networkFee = networkFees.getOrElse(channelId, 0 sat) val networkFee = networkFees.getOrElse(channelId, 0 sat)
val r = relayed.getOrElse(channelId, Nil) val (in, out) = relayed.getOrElse(channelId, Nil).partition(_.direction == "IN")
val paymentCount = r.length ((in, "IN") :: (out, "OUT") :: Nil).map { case (r, direction) =>
if (paymentCount == 0) { val paymentCount = r.length
Stats(channelId, 0 sat, 0, 0 sat, networkFee) if (paymentCount == 0) {
} else { Stats(channelId, direction, 0 sat, 0, 0 sat, networkFee)
val avgPaymentAmount = r.map(_.amountOut).sum / paymentCount } else {
val relayFee = r.map { val avgPaymentAmount = r.map(_.amount).sum / paymentCount
case c: ChannelPaymentRelayed => c.amountIn - c.amountOut val relayFee = r.map(_.fee).sum
case t: TrampolinePaymentRelayed => (t.amountIn - t.amountOut) * t.outgoing.count(_.channelId == channelId) / t.outgoing.length Stats(channelId, direction, avgPaymentAmount.truncateToSatoshi, paymentCount, relayFee.truncateToSatoshi, networkFee)
}.sum }
Stats(channelId, avgPaymentAmount.truncateToSatoshi, paymentCount, relayFee.truncateToSatoshi, networkFee)
} }
}).toSeq })
} }
// used by mobile apps // used by mobile apps

View file

@ -27,9 +27,14 @@ object Monitoring {
val PaymentParts = Kamon.histogram("payment.parts", "Number of HTLCs per payment (MPP)") val PaymentParts = Kamon.histogram("payment.parts", "Number of HTLCs per payment (MPP)")
val PaymentFailed = Kamon.counter("payment.failed", "Number of failed payment") val PaymentFailed = Kamon.counter("payment.failed", "Number of failed payment")
val PaymentError = Kamon.counter("payment.error", "Non-fatal errors encountered during payment attempts") val PaymentError = Kamon.counter("payment.error", "Non-fatal errors encountered during payment attempts")
val PaymentAttempt = Kamon.histogram("payment.attempt", "Number of attempts before a payment succeeds")
val SentPaymentDuration = Kamon.timer("payment.duration.sent", "Outgoing payment duration") val SentPaymentDuration = Kamon.timer("payment.duration.sent", "Outgoing payment duration")
val ReceivedPaymentDuration = Kamon.timer("payment.duration.received", "Incoming payment duration") val ReceivedPaymentDuration = Kamon.timer("payment.duration.received", "Incoming payment duration")
// The goal of this metric is to measure whether retrying MPP payments on failing channels yields useful results.
// Once enough data has been collected, we will update the MultiPartPaymentLifecycle logic accordingly.
val RetryFailedChannelsResult = Kamon.counter("payment.mpp.retry-failed-channels-result")
def recordPaymentRelayFailed(failureType: String, relayType: String): Unit = def recordPaymentRelayFailed(failureType: String, relayType: String): Unit =
Metrics.PaymentFailed Metrics.PaymentFailed
.withTag(Tags.Direction, Tags.Directions.Relayed) .withTag(Tags.Direction, Tags.Directions.Relayed)

View file

@ -23,7 +23,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop} import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore}
import fr.acinq.eclair.wire.Node import fr.acinq.eclair.wire.Node
/** /**
@ -162,43 +162,43 @@ object PaymentFailure {
.isDefined .isDefined
/** Update the set of nodes and channels to ignore in retries depending on the failure we received. */ /** Update the set of nodes and channels to ignore in retries depending on the failure we received. */
def updateIgnored(failure: PaymentFailure, ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]): (Set[PublicKey], Set[ChannelDesc]) = failure match { def updateIgnored(failure: PaymentFailure, ignore: Ignore): Ignore = failure match {
case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId => case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId =>
// The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route. // The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route.
(ignoreNodes, ignoreChannels) ignore
case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) => case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) =>
(ignoreNodes + nodeId, ignoreChannels) ignore + nodeId
case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
if (Announcements.checkSig(failureMessage.update, nodeId)) { if (Announcements.checkSig(failureMessage.update, nodeId)) {
// We were using an outdated channel update, we should retry with the new one and nobody should be penalized. // We were using an outdated channel update, we should retry with the new one and nobody should be penalized.
(ignoreNodes, ignoreChannels) ignore
} else { } else {
// This node is fishy, it gave us a bad signature, so let's filter it out. // This node is fishy, it gave us a bad signature, so let's filter it out.
(ignoreNodes + nodeId, ignoreChannels) ignore + nodeId
} }
case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) => case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) =>
// Let's ignore the channel outgoing from nodeId. // Let's ignore the channel outgoing from nodeId.
hops.collectFirst { hops.collectFirst {
case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)
} match { } match {
case Some(faultyChannel) => (ignoreNodes, ignoreChannels + faultyChannel) case Some(faultyChannel) => ignore + faultyChannel
case None => (ignoreNodes, ignoreChannels) case None => ignore
} }
case UnreadableRemoteFailure(hops) => case UnreadableRemoteFailure(hops) =>
// We don't know which node is sending garbage, let's blacklist all nodes except the one we are directly connected to and the final recipient. // We don't know which node is sending garbage, let's blacklist all nodes except the one we are directly connected to and the final recipient.
val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1).toSet
(ignoreNodes ++ blacklist, ignoreChannels) ignore ++ blacklist
case LocalFailure(hops, _) => hops.headOption match { case LocalFailure(hops, _) => hops.headOption match {
case Some(hop: ChannelHop) => case Some(hop: ChannelHop) =>
val faultyChannel = ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) val faultyChannel = ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)
(ignoreNodes, ignoreChannels + faultyChannel) ignore + faultyChannel
case _ => (ignoreNodes, ignoreChannels) case _ => ignore
} }
} }
/** Update the set of nodes and channels to ignore in retries depending on the failures we received. */ /** Update the set of nodes and channels to ignore in retries depending on the failures we received. */
def updateIgnored(failures: Seq[PaymentFailure], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]): (Set[PublicKey], Set[ChannelDesc]) = { def updateIgnored(failures: Seq[PaymentFailure], ignore: Ignore): Ignore = {
failures.foldLeft((ignoreNodes, ignoreChannels)) { case ((nodes, channels), failure) => updateIgnored(failure, nodes, channels) } failures.foldLeft(ignore) { case (current, failure) => updateIgnored(failure, current) }
} }
} }

View file

@ -219,6 +219,7 @@ object ChannelRelayer {
case (_: ExpiryTooBig, _) => ExpiryTooFar case (_: ExpiryTooBig, _) => ExpiryTooFar
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate) case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate) case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: FeerateTooDifferent, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.channelFlags) => ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate) case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.channelFlags) => ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)
case (_: ChannelUnavailable, None) => PermanentChannelFailure case (_: ChannelUnavailable, None) => PermanentChannelFailure
case _ => TemporaryNodeFailure case _ => TemporaryNodeFailure

View file

@ -20,20 +20,20 @@ import java.util.UUID
import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, PoisonPill, Props} import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, PoisonPill, Props}
import akka.event.Logging.MDC import akka.event.Logging.MDC
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentError, PaymentLifecycle} import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentLifecycle}
import fr.acinq.eclair.router.Router.RouteParams import fr.acinq.eclair.router.Router.RouteParams
import fr.acinq.eclair.router.{RouteCalculation, RouteNotFound} import fr.acinq.eclair.router.{BalanceTooLow, RouteCalculation, RouteNotFound}
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, Logs, MilliSatoshi, NodeParams, nodeFee, randomBytes32, _} import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, nodeFee, randomBytes32, _}
import scala.collection.immutable.Queue import scala.collection.immutable.Queue
@ -46,7 +46,7 @@ import scala.collection.immutable.Queue
* It aggregates incoming HTLCs (in case multi-part was used upstream) and then forwards the requested amount (using the * It aggregates incoming HTLCs (in case multi-part was used upstream) and then forwards the requested amount (using the
* router to find a route to the remote node and potentially splitting the payment using multi-part). * router to find a route to the remote node and potentially splitting the payment using multi-part).
*/ */
class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging { class NodeRelayer(nodeParams: NodeParams, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging {
import NodeRelayer._ import NodeRelayer._
@ -109,18 +109,13 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
case None => log.error("could not find pending incoming payment: payment will not be relayed: please investigate") case None => log.error("could not find pending incoming payment: payment will not be relayed: please investigate")
} }
case ff: Relayer.ForwardFulfill => ff.to match { case PreimageReceived(paymentHash, paymentPreimage) =>
case Origin.TrampolineRelayed(_, Some(paymentSender)) => log.debug("trampoline payment successfully relayed")
paymentSender ! ff pendingOutgoing.get(paymentHash).foreach(p => if (!p.fulfilledUpstream) {
val paymentHash = Crypto.sha256(ff.paymentPreimage) // We want to fulfill upstream as soon as we receive the preimage (even if not all HTLCs have fulfilled downstream).
pendingOutgoing.get(paymentHash).foreach(p => if (!p.fulfilledUpstream) { fulfillPayment(p.upstream, paymentPreimage)
// We want to fulfill upstream as soon as we receive the preimage (even if not all HTLCs have fulfilled downstream). context become main(pendingIncoming, pendingOutgoing + (paymentHash -> p.copy(fulfilledUpstream = true)))
log.debug("trampoline payment successfully relayed") })
fulfillPayment(p.upstream, ff.paymentPreimage)
context become main(pendingIncoming, pendingOutgoing + (paymentHash -> p.copy(fulfilledUpstream = true)))
})
case _ => log.error(s"unexpected non-trampoline fulfill: $ff")
}
case PaymentSent(id, paymentHash, paymentPreimage, _, _, parts) => case PaymentSent(id, paymentHash, paymentPreimage, _, _, parts) =>
// We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources. // We may have already fulfilled upstream, but we can now emit an accurate relayed event and clean-up resources.
@ -148,7 +143,7 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = { def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
if (multiPart) { if (multiPart) {
context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, cfg, relayer, router, register)) context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, cfg, router, register))
} else { } else {
context.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register)) context.actorOf(PaymentLifecycle.props(nodeParams, cfg, router, register))
} }
@ -158,15 +153,21 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
val paymentId = UUID.randomUUID() val paymentId = UUID.randomUUID()
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, paymentHash, payloadOut.amountToForward, payloadOut.outgoingNodeId, upstream, None, storeInDb = false, publishEvent = false, Nil) val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, paymentHash, payloadOut.amountToForward, payloadOut.outgoingNodeId, upstream, None, storeInDb = false, publishEvent = false, Nil)
val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv) val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv)
// If invoice features are provided in the onion, the sender is asking us to relay to a non-trampoline recipient.
payloadOut.invoiceFeatures match { payloadOut.invoiceFeatures match {
case Some(_) => case Some(features) =>
log.debug("relaying trampoline payment to non-trampoline recipient")
val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil) val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil)
// TODO: @t-bast: MPP is disabled for trampoline to non-trampoline payments until we improve the splitting algorithm for nodes with a lot of channels. payloadOut.paymentSecret match {
val payFSM = spawnOutgoingPayFSM(paymentCfg, multiPart = false) case Some(paymentSecret) if Features(features).hasFeature(Features.BasicMultiPartPayment) =>
val finalPayload = Onion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, payloadOut.paymentSecret) log.debug("relaying trampoline payment to non-trampoline recipient using MPP")
val payment = SendPayment(payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams)) val payment = SendMultiPartPayment(paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
payFSM ! payment spawnOutgoingPayFSM(paymentCfg, multiPart = true) ! payment
case _ =>
log.debug("relaying trampoline payment to non-trampoline recipient without MPP")
val finalPayload = Onion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, payloadOut.paymentSecret)
val payment = SendPayment(payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
spawnOutgoingPayFSM(paymentCfg, multiPart = false) ! payment
}
case None => case None =>
log.debug("relaying trampoline payment to next trampoline node") log.debug("relaying trampoline payment to next trampoline node")
val payFSM = spawnOutgoingPayFSM(paymentCfg, multiPart = true) val payFSM = spawnOutgoingPayFSM(paymentCfg, multiPart = true)
@ -209,7 +210,7 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
object NodeRelayer { object NodeRelayer {
def props(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) = Props(new NodeRelayer(nodeParams, relayer, router, commandBuffer, register)) def props(nodeParams: NodeParams, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) = Props(new NodeRelayer(nodeParams, router, commandBuffer, register))
/** /**
* We start by aggregating an incoming HTLC set. Once we received the whole set, we will compute a route to the next * We start by aggregating an incoming HTLC set. Once we received the whole set, we will compute a route to the next
@ -260,15 +261,11 @@ object NodeRelayer {
* should return upstream. * should return upstream.
*/ */
private def translateError(failures: Seq[PaymentFailure], outgoingNodeId: PublicKey): Option[FailureMessage] = { private def translateError(failures: Seq[PaymentFailure], outgoingNodeId: PublicKey): Option[FailureMessage] = {
def tooManyRouteNotFound(failures: Seq[PaymentFailure]): Boolean = { val routeNotFound = failures.collectFirst { case f@LocalFailure(_, RouteNotFound) => f }.nonEmpty
val routeNotFoundCount = failures.collect { case f@LocalFailure(_, RouteNotFound) => f }.length
routeNotFoundCount > failures.length / 2
}
failures match { failures match {
case Nil => None case Nil => None
case LocalFailure(_, PaymentError.BalanceTooLow) :: Nil => Some(TemporaryNodeFailure) // we don't have enough outgoing liquidity at the moment case LocalFailure(_, BalanceTooLow) :: Nil => Some(TemporaryNodeFailure) // we don't have enough outgoing liquidity at the moment
case _ if tooManyRouteNotFound(failures) => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient case _ if routeNotFound => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
case _ => case _ =>
// Otherwise, we try to find a downstream error that we could decrypt. // Otherwise, we try to find a downstream error that we could decrypt.
val outgoingNodeFailure = failures.collectFirst { case RemoteFailure(_, e) if e.originNode == outgoingNodeId => e.failureMessage } val outgoingNodeFailure = failures.collectFirst { case RemoteFailure(_, e) if e.originNode == outgoingNodeId => e.failureMessage }

View file

@ -77,9 +77,9 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged]) context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged])
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, commandBuffer, initialized)) private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, commandBuffer, initialized), "post-restart-htlc-cleaner")
private val channelRelayer = context.actorOf(ChannelRelayer.props(nodeParams, self, register, commandBuffer)) private val channelRelayer = context.actorOf(ChannelRelayer.props(nodeParams, self, register, commandBuffer), "channel-relayer")
private val nodeRelayer = context.actorOf(NodeRelayer.props(nodeParams, self, router, commandBuffer, register)) private val nodeRelayer = context.actorOf(NodeRelayer.props(nodeParams, router, commandBuffer, register), "node-relayer")
override def receive: Receive = main(Map.empty, new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]) override def receive: Receive = main(Map.empty, new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId])
@ -167,7 +167,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd) commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd)
context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, amountOut, ff.htlc.paymentHash, originChannelId, ff.htlc.channelId)) context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, amountOut, ff.htlc.paymentHash, originChannelId, ff.htlc.channelId))
case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff
case Origin.TrampolineRelayed(_, Some(_)) => nodeRelayer forward ff case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! ff
} }
case ff: ForwardFail => ff.to match { case ff: ForwardFail => ff.to match {
@ -206,7 +206,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
object Relayer extends Logging { object Relayer extends Logging {
def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) = def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) =
Props(classOf[Relayer], nodeParams, router, register, commandBuffer, paymentHandler, initialized) Props(new Relayer(nodeParams, router, register, commandBuffer, paymentHandler, initialized))
type ChannelUpdates = Map[ShortChannelId, OutgoingChannel] type ChannelUpdates = Map[ShortChannelId, OutgoingChannel]
type NodeChannels = mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId] type NodeChannels = mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]

View file

@ -19,29 +19,23 @@ package fr.acinq.eclair.payment.send
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import akka.actor.{ActorRef, FSM, Props} import akka.actor.{ActorRef, FSM, Props, Status}
import akka.event.Logging.MDC import akka.event.Logging.MDC
import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.{Commitments, Upstream} import fr.acinq.eclair.channel.Upstream
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.PaymentSent.PartialPayment
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannel, OutgoingChannels}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
import fr.acinq.eclair.router.RouteCalculation
import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, ToMilliSatoshiConversion} import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, LongToBtcAmount, MilliSatoshi, NodeParams}
import kamon.Kamon import kamon.Kamon
import kamon.context.Context import kamon.context.Context
import scodec.bits.ByteVector
import scala.annotation.tailrec
import scala.util.Random
/** /**
* Created by t-bast on 18/07/2019. * Created by t-bast on 18/07/2019.
@ -51,7 +45,7 @@ import scala.util.Random
* Sender for a multi-part payment (see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#basic-multi-part-payments). * Sender for a multi-part payment (see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#basic-multi-part-payments).
* The payment will be split into multiple sub-payments that will be sent in parallel. * The payment will be split into multiple sub-payments that will be sent in parallel.
*/ */
class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, relayer: ActorRef, router: ActorRef, register: ActorRef) extends FSMDiagnosticActorLogging[MultiPartPaymentLifecycle.State, MultiPartPaymentLifecycle.Data] { class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) extends FSMDiagnosticActorLogging[MultiPartPaymentLifecycle.State, MultiPartPaymentLifecycle.Data] {
import MultiPartPaymentLifecycle._ import MultiPartPaymentLifecycle._
@ -60,6 +54,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
val id = cfg.id val id = cfg.id
val paymentHash = cfg.paymentHash val paymentHash = cfg.paymentHash
val start = System.currentTimeMillis val start = System.currentTimeMillis
private var retriedFailedChannels = false
private val span = Kamon.spanBuilder("multi-part-payment") private val span = Kamon.spanBuilder("multi-part-payment")
.tag(Tags.ParentId, cfg.parentId.toString) .tag(Tags.ParentId, cfg.parentId.toString)
@ -72,109 +67,95 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
when(WAIT_FOR_PAYMENT_REQUEST) { when(WAIT_FOR_PAYMENT_REQUEST) {
case Event(r: SendMultiPartPayment, _) => case Event(r: SendMultiPartPayment, _) =>
router ! GetNetworkStats val routeParams = r.getRouteParams(nodeParams, randomize = false) // we don't randomize the first attempt, regardless of configuration choices
goto(WAIT_FOR_NETWORK_STATS) using WaitingForNetworkStats(sender, r) val maxFee = routeParams.getMaxFee(r.totalAmount)
log.debug("sending {} with maximum fee {}", r.totalAmount, maxFee)
val d = PaymentProgress(sender, r, r.maxAttempts, Map.empty, Ignore.empty, Nil)
router ! createRouteRequest(nodeParams, r.totalAmount, maxFee, routeParams, d, cfg)
goto(WAIT_FOR_ROUTES) using d
} }
when(WAIT_FOR_NETWORK_STATS) { when(WAIT_FOR_ROUTES) {
case Event(s: GetNetworkStatsResponse, d: WaitingForNetworkStats) => case Event(RouteResponse(routes), d: PaymentProgress) =>
log.debug("network stats: {}", s.stats.map(_.capacity)) log.info("{} routes found (attempt={}/{})", routes.length, d.request.maxAttempts - d.remainingAttempts + 1, d.request.maxAttempts)
// If we don't have network stats it's ok, we'll use data about our local channels instead. // We may have already succeeded sending parts of the payment and only need to take care of the rest.
// We tell the router to compute those stats though: in case our payment attempt fails, they will be available for val (toSend, maxFee) = remainingToSend(nodeParams, d.request, d.pending.values)
// another payment attempt. if (routes.map(_.amount).sum == toSend) {
if (s.stats.isEmpty) { val childPayments = routes.map(route => (UUID.randomUUID(), route)).toMap
router ! TickComputeNetworkStats
}
relayer ! GetOutgoingChannels()
goto(WAIT_FOR_CHANNEL_BALANCES) using WaitingForChannelBalances(d.sender, d.request, s.stats)
}
when(WAIT_FOR_CHANNEL_BALANCES) {
case Event(OutgoingChannels(channels), d: WaitingForChannelBalances) =>
log.debug("trying to send {} with local channels: {}", d.request.totalAmount, channels.map(_.toUsableBalance).mkString(","))
val (remaining, payments) = splitPayment(nodeParams, d.request.totalAmount, channels, d.networkStats, d.request, randomize = false)
if (remaining > 0.msat) {
log.warning(s"cannot send ${d.request.totalAmount} with our current balance")
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, PaymentError.BalanceTooLow)))
goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, LocalFailure(Nil, PaymentError.BalanceTooLow) :: Nil, Set.empty)
} else {
val pending = setFees(d.request.routeParams, payments, payments.size)
Kamon.runWithContextEntry(parentPaymentIdKey, cfg.parentId) { Kamon.runWithContextEntry(parentPaymentIdKey, cfg.parentId) {
Kamon.runWithSpan(span, finishSpan = true) { Kamon.runWithSpan(span, finishSpan = true) {
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment } childPayments.foreach { case (childId, route) => spawnChildPaymentFsm(childId) ! createChildPayment(route, d.request) }
} }
} }
goto(PAYMENT_IN_PROGRESS) using PaymentProgress(d.sender, d.request, d.networkStats, channels.length, 0 msat, d.request.maxAttempts - 1, pending, Set.empty, Nil) goto(PAYMENT_IN_PROGRESS) using d.copy(remainingAttempts = (d.remainingAttempts - 1).max(0), pending = d.pending ++ childPayments)
} else {
// If a child payment failed while we were waiting for routes, the routes we received don't cover the whole
// remaining amount. In that case we discard these routes and send a new request to the router.
log.info("discarding routes, another child payment failed so we need to recompute them (amount = {}, maximum fee = {})", toSend, maxFee)
val routeParams = d.request.getRouteParams(nodeParams, randomize = true) // we randomize route selection when we retry
router ! createRouteRequest(nodeParams, toSend, maxFee, routeParams, d, cfg)
stay
} }
case Event(Status.Failure(t), d: PaymentProgress) =>
log.warning("router error: {}", t.getMessage)
if (d.ignore.channels.nonEmpty) {
// If no route can be found, we will retry once with the channels that we previously ignored.
// Channels are mostly ignored for temporary reasons, likely because they didn't have enough balance to forward
// the payment. When we're retrying an MPP split, it may make sense to retry those ignored channels because with
// a different split, they may have enough balance to forward the payment.
val (toSend, maxFee) = remainingToSend(nodeParams, d.request, d.pending.values)
log.debug("retry sending {} with maximum fee {} without ignoring channels ({})", toSend, maxFee, d.ignore.channels.map(_.shortChannelId).mkString(","))
val routeParams = d.request.getRouteParams(nodeParams, randomize = true) // we randomize route selection when we retry
router ! createRouteRequest(nodeParams, toSend, maxFee, routeParams, d, cfg).copy(ignore = d.ignore.emptyChannels())
retriedFailedChannels = true
stay using d.copy(remainingAttempts = (d.remainingAttempts - 1).max(0), ignore = d.ignore.emptyChannels())
} else {
val failure = LocalFailure(Nil, t)
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(failure)).increment()
gotoAbortedOrStop(PaymentAborted(d.sender, d.request, d.failures :+ failure, d.pending.keySet))
}
case Event(pf: PaymentFailed, d: PaymentProgress) =>
if (isFinalRecipientFailure(pf, d)) {
gotoAbortedOrStop(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id))
} else {
val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore)
stay using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures)
}
// The recipient released the preimage without receiving the full payment amount.
// This is a spec violation and is too bad for them, we obtained a proof of payment without paying the full amount.
case Event(ps: PaymentSent, d: PaymentProgress) =>
require(ps.parts.length == 1, "child payment must contain only one part")
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
gotoSucceededOrStop(PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id))
} }
when(PAYMENT_IN_PROGRESS) { when(PAYMENT_IN_PROGRESS) {
case Event(pf: PaymentFailed, d: PaymentProgress) => handleChildFailure(pf, d) match { case Event(pf: PaymentFailed, d: PaymentProgress) =>
case Some(paymentAborted) => if (isFinalRecipientFailure(pf, d)) {
goto(PAYMENT_ABORTED) using paymentAborted gotoAbortedOrStop(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id))
case None => } else if (d.remainingAttempts == 0) {
// Get updated local channels (will take into account the child payments that are in-flight). val failure = LocalFailure(Nil, PaymentError.RetryExhausted)
relayer ! GetOutgoingChannels() Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(failure)).increment()
val failedPayment = d.pending(pf.id) gotoAbortedOrStop(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id))
val shouldBlacklist = shouldBlacklistChannel(pf)
if (shouldBlacklist) {
log.debug(s"ignoring channel ${getFirstHopShortChannelId(failedPayment)} to ${failedPayment.routePrefix.head.nextNodeId}")
}
val ignoreChannels = if (shouldBlacklist) d.ignoreChannels + getFirstHopShortChannelId(failedPayment) else d.ignoreChannels
val remainingAttempts = if (shouldBlacklist && Random.nextDouble() * math.log(d.channelsCount) > 2.0) {
// When we have a lot of channels, many of them may end up being a bad route prefix for the destination we're
// trying to reach. This is a cheap error that is detected quickly (RouteNotFound), so we don't want to count
// it in our payment attempts to avoid failing too fast.
// However we don't want to test all of our channels either which would be expensive, so we only probabilistically
// count the failure in our payment attempts.
// With the log-scale used, here are the probabilities and the corresponding number of retries:
// * 10 channels -> refund 13% of failures -> with 5 initial retries we will actually try 5/(1-0.13) = ~6 times
// * 20 channels -> refund 32% of failures -> with 5 initial retries we will actually try 5/(1-0.32) = ~7 times
// * 50 channels -> refund 50% of failures -> with 5 initial retries we will actually try 5/(1-0.50) = ~10 times
// * 100 channels -> refund 56% of failures -> with 5 initial retries we will actually try 5/(1-0.56) = ~11 times
// * 1000 channels -> refund 70% of failures -> with 5 initial retries we will actually try 5/(1-0.70) = ~17 times
// NB: this hack won't be necessary once multi-part is directly handled by the router.
d.remainingAttempts + 1
} else {
d.remainingAttempts
}
goto(RETRY_WITH_UPDATED_BALANCES) using d.copy(toSend = d.toSend + failedPayment.finalPayload.amount, pending = d.pending - pf.id, failures = d.failures ++ pf.failures, ignoreChannels = ignoreChannels, remainingAttempts = remainingAttempts)
}
case Event(ps: PaymentSent, d: PaymentProgress) =>
require(ps.parts.length == 1, "child payment must contain only one part")
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id)
}
when(RETRY_WITH_UPDATED_BALANCES) {
case Event(OutgoingChannels(channels), d: PaymentProgress) =>
log.debug("trying to send {} with local channels: {}", d.toSend, channels.map(_.toUsableBalance).mkString(","))
val filteredChannels = channels.filter(c => !d.ignoreChannels.contains(c.channelUpdate.shortChannelId))
val (remaining, payments) = splitPayment(nodeParams, d.toSend, filteredChannels, d.networkStats, d.request, randomize = true) // we randomize channel selection when we retry
if (remaining > 0.msat) {
log.warning(s"cannot send ${d.toSend} with our current balance")
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, PaymentError.BalanceTooLow)))
goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, d.failures :+ LocalFailure(Nil, PaymentError.BalanceTooLow), d.pending.keySet)
} else { } else {
val pending = setFees(d.request.routeParams, payments, payments.size + d.pending.size) val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore)
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment } val stillPending = d.pending - pf.id
goto(PAYMENT_IN_PROGRESS) using d.copy(toSend = 0 msat, remainingAttempts = d.remainingAttempts - 1, pending = d.pending ++ pending, channelsCount = channels.length) val (toSend, maxFee) = remainingToSend(nodeParams, d.request, stillPending.values)
log.debug("child payment failed, retry sending {} with maximum fee {}", toSend, maxFee)
val routeParams = d.request.getRouteParams(nodeParams, randomize = true) // we randomize route selection when we retry
val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures)
router ! createRouteRequest(nodeParams, toSend, maxFee, routeParams, d1, cfg)
goto(WAIT_FOR_ROUTES) using d1
} }
case Event(pf: PaymentFailed, d: PaymentProgress) => handleChildFailure(pf, d) match {
case Some(paymentAborted) =>
goto(PAYMENT_ABORTED) using paymentAborted
case None =>
val failedPayment = d.pending(pf.id)
val ignoreChannels = if (shouldBlacklistChannel(pf)) d.ignoreChannels + getFirstHopShortChannelId(failedPayment) else d.ignoreChannels
stay using d.copy(toSend = d.toSend + failedPayment.finalPayload.amount, pending = d.pending - pf.id, failures = d.failures ++ pf.failures, ignoreChannels = ignoreChannels)
}
case Event(ps: PaymentSent, d: PaymentProgress) => case Event(ps: PaymentSent, d: PaymentProgress) =>
require(ps.parts.length == 1, "child payment must contain only one part") require(ps.parts.length == 1, "child payment must contain only one part")
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment). // As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id) Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = true).record(d.request.maxAttempts - d.remainingAttempts)
gotoSucceededOrStop(PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id))
} }
when(PAYMENT_ABORTED) { when(PAYMENT_ABORTED) {
@ -192,7 +173,10 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
case Event(ps: PaymentSent, d: PaymentAborted) => case Event(ps: PaymentSent, d: PaymentAborted) =>
require(ps.parts.length == 1, "child payment must contain only one part") require(ps.parts.length == 1, "child payment must contain only one part")
log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})") log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})")
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id) gotoSucceededOrStop(PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id))
case Event(_: RouteResponse, _) => stay
case Event(_: Status.Failure, _) => stay
} }
when(PAYMENT_SUCCEEDED) { when(PAYMENT_SUCCEEDED) {
@ -216,20 +200,9 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
} else { } else {
stay using d.copy(pending = pending) stay using d.copy(pending = pending)
} }
}
onTransition { case Event(_: RouteResponse, _) => stay
case _ -> PAYMENT_ABORTED => nextStateData match { case Event(_: Status.Failure, _) => stay
case d: PaymentAborted if d.pending.isEmpty =>
myStop(d.sender, Left(PaymentFailed(id, paymentHash, d.failures)))
case _ =>
}
case _ -> PAYMENT_SUCCEEDED => nextStateData match {
case d: PaymentSucceeded if d.pending.isEmpty =>
myStop(d.sender, Right(cfg.createPaymentSent(d.preimage, d.parts)))
case _ =>
}
} }
def spawnChildPaymentFsm(childId: UUID): ActorRef = { def spawnChildPaymentFsm(childId: UUID): ActorRef = {
@ -241,6 +214,21 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
context.actorOf(PaymentLifecycle.props(nodeParams, childCfg, router, register)) context.actorOf(PaymentLifecycle.props(nodeParams, childCfg, router, register))
} }
private def gotoAbortedOrStop(d: PaymentAborted): State = {
if (d.pending.isEmpty) {
myStop(d.sender, Left(PaymentFailed(id, paymentHash, d.failures)))
} else
goto(PAYMENT_ABORTED) using d
}
private def gotoSucceededOrStop(d: PaymentSucceeded): State = {
d.sender ! PreimageReceived(paymentHash, d.preimage)
if (d.pending.isEmpty) {
myStop(d.sender, Right(cfg.createPaymentSent(d.preimage, d.parts)))
} else
goto(PAYMENT_SUCCEEDED) using d
}
def myStop(origin: ActorRef, event: Either[PaymentFailed, PaymentSent]): State = { def myStop(origin: ActorRef, event: Either[PaymentFailed, PaymentSent]): State = {
event match { event match {
case Left(paymentFailed) => case Left(paymentFailed) =>
@ -255,6 +243,9 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
.withTag(Tags.MultiPart, Tags.MultiPartType.Parent) .withTag(Tags.MultiPart, Tags.MultiPartType.Parent)
.withTag(Tags.Success, value = event.isRight) .withTag(Tags.Success, value = event.isRight)
.record(System.currentTimeMillis - start, TimeUnit.MILLISECONDS) .record(System.currentTimeMillis - start, TimeUnit.MILLISECONDS)
if (retriedFailedChannels) {
Metrics.RetryFailedChannelsResult.withTag(Tags.Success, event.isRight).increment()
}
span.finish() span.finish()
stop(FSM.Normal) stop(FSM.Normal)
} }
@ -264,21 +255,13 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
if (cfg.publishEvent) context.system.eventStream.publish(e) if (cfg.publishEvent) context.system.eventStream.publish(e)
} }
def handleChildFailure(pf: PaymentFailed, d: PaymentProgress): Option[PaymentAborted] = {
val isFromFinalRecipient = pf.failures.collectFirst { case f: RemoteFailure if f.e.originNode == d.request.targetNodeId => true }.isDefined
if (isFromFinalRecipient) {
Some(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id))
} else if (d.remainingAttempts == 0) {
val failure = LocalFailure(Nil, PaymentError.RetryExhausted)
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(failure))
Some(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id))
} else {
None
}
}
override def mdc(currentMessage: Any): MDC = { override def mdc(currentMessage: Any): MDC = {
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(paymentHash)) Logs.mdc(
category_opt = Some(Logs.LogCategory.PAYMENT),
parentPaymentId_opt = Some(cfg.parentId),
paymentId_opt = Some(id),
paymentHash_opt = Some(paymentHash),
remoteNodeId_opt = Some(cfg.recipientNodeId))
} }
initialize() initialize()
@ -289,7 +272,7 @@ object MultiPartPaymentLifecycle {
val parentPaymentIdKey = Context.key[UUID]("parentPaymentId", UUID.fromString("00000000-0000-0000-0000-000000000000")) val parentPaymentIdKey = Context.key[UUID]("parentPaymentId", UUID.fromString("00000000-0000-0000-0000-000000000000"))
def props(nodeParams: NodeParams, cfg: SendPaymentConfig, relayer: ActorRef, router: ActorRef, register: ActorRef) = Props(new MultiPartPaymentLifecycle(nodeParams, cfg, relayer, router, register)) def props(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) = Props(new MultiPartPaymentLifecycle(nodeParams, cfg, router, register))
/** /**
* Send a payment to a given node. The payment may be split into multiple child payments, for which a path-finding * Send a payment to a given node. The payment may be split into multiple child payments, for which a path-finding
@ -316,53 +299,52 @@ object MultiPartPaymentLifecycle {
additionalTlvs: Seq[OnionTlv] = Nil, additionalTlvs: Seq[OnionTlv] = Nil,
userCustomTlvs: Seq[GenericTlv] = Nil) { userCustomTlvs: Seq[GenericTlv] = Nil) {
require(totalAmount > 0.msat, s"total amount must be > 0") require(totalAmount > 0.msat, s"total amount must be > 0")
def getRouteParams(nodeParams: NodeParams, randomize: Boolean): RouteParams =
routeParams.getOrElse(RouteCalculation.getDefaultRouteParams(nodeParams.routerConf)).copy(randomize = randomize)
} }
/**
* The payment FSM will wait for all child payments to settle before emitting payment events, but the preimage will be
* shared as soon as it's received to unblock other actors that may need it.
*/
case class PreimageReceived(paymentHash: ByteVector32, paymentPreimage: ByteVector32)
// @formatter:off // @formatter:off
sealed trait State sealed trait State
case object WAIT_FOR_PAYMENT_REQUEST extends State case object WAIT_FOR_PAYMENT_REQUEST extends State
case object WAIT_FOR_NETWORK_STATS extends State case object WAIT_FOR_ROUTES extends State
case object WAIT_FOR_CHANNEL_BALANCES extends State
case object PAYMENT_IN_PROGRESS extends State case object PAYMENT_IN_PROGRESS extends State
case object RETRY_WITH_UPDATED_BALANCES extends State
case object PAYMENT_ABORTED extends State case object PAYMENT_ABORTED extends State
case object PAYMENT_SUCCEEDED extends State case object PAYMENT_SUCCEEDED extends State
// @formatter:on
sealed trait Data sealed trait Data
/** /**
* During initialization, we wait for a multi-part payment request containing the total amount to send. * During initialization, we wait for a multi-part payment request containing the total amount to send and the maximum
* fee budget.
*/ */
case object WaitingForRequest extends Data case object WaitingForRequest extends Data
/** /**
* During initialization, we collect network statistics to help us decide how to best split a big payment. * While the payment is in progress, we listen to child payment failures. When we receive such failures, we retry the
* * failed amount with different routes.
* @param sender the sender of the payment request.
* @param request payment request containing the total amount to send.
*/
case class WaitingForNetworkStats(sender: ActorRef, request: SendMultiPartPayment) extends Data
/**
* During initialization, we request our local channels balances.
*
* @param sender the sender of the payment request.
* @param request payment request containing the total amount to send.
* @param networkStats network statistics help us decide how to best split a big payment.
*/
case class WaitingForChannelBalances(sender: ActorRef, request: SendMultiPartPayment, networkStats: Option[NetworkStats]) extends Data
/**
* While the payment is in progress, we listen to child payment failures. When we receive such failures, we request
* our up-to-date local channels balances and retry the failed child payments with a potentially different route.
* *
* @param sender the sender of the payment request. * @param sender the sender of the payment request.
* @param request payment request containing the total amount to send. * @param request payment request containing the total amount to send.
* @param networkStats network statistics help us decide how to best split a big payment.
* @param channelsCount number of local channels.
* @param toSend remaining amount that should be split and sent.
* @param remainingAttempts remaining attempts (after child payments fail). * @param remainingAttempts remaining attempts (after child payments fail).
* @param pending pending child payments (payment sent, we are waiting for a fulfill or a failure). * @param pending pending child payments (payment sent, we are waiting for a fulfill or a failure).
* @param ignoreChannels channels that should be ignored (previously returned a permanent error). * @param ignore channels and nodes that should be ignored (previously returned a permanent error).
* @param failures previous child payment failures. * @param failures previous child payment failures.
*/ */
case class PaymentProgress(sender: ActorRef, request: SendMultiPartPayment, networkStats: Option[NetworkStats], channelsCount: Int, toSend: MilliSatoshi, remainingAttempts: Int, pending: Map[UUID, SendPayment], ignoreChannels: Set[ShortChannelId], failures: Seq[PaymentFailure]) extends Data case class PaymentProgress(sender: ActorRef,
request: SendMultiPartPayment,
remainingAttempts: Int,
pending: Map[UUID, Route],
ignore: Ignore,
failures: Seq[PaymentFailure]) extends Data
/** /**
* When we exhaust our retry attempts without success, we abort the payment. * When we exhaust our retry attempts without success, we abort the payment.
* Once we're in that state, we wait for all the pending child payments to settle. * Once we're in that state, we wait for all the pending child payments to settle.
@ -373,6 +355,7 @@ object MultiPartPaymentLifecycle {
* @param pending pending child payments (we are waiting for them to be failed downstream). * @param pending pending child payments (we are waiting for them to be failed downstream).
*/ */
case class PaymentAborted(sender: ActorRef, request: SendMultiPartPayment, failures: Seq[PaymentFailure], pending: Set[UUID]) extends Data case class PaymentAborted(sender: ActorRef, request: SendMultiPartPayment, failures: Seq[PaymentFailure], pending: Set[UUID]) extends Data
/** /**
* Once we receive a first fulfill for a child payment, we can consider that the whole payment succeeded (because we * Once we receive a first fulfill for a child payment, we can consider that the whole payment succeeded (because we
* received the payment preimage that we can use as a proof of payment). * received the payment preimage that we can use as a proof of payment).
@ -385,160 +368,34 @@ object MultiPartPaymentLifecycle {
* @param pending pending child payments (we are waiting for them to be fulfilled downstream). * @param pending pending child payments (we are waiting for them to be fulfilled downstream).
*/ */
case class PaymentSucceeded(sender: ActorRef, request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID]) extends Data case class PaymentSucceeded(sender: ActorRef, request: SendMultiPartPayment, preimage: ByteVector32, parts: Seq[PartialPayment], pending: Set[UUID]) extends Data
// @formatter:on
/** If the payment failed immediately with a RouteNotFound, the channel we selected should be ignored in retries. */ private def createRouteRequest(nodeParams: NodeParams, toSend: MilliSatoshi, maxFee: MilliSatoshi, routeParams: RouteParams, d: PaymentProgress, cfg: SendPaymentConfig): RouteRequest =
private def shouldBlacklistChannel(pf: PaymentFailed): Boolean = pf.failures match { RouteRequest(
case LocalFailure(_, RouteNotFound) :: Nil => true nodeParams.nodeId,
case _ => false d.request.targetNodeId,
toSend,
maxFee,
d.request.assistedRoutes,
d.ignore,
Some(routeParams),
allowMultiPart = true,
d.pending.values.toSeq,
Some(cfg.paymentContext))
private def createChildPayment(route: Route, request: SendMultiPartPayment): SendPaymentToRoute = {
val finalPayload = Onion.createMultiPartPayload(route.amount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.additionalTlvs, request.userCustomTlvs)
SendPaymentToRoute(Right(route), finalPayload)
} }
def getFirstHopShortChannelId(payment: SendPayment): ShortChannelId = { /** When we receive an error from the final recipient, we should fail the whole payment, it's useless to retry. */
require(payment.routePrefix.nonEmpty, "multi-part payment must have a route prefix") private def isFinalRecipientFailure(pf: PaymentFailed, d: PaymentProgress): Boolean = pf.failures.collectFirst {
payment.routePrefix.head.lastUpdate.shortChannelId case f: RemoteFailure if f.e.originNode == d.request.targetNodeId => true
} }.getOrElse(false)
/** private def remainingToSend(nodeParams: NodeParams, request: SendMultiPartPayment, pending: Iterable[Route]): (MilliSatoshi, MilliSatoshi) = {
* If fee limits are provided, we need to divide them between all child payments. Otherwise we could end up paying val sentAmount = pending.map(_.amount).sum
* N * maxFee (where N is the number of child payments). val sentFees = pending.map(_.fee).sum
* Note that payment retries may mess up this calculation and make us pay a bit more than our fee limit. (request.totalAmount - sentAmount, request.getRouteParams(nodeParams, randomize = false).getMaxFee(request.totalAmount) - sentFees)
*
* TODO: @t-bast: the router should expose a GetMultiRouteRequest API; this is where fee calculations will be more
* accurate and path-finding will be more efficient.
*/
private def setFees(routeParams: Option[RouteParams], payments: Seq[SendPayment], paymentsCount: Int): Map[UUID, SendPayment] =
payments.map(p => {
val payment = routeParams match {
case Some(routeParams) => p.copy(routeParams = Some(routeParams.copy(maxFeeBase = routeParams.maxFeeBase / paymentsCount)))
case None => p
}
(UUID.randomUUID(), payment)
}).toMap
private def createChildPayment(nodeParams: NodeParams, request: SendMultiPartPayment, childAmount: MilliSatoshi, channel: OutgoingChannel): SendPayment = {
SendPayment(
request.targetNodeId,
Onion.createMultiPartPayload(childAmount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.additionalTlvs, request.userCustomTlvs),
request.maxAttempts,
request.assistedRoutes,
request.routeParams,
ChannelHop(nodeParams.nodeId, channel.nextNodeId, channel.channelUpdate) :: Nil)
}
/** Compute the maximum amount we should send in a single child payment. */
private def computeThreshold(networkStats: Option[NetworkStats], localChannels: Seq[OutgoingChannel]): MilliSatoshi = {
import com.google.common.math.Quantiles.median
import scala.collection.JavaConverters.asJavaCollectionConverter
// We use network statistics with a random factor to decide on the maximum amount for child payments.
// The current choice of parameters is completely arbitrary and could be made configurable.
// We could also learn from previous payment failures to dynamically tweak that value.
val maxAmount = networkStats.map(_.capacity.percentile75.toMilliSatoshi * ((75.0 + Random.nextInt(25)) / 100))
// If network statistics aren't available, we'll use our local channels to choose a value.
maxAmount.getOrElse({
val localBalanceMedian = median().compute(localChannels.map(b => java.lang.Long.valueOf(b.commitments.availableBalanceForSend.toLong)).asJavaCollection)
MilliSatoshi(localBalanceMedian.toLong)
})
}
/**
* Split a payment to a remote node inside a given channel.
*
* @param nodeParams node params.
* @param toSend total amount to send (may exceed the channel capacity if we have other channels available).
* @param request parent payment request.
* @param maxChildAmount maximum amount of each child payment inside that channel.
* @param maxFeeBase maximum base fee (for the future payment route).
* @param maxFeePct maximum proportional fee (for the future payment route).
* @param channel channel to use.
* @param channelCommitments channel commitments.
* @param channelPayments already-constructed child payments inside this channel.
* @return child payments to send through this channel.
*/
@tailrec
private def splitInsideChannel(nodeParams: NodeParams,
toSend: MilliSatoshi,
request: SendMultiPartPayment,
maxChildAmount: MilliSatoshi,
maxFeeBase: MilliSatoshi,
maxFeePct: Double,
channel: OutgoingChannel,
channelCommitments: Commitments,
channelPayments: Seq[SendPayment]): Seq[SendPayment] = {
// We can't use all the available balance because we need to take the fees for each child payment into account and
// we don't know the exact fee before-hand because we don't know the rest of the route yet (so we assume the worst
// case where the max fee is used).
val previousFees = channelPayments.map(p => maxFeeBase.max(p.finalPayload.amount * maxFeePct))
val totalPreviousFee = previousFees.sum
val withFeeBase = channelCommitments.availableBalanceForSend - maxFeeBase - totalPreviousFee
val withFeePct = channelCommitments.availableBalanceForSend * (1 - maxFeePct) - totalPreviousFee
val childAmount = Seq(maxChildAmount, toSend - channelPayments.map(_.finalPayload.amount).sum, withFeeBase, withFeePct).min
if (childAmount <= 0.msat) {
channelPayments
} else if (previousFees.nonEmpty && childAmount < previousFees.max) {
// We avoid sending tiny HTLCs: that would be a waste of fees.
channelPayments
} else {
val childPayment = createChildPayment(nodeParams, request, childAmount, channel)
// Splitting into multiple HTLCs in the same channel will also increase the size of the CommitTx (and thus its
// fee), which decreases the available balance.
// We need to take that into account when trying to send multiple payments through the same channel, which is
// why we simulate adding the HTLC to the commitments.
val fakeOnion = OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(Sphinx.PaymentPacket.PayloadLength)(0), ByteVector32.Zeroes)
val add = UpdateAddHtlc(channelCommitments.channelId, channelCommitments.localNextHtlcId + channelPayments.size, childAmount, ByteVector32.Zeroes, CltvExpiry(0), fakeOnion)
val updatedCommitments = channelCommitments.addLocalProposal(add)
splitInsideChannel(nodeParams, toSend, request, maxChildAmount, maxFeeBase, maxFeePct, channel, updatedCommitments, childPayment +: channelPayments)
}
}
/**
* Split a payment into many child payments.
*
* @param toSend amount to split.
* @param localChannels local channels balances.
* @param request payment request containing the total amount to send and routing hints and parameters.
* @param randomize randomize the channel selection.
* @return the child payments that should be then sent to PaymentLifecycle actors.
*/
def splitPayment(nodeParams: NodeParams, toSend: MilliSatoshi, localChannels: Seq[OutgoingChannel], networkStats: Option[NetworkStats], request: SendMultiPartPayment, randomize: Boolean): (MilliSatoshi, Seq[SendPayment]) = {
require(toSend > 0.msat, "amount to send must be greater than 0")
val maxFeePct = request.routeParams.map(_.maxFeePct).getOrElse(nodeParams.routerConf.searchMaxFeePct)
val maxFeeBase = request.routeParams.map(_.maxFeeBase).getOrElse(nodeParams.routerConf.searchMaxFeeBase.toMilliSatoshi)
@tailrec
def split(remaining: MilliSatoshi, payments: Seq[SendPayment], channels: Seq[OutgoingChannel], splitInsideChannel: (MilliSatoshi, OutgoingChannel) => Seq[SendPayment]): Seq[SendPayment] = channels match {
case Nil => payments
case _ if remaining == 0.msat => payments
case _ if remaining < 0.msat => throw new RuntimeException(s"payment splitting error: remaining amount must not be negative ($remaining): sending $toSend to ${request.targetNodeId} with local channels=${localChannels.map(_.toUsableBalance)}, current channels=${channels.map(_.toUsableBalance)}, network=${networkStats.map(_.capacity)}, fees=($maxFeeBase, $maxFeePct)")
case channel :: rest if channel.commitments.availableBalanceForSend == 0.msat => split(remaining, payments, rest, splitInsideChannel)
case channel :: rest =>
val childPayments = splitInsideChannel(remaining, channel)
split(remaining - childPayments.map(_.finalPayload.amount).sum, payments ++ childPayments, rest, splitInsideChannel)
}
// If we have direct channels to the target, we use them without splitting the payment inside each channel.
val channelsToTarget = localChannels.filter(p => p.nextNodeId == request.targetNodeId).sortBy(_.commitments.availableBalanceForSend)
val directPayments = split(toSend, Seq.empty, channelsToTarget, (remaining: MilliSatoshi, channel: OutgoingChannel) => {
// When using direct channels to the destination, it doesn't make sense to use retries so we set maxAttempts to 1.
createChildPayment(nodeParams, request.copy(maxAttempts = 1), remaining.min(channel.commitments.availableBalanceForSend), channel) :: Nil
})
// Otherwise we need to split the amount based on network statistics and pessimistic fees estimates.
// Note that this will be handled more gracefully once this logic is migrated inside the router.
val channels = if (randomize) {
Random.shuffle(localChannels.filter(p => p.nextNodeId != request.targetNodeId))
} else {
localChannels.filter(p => p.nextNodeId != request.targetNodeId).sortBy(_.commitments.availableBalanceForSend)
}
val remotePayments = split(toSend - directPayments.map(_.finalPayload.amount).sum, Seq.empty, channels, (remaining: MilliSatoshi, channel: OutgoingChannel) => {
// We re-generate a split threshold for each channel to randomize the amounts.
val maxChildAmount = computeThreshold(networkStats, localChannels)
splitInsideChannel(nodeParams, remaining, request, maxChildAmount, maxFeeBase, maxFeePct, channel, channel.commitments, Nil)
})
val childPayments = directPayments ++ remotePayments
(toSend - childPayments.map(_.finalPayload.amount).sum, childPayments)
} }
} }

View file

@ -41,8 +41,6 @@ object PaymentError {
// @formatter:on // @formatter:on
// @formatter:off // @formatter:off
/** Outbound capacity is too low. */
case object BalanceTooLow extends PaymentError
/** Payment attempts exhausted without success. */ /** Payment attempts exhausted without success. */
case object RetryExhausted extends PaymentError case object RetryExhausted extends PaymentError
// @formatter:on // @formatter:on

View file

@ -26,10 +26,11 @@ import fr.acinq.eclair.channel.{Channel, Upstream}
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment}
import fr.acinq.eclair.payment.send.PaymentError._ import fr.acinq.eclair.payment.send.PaymentError._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute} import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.router.Router.{Hop, NodeHop, Route, RouteParams} import fr.acinq.eclair.router.RouteNotFound
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
@ -37,7 +38,7 @@ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatos
/** /**
* Created by PM on 29/08/2016. * Created by PM on 29/08/2016.
*/ */
class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorRef, register: ActorRef) extends Actor with ActorLogging { class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends Actor with ActorLogging {
import PaymentInitiator._ import PaymentInitiator._
@ -83,19 +84,33 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
case pf: PaymentFailed => pending.get(pf.id).foreach(pp => { case pf: PaymentFailed => pending.get(pf.id).foreach(pp => {
val decryptedFailures = pf.failures.collect { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(_, f)) => f } val decryptedFailures = pf.failures.collect { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(_, f)) => f }
val canRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon) val shouldRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon)
pp.remainingAttempts match { if (shouldRetry) {
case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts if canRetry => pp.remainingAttempts match {
log.info(s"retrying trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta") case (trampolineFees, trampolineExpiryDelta) :: remaining =>
sendTrampolinePayment(pf.id, pp.r, trampolineFees, trampolineExpiryDelta) log.info(s"retrying trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta")
context become main(pending + (pf.id -> pp.copy(remainingAttempts = remainingAttempts))) sendTrampolinePayment(pf.id, pp.r, trampolineFees, trampolineExpiryDelta)
case _ => context become main(pending + (pf.id -> pp.copy(remainingAttempts = remaining)))
pp.sender ! pf case Nil =>
context.system.eventStream.publish(pf) log.info("trampoline node couldn't find a route after all retries")
context become main(pending - pf.id) val trampolineRoute = Seq(
NodeHop(nodeParams.nodeId, pp.r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat),
NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1)
)
val localFailure = pf.copy(failures = Seq(LocalFailure(trampolineRoute, RouteNotFound)))
pp.sender ! localFailure
context.system.eventStream.publish(localFailure)
context become main(pending - pf.id)
}
} else {
pp.sender ! pf
context.system.eventStream.publish(pf)
context become main(pending - pf.id)
} }
}) })
case _: PreimageReceived => // we received the preimage, but we wait for the PaymentSent event that will contain more data
case ps: PaymentSent => pending.get(ps.id).foreach(pp => { case ps: PaymentSent => pending.get(ps.id).foreach(pp => {
pp.sender ! ps pp.sender ! ps
context.system.eventStream.publish(ps) context.system.eventStream.publish(ps)
@ -116,12 +131,12 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32) val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32)
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePaymentRequest(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.finalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta) val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePaymentRequest(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.finalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo) payFsm forward SendPaymentToRoute(Left(r.route), Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo)
case Nil => case Nil =>
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
r.paymentRequest.paymentSecret match { r.paymentRequest.paymentSecret match {
case Some(paymentSecret) => payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret), r.paymentRequest.routingInfo) case Some(paymentSecret) => payFsm forward SendPaymentToRoute(Left(r.route), Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret), r.paymentRequest.routingInfo)
case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.paymentRequest.routingInfo) case None => payFsm forward SendPaymentToRoute(Left(r.route), FinalLegacyPayload(r.recipientAmount, finalExpiry), r.paymentRequest.routingInfo)
} }
case _ => case _ =>
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, TrampolineMultiNodeNotSupported) :: Nil) sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, TrampolineMultiNodeNotSupported) :: Nil)
@ -130,7 +145,7 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register)) def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register))
def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, relayer, router, register)) def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, router, register))
private def buildTrampolinePayment(r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = { private def buildTrampolinePayment(r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = {
val trampolineRoute = Seq( val trampolineRoute = Seq(
@ -163,7 +178,7 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
object PaymentInitiator { object PaymentInitiator {
def props(nodeParams: NodeParams, router: ActorRef, relayer: ActorRef, register: ActorRef) = Props(new PaymentInitiator(nodeParams, router, relayer, register)) def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef) = Props(new PaymentInitiator(nodeParams, router, register))
case class PendingPayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePaymentRequest) case class PendingPayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePaymentRequest)
@ -316,6 +331,8 @@ object PaymentInitiator {
def fullRoute(route: Route): Seq[Hop] = route.hops ++ additionalHops def fullRoute(route: Route): Seq[Hop] = route.hops ++ additionalHops
def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts) def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts)
def paymentContext: PaymentContext = PaymentContext(id, parentId, paymentHash)
} }
} }

View file

@ -75,44 +75,39 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
span.tag(Tags.Amount, c.finalPayload.amount.toLong) span.tag(Tags.Amount, c.finalPayload.amount.toLong)
span.tag(Tags.TotalAmount, c.finalPayload.totalAmount.toLong) span.tag(Tags.TotalAmount, c.finalPayload.totalAmount.toLong)
span.tag(Tags.Expiry, c.finalPayload.expiry.toLong) span.tag(Tags.Expiry, c.finalPayload.expiry.toLong)
log.debug("sending {} to route {}", c.finalPayload.amount, c.hops.mkString("->")) log.debug("sending {} to route {}", c.finalPayload.amount, c.printRoute())
val send = SendPayment(c.hops.last, c.finalPayload, maxAttempts = 1) val send = SendPayment(c.targetNodeId, c.finalPayload, maxAttempts = 1, assistedRoutes = c.assistedRoutes)
router ! FinalizeRoute(c.finalPayload.amount, c.hops, c.assistedRoutes) c.route.fold(
hops => router ! FinalizeRoute(c.finalPayload.amount, hops, c.assistedRoutes, paymentContext = Some(cfg.paymentContext)),
route => self ! RouteResponse(route :: Nil)
)
if (cfg.storeInDb) { if (cfg.storeInDb) {
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending)) paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
} }
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, Nil, Set.empty, Set.empty) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, Nil, Ignore.empty)
case Event(c: SendPayment, WaitingForRequest) => case Event(c: SendPayment, WaitingForRequest) =>
span.tag(Tags.TargetNodeId, c.targetNodeId.toString()) span.tag(Tags.TargetNodeId, c.targetNodeId.toString())
span.tag(Tags.Amount, c.finalPayload.amount.toLong) span.tag(Tags.Amount, c.finalPayload.amount.toLong)
span.tag(Tags.TotalAmount, c.finalPayload.totalAmount.toLong) span.tag(Tags.TotalAmount, c.finalPayload.totalAmount.toLong)
span.tag(Tags.Expiry, c.finalPayload.expiry.toLong) span.tag(Tags.Expiry, c.finalPayload.expiry.toLong)
log.debug("sending {} to {}{}", c.finalPayload.amount, c.targetNodeId, c.routePrefix.mkString(" with route prefix ", "->", "")) log.debug("sending {} to {}", c.finalPayload.amount, c.targetNodeId)
// We don't want the router to try cycling back to nodes that are at the beginning of the route. router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext))
val ignoredNodes = c.routePrefix.map(_.nodeId).toSet
if (c.routePrefix.lastOption.exists(_.nextNodeId == c.targetNodeId)) {
// If the sender already provided a route to the target, no need to involve the router.
self ! RouteResponse(Seq(Route(c.finalPayload.amount, Nil, allowEmpty = true)))
} else {
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, routeParams = c.routeParams, ignoreNodes = ignoredNodes)
}
if (cfg.storeInDb) { if (cfg.storeInDb) {
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending)) paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
} }
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, Nil, ignoredNodes, Set.empty) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, Nil, Ignore.empty)
} }
when(WAITING_FOR_ROUTE) { when(WAITING_FOR_ROUTE) {
case Event(RouteResponse(routes), WaitingForRoute(s, c, failures, ignoreNodes, ignoreChannels)) => case Event(RouteResponse(route +: _), WaitingForRoute(s, c, failures, ignore)) =>
val hops = c.routePrefix ++ routes.head.hops log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}")
log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId).mkString("->")}") val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(cfg.upstream, paymentHash, route.hops, c.finalPayload)
val firstHop = hops.head register ! Register.ForwardShortId(route.hops.head.lastUpdate.shortChannelId, cmd)
val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(cfg.upstream, paymentHash, hops, c.finalPayload) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignore, route)
register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd)
goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, Route(c.finalPayload.amount, hops))
case Event(Status.Failure(t), WaitingForRoute(s, _, failures, _, _)) => case Event(Status.Failure(t), WaitingForRoute(s, _, failures, _)) =>
log.warning("router error: {}", t.getMessage)
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, t))).increment() Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, t))).increment()
onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(Nil, t))) onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(Nil, t)))
myStop() myStop()
@ -121,7 +116,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
when(WAITING_FOR_PAYMENT_COMPLETE) { when(WAITING_FOR_PAYMENT_COMPLETE) {
case Event(ChannelCommandResponse.Ok, _) => stay case Event(ChannelCommandResponse.Ok, _) => stay
case Event(fulfill: Relayer.ForwardFulfill, WaitingForComplete(s, c, cmd, _, _, _, _, route)) => case Event(fulfill: Relayer.ForwardFulfill, WaitingForComplete(s, c, cmd, failures, _, _, route)) =>
Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(failures.size + 1)
val p = PartialPayment(id, c.finalPayload.amount, cmd.amount - c.finalPayload.amount, fulfill.htlc.channelId, Some(cfg.fullRoute(route))) val p = PartialPayment(id, c.finalPayload.amount, cmd.amount - c.finalPayload.amount, fulfill.htlc.channelId, Some(cfg.fullRoute(route)))
onSuccess(s, cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil)) onSuccess(s, cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil))
myStop() myStop()
@ -134,7 +130,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
} }
stay stay
case Event(fail: UpdateFailHtlc, data@WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, route)) => case Event(fail: UpdateFailHtlc, data@WaitingForComplete(s, c, _, failures, sharedSecrets, ignore, route)) =>
(Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { (Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match {
case success@Success(e) => case success@Success(e) =>
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(Nil, e))).increment() Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(Nil, e))).increment()
@ -153,6 +149,10 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
val failure = res match { val failure = res match {
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
failureMessage match {
case failureMessage: Update => handleUpdate(nodeId, failureMessage, data)
case _ =>
}
RemoteFailure(cfg.fullRoute(route), e) RemoteFailure(cfg.fullRoute(route), e)
case Failure(t) => case Failure(t) =>
log.warning(s"cannot parse returned error: ${t.getMessage}") log.warning(s"cannot parse returned error: ${t.getMessage}")
@ -162,7 +162,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
onFailure(s, PaymentFailed(id, paymentHash, failures :+ failure)) onFailure(s, PaymentFailed(id, paymentHash, failures :+ failure))
myStop() myStop()
case Failure(t) => case Failure(t) =>
log.warning(s"cannot parse returned error: ${t.getMessage}, route=${route.hops.map(_.nextNodeId)}") log.warning(s"cannot parse returned error: ${t.getMessage}, route=${route.printNodes()}")
val failure = UnreadableRemoteFailure(cfg.fullRoute(route)) val failure = UnreadableRemoteFailure(cfg.fullRoute(route))
retry(failure, data) retry(failure, data)
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) =>
@ -171,48 +171,18 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
retry(failure, data) retry(failure, data)
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
val ignoreNodes1 = if (Announcements.checkSig(failureMessage.update, nodeId)) { val ignore1 = if (Announcements.checkSig(failureMessage.update, nodeId)) {
route.getChannelUpdateForNode(nodeId) match { val assistedRoutes1 = handleUpdate(nodeId, failureMessage, data)
case Some(u) if u.shortChannelId != failureMessage.update.shortChannelId =>
// it is possible that nodes in the route prefer using a different channel (to the same N+1 node) than the one we requested, that's fine
log.info(s"received an update for a different channel than the one we asked: requested=${u.shortChannelId} actual=${failureMessage.update.shortChannelId} update=${failureMessage.update}")
case Some(u) if Announcements.areSame(u, failureMessage.update) =>
// node returned the exact same update we used, this can happen e.g. if the channel is imbalanced
// in that case, let's temporarily exclude the channel from future routes, giving it time to recover
log.info(s"received exact same update from nodeId=$nodeId, excluding the channel from futures routes")
val nextNodeId = route.hops.find(_.nodeId == nodeId).get.nextNodeId
router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId))
case Some(u) if PaymentFailure.hasAlreadyFailedOnce(nodeId, failures) =>
// this node had already given us a new channel update and is still unhappy, it is probably messing with us, let's exclude it
log.warning(s"it is the second time nodeId=$nodeId answers with a new update, excluding it: old=$u new=${failureMessage.update}")
val nextNodeId = route.hops.find(_.nodeId == nodeId).get.nextNodeId
router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId))
case Some(u) =>
log.info(s"got a new update for shortChannelId=${u.shortChannelId}: old=$u new=${failureMessage.update}")
case None =>
log.error(s"couldn't find a channel update for node=$nodeId, this should never happen")
}
// in any case, we forward the update to the router
router ! failureMessage.update
// we also update assisted routes, because they take precedence over the router's routing table
val assistedRoutes1 = c.assistedRoutes.map(_.map {
case extraHop: ExtraHop if extraHop.shortChannelId == failureMessage.update.shortChannelId => extraHop.copy(
cltvExpiryDelta = failureMessage.update.cltvExpiryDelta,
feeBase = failureMessage.update.feeBaseMsat,
feeProportionalMillionths = failureMessage.update.feeProportionalMillionths
)
case extraHop => extraHop
})
// let's try again, router will have updated its state // let's try again, router will have updated its state
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), assistedRoutes1, ignoreNodes, ignoreChannels, c.routeParams) router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), assistedRoutes1, ignore, c.routeParams, paymentContext = Some(cfg.paymentContext))
ignoreNodes ignore
} else { } else {
// this node is fishy, it gave us a bad sig!! let's filter it out // this node is fishy, it gave us a bad sig!! let's filter it out
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}") log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.getMaxFee(nodeParams), c.assistedRoutes, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext))
ignoreNodes + nodeId ignore + nodeId
} }
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(route), e), ignoreNodes1, ignoreChannels) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(route), e), ignore1)
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
val failure = RemoteFailure(cfg.fullRoute(route), e) val failure = RemoteFailure(cfg.fullRoute(route), e)
@ -228,10 +198,9 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
self ! Status.Failure(new RuntimeException("first hop returned an UpdateFailMalformedHtlc message")) self ! Status.Failure(new RuntimeException("first hop returned an UpdateFailMalformedHtlc message"))
stay stay
case Event(Status.Failure(t), data@WaitingForComplete(s, c, _, failures, _, _, _, hops)) => case Event(Status.Failure(t), data@WaitingForComplete(s, c, _, failures, _, _, hops)) =>
Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(cfg.fullRoute(hops), t))).increment() Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(cfg.fullRoute(hops), t))).increment()
val isFatal = failures.size + 1 >= c.maxAttempts || // retries exhausted val isFatal = failures.size + 1 >= c.maxAttempts || // retries exhausted
c.routePrefix.nonEmpty || // first hop was selected by the sender and failed, it doesn't make sense to retry
t.isInstanceOf[HtlcsTimedoutDownstream] // htlc timed out so retrying won't help, we need to re-compute cltvs t.isInstanceOf[HtlcsTimedoutDownstream] // htlc timed out so retrying won't help, we need to re-compute cltvs
if (isFatal) { if (isFatal) {
onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(cfg.fullRoute(hops), t))) onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(cfg.fullRoute(hops), t)))
@ -266,9 +235,57 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
} }
private def retry(failure: PaymentFailure, data: WaitingForComplete): FSM.State[PaymentLifecycle.State, PaymentLifecycle.Data] = { private def retry(failure: PaymentFailure, data: WaitingForComplete): FSM.State[PaymentLifecycle.State, PaymentLifecycle.Data] = {
val (ignoreNodes1, ignoreChannels1) = PaymentFailure.updateIgnored(failure, data.ignoreNodes, data.ignoreChannels) val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore)
router ! RouteRequest(data.c.getRouteRequestStart(nodeParams), data.c.targetNodeId, data.c.finalPayload.amount, data.c.getMaxFee(nodeParams), data.c.assistedRoutes, ignoreNodes1, ignoreChannels1, data.c.routeParams) router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.finalPayload.amount, data.c.getMaxFee(nodeParams), data.c.assistedRoutes, ignore1, data.c.routeParams, paymentContext = Some(cfg.paymentContext))
goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.sender, data.c, data.failures :+ failure, ignoreNodes1, ignoreChannels1) goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.sender, data.c, data.failures :+ failure, ignore1)
}
/**
* Apply the channel update to our routing table.
*
* @return updated routing hints if applicable.
*/
private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[Seq[ExtraHop]] = {
data.route.getChannelUpdateForNode(nodeId) match {
case Some(u) if u.shortChannelId != failure.update.shortChannelId =>
// it is possible that nodes in the route prefer using a different channel (to the same N+1 node) than the one we requested, that's fine
log.info(s"received an update for a different channel than the one we asked: requested=${u.shortChannelId} actual=${failure.update.shortChannelId} update=${failure.update}")
case Some(u) if Announcements.areSame(u, failure.update) =>
// node returned the exact same update we used, this can happen e.g. if the channel is imbalanced
// in that case, let's temporarily exclude the channel from future routes, giving it time to recover
log.info(s"received exact same update from nodeId=$nodeId, excluding the channel from futures routes")
val nextNodeId = data.route.hops.find(_.nodeId == nodeId).get.nextNodeId
router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId))
case Some(u) if PaymentFailure.hasAlreadyFailedOnce(nodeId, data.failures) =>
// this node had already given us a new channel update and is still unhappy, it is probably messing with us, let's exclude it
log.warning(s"it is the second time nodeId=$nodeId answers with a new update, excluding it: old=$u new=${failure.update}")
val nextNodeId = data.route.hops.find(_.nodeId == nodeId).get.nextNodeId
router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId))
case Some(u) =>
log.info(s"got a new update for shortChannelId=${u.shortChannelId}: old=$u new=${failure.update}")
case None =>
log.error(s"couldn't find a channel update for node=$nodeId, this should never happen")
}
// in any case, we forward the update to the router: if the channel is disabled, the router will remove it from its routing table
router ! failure.update
// we return updated assisted routes: they take precedence over the router's routing table
if (Announcements.isEnabled(failure.update.channelFlags)) {
data.c.assistedRoutes.map(_.map {
case extraHop: ExtraHop if extraHop.shortChannelId == failure.update.shortChannelId => extraHop.copy(
cltvExpiryDelta = failure.update.cltvExpiryDelta,
feeBase = failure.update.feeBaseMsat,
feeProportionalMillionths = failure.update.feeProportionalMillionths
)
case extraHop => extraHop
})
} else {
// if the channel is disabled, we temporarily exclude it: this is necessary because the routing hint doesn't contain
// channel flags to indicate that it's disabled
data.c.assistedRoutes.flatMap(r => RouteCalculation.toChannelDescs(r, data.c.targetNodeId))
.find(_.shortChannelId == failure.update.shortChannelId)
.foreach(desc => router ! ExcludeChannel(desc)) // we want the exclusion to be router-wide so that sister payments in the case of MPP are aware the channel is faulty
data.c.assistedRoutes
}
} }
private def myStop(): State = { private def myStop(): State = {
@ -312,12 +329,15 @@ object PaymentLifecycle {
/** /**
* Send a payment to a pre-defined route without running the path-finding algorithm. * Send a payment to a pre-defined route without running the path-finding algorithm.
* *
* @param hops payment route to use. * @param route payment route to use.
* @param finalPayload onion payload for the target node. * @param finalPayload onion payload for the target node.
*/ */
case class SendPaymentToRoute(hops: Seq[PublicKey], finalPayload: FinalPayload, assistedRoutes: Seq[Seq[ExtraHop]] = Nil) { case class SendPaymentToRoute(route: Either[Seq[PublicKey], Route], finalPayload: FinalPayload, assistedRoutes: Seq[Seq[ExtraHop]] = Nil) {
require(hops.nonEmpty, s"payment route must not be empty") require(route.fold(_.nonEmpty, _.hops.nonEmpty), "payment route must not be empty")
val targetNodeId = hops.last
val targetNodeId = route.fold(_.last, _.hops.last.nextNodeId)
def printRoute(): String = route.fold(nodes => nodes, _.hops.map(_.nextNodeId)).mkString("->")
} }
/** /**
@ -329,32 +349,24 @@ object PaymentLifecycle {
* @param maxAttempts maximum number of retries. * @param maxAttempts maximum number of retries.
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice). * @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
* @param routeParams parameters to fine-tune the routing algorithm. * @param routeParams parameters to fine-tune the routing algorithm.
* @param routePrefix when provided, the payment route will start with these hops. Path-finding will run only to
* find how to route from the last node of the route prefix to the target node.
*/ */
case class SendPayment(targetNodeId: PublicKey, case class SendPayment(targetNodeId: PublicKey,
finalPayload: FinalPayload, finalPayload: FinalPayload,
maxAttempts: Int, maxAttempts: Int,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil, assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: Option[RouteParams] = None, routeParams: Option[RouteParams] = None) {
routePrefix: Seq[ChannelHop] = Nil) {
require(finalPayload.amount > 0.msat, s"amount must be > 0") require(finalPayload.amount > 0.msat, s"amount must be > 0")
def getMaxFee(nodeParams: NodeParams): MilliSatoshi = def getMaxFee(nodeParams: NodeParams): MilliSatoshi =
routeParams.getOrElse(RouteCalculation.getDefaultRouteParams(nodeParams.routerConf)).getMaxFee(finalPayload.amount) routeParams.getOrElse(RouteCalculation.getDefaultRouteParams(nodeParams.routerConf)).getMaxFee(finalPayload.amount)
/** Returns the node from which the path-finding algorithm should start. */
def getRouteRequestStart(nodeParams: NodeParams): PublicKey = routePrefix match {
case Nil => nodeParams.nodeId
case prefix => prefix.last.nextNodeId
}
} }
// @formatter:off // @formatter:off
sealed trait Data sealed trait Data
case object WaitingForRequest extends Data case object WaitingForRequest extends Data
case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) extends Data case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure], ignore: Ignore) extends Data
case class WaitingForComplete(sender: ActorRef, c: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[(ByteVector32, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc], route: Route) extends Data case class WaitingForComplete(sender: ActorRef, c: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[(ByteVector32, PublicKey)], ignore: Ignore, route: Route) extends Data
sealed trait State sealed trait State
case object WAITING_FOR_REQUEST extends State case object WAITING_FOR_REQUEST extends State

View file

@ -28,6 +28,7 @@ object Monitoring {
val FindRouteDuration = Kamon.timer("router.find-route.duration", "Path-finding duration") val FindRouteDuration = Kamon.timer("router.find-route.duration", "Path-finding duration")
val FindRouteErrors = Kamon.counter("router.find-route.errors", "Path-finding errors") val FindRouteErrors = Kamon.counter("router.find-route.errors", "Path-finding errors")
val RouteLength = Kamon.histogram("router.find-route.length", "Path-finding result length") val RouteLength = Kamon.histogram("router.find-route.length", "Path-finding result length")
val RouteResults = Kamon.histogram("router.find-route.results", "Path-finding number of routes found")
object QueryChannelRange { object QueryChannelRange {
val Blocks = Kamon.histogram("router.gossip.query-channel-range.blocks", "Number of blocks requested in query-channel-range") val Blocks = Kamon.histogram("router.gossip.query-channel-range.blocks", "Number of blocks requested in query-channel-range")
@ -71,6 +72,7 @@ object Monitoring {
val Announced = "announced" val Announced = "announced"
val Direction = "direction" val Direction = "direction"
val Error = "error" val Error = "error"
val MultiPart = "multiPart"
val NumberOfRoutes = "numRoutes" val NumberOfRoutes = "numRoutes"
object Directions { object Directions {

View file

@ -17,9 +17,10 @@
package fr.acinq.eclair.router package fr.acinq.eclair.router
import akka.actor.{ActorContext, ActorRef, Status} import akka.actor.{ActorContext, ActorRef, Status}
import akka.event.LoggingAdapter import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi}
import fr.acinq.eclair.Logs.LogCategory
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop
import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge}
@ -28,6 +29,7 @@ import fr.acinq.eclair.router.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.ChannelUpdate import fr.acinq.eclair.wire.ChannelUpdate
import fr.acinq.eclair.{ShortChannelId, _} import fr.acinq.eclair.{ShortChannelId, _}
import kamon.tag.TagSet
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable import scala.collection.mutable
@ -36,54 +38,75 @@ import scala.util.{Failure, Random, Success, Try}
object RouteCalculation { object RouteCalculation {
def finalizeRoute(d: Data, fr: FinalizeRoute)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { def finalizeRoute(d: Data, fr: FinalizeRoute)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors Logs.withMdc(log)(Logs.mdc(
category_opt = Some(LogCategory.PAYMENT),
parentPaymentId_opt = fr.paymentContext.map(_.parentId),
paymentId_opt = fr.paymentContext.map(_.id),
paymentHash_opt = fr.paymentContext.map(_.paymentHash))) {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.hops.last, fr.amount)).toMap val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.hops.last, fr.amount)).toMap
val extraEdges = assistedChannels.values.map(ac => val extraEdges = assistedChannels.values.map(ac =>
GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum)) GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum))
).toSet ).toSet
val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) }
// split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs // split into sublists [(a,b),(b,c), ...] then get the edges between each of those pairs
fr.hops.sliding(2).map { case List(v1, v2) => g.getEdgesBetween(v1, v2) }.toList match { fr.hops.sliding(2).map { case List(v1, v2) => g.getEdgesBetween(v1, v2) }.toList match {
case edges if edges.nonEmpty && edges.forall(_.nonEmpty) => case edges if edges.nonEmpty && edges.forall(_.nonEmpty) =>
// select the largest edge (using balance when available, otherwise capacity). // select the largest edge (using balance when available, otherwise capacity).
val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi))) val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi)))
val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update)) val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update))
ctx.sender ! RouteResponse(Route(fr.amount, hops) :: Nil) ctx.sender ! RouteResponse(Route(fr.amount, hops) :: Nil)
case _ => // some nodes in the supplied route aren't connected in our graph case _ => // some nodes in the supplied route aren't connected in our graph
ctx.sender ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) ctx.sender ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels"))
}
d
} }
d
} }
def handleRouteRequest(d: Data, routerConf: RouterConf, currentBlockHeight: Long, r: RouteRequest)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { def handleRouteRequest(d: Data, routerConf: RouterConf, currentBlockHeight: Long, r: RouteRequest)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors Logs.withMdc(log)(Logs.mdc(
category_opt = Some(LogCategory.PAYMENT),
parentPaymentId_opt = r.paymentContext.map(_.parentId),
paymentId_opt = r.paymentContext.map(_.id),
paymentHash_opt = r.paymentContext.map(_.paymentHash))) {
implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors
// we convert extra routing info provided in the payment request to fake channel_update // we convert extra routing info provided in the payment request to fake channel_update
// it takes precedence over all other channel_updates we know // it takes precedence over all other channel_updates we know
val assistedChannels: Map[ShortChannelId, AssistedChannel] = r.assistedRoutes.flatMap(toAssistedChannels(_, r.target, r.amount)).toMap val assistedChannels: Map[ShortChannelId, AssistedChannel] = r.assistedRoutes.flatMap(toAssistedChannels(_, r.target, r.amount))
val extraEdges = assistedChannels.values.map(ac => .filterNot { case (_, ac) => ac.extraHop.nodeId == r.source } // we ignore routing hints for our own channels, we have more accurate information
GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum)) .toMap
).toSet val extraEdges = assistedChannels.values.map(ac =>
val ignoredEdges = r.ignoreChannels ++ d.excludedChannels GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum))
val defaultRouteParams: RouteParams = getDefaultRouteParams(routerConf) ).toSet
val params = r.routeParams.getOrElse(defaultRouteParams) val ignoredEdges = r.ignore.channels ++ d.excludedChannels
val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 val params = r.routeParams.getOrElse(getDefaultRouteParams(routerConf))
val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1
log.info(s"finding a route ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignoreNodes.map(_.value).mkString(","), r.ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(","))
log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params) log.info("finding routes with randomize={} params={}", params.randomize, params)
KamonExt.time(Metrics.FindRouteDuration.withTag(Tags.NumberOfRoutes, routesToFind).withTag(Tags.Amount, Tags.amountBucket(r.amount))) { val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount))
findRoute(d.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignoreNodes, params, currentBlockHeight) match { KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) {
case Success(routes) => val result = if (r.allowMultiPart) {
Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(r.amount)).record(routes.head.length) findMultiPartRoute(d.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight)
ctx.sender ! RouteResponse(routes) } else {
case Failure(t) => findRoute(d.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight)
Metrics.FindRouteErrors.withTag(Tags.Amount, Tags.amountBucket(r.amount)).withTag(Tags.Error, t.getClass.getSimpleName).increment() }
ctx.sender ! Status.Failure(t) result match {
case Success(routes) =>
Metrics.RouteResults.withTags(tags).record(routes.length)
routes.foreach(route => Metrics.RouteLength.withTags(tags).record(route.length))
ctx.sender ! RouteResponse(routes)
case Failure(t) =>
val failure = if (isNeighborBalanceTooLow(d.graph, r)) BalanceTooLow else t
Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, failure.getClass.getSimpleName)).increment()
ctx.sender ! Status.Failure(failure)
}
} }
d
} }
d
} }
private def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = { private def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = {
@ -107,6 +130,11 @@ object RouteCalculation {
}._2 }._2
} }
def toChannelDescs(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Seq[ChannelDesc] = {
val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId
extraRoute.zip(nextNodeIds).map { case (hop, nextNodeId) => ChannelDesc(hop.shortChannelId, hop.nodeId, nextNodeId) }
}
/** Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount. */ /** Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount. */
private def htlcMaxToCapacity(htlcMaximum: MilliSatoshi): Satoshi = htlcMaximum.truncateToSatoshi + 1.sat private def htlcMaxToCapacity(htlcMaximum: MilliSatoshi): Satoshi = htlcMaximum.truncateToSatoshi + 1.sat
@ -357,4 +385,14 @@ object RouteCalculation {
amountOk && feeOk amountOk && feeOk
} }
/**
* Checks if we are directly connected to the target but don't have enough balance in our local channels to send the
* requested amount. We could potentially relay the payment by using indirect routes, but since we're connected to
* the target node it means we'd like to reach it via direct channels as much as possible.
*/
private def isNeighborBalanceTooLow(g: DirectedGraph, r: RouteRequest): Boolean = {
val neighborEdges = g.getEdgesBetween(r.source, r.target)
neighborEdges.nonEmpty && neighborEdges.map(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi)).sum < r.amount
}
} }

View file

@ -16,6 +16,8 @@
package fr.acinq.eclair.router package fr.acinq.eclair.router
import java.util.UUID
import akka.Done import akka.Done
import akka.actor.{ActorRef, Props} import akka.actor.{ActorRef, Props}
import akka.event.DiagnosticLoggingAdapter import akka.event.DiagnosticLoggingAdapter
@ -31,6 +33,7 @@ import fr.acinq.eclair.crypto.TransportHandler
import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.db.NetworkDb
import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.io.Peer.PeerRoutingMessage
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph
import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Graph.WeightRatios
import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Monitoring.{Metrics, Tags}
@ -341,19 +344,44 @@ object Router {
} }
} }
case class Ignore(nodes: Set[PublicKey], channels: Set[ChannelDesc]) {
// @formatter:off
def +(ignoreNode: PublicKey): Ignore = copy(nodes = nodes + ignoreNode)
def ++(ignoreNodes: Set[PublicKey]): Ignore = copy(nodes = nodes ++ ignoreNodes)
def +(ignoreChannel: ChannelDesc): Ignore = copy(channels = channels + ignoreChannel)
def emptyNodes(): Ignore = copy(nodes = Set.empty)
def emptyChannels(): Ignore = copy(channels = Set.empty)
// @formatter:on
}
object Ignore {
def empty: Ignore = Ignore(Set.empty, Set.empty)
}
case class RouteRequest(source: PublicKey, case class RouteRequest(source: PublicKey,
target: PublicKey, target: PublicKey,
amount: MilliSatoshi, amount: MilliSatoshi,
maxFee: MilliSatoshi, maxFee: MilliSatoshi,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil, assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
ignoreNodes: Set[PublicKey] = Set.empty, ignore: Ignore = Ignore.empty,
ignoreChannels: Set[ChannelDesc] = Set.empty, routeParams: Option[RouteParams] = None,
routeParams: Option[RouteParams] = None) allowMultiPart: Boolean = false,
pendingPayments: Seq[Route] = Nil,
paymentContext: Option[PaymentContext] = None)
case class FinalizeRoute(amount: MilliSatoshi, hops: Seq[PublicKey], assistedRoutes: Seq[Seq[ExtraHop]] = Nil) case class FinalizeRoute(amount: MilliSatoshi,
hops: Seq[PublicKey],
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
paymentContext: Option[PaymentContext] = None)
/**
* Useful for having appropriate logging context at hand when finding routes
*/
case class PaymentContext(id: UUID, parentId: UUID, paymentHash: ByteVector32)
case class Route(amount: MilliSatoshi, hops: Seq[ChannelHop]) {
require(hops.nonEmpty, "route cannot be empty")
case class Route(amount: MilliSatoshi, hops: Seq[ChannelHop], allowEmpty: Boolean = false) {
require(allowEmpty || hops.nonEmpty, "route cannot be empty")
val length = hops.length val length = hops.length
lazy val fee: MilliSatoshi = { lazy val fee: MilliSatoshi = {
val amountToSend = hops.drop(1).reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } val amountToSend = hops.drop(1).reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) }
@ -362,6 +390,11 @@ object Router {
/** This method retrieves the channel update that we used when we built the route. */ /** This method retrieves the channel update that we used when we built the route. */
def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate) def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate)
def printNodes(): String = hops.map(_.nextNodeId).mkString("->")
def printChannels(): String = hops.map(_.lastUpdate.shortChannelId).mkString("->")
} }
case class RouteResponse(routes: Seq[Route]) { case class RouteResponse(routes: Seq[Route]) {

View file

@ -24,4 +24,6 @@ class RouterException(message: String) extends RuntimeException(message)
object RouteNotFound extends RouterException("route not found") object RouteNotFound extends RouterException("route not found")
object BalanceTooLow extends RouterException("balance too low")
object CannotRouteToSelf extends RouterException("cannot route to self") object CannotRouteToSelf extends RouterException("cannot route to self")

View file

@ -475,25 +475,29 @@ object Validation {
} }
} }
def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged): Data = { def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = {
val (channels1, graph1) = d.channels.get(e.shortChannelId) match { val desc = ChannelDesc(e.shortChannelId, e.commitments.localParams.nodeId, e.commitments.remoteParams.nodeId)
val (publicChannels1, graph1) = d.channels.get(e.shortChannelId) match {
case Some(pc) => case Some(pc) =>
val pc1 = pc.updateBalances(e.commitments) val pc1 = pc.updateBalances(e.commitments)
val desc = ChannelDesc(e.shortChannelId, e.commitments.localParams.nodeId, e.commitments.remoteParams.nodeId) log.debug("public channel balance updated: {}", pc1)
val update_opt = if (e.commitments.localParams.nodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt val update_opt = if (e.commitments.localParams.nodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt
val graph1 = update_opt.map(u => d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph) val graph1 = update_opt.map(u => d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph)
(d.channels + (e.shortChannelId -> pc1), graph1) (d.channels + (e.shortChannelId -> pc1), graph1)
case None => case None =>
(d.channels, d.graph) (d.channels, d.graph)
} }
val privateChannels1 = d.privateChannels.get(e.shortChannelId) match { val (privateChannels1, graph2) = d.privateChannels.get(e.shortChannelId) match {
case Some(pc) => case Some(pc) =>
val pc1 = pc.updateBalances(e.commitments) val pc1 = pc.updateBalances(e.commitments)
d.privateChannels + (e.shortChannelId -> pc1) log.debug("private channel balance updated: {}", pc1)
val update_opt = if (e.commitments.localParams.nodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt
val graph2 = update_opt.map(u => graph1.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(graph1)
(d.privateChannels + (e.shortChannelId -> pc1), graph2)
case None => case None =>
d.privateChannels (d.privateChannels, graph1)
} }
d.copy(channels = channels1, privateChannels = privateChannels1, graph = graph1) d.copy(channels = publicChannels1, privateChannels = privateChannels1, graph = graph2)
} }
} }

View file

@ -21,7 +21,7 @@ object Kamon {
def withoutTags() = this def withoutTags() = this
def withTags(args: TagSet, a: Boolean) = this def withTags(args: TagSet) = this
def withTags(a: TagSet, b: TagSet, c: Boolean) = this def withTags(a: TagSet, b: TagSet, c: Boolean) = this
@ -39,6 +39,8 @@ object Kamon {
def increment(a: Int) = this def increment(a: Int) = this
def increment(a: Long) = this
def decrement() = this def decrement() = this
def record(a: Long) = this def record(a: Long) = this

View file

@ -1,6 +1,10 @@
package kamon.tag package kamon.tag
trait TagSet trait TagSet {
def withTag(t: String, s: Boolean) = this
def withTag(a: String, value: Long) = this
def withTag(a: String, value: String) = this
}
object TagSet extends TagSet { object TagSet extends TagSet {
def Empty: TagSet = this def Empty: TagSet = this
def of(t: String, s: String) = this def of(t: String, s: String) = this

View file

@ -35,6 +35,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPa
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort
import fr.acinq.eclair.router.Router.{GetNetworkStats, GetNetworkStatsResponse, PublicChannel} import fr.acinq.eclair.router.Router.{GetNetworkStats, GetNetworkStatsResponse, PublicChannel}
import fr.acinq.eclair.router.{Announcements, NetworkStats, Router, Stats} import fr.acinq.eclair.router.{Announcements, NetworkStats, Router, Stats}
import fr.acinq.eclair.wire.{Color, NodeAnnouncement}
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.scalatest.IdiomaticMockito import org.mockito.scalatest.IdiomaticMockito
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
@ -150,6 +151,54 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
assertThrows[IllegalArgumentException](Await.result(eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(expiredInvoice)), 50 millis)) assertThrows[IllegalArgumentException](Await.result(eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(expiredInvoice)), 50 millis))
} }
test("return node announcements") { f =>
import f._
val eclair = new EclairImpl(kit)
val remoteNodeAnn1 = NodeAnnouncement(randomBytes64, Features.empty, 42L, randomKey.publicKey, Color(42, 42, 42), "LN-rocks", Nil)
val remoteNodeAnn2 = NodeAnnouncement(randomBytes64, Features.empty, 43L, randomKey.publicKey, Color(43, 43, 43), "LN-papers", Nil)
val allNodes = Seq(
NodeAnnouncement(randomBytes64, Features.empty, 561L, randomKey.publicKey, Color(0, 0, 0), "some-node", Nil),
remoteNodeAnn1,
remoteNodeAnn2,
NodeAnnouncement(randomBytes64, Features.empty, 1105L, randomKey.publicKey, Color(0, 0, 0), "some-other-node", Nil)
)
{
val fRes = eclair.nodes()
router.expectMsg(Symbol("nodes"))
router.reply(allNodes)
awaitCond(fRes.value match {
case Some(Success(nodes)) =>
assert(nodes.toSet === allNodes.toSet)
true
case _ => false
})
}
{
val fRes = eclair.nodes(Some(Set(remoteNodeAnn1.nodeId, remoteNodeAnn2.nodeId)))
router.expectMsg(Symbol("nodes"))
router.reply(allNodes)
awaitCond(fRes.value match {
case Some(Success(nodes)) =>
assert(nodes.toSet === Set(remoteNodeAnn1, remoteNodeAnn2))
true
case _ => false
})
}
{
val fRes = eclair.nodes(Some(Set(randomKey.publicKey)))
router.expectMsg(Symbol("nodes"))
router.reply(allNodes)
awaitCond(fRes.value match {
case Some(Success(nodes)) =>
assert(nodes.isEmpty)
true
case _ => false
})
}
}
ignore("allupdates can filter by nodeId") { f => ignore("allupdates can filter by nodeId") { f =>
import f._ import f._

View file

@ -22,10 +22,10 @@ import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.Block
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.FeatureSupport.Mandatory import fr.acinq.eclair.FeatureSupport.Mandatory
import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.Features._
import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.crypto.LocalKeyManager
import scodec.bits.ByteVector
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.ByteVector
import scala.collection.JavaConversions._ import scala.collection.JavaConversions._
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
@ -46,6 +46,7 @@ class StartupSpec extends AnyFunSuite {
test("check configuration") { test("check configuration") {
assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair"))).isSuccess) assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair"))).isSuccess)
assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair").withFallback(ConfigFactory.parseMap(Map("max-feerate-mismatch" -> 42).asJava)))).isFailure) assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair").withFallback(ConfigFactory.parseMap(Map("max-feerate-mismatch" -> 42).asJava)))).isFailure)
assert(Try(makeNodeParamsWithDefaults(ConfigFactory.load().getConfig("eclair").withFallback(ConfigFactory.parseMap(Map("on-chain-fees.max-feerate-mismatch" -> 1.56).asJava)))).isFailure)
} }
test("NodeParams should fail if the alias is illegal (over 32 bytes)") { test("NodeParams should fail if the alias is illegal (over 32 bytes)") {
@ -84,6 +85,10 @@ class StartupSpec extends AnyFunSuite {
} }
test("NodeParams should fail if features are inconsistent") { test("NodeParams should fail if features are inconsistent") {
// Because of https://github.com/ACINQ/eclair/issues/1434, we need to remove the default features when falling back
// to the default configuration.
def finalizeConf(testCfg: Config): Config = testCfg.withFallback(defaultConf.withoutPath("features"))
val legalFeaturesConf = ConfigFactory.parseMap(Map( val legalFeaturesConf = ConfigFactory.parseMap(Map(
s"features.${OptionDataLossProtect.rfcName}" -> "optional", s"features.${OptionDataLossProtect.rfcName}" -> "optional",
s"features.${InitialRoutingSync.rfcName}" -> "optional", s"features.${InitialRoutingSync.rfcName}" -> "optional",
@ -105,9 +110,9 @@ class StartupSpec extends AnyFunSuite {
s"features.${BasicMultiPartPayment.rfcName}" -> "optional" s"features.${BasicMultiPartPayment.rfcName}" -> "optional"
).asJava) ).asJava)
assert(Try(makeNodeParamsWithDefaults(legalFeaturesConf.withFallback(defaultConf))).isSuccess) assert(Try(makeNodeParamsWithDefaults(finalizeConf(legalFeaturesConf))).isSuccess)
assert(Try(makeNodeParamsWithDefaults(illegalButAllowedFeaturesConf.withFallback(defaultConf))).isSuccess) assert(Try(makeNodeParamsWithDefaults(finalizeConf(illegalButAllowedFeaturesConf))).isSuccess)
assert(Try(makeNodeParamsWithDefaults(illegalFeaturesConf.withFallback(defaultConf))).isFailure) assert(Try(makeNodeParamsWithDefaults(finalizeConf(illegalFeaturesConf))).isFailure)
} }
test("parse human readable override features") { test("parse human readable override features") {

View file

@ -22,9 +22,9 @@ import java.util.concurrent.atomic.AtomicLong
import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Block, ByteVector32, Script} import fr.acinq.bitcoin.{Block, ByteVector32, Script}
import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.FeatureSupport.Optional
import fr.acinq.eclair.Features.{ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, VariableLengthOnion} import fr.acinq.eclair.Features._
import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.NodeParams.BITCOIND
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.crypto.LocalKeyManager
import fr.acinq.eclair.db._ import fr.acinq.eclair.db._
import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer
@ -84,7 +84,7 @@ object TestConstants {
onChainFeeConf = OnChainFeeConf( onChainFeeConf = OnChainFeeConf(
feeTargets = FeeTargets(6, 2, 2, 6), feeTargets = FeeTargets(6, 2, 2, 6),
feeEstimator = new TestFeeEstimator, feeEstimator = new TestFeeEstimator,
maxFeerateMismatch = 1.5, maxFeerateMismatch = FeerateTolerance(0.5, 8.0),
closeOnOfflineMismatch = true, closeOnOfflineMismatch = true,
updateFeeMinDiffRatio = 0.1 updateFeeMinDiffRatio = 0.1
), ),
@ -170,7 +170,7 @@ object TestConstants {
onChainFeeConf = OnChainFeeConf( onChainFeeConf = OnChainFeeConf(
feeTargets = FeeTargets(6, 2, 2, 6), feeTargets = FeeTargets(6, 2, 2, 6),
feeEstimator = new TestFeeEstimator, feeEstimator = new TestFeeEstimator,
maxFeerateMismatch = 1.0, maxFeerateMismatch = FeerateTolerance(0.75, 1.5),
closeOnOfflineMismatch = true, closeOnOfflineMismatch = true,
updateFeeMinDiffRatio = 0.1 updateFeeMinDiffRatio = 0.1
), ),

View file

@ -20,6 +20,8 @@ import java.util.UUID
import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{DeterministicWallet, Satoshi, Transaction} import fr.acinq.bitcoin.{DeterministicWallet, Satoshi, Transaction}
import fr.acinq.eclair.TestConstants.TestFeeEstimator
import fr.acinq.eclair.blockchain.fee.{FeeTargets, FeerateTolerance, OnChainFeeConf}
import fr.acinq.eclair.channel.Commitments._ import fr.acinq.eclair.channel.Commitments._
import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.states.StateTestsHelperMethods
@ -42,6 +44,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging
val feeConfNoMismatch = OnChainFeeConf(FeeTargets(6, 2, 2, 6), new TestFeeEstimator, FeerateTolerance(0.00001, 100000.0), closeOnOfflineMismatch = false, 1.0)
override def withFixture(test: OneArgTest): Outcome = { override def withFixture(test: OneArgTest): Outcome = {
val setup = init() val setup = init()
import setup._ import setup._
@ -66,8 +70,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a - htlcOutputFee) assert(bc0.availableBalanceForReceive == a - htlcOutputFee)
val (_, cmdAdd) = makeCmdAdd(a - htlcOutputFee - 1000.msat, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(a - htlcOutputFee - 1000.msat, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
val Success(bc1) = receiveAdd(bc0, add) val Success(bc1) = receiveAdd(bc0, add, bob.underlyingActor.nodeParams.onChainFeeConf)
val Success((_, commit1)) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager) val Success((_, commit1)) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager)
val Success((bc2, _)) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager) val Success((bc2, _)) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager)
// we don't take into account the additional HTLC fee since Alice's balance is below the trim threshold. // we don't take into account the additional HTLC fee since Alice's balance is below the trim threshold.
@ -94,11 +98,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a) assert(bc0.availableBalanceForReceive == a)
val (payment_preimage, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (payment_preimage, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac1.availableBalanceForSend == a - p - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac1.availableBalanceForSend == a - p - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac1.availableBalanceForReceive == b) assert(ac1.availableBalanceForReceive == b)
val Success(bc1) = receiveAdd(bc0, add) val Success(bc1) = receiveAdd(bc0, add, bob.underlyingActor.nodeParams.onChainFeeConf)
assert(bc1.availableBalanceForSend == b) assert(bc1.availableBalanceForSend == b)
assert(bc1.availableBalanceForReceive == a - p - fee) assert(bc1.availableBalanceForReceive == a - p - fee)
@ -179,11 +183,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a) assert(bc0.availableBalanceForReceive == a)
val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac1.availableBalanceForSend == a - p - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac1.availableBalanceForSend == a - p - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac1.availableBalanceForReceive == b) assert(ac1.availableBalanceForReceive == b)
val Success(bc1) = receiveAdd(bc0, add) val Success(bc1) = receiveAdd(bc0, add, bob.underlyingActor.nodeParams.onChainFeeConf)
assert(bc1.availableBalanceForSend == b) assert(bc1.availableBalanceForSend == b)
assert(bc1.availableBalanceForReceive == a - p - fee) assert(bc1.availableBalanceForReceive == a - p - fee)
@ -267,29 +271,29 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(bc0.availableBalanceForReceive == a) assert(bc0.availableBalanceForReceive == a)
val (payment_preimage1, cmdAdd1) = makeCmdAdd(p1, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (payment_preimage1, cmdAdd1) = makeCmdAdd(p1, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((ac1, add1)) = sendAdd(ac0, cmdAdd1, Local(UUID.randomUUID, None), currentBlockHeight) val Success((ac1, add1)) = sendAdd(ac0, cmdAdd1, Local(UUID.randomUUID, None), currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac1.availableBalanceForSend == a - p1 - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac1.availableBalanceForSend == a - p1 - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac1.availableBalanceForReceive == b) assert(ac1.availableBalanceForReceive == b)
val (_, cmdAdd2) = makeCmdAdd(p2, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (_, cmdAdd2) = makeCmdAdd(p2, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((ac2, add2)) = sendAdd(ac1, cmdAdd2, Local(UUID.randomUUID, None), currentBlockHeight) val Success((ac2, add2)) = sendAdd(ac1, cmdAdd2, Local(UUID.randomUUID, None), currentBlockHeight, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac2.availableBalanceForSend == a - p1 - fee - p2 - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees) assert(ac2.availableBalanceForSend == a - p1 - fee - p2 - fee) // as soon as htlc is sent, alice sees its balance decrease (more than the payment amount because of the commitment fees)
assert(ac2.availableBalanceForReceive == b) assert(ac2.availableBalanceForReceive == b)
val (payment_preimage3, cmdAdd3) = makeCmdAdd(p3, alice.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (payment_preimage3, cmdAdd3) = makeCmdAdd(p3, alice.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((bc1, add3)) = sendAdd(bc0, cmdAdd3, Local(UUID.randomUUID, None), currentBlockHeight) val Success((bc1, add3)) = sendAdd(bc0, cmdAdd3, Local(UUID.randomUUID, None), currentBlockHeight, bob.underlyingActor.nodeParams.onChainFeeConf)
assert(bc1.availableBalanceForSend == b - p3) // bob doesn't pay the fee assert(bc1.availableBalanceForSend == b - p3) // bob doesn't pay the fee
assert(bc1.availableBalanceForReceive == a) assert(bc1.availableBalanceForReceive == a)
val Success(bc2) = receiveAdd(bc1, add1) val Success(bc2) = receiveAdd(bc1, add1, bob.underlyingActor.nodeParams.onChainFeeConf)
assert(bc2.availableBalanceForSend == b - p3) assert(bc2.availableBalanceForSend == b - p3)
assert(bc2.availableBalanceForReceive == a - p1 - fee) assert(bc2.availableBalanceForReceive == a - p1 - fee)
val Success(bc3) = receiveAdd(bc2, add2) val Success(bc3) = receiveAdd(bc2, add2, bob.underlyingActor.nodeParams.onChainFeeConf)
assert(bc3.availableBalanceForSend == b - p3) assert(bc3.availableBalanceForSend == b - p3)
assert(bc3.availableBalanceForReceive == a - p1 - fee - p2 - fee) assert(bc3.availableBalanceForReceive == a - p1 - fee - p2 - fee)
val Success(ac3) = receiveAdd(ac2, add3) val Success(ac3) = receiveAdd(ac2, add3, alice.underlyingActor.nodeParams.onChainFeeConf)
assert(ac3.availableBalanceForSend == a - p1 - fee - p2 - fee) assert(ac3.availableBalanceForSend == a - p1 - fee - p2 - fee)
assert(ac3.availableBalanceForReceive == b - p3) assert(ac3.availableBalanceForReceive == b - p3)
@ -398,7 +402,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val isFunder = true val isFunder = true
val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, 2500, 546 sat, isFunder) val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, 2500, 546 sat, isFunder)
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight)
val Success((c1, _)) = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight) val Success((c1, _)) = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight, feeConfNoMismatch)
assert(c1.availableBalanceForSend === 0.msat) assert(c1.availableBalanceForSend === 0.msat)
// We should be able to handle a fee increase. // We should be able to handle a fee increase.
@ -406,7 +410,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// Now we shouldn't be able to send until we receive enough to handle the updated commit tx fee (even trimmed HTLCs shouldn't be sent). // Now we shouldn't be able to send until we receive enough to handle the updated commit tx fee (even trimmed HTLCs shouldn't be sent).
val (_, cmdAdd1) = makeCmdAdd(100 msat, randomKey.publicKey, f.currentBlockHeight) val (_, cmdAdd1) = makeCmdAdd(100 msat, randomKey.publicKey, f.currentBlockHeight)
val Failure(e) = sendAdd(c2, cmdAdd1, Local(UUID.randomUUID, None), f.currentBlockHeight) val Failure(e) = sendAdd(c2, cmdAdd1, Local(UUID.randomUUID, None), f.currentBlockHeight, feeConfNoMismatch)
assert(e.isInstanceOf[InsufficientFunds]) assert(e.isInstanceOf[InsufficientFunds])
} }
@ -414,7 +418,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
for (isFunder <- Seq(true, false)) { for (isFunder <- Seq(true, false)) {
val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, 2679, 546 sat, isFunder) val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, 2679, 546 sat, isFunder)
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight)
val result = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight) val result = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight, feeConfNoMismatch)
assert(result.isSuccess, result) assert(result.isSuccess, result)
} }
} }
@ -423,7 +427,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
for (isFunder <- Seq(true, false)) { for (isFunder <- Seq(true, false)) {
val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, 2679, 546 sat, isFunder) val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, 2679, 546 sat, isFunder)
val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket)
receiveAdd(c, add) receiveAdd(c, add, feeConfNoMismatch)
} }
} }
@ -444,13 +448,13 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
for (_ <- 0 to t.pendingHtlcs) { for (_ <- 0 to t.pendingHtlcs) {
val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat)
val (_, cmdAdd) = makeCmdAdd(amount, randomKey.publicKey, f.currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(amount, randomKey.publicKey, f.currentBlockHeight)
sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight) match { sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight, feeConfNoMismatch) match {
case Success((cc, _)) => c = cc case Success((cc, _)) => c = cc
case Failure(e) => fail(s"$t -> could not setup initial htlcs: $e") case Failure(e) => fail(s"$t -> could not setup initial htlcs: $e")
} }
} }
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight)
val result = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight) val result = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight, feeConfNoMismatch)
assert(result.isSuccess, s"$t -> $result") assert(result.isSuccess, s"$t -> $result")
} }
} }
@ -472,13 +476,13 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
for (_ <- 0 to t.pendingHtlcs) { for (_ <- 0 to t.pendingHtlcs) {
val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat)
val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, amount, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, amount, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket)
receiveAdd(c, add) match { receiveAdd(c, add, feeConfNoMismatch) match {
case Success(cc) => c = cc case Success(cc) => c = cc
case Failure(e) => fail(s"$t -> could not setup initial htlcs: $e") case Failure(e) => fail(s"$t -> could not setup initial htlcs: $e")
} }
} }
val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket) val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket)
receiveAdd(c, add) match { receiveAdd(c, add, feeConfNoMismatch) match {
case Success(_) => () case Success(_) => ()
case Failure(e) => fail(s"$t -> $e") case Failure(e) => fail(s"$t -> $e")
} }
@ -496,8 +500,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments val bc0 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments
val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight) val (_, cmdAdd) = makeCmdAdd(p, bob.underlyingActor.nodeParams.nodeId, currentBlockHeight)
val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight) val Success((ac1, add)) = sendAdd(ac0, cmdAdd, Local(UUID.randomUUID, None), currentBlockHeight, feeConfNoMismatch)
val Success(bc1) = receiveAdd(bc0, add) val Success(bc1) = receiveAdd(bc0, add, feeConfNoMismatch)
val Success((ac2, commit1)) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager) val Success((ac2, commit1)) = sendCommit(ac1, alice.underlyingActor.nodeParams.keyManager)
val Success((bc2, revocation1)) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager) val Success((bc2, revocation1)) = receiveCommit(bc1, commit1, bob.underlyingActor.nodeParams.keyManager)
val Success((ac3, _)) = receiveRevocation(ac2, revocation1) val Success((ac3, _)) = receiveRevocation(ac2, revocation1)

View file

@ -27,7 +27,7 @@ import fr.acinq.eclair.wire.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel
import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag} import org.scalatest.{Outcome, Tag}
import scodec.bits.{ByteVector, HexStringSyntax} import scodec.bits.ByteVector
import scala.concurrent.duration._ import scala.concurrent.duration._

View file

@ -24,8 +24,9 @@ import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction}
import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.Features.StaticRemoteKey
import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator}
import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.UInt64.Conversions._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.channel.Channel._
@ -40,7 +41,6 @@ import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing}
import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, htlcSuccessWeight, htlcTimeoutWeight, weight2fee} import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, htlcSuccessWeight, htlcTimeoutWeight, weight2fee}
import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
import fr.acinq.eclair._
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag} import org.scalatest.{Outcome, Tag}
import scodec.bits._ import scodec.bits._
@ -383,6 +383,31 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
alice2bob.expectNoMsg(200 millis) alice2bob.expectNoMsg(200 millis)
} }
test("recv CMD_ADD_HTLC (channel feerate mismatch)") { f =>
import f._
val sender = TestProbe()
bob.feeEstimator.setFeerate(FeeratesPerKw.single(20000))
sender.send(bob, CurrentFeerates(FeeratesPerKw.single(20000)))
bob2alice.expectNoMsg(100 millis) // we don't close because the commitment doesn't contain any HTLC
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
val upstream = Upstream.Local(UUID.randomUUID())
val add = CMD_ADD_HTLC(500000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, upstream)
sender.send(bob, add)
val error = FeerateTooDifferent(channelId(bob), 20000, 10000)
sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Origin.Local(upstream.id, Some(sender.ref)), Some(initialState.channelUpdate), Some(add))))
bob2alice.expectNoMsg(100 millis) // we don't close the channel, we can simply avoid using it while we disagree on feerate
// we now agree on feerate so we can send HTLCs
bob.feeEstimator.setFeerate(FeeratesPerKw.single(11000))
sender.send(bob, CurrentFeerates(FeeratesPerKw.single(11000)))
bob2alice.expectNoMsg(100 millis)
sender.send(bob, add)
sender.expectMsg(ChannelCommandResponse.Ok)
bob2alice.expectMsgType[UpdateAddHtlc]
}
test("recv CMD_ADD_HTLC (after having sent Shutdown)") { f => test("recv CMD_ADD_HTLC (after having sent Shutdown)") { f =>
import f._ import f._
val sender = TestProbe() val sender = TestProbe()
@ -1273,9 +1298,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill))))
} }
test("recv CMD_FULFILL_HTLC") { testReceiveCmdFulfillHtlc _ } test("recv CMD_FULFILL_HTLC") {
testReceiveCmdFulfillHtlc _
}
test("recv CMD_FULFILL_HTLC (static_remotekey)", Tag("static_remotekey")) { testReceiveCmdFulfillHtlc _ } test("recv CMD_FULFILL_HTLC (static_remotekey)", Tag("static_remotekey")) {
testReceiveCmdFulfillHtlc _
}
test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f =>
import f._ import f._
@ -1331,9 +1360,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(forward.htlc === htlc) assert(forward.htlc === htlc)
} }
test("recv UpdateFulfillHtlc") { testUpdateFulfillHtlc _ } test("recv UpdateFulfillHtlc") {
testUpdateFulfillHtlc _
}
test("recv UpdateFulfillHtlc (static_remotekey)", Tag("(static_remotekey)")) { testUpdateFulfillHtlc _ } test("recv UpdateFulfillHtlc (static_remotekey)", Tag("(static_remotekey)")) {
testUpdateFulfillHtlc _
}
test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f =>
import f._ import f._
@ -1407,9 +1440,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
} }
test("recv CMD_FAIL_HTLC") { testCmdFailHtlc _ } test("recv CMD_FAIL_HTLC") {
testCmdFailHtlc _
}
test("recv CMD_FAIL_HTLC (static_remotekey)", Tag("static_remotekey")) { testCmdFailHtlc _ } test("recv CMD_FAIL_HTLC (static_remotekey)", Tag("static_remotekey")) {
testCmdFailHtlc _
}
test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => test("recv CMD_FAIL_HTLC (unknown htlc id)") { f =>
import f._ import f._
@ -1496,8 +1533,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
relayerA.expectNoMsg() relayerA.expectNoMsg()
} }
test("recv UpdateFailHtlc") { testUpdateFailHtlc _ } test("recv UpdateFailHtlc") {
test("recv UpdateFailHtlc (static_remotekey)", Tag("static_remotekey")) { testUpdateFailHtlc _ } testUpdateFailHtlc _
}
test("recv UpdateFailHtlc (static_remotekey)", Tag("static_remotekey")) {
testUpdateFailHtlc _
}
test("recv UpdateFailMalformedHtlc") { f => test("recv UpdateFailMalformedHtlc") { f =>
import f._ import f._
@ -1635,19 +1676,19 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv UpdateFee") { f => test("recv UpdateFee") { f =>
import f._ import f._
val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] val initialData = bob.stateData.asInstanceOf[DATA_NORMAL]
val fee1 = UpdateFee(ByteVector32.Zeroes, 12000) val fee = UpdateFee(ByteVector32.Zeroes, 12000)
bob ! fee1 bob ! fee
val fee2 = UpdateFee(ByteVector32.Zeroes, 14000) awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0)))
bob ! fee2
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee2), remoteNextHtlcId = 0)))
} }
test("recv UpdateFee (two in a row)") { f => test("recv UpdateFee (two in a row)") { f =>
import f._ import f._
val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] val initialData = bob.stateData.asInstanceOf[DATA_NORMAL]
val fee = UpdateFee(ByteVector32.Zeroes, 12000) val fee1 = UpdateFee(ByteVector32.Zeroes, 12000)
bob ! fee bob ! fee1
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) val fee2 = UpdateFee(ByteVector32.Zeroes, 14000)
bob ! fee2
awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee2), remoteNextHtlcId = 0)))
} }
test("recv UpdateFee (when sender is not funder)") { f => test("recv UpdateFee (when sender is not funder)") { f =>
@ -1684,16 +1725,20 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv UpdateFee (local/remote feerates are too different)") { f => test("recv UpdateFee (local/remote feerates are too different)") { f =>
import f._ import f._
bob.feeEstimator.setFeerate(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000, 140000)) bob.feeEstimator.setFeerate(FeeratesPerKw(1000, 2000, 6000, 12000, 36000, 72000, 140000))
val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
val sender = TestProbe() val sender = TestProbe()
// Alice will use $localFeeRate when performing the checks for update_fee val localFeerate = bob.feeEstimator.getFeeratePerKw(bob.feeTargets.commitmentBlockTarget)
val localFeeRate = bob.feeEstimator.getFeeratePerKw(bob.feeTargets.commitmentBlockTarget) assert(localFeerate === 2000)
assert(localFeeRate === 2000) val remoteFeerate = 4000
val remoteFeeUpdate = 85000 sender.send(bob, UpdateFee(ByteVector32.Zeroes, remoteFeerate))
sender.send(bob, UpdateFee(ByteVector32.Zeroes, remoteFeeUpdate)) bob2alice.expectNoMsg(250 millis) // we don't close because the commitment doesn't contain any HTLC
// when we try to add an HTLC, we still disagree on the feerate so we close
alice2bob.send(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
val error = bob2alice.expectMsgType[Error] val error = bob2alice.expectMsgType[Error]
assert(new String(error.data.toArray) === s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeUpdate localFeeratePerKw=$localFeeRate") assert(new String(error.data.toArray).contains("local/remote feerates are too different"))
awaitCond(bob.stateName == CLOSING) awaitCond(bob.stateName == CLOSING)
// channel should be advertised as down // channel should be advertised as down
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId)
@ -2105,10 +2150,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
bob2alice.expectNoMsg(500 millis) bob2alice.expectNoMsg(500 millis)
} }
test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different)") { f => test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, with HTLCs)") { f =>
import f._ import f._
addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val sender = TestProbe() val sender = TestProbe()
val event = CurrentFeerates(FeeratesPerKw.single(100)) bob.feeEstimator.setFeerate(FeeratesPerKw.single(14000))
val event = CurrentFeerates(FeeratesPerKw.single(14000))
sender.send(bob, event) sender.send(bob, event)
bob2alice.expectMsgType[Error] bob2alice.expectMsgType[Error]
bob2blockchain.expectMsgType[PublishAsap] // commit tx bob2blockchain.expectMsgType[PublishAsap] // commit tx
@ -2117,6 +2167,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(bob.stateName == CLOSING) awaitCond(bob.stateName == CLOSING)
} }
test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, without HTLCs)") { f =>
import f._
val sender = TestProbe()
bob.feeEstimator.setFeerate(FeeratesPerKw.single(1000))
val event = CurrentFeerates(FeeratesPerKw.single(1000))
sender.send(bob, event)
bob2alice.expectNoMsg(250 millis) // we don't close because the commitment doesn't contain any HTLC
// when we try to add an HTLC, we still disagree on the feerate so we close
alice2bob.send(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket))
bob2alice.expectMsgType[Error]
bob2blockchain.expectMsgType[PublishAsap] // commit tx
bob2blockchain.expectMsgType[PublishAsap] // main delayed
bob2blockchain.expectMsgType[WatchConfirmed]
awaitCond(bob.stateName == CLOSING)
}
test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { f => test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { f =>
import f._ import f._
val sender = TestProbe() val sender = TestProbe()

View file

@ -469,9 +469,23 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("handle feerate changes while offline (funder scenario)") { f => test("handle feerate changes while offline (funder scenario)") { f =>
import f._ import f._
val sender = TestProbe()
// we only close channels on feerate mismatch if there are HTLCs at risk in the commitment
addHtlc(125000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
testHandleFeerateFunder(f, shouldClose = true)
}
test("handle feerate changes while offline without HTLCs (funder scenario)") { f =>
testHandleFeerateFunder(f, shouldClose = false)
}
def testHandleFeerateFunder(f: FixtureParam, shouldClose: Boolean): Unit = {
import f._
// we simulate a disconnection // we simulate a disconnection
val sender = TestProbe()
sender.send(alice, INPUT_DISCONNECTED) sender.send(alice, INPUT_DISCONNECTED)
sender.send(bob, INPUT_DISCONNECTED) sender.send(bob, INPUT_DISCONNECTED)
awaitCond(alice.stateName == OFFLINE) awaitCond(alice.stateName == OFFLINE)
@ -480,32 +494,44 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL]
val aliceCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx val aliceCommitTx = aliceStateData.commitments.localCommit.publishableTxs.commitTx.tx
val localFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw
val tooHighFeeratePerKw = ((alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1
val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) // to ensure the network's feerate is 10% above our threshold).
val networkFeeratePerKw = (1.1 * currentFeeratePerKw / alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch.ratioLow).toLong
val networkFeerate = FeeratesPerKw.single(networkFeeratePerKw)
// alice is funder // alice is funder
sender.send(alice, CurrentFeerates(highFeerate)) sender.send(alice, CurrentFeerates(networkFeerate))
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx)) if (shouldClose) {
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx))
} else {
alice2blockchain.expectNoMsg()
}
} }
test("handle feerate changes while offline (don't close on mismatch)", Tag("disable-offline-mismatch")) { f => test("handle feerate changes while offline (don't close on mismatch)", Tag("disable-offline-mismatch")) { f =>
import f._ import f._
val sender = TestProbe()
// we only close channels on feerate mismatch if there are HTLCs at risk in the commitment
addHtlc(125000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
// we simulate a disconnection // we simulate a disconnection
val sender = TestProbe()
sender.send(alice, INPUT_DISCONNECTED) sender.send(alice, INPUT_DISCONNECTED)
sender.send(bob, INPUT_DISCONNECTED) sender.send(bob, INPUT_DISCONNECTED)
awaitCond(alice.stateName == OFFLINE) awaitCond(alice.stateName == OFFLINE)
awaitCond(bob.stateName == OFFLINE) awaitCond(bob.stateName == OFFLINE)
val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL]
val localFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw val currentFeeratePerKw = aliceStateData.commitments.localCommit.spec.feeratePerKw
val tooHighFeeratePerKw = ((alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1
val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) // to ensure the network's feerate is 10% above our threshold).
val networkFeeratePerKw = (1.1 * currentFeeratePerKw / alice.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch.ratioLow).toLong
val networkFeerate = FeeratesPerKw.single(networkFeeratePerKw)
// this time Alice will ignore feerate changes for the offline channel // this time Alice will ignore feerate changes for the offline channel
sender.send(alice, CurrentFeerates(highFeerate)) sender.send(alice, CurrentFeerates(networkFeerate))
alice2blockchain.expectNoMsg() alice2blockchain.expectNoMsg()
alice2bob.expectNoMsg() alice2bob.expectNoMsg()
} }
@ -546,9 +572,23 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("handle feerate changes while offline (fundee scenario)") { f => test("handle feerate changes while offline (fundee scenario)") { f =>
import f._ import f._
val sender = TestProbe()
// we only close channels on feerate mismatch if there are HTLCs at risk in the commitment
addHtlc(125000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
testHandleFeerateFundee(f, shouldClose = true)
}
test("handle feerate changes while offline without HTLCs (fundee scenario)") { f =>
testHandleFeerateFundee(f, shouldClose = false)
}
def testHandleFeerateFundee(f: FixtureParam, shouldClose: Boolean): Unit = {
import f._
// we simulate a disconnection // we simulate a disconnection
val sender = TestProbe()
sender.send(alice, INPUT_DISCONNECTED) sender.send(alice, INPUT_DISCONNECTED)
sender.send(bob, INPUT_DISCONNECTED) sender.send(bob, INPUT_DISCONNECTED)
awaitCond(alice.stateName == OFFLINE) awaitCond(alice.stateName == OFFLINE)
@ -557,13 +597,19 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL] val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL]
val bobCommitTx = bobStateData.commitments.localCommit.publishableTxs.commitTx.tx val bobCommitTx = bobStateData.commitments.localCommit.publishableTxs.commitTx.tx
val localFeeratePerKw = bobStateData.commitments.localCommit.spec.feeratePerKw val currentFeeratePerKw = bobStateData.commitments.localCommit.spec.feeratePerKw
val tooHighFeeratePerKw = ((bob.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch + 6) * localFeeratePerKw).toLong // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1
val highFeerate = FeeratesPerKw.single(tooHighFeeratePerKw) // to ensure the network's feerate is 10% above our threshold).
val networkFeeratePerKw = (1.1 * currentFeeratePerKw / bob.underlyingActor.nodeParams.onChainFeeConf.maxFeerateMismatch.ratioLow).toLong
val networkFeerate = FeeratesPerKw.single(networkFeeratePerKw)
// bob is fundee // bob is fundee
sender.send(bob, CurrentFeerates(highFeerate)) sender.send(bob, CurrentFeerates(networkFeerate))
bob2blockchain.expectMsg(PublishAsap(bobCommitTx)) if (shouldClose) {
bob2blockchain.expectMsg(PublishAsap(bobCommitTx))
} else {
bob2blockchain.expectNoMsg()
}
} }
test("re-send channel_update at reconnection for private channels") { f => test("re-send channel_update at reconnection for private channels") { f =>

View file

@ -90,7 +90,6 @@ class SqliteAuditDbSpec extends AnyFunSuite {
val sqlite = TestConstants.sqliteInMemory() val sqlite = TestConstants.sqliteInMemory()
val db = new SqliteAuditDb(sqlite) val db = new SqliteAuditDb(sqlite)
val n1 = randomKey.publicKey
val n2 = randomKey.publicKey val n2 = randomKey.publicKey
val n3 = randomKey.publicKey val n3 = randomKey.publicKey
val n4 = randomKey.publicKey val n4 = randomKey.publicKey
@ -99,24 +98,36 @@ class SqliteAuditDbSpec extends AnyFunSuite {
val c2 = randomBytes32 val c2 = randomBytes32
val c3 = randomBytes32 val c3 = randomBytes32
val c4 = randomBytes32 val c4 = randomBytes32
val c5 = randomBytes32
val c6 = randomBytes32
db.add(ChannelPaymentRelayed(46000 msat, 44000 msat, randomBytes32, randomBytes32, c1)) db.add(ChannelPaymentRelayed(46000 msat, 44000 msat, randomBytes32, c6, c1))
db.add(ChannelPaymentRelayed(41000 msat, 40000 msat, randomBytes32, randomBytes32, c1)) db.add(ChannelPaymentRelayed(41000 msat, 40000 msat, randomBytes32, c6, c1))
db.add(ChannelPaymentRelayed(43000 msat, 42000 msat, randomBytes32, randomBytes32, c1)) db.add(ChannelPaymentRelayed(43000 msat, 42000 msat, randomBytes32, c5, c1))
db.add(ChannelPaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomBytes32, c2)) db.add(ChannelPaymentRelayed(42000 msat, 40000 msat, randomBytes32, c5, c2))
db.add(TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(25000 msat, randomBytes32)), Seq(PaymentRelayed.Part(20000 msat, c4)))) db.add(ChannelPaymentRelayed(45000 msat, 40000 msat, randomBytes32, c5, c6))
db.add(TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(46000 msat, randomBytes32)), Seq(PaymentRelayed.Part(16000 msat, c2), PaymentRelayed.Part(10000 msat, c4), PaymentRelayed.Part(14000 msat, c4)))) db.add(TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(25000 msat, c6)), Seq(PaymentRelayed.Part(20000 msat, c4))))
db.add(TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(46000 msat, c6)), Seq(PaymentRelayed.Part(16000 msat, c2), PaymentRelayed.Part(10000 msat, c4), PaymentRelayed.Part(14000 msat, c4))))
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 200 sat, "funding")) db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 200 sat, "funding"))
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 300 sat, "mutual")) db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 300 sat, "mutual"))
db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), 400 sat, "funding")) db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), 400 sat, "funding"))
db.add(NetworkFeePaid(null, n4, c4, Transaction(0, Seq.empty, Seq.empty, 0), 500 sat, "funding")) db.add(NetworkFeePaid(null, n4, c4, Transaction(0, Seq.empty, Seq.empty, 0), 500 sat, "funding"))
assert(db.stats.toSet === Set( // NB: we only count a relay fee for the outgoing channel, no the incoming one.
Stats(channelId = c1, avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 0 sat), assert(db.stats(0, System.currentTimeMillis + 1).toSet === Set(
Stats(channelId = c2, avgPaymentAmount = 40 sat, paymentCount = 2, relayFee = 4 sat, networkFee = 500 sat), Stats(channelId = c1, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 0 sat),
Stats(channelId = c3, avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat), Stats(channelId = c1, direction = "OUT", avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 0 sat),
Stats(channelId = c4, avgPaymentAmount = 30 sat, paymentCount = 2, relayFee = 9 sat, networkFee = 500 sat) Stats(channelId = c2, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 500 sat),
Stats(channelId = c2, direction = "OUT", avgPaymentAmount = 28 sat, paymentCount = 2, relayFee = 4 sat, networkFee = 500 sat),
Stats(channelId = c3, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat),
Stats(channelId = c3, direction = "OUT", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat),
Stats(channelId = c4, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 500 sat),
Stats(channelId = c4, direction = "OUT", avgPaymentAmount = 22 sat, paymentCount = 2, relayFee = 9 sat, networkFee = 500 sat),
Stats(channelId = c5, direction = "IN", avgPaymentAmount = 43 sat, paymentCount = 3, relayFee = 0 sat, networkFee = 0 sat),
Stats(channelId = c5, direction = "OUT", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 0 sat),
Stats(channelId = c6, direction = "IN", avgPaymentAmount = 39 sat, paymentCount = 4, relayFee = 0 sat, networkFee = 0 sat),
Stats(channelId = c6, direction = "OUT", avgPaymentAmount = 40 sat, paymentCount = 1, relayFee = 5 sat, networkFee = 0 sat)
)) ))
} }
@ -148,7 +159,7 @@ class SqliteAuditDbSpec extends AnyFunSuite {
}) })
// Test starts here. // Test starts here.
val start = System.currentTimeMillis val start = System.currentTimeMillis
assert(db.stats.nonEmpty) assert(db.stats(0, start + 1).nonEmpty)
val end = System.currentTimeMillis val end = System.currentTimeMillis
fail(s"took ${end - start}ms") fail(s"took ${end - start}ms")
} }

View file

@ -18,29 +18,25 @@ package fr.acinq.eclair.payment
import java.util.UUID import java.util.UUID
import akka.actor.ActorRef import akka.actor.{ActorRef, Status}
import akka.testkit.{TestFSMRef, TestProbe} import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.bitcoin.{Block, Crypto}
import fr.acinq.eclair.TestConstants.TestFeeEstimator
import fr.acinq.eclair._ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelFlags, ChannelUnavailable, Upstream}
import fr.acinq.eclair.channel.{ChannelFlags, Commitments, CommitmentsSpec, Upstream}
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannel, OutgoingChannels}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._
import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute
import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentError} import fr.acinq.eclair.router.RouteNotFound
import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router._
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import org.scalatest.Outcome
import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag} import scodec.bits.{ByteVector, HexStringSyntax}
import scodec.bits.ByteVector
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Random
/** /**
* Created by t-bast on 18/07/2019. * Created by t-bast on 18/07/2019.
@ -50,11 +46,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
import MultiPartPaymentLifecycleSpec._ import MultiPartPaymentLifecycleSpec._
case class FixtureParam(paymentId: UUID, case class FixtureParam(cfg: SendPaymentConfig,
nodeParams: NodeParams, nodeParams: NodeParams,
payFsm: TestFSMRef[MultiPartPaymentLifecycle.State, MultiPartPaymentLifecycle.Data, MultiPartPaymentLifecycle], payFsm: TestFSMRef[MultiPartPaymentLifecycle.State, MultiPartPaymentLifecycle.Data, MultiPartPaymentLifecycle],
router: TestProbe, router: TestProbe,
relayer: TestProbe,
sender: TestProbe, sender: TestProbe,
childPayFsm: TestProbe, childPayFsm: TestProbe,
eventListener: TestProbe) eventListener: TestProbe)
@ -63,445 +58,396 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS
val id = UUID.randomUUID() val id = UUID.randomUUID()
val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, finalAmount, finalRecipient, Upstream.Local(id), None, storeInDb = true, publishEvent = true, Nil) val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, finalAmount, finalRecipient, Upstream.Local(id), None, storeInDb = true, publishEvent = true, Nil)
val nodeParams = TestConstants.Alice.nodeParams val nodeParams = TestConstants.Alice.nodeParams
nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator].setFeerate(FeeratesPerKw.single(500)) val (childPayFsm, router, sender, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe())
val (childPayFsm, router, relayer, sender, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe()) class TestMultiPartPaymentLifecycle extends MultiPartPaymentLifecycle(nodeParams, cfg, router.ref, TestProbe().ref) {
class TestMultiPartPaymentLifecycle extends MultiPartPaymentLifecycle(nodeParams, cfg, relayer.ref, router.ref, TestProbe().ref) {
override def spawnChildPaymentFsm(childId: UUID): ActorRef = childPayFsm.ref override def spawnChildPaymentFsm(childId: UUID): ActorRef = childPayFsm.ref
} }
val paymentHandler = TestFSMRef(new TestMultiPartPaymentLifecycle().asInstanceOf[MultiPartPaymentLifecycle]) val paymentHandler = TestFSMRef(new TestMultiPartPaymentLifecycle().asInstanceOf[MultiPartPaymentLifecycle])
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
withFixture(test.toNoArgTest(FixtureParam(id, nodeParams, paymentHandler, router, relayer, sender, childPayFsm, eventListener))) withFixture(test.toNoArgTest(FixtureParam(cfg, nodeParams, paymentHandler, router, sender, childPayFsm, eventListener)))
} }
def initPayment(f: FixtureParam, request: SendMultiPartPayment, networkStats: NetworkStats, localChannels: OutgoingChannels): Unit = { test("successful first attempt (single part)") { f =>
import f._
sender.send(payFsm, request)
router.expectMsg(GetNetworkStats)
router.send(payFsm, GetNetworkStatsResponse(Some(networkStats)))
relayer.expectMsg(GetOutgoingChannels())
relayer.send(payFsm, localChannels)
}
def waitUntilAmountSent(f: FixtureParam, amount: MilliSatoshi): Unit = {
Iterator.iterate(0 msat)(sent => {
sent + f.childPayFsm.expectMsgType[SendPayment].finalPayload.amount
}).takeWhile(sent => sent < amount)
}
test("get network statistics and usable balances before paying") { f =>
import f._ import f._
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
val payment = SendMultiPartPayment(randomBytes32, b, 1500 * 1000 msat, expiry, 1) val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 1, routeParams = Some(routeParams.copy(randomize = true)))
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsg(GetNetworkStats)
assert(payFsm.stateName === WAIT_FOR_NETWORK_STATS) router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = Some(routeParams.copy(randomize = false)), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
router.send(payFsm, GetNetworkStatsResponse(Some(emptyStats))) assert(payFsm.stateName === WAIT_FOR_ROUTES)
relayer.expectMsg(GetOutgoingChannels())
awaitCond(payFsm.stateName === WAIT_FOR_CHANNEL_BALANCES) val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
assert(payFsm.stateData.asInstanceOf[WaitingForChannelBalances].networkStats === Some(emptyStats)) router.send(payFsm, RouteResponse(Seq(singleRoute)))
val childPayment = childPayFsm.expectMsgType[SendPaymentToRoute]
assert(childPayment.route === Right(singleRoute))
assert(childPayment.finalPayload.expiry === expiry)
assert(childPayment.finalPayload.paymentSecret === Some(payment.paymentSecret))
assert(childPayment.finalPayload.amount === finalAmount)
assert(childPayment.finalPayload.totalAmount === finalAmount)
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
val result = fulfillPendingPayments(f, 1)
assert(result.amountWithFees === finalAmount + 100.msat)
assert(result.trampolineFees === 0.msat)
assert(result.nonTrampolineFees === 100.msat)
} }
test("get network statistics not available") { f => test("successful first attempt (multiple parts)") { f =>
import f._ import f._
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
val payment = SendMultiPartPayment(randomBytes32, b, 2500 * 1000 msat, expiry, 1) val payment = SendMultiPartPayment(randomBytes32, e, 1200000 msat, expiry, 1, routeParams = Some(routeParams.copy(randomize = false)))
sender.send(payFsm, payment) sender.send(payFsm, payment)
router.expectMsg(GetNetworkStats)
assert(payFsm.stateName === WAIT_FOR_NETWORK_STATS)
router.send(payFsm, GetNetworkStatsResponse(None))
// If network stats aren't available we'll use local channel balance information instead.
// We should ask the router to compute statistics (for next payment attempts).
router.expectMsg(TickComputeNetworkStats)
relayer.expectMsg(GetOutgoingChannels())
awaitCond(payFsm.stateName === WAIT_FOR_CHANNEL_BALANCES)
assert(payFsm.stateData.asInstanceOf[WaitingForChannelBalances].networkStats === None)
relayer.send(payFsm, localChannels()) router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = Some(routeParams.copy(randomize = false)), allowMultiPart = true, paymentContext = Some(cfg.paymentContext)))
awaitCond(payFsm.stateName === PAYMENT_IN_PROGRESS) assert(payFsm.stateName === WAIT_FOR_ROUTES)
waitUntilAmountSent(f, payment.totalAmount)
val payments = payFsm.stateData.asInstanceOf[PaymentProgress].pending.values
assert(payments.size > 1)
}
test("send to peer node via multiple channels") { f => val routes = Seq(
import f._ Route(500000 msat, hop_ab_1 :: hop_be :: Nil),
val payment = SendMultiPartPayment(randomBytes32, b, 2000 * 1000 msat, expiry, 3) Route(700000 msat, hop_ac_1 :: hop_ce :: Nil)
// When sending to a peer node, we should not filter out unannounced channels.
val channels = OutgoingChannels(Seq(
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1000 * 1000 msat, 0)),
OutgoingChannel(c, channelUpdate_ac_3, makeCommitments(1500 * 1000 msat, 0)),
OutgoingChannel(b, channelUpdate_ab_1.copy(channelFlags = ChannelFlags.Empty), makeCommitments(1000 * 1000 msat, 0, announceChannel = false)),
OutgoingChannel(b, channelUpdate_ab_2.copy(channelFlags = ChannelFlags.Empty), makeCommitments(1500 * 1000 msat, 0, announceChannel = false)),
OutgoingChannel(d, channelUpdate_ad_1, makeCommitments(1000 * 1000 msat, 0))))
// Network statistics should be ignored when sending to peer.
initPayment(f, payment, emptyStats, channels)
// The payment should be split in two, using direct channels with b.
// MaxAttempts should be set to 1 when using direct channels to the destination.
childPayFsm.expectMsgAllOf(
SendPayment(b, Onion.createMultiPartPayload(1000 * 1000 msat, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_1.copy(channelFlags = ChannelFlags.Empty)))),
SendPayment(b, Onion.createMultiPartPayload(1000 * 1000 msat, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_2.copy(channelFlags = ChannelFlags.Empty))))
) )
childPayFsm.expectNoMsg(50 millis) router.send(payFsm, RouteResponse(routes))
val childIds = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.toSeq val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil
assert(childIds.length === 2) assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet)
assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry))
assert(childPayments.map(_.finalPayload.paymentSecret.get).toSet === Set(payment.paymentSecret))
assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat))
assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat))
assert(payFsm.stateName === PAYMENT_IN_PROGRESS)
val pp1 = PartialPayment(childIds.head, 1000 * 1000 msat, 0 msat, randomBytes32, None) val result = fulfillPendingPayments(f, 2)
val pp2 = PartialPayment(childIds(1), 1000 * 1000 msat, 0 msat, randomBytes32, None) assert(result.amountWithFees === 1200200.msat)
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, b, Seq(pp1))) assert(result.trampolineFees === 200000.msat)
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, b, Seq(pp2))) assert(result.nonTrampolineFees === 200.msat)
val expectedMsg = PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, finalRecipient, Seq(pp1, pp2))
sender.expectMsg(expectedMsg)
eventListener.expectMsg(expectedMsg)
assert(expectedMsg.recipientAmount === finalAmount)
assert(expectedMsg.amountWithFees === (2000 * 1000).msat)
assert(expectedMsg.trampolineFees === (1000 * 1000).msat)
assert(expectedMsg.nonTrampolineFees === 0.msat)
assert(expectedMsg.feesPaid === expectedMsg.trampolineFees)
} }
test("send to peer node via single big channel") { f => test("send custom tlv records") { f =>
import f._ import f._
val payment = SendMultiPartPayment(randomBytes32, b, 1000 * 1000 msat, expiry, 1)
// Network statistics should be ignored when sending to peer (otherwise we should have split into multiple payments).
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(100), d => Satoshi(d.toLong))), localChannels(0))
childPayFsm.expectMsg(SendPayment(b, Onion.createMultiPartPayload(payment.totalAmount, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_1))))
childPayFsm.expectNoMsg(50 millis)
}
test("send to peer node via remote channels") { f => // We include a bunch of additional tlv records.
import f._
// d only has a single channel with capacity 1000 sat, we try to send more.
val payment = SendMultiPartPayment(randomBytes32, d, 2000 * 1000 msat, expiry, 1)
val testChannels = localChannels()
val balanceToTarget = testChannels.channels.filter(_.nextNodeId == d).map(_.commitments.availableBalanceForSend).sum
assert(balanceToTarget < (1000 * 1000).msat) // the commit tx fee prevents us from completely emptying our channel
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(500), d => Satoshi(d.toLong))), testChannels)
waitUntilAmountSent(f, payment.totalAmount)
val payments = payFsm.stateData.asInstanceOf[PaymentProgress].pending.values
assert(payments.size > 1)
val directPayments = payments.filter(p => p.routePrefix.head.nextNodeId == d)
assert(directPayments.size === 1)
assert(directPayments.head.finalPayload.amount === balanceToTarget)
}
test("send to remote node without splitting") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 300 * 1000 msat, expiry, 1)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1500), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, payment.totalAmount)
payFsm.stateData.asInstanceOf[PaymentProgress].pending.foreach {
case (id, payment) => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, e, Seq(PartialPayment(id, payment.finalPayload.amount, 5 msat, randomBytes32, None))))
}
val result = sender.expectMsgType[PaymentSent]
assert(result.id === paymentId)
assert(result.amountWithFees === payment.totalAmount + result.nonTrampolineFees)
assert(result.parts.length === 1)
}
test("send to remote node via multiple channels") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3200 * 1000 msat, expiry, 3)
// A network capacity of 1000 sat should split the payment in at least 3 parts.
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), localChannels())
val payments = Iterator.iterate(0 msat)(sent => {
val child = childPayFsm.expectMsgType[SendPayment]
assert(child.targetNodeId === e)
assert(child.maxAttempts === 3)
assert(child.finalPayload.expiry === expiry)
assert(child.finalPayload.paymentSecret === Some(payment.paymentSecret))
assert(child.finalPayload.totalAmount === payment.totalAmount)
assert(child.routePrefix.length === 1 && child.routePrefix.head.nodeId === nodeParams.nodeId)
assert(sent + child.finalPayload.amount <= payment.totalAmount)
sent + child.finalPayload.amount
}).takeWhile(sent => sent != payment.totalAmount).toSeq
assert(payments.length > 2)
assert(payments.length < 10)
childPayFsm.expectNoMsg(50 millis)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
val partialPayments = pending.map {
case (id, payment) => PartialPayment(id, payment.finalPayload.amount, 1 msat, randomBytes32, Some(hop_ac_1 :: hop_ab_2 :: Nil))
}
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, e, Seq(pp))))
val result = sender.expectMsgType[PaymentSent]
assert(result.id === paymentId)
assert(result.paymentHash === paymentHash)
assert(result.paymentPreimage === paymentPreimage)
assert(result.parts === partialPayments)
assert(result.recipientAmount === finalAmount)
assert(result.amountWithFees > (3200 * 1000).msat)
assert(result.trampolineFees === (2200 * 1000).msat)
assert(result.nonTrampolineFees === partialPayments.map(_.feesPaid).sum)
}
test("send to remote node via single big channel") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3500 * 1000 msat, expiry, 3)
// When splitting inside a channel, we need to take the fees of the commit tx into account (multiple outgoing HTLCs
// will increase the size of the commit tx and thus its fee.
val feeRatePerKw = 100
// A network capacity of 1500 sat should split the payment in at least 2 parts.
// We have a single big channel inside which we'll send multiple payments.
val localChannel = OutgoingChannels(Seq(OutgoingChannel(b, channelUpdate_ab_1, makeCommitments(5000 * 1000 msat, feeRatePerKw))))
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1500), d => Satoshi(d.toLong))), localChannel)
waitUntilAmountSent(f, payment.totalAmount)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
assert(pending.size >= 2)
val partialPayments = pending.map {
case (id, payment) => PartialPayment(id, payment.finalPayload.amount, 1 msat, randomBytes32, None)
}
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(pp))))
val result = sender.expectMsgType[PaymentSent]
assert(result.id === paymentId)
assert(result.paymentHash === paymentHash)
assert(result.paymentPreimage === paymentPreimage)
assert(result.parts === partialPayments)
assert(result.amountWithFees - result.nonTrampolineFees === (3500 * 1000).msat)
assert(result.recipientNodeId === finalRecipient) // the recipient is obtained from the config, not from the request (which may be to the first trampoline node)
assert(result.nonTrampolineFees === partialPayments.map(_.feesPaid).sum)
}
test("send to remote trampoline node") { f =>
import f._
val trampolineTlv = OnionTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32)) val trampolineTlv = OnionTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32))
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3, additionalTlvs = Seq(trampolineTlv)) val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef")
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), localChannels()) val payment = SendMultiPartPayment(randomBytes32, e, finalAmount + 1000.msat, expiry, 1, routeParams = Some(routeParams), additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv))
waitUntilAmountSent(f, payment.totalAmount) sender.send(payFsm, payment)
router.expectMsgType[RouteRequest]
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil))))
pending.foreach { val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil
case (_, p) => assert(p.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionTlv.TrampolineOnion] === Some(trampolineTlv)) childPayments.map(_.finalPayload.asInstanceOf[Onion.FinalTlvPayload]).foreach(p => {
} assert(p.records.get[OnionTlv.TrampolineOnion] === Some(trampolineTlv))
} assert(p.records.unknown.toSeq === Seq(userCustomTlv))
test("split fees between child payments") { f =>
import f._
val routeParams = RouteParams(randomize = false, 100 msat, 0.05, 20, CltvExpiryDelta(144), None, MultiPartParams(10000 msat, 5))
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3, routeParams = Some(routeParams))
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, 3000 * 1000 msat)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
assert(pending.size >= 2)
pending.foreach {
case (_, p) =>
assert(p.routeParams.get.maxFeeBase < 50.msat)
assert(p.routeParams.get.maxFeePct == 0.05) // fee percent doesn't need to change
}
}
test("skip empty channels") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3)
val testChannels = localChannels()
val testChannels1 = testChannels.copy(channels = testChannels.channels ++ Seq(
OutgoingChannel(b, channelUpdate_ab_1.copy(shortChannelId = ShortChannelId(42)), makeCommitments(0 msat, 10)),
OutgoingChannel(e, channelUpdate_ab_1.copy(shortChannelId = ShortChannelId(43)), makeCommitments(0 msat, 10)
)))
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), testChannels1)
waitUntilAmountSent(f, payment.totalAmount)
payFsm.stateData.asInstanceOf[PaymentProgress].pending.foreach {
case (id, p) => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id, p.finalPayload.amount, 5 msat, randomBytes32, None))))
}
val result = sender.expectMsgType[PaymentSent]
assert(result.id === paymentId)
assert(result.amountWithFees > payment.totalAmount)
}
test("retry after error") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3)
val testChannels = localChannels()
// A network capacity of 1000 sat should split the payment in at least 3 parts.
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), testChannels)
waitUntilAmountSent(f, payment.totalAmount)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
assert(pending.size > 2)
// Simulate a local channel failure and a remote failure.
val faultyLocalChannelId = getFirstHopShortChannelId(pending.head._2)
val faultyLocalPayments = pending.filter { case (_, p) => getFirstHopShortChannelId(p) == faultyLocalChannelId }
val faultyRemotePayment = pending.filter { case (_, p) => getFirstHopShortChannelId(p) != faultyLocalChannelId }.head
faultyLocalPayments.keys.foreach(id => {
childPayFsm.send(payFsm, PaymentFailed(id, paymentHash, LocalFailure(Nil, RouteNotFound) :: Nil))
}) })
childPayFsm.send(payFsm, PaymentFailed(faultyRemotePayment._1, paymentHash, UnreadableRemoteFailure(Nil) :: Nil))
// We should ask for updated balance to take into account pending payments. val result = fulfillPendingPayments(f, 2)
relayer.expectMsg(GetOutgoingChannels()) assert(result.trampolineFees === 1000.msat)
relayer.send(payFsm, testChannels.copy(channels = testChannels.channels.dropRight(2)))
// The channel that lead to a RouteNotFound should be ignored.
assert(payFsm.stateData.asInstanceOf[PaymentProgress].ignoreChannels === Set(faultyLocalChannelId))
// New payments should be sent that match the failed amount.
waitUntilAmountSent(f, faultyRemotePayment._2.finalPayload.amount + faultyLocalPayments.values.map(_.finalPayload.amount).sum)
val stateData = payFsm.stateData.asInstanceOf[PaymentProgress]
assert(stateData.failures.toSet === Set(LocalFailure(Nil, RouteNotFound), UnreadableRemoteFailure(Nil)))
assert(stateData.pending.values.forall(p => getFirstHopShortChannelId(p) != faultyLocalChannelId))
} }
test("cannot send (not enough capacity on local channels)") { f => test("successful retry") { f =>
import f._ import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), OutgoingChannels(Seq( val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 3, routeParams = Some(routeParams))
OutgoingChannel(b, channelUpdate_ab_1, makeCommitments(1000 * 1000 msat, 10)), sender.send(payFsm, payment)
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1000 * 1000 msat, 10)), router.expectMsgType[RouteRequest]
OutgoingChannel(d, channelUpdate_ad_1, makeCommitments(1000 * 1000 msat, 10)))) val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil)
) router.send(payFsm, RouteResponse(Seq(failingRoute)))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectNoMsg(100 millis)
val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head
childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(failingRoute.hops, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)))))
// We retry ignoring the failing channel.
router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = Some(routeParams.copy(randomize = true)), allowMultiPart = true, ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_be, b, e))), paymentContext = Some(cfg.paymentContext)))
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ac_1 :: hop_ce :: Nil), Route(600000 msat, hop_ad :: hop_de :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(childId))
val result = fulfillPendingPayments(f, 2)
assert(result.amountWithFees === 1000200.msat)
assert(result.trampolineFees === 0.msat)
assert(result.nonTrampolineFees === 200.msat)
}
test("retry failures while waiting for routes") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 3, routeParams = Some(routeParams))
sender.send(payFsm, payment)
router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectNoMsg(100 millis)
val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toList
childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))))
// When we retry, we ignore the failing node and we let the router know about the remaining pending route.
router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee, ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = Some(routeParams.copy(randomize = true)), paymentContext = Some(cfg.paymentContext)))
// The second part fails while we're still waiting for new routes.
childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))))
// We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into
// account the latest failures.
router.send(payFsm, RouteResponse(Seq(Route(failedRoute1.amount, hop_ac_1 :: hop_ce :: Nil))))
router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, ignore = Ignore(Set(b), Set.empty), allowMultiPart = true, routeParams = Some(routeParams.copy(randomize = true)), paymentContext = Some(cfg.paymentContext)))
awaitCond(payFsm.stateData.asInstanceOf[PaymentProgress].pending.isEmpty)
childPayFsm.expectNoMsg(100 millis)
// We receive new routes that work.
router.send(payFsm, RouteResponse(Seq(Route(300000 msat, hop_ac_1 :: hop_ce :: Nil), Route(700000 msat, hop_ad :: hop_de :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
val result = fulfillPendingPayments(f, 2)
assert(result.amountWithFees === 1000200.msat)
assert(result.nonTrampolineFees === 200.msat)
}
test("retry without ignoring channels") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 3, routeParams = Some(routeParams))
sender.send(payFsm, payment)
router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectNoMsg(100 millis)
val (failedId, failedRoute) :: (_, pendingRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toList
childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.hops, AddHtlcFailed(randomBytes32, paymentHash, ChannelUnavailable(randomBytes32), null, None, None)))))
// If the router doesn't find routes, we will retry without ignoring the channel: it may work with a different split
// of the amount to send.
val expectedRouteRequest = RouteRequest(
nodeParams.nodeId, e,
failedRoute.amount, maxFee - failedRoute.fee,
ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab_1, a, b))),
pendingPayments = Seq(pendingRoute),
allowMultiPart = true,
routeParams = Some(routeParams.copy(randomize = true)),
paymentContext = Some(cfg.paymentContext))
router.expectMsg(expectedRouteRequest)
router.send(payFsm, Status.Failure(RouteNotFound))
router.expectMsg(expectedRouteRequest.copy(ignore = Ignore.empty))
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ac_1 :: hop_ce :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
val result = fulfillPendingPayments(f, 2)
assert(result.amountWithFees === 1000200.msat)
}
test("abort after too many failed attempts") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 2, routeParams = Some(routeParams))
sender.send(payFsm, payment)
router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId1, failedRoute1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.hops))))
router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ad :: hop_de :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(failedId1))
val (failedId2, failedRoute2) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(UnreadableRemoteFailure(failedRoute2.hops))))
assert(result.failures.length >= 3)
assert(result.failures.contains(LocalFailure(Nil, RetryExhausted)))
}
test("abort if no routes found") { f =>
import f._
sender.watch(payFsm)
val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
sender.send(payFsm, payment)
router.expectMsgType[RouteRequest]
router.send(payFsm, Status.Failure(RouteNotFound))
val result = sender.expectMsgType[PaymentFailed] val result = sender.expectMsgType[PaymentFailed]
assert(result.id === paymentId) assert(result.id === cfg.id)
assert(result.paymentHash === paymentHash) assert(result.paymentHash === paymentHash)
assert(result.failures === Seq(LocalFailure(Nil, RouteNotFound)))
sender.expectTerminated(payFsm)
sender.expectNoMsg(100 millis)
router.expectNoMsg(100 millis)
childPayFsm.expectNoMsg(100 millis)
}
test("abort if recipient sends error") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
sender.send(payFsm, payment)
router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.hops, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, 0))))))
assert(result.failures.length === 1) assert(result.failures.length === 1)
assert(result.failures.head.asInstanceOf[LocalFailure].t === PaymentError.BalanceTooLow)
} }
test("cannot send (fee rate too high)") { f => test("abort if recipient sends error during retry") { f =>
import f._ import f._
val payment = SendMultiPartPayment(randomBytes32, e, 2500 * 1000 msat, expiry, 3)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), OutgoingChannels(Seq( val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
OutgoingChannel(b, channelUpdate_ab_1, makeCommitments(1500 * 1000 msat, 1000)), sender.send(payFsm, payment)
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1500 * 1000 msat, 1000)), router.expectMsgType[RouteRequest]
OutgoingChannel(d, channelUpdate_ad_1, makeCommitments(1500 * 1000 msat, 1000)))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
) childPayFsm.expectMsgType[SendPaymentToRoute]
val result = sender.expectMsgType[PaymentFailed] childPayFsm.expectMsgType[SendPaymentToRoute]
assert(result.id === paymentId)
assert(result.paymentHash === paymentHash) val (failedId1, failedRoute1) :: (failedId2, failedRoute2) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toList
assert(result.failures.length === 1) childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(UnreadableRemoteFailure(failedRoute1.hops))))
assert(result.failures.head.asInstanceOf[LocalFailure].t === PaymentError.BalanceTooLow) router.expectMsgType[RouteRequest]
val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout)))))
assert(result.failures.length === 2)
} }
test("payment timeout") { f => test("receive partial success after retriable failure (recipient spec violation)") { f =>
import f._ import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 5)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, payment.totalAmount)
val (childId1, _) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
// If we receive a timeout failure, we directly abort the payment instead of retrying. val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
childPayFsm.send(payFsm, PaymentFailed(childId1, paymentHash, RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(e, PaymentTimeout)) :: Nil)) sender.send(payFsm, payment)
relayer.expectNoMsg(50 millis) router.expectMsgType[RouteRequest]
awaitCond(payFsm.stateName === PAYMENT_ABORTED) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
} childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
test("failure received from final recipient") { f => val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toList
import f._ childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(UnreadableRemoteFailure(failedRoute.hops))))
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 5) router.expectMsgType[RouteRequest]
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, payment.totalAmount)
val (childId1, _) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
// If we receive a failure from the final node, we directly abort the payment instead of retrying. val result = fulfillPendingPayments(f, 1)
childPayFsm.send(payFsm, PaymentFailed(childId1, paymentHash, RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(3000 * 1000 msat, 42))) :: Nil)) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount
relayer.expectNoMsg(50 millis) assert(result.nonTrampolineFees === successRoute.fee) // we paid the fee for only one of the partial payments
awaitCond(payFsm.stateName === PAYMENT_ABORTED) assert(result.parts.length === 1 && result.parts.head.id === successId)
}
test("fail after too many attempts") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 2)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1000), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, payment.totalAmount)
val (childId1, childPayment1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
// We retry one failure.
val failures = Seq(UnreadableRemoteFailure(hop_ab_1 :: Nil), UnreadableRemoteFailure(hop_ac_1 :: hop_ab_2 :: Nil))
childPayFsm.send(payFsm, PaymentFailed(childId1, paymentHash, failures.slice(0, 1)))
relayer.expectMsg(GetOutgoingChannels())
relayer.send(payFsm, localChannels())
waitUntilAmountSent(f, childPayment1.finalPayload.amount)
// But another failure occurs...
val (childId2, _) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
childPayFsm.send(payFsm, PaymentFailed(childId2, paymentHash, failures.slice(1, 2)))
relayer.expectNoMsg(50 millis)
awaitCond(payFsm.stateName === PAYMENT_ABORTED)
// And then all other payments time out.
payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId => childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Nil)))
val result = sender.expectMsgType[PaymentFailed]
assert(result.id === paymentId)
assert(result.paymentHash === paymentHash)
assert(result.failures.length === 3)
assert(result.failures.slice(0, 2) === failures)
assert(result.failures.last.asInstanceOf[LocalFailure].t === PaymentError.RetryExhausted)
}
test("receive partial failure after success (recipient spec violation)") { f =>
import f._
val payment = SendMultiPartPayment(randomBytes32, e, 4000 * 1000 msat, expiry, 2)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(1500), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, payment.totalAmount)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
// If one of the payments succeeds, the recipient MUST succeed them all: we can consider the whole payment succeeded.
val (id1, payment1) = pending.head
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id1, payment1.finalPayload.amount, 0 msat, randomBytes32, None))))
awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED)
// A partial failure should simply be ignored.
val (id2, payment2) = pending.tail.head
childPayFsm.send(payFsm, PaymentFailed(id2, paymentHash, Nil))
pending.tail.tail.foreach {
case (id, p) => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id, p.finalPayload.amount, 0 msat, randomBytes32, None))))
}
val result = sender.expectMsgType[PaymentSent]
assert(result.id === paymentId)
assert(result.amountWithFees === payment.totalAmount - payment2.finalPayload.amount)
} }
test("receive partial success after abort (recipient spec violation)") { f => test("receive partial success after abort (recipient spec violation)") { f =>
import f._ import f._
val payment = SendMultiPartPayment(randomBytes32, e, 5000 * 1000 msat, expiry, 1)
initPayment(f, payment, emptyStats.copy(capacity = Stats.generate(Seq(2000), d => Satoshi(d.toLong))), localChannels())
waitUntilAmountSent(f, payment.totalAmount)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
// One of the payments failed and we configured maxAttempts = 1, so we abort. val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
val (id1, _) = pending.head sender.send(payFsm, payment)
childPayFsm.send(payFsm, PaymentFailed(id1, paymentHash, Nil)) router.expectMsgType[RouteRequest]
router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
childPayFsm.expectMsgType[SendPaymentToRoute]
childPayFsm.expectMsgType[SendPaymentToRoute]
val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toList
childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout)))))
awaitCond(payFsm.stateName === PAYMENT_ABORTED) awaitCond(payFsm.stateName === PAYMENT_ABORTED)
// The in-flight HTLC set doesn't pay the full amount, so the recipient MUST not fulfill any of those. sender.watch(payFsm)
// But if he does, it's too bad for him as we have obtained a cheaper proof of payment. childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee, randomBytes32, Some(successRoute.hops)))))
val (id2, payment2) = pending.tail.head sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage))
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id2, payment2.finalPayload.amount, 5 msat, randomBytes32, None))))
awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED)
// Even if all other child payments fail, we obtained the preimage so the payment is a success from our point of view.
pending.tail.tail.foreach {
case (id, _) => childPayFsm.send(payFsm, PaymentFailed(id, paymentHash, Nil))
}
val result = sender.expectMsgType[PaymentSent] val result = sender.expectMsgType[PaymentSent]
assert(result.id === paymentId) assert(result.id === cfg.id)
assert(result.amountWithFees === payment2.finalPayload.amount + 5.msat) assert(result.paymentHash === paymentHash)
assert(result.nonTrampolineFees === 5.msat) assert(result.paymentPreimage === paymentPreimage)
assert(result.parts.length === 1 && result.parts.head.id === successId)
assert(result.recipientAmount === finalAmount)
assert(result.recipientNodeId === finalRecipient)
assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount
assert(result.nonTrampolineFees === successRoute.fee) // we paid the fee for only one of the partial payments
sender.expectTerminated(payFsm)
sender.expectNoMsg(100 millis)
router.expectNoMsg(100 millis)
childPayFsm.expectNoMsg(100 millis)
} }
test("split payment", Tag("fuzzy")) { f => test("receive partial failure after success (recipient spec violation)") { f =>
// The fees for a single HTLC will be 100 * 172 / 1000 = 17 satoshis. import f._
val testChannels = localChannels(100)
for (_ <- 1 to 100) { val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 5, routeParams = Some(routeParams))
// We have a total of 6500 satoshis across all channels. We try to send lower amounts to take fees into account. sender.send(payFsm, payment)
val toSend = ((1 + Random.nextInt(3500)) * 1000).msat router.expectMsgType[RouteRequest]
val networkStats = emptyStats.copy(capacity = Stats.generate(Seq(400 + Random.nextInt(1600)), d => Satoshi(d.toLong))) router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil))))
val routeParams = RouteParams(randomize = true, Random.nextInt(1000).msat, Random.nextInt(10).toDouble / 100, 20, CltvExpiryDelta(144), None, MultiPartParams(10000 msat, 5)) childPayFsm.expectMsgType[SendPaymentToRoute]
val request = SendMultiPartPayment(randomBytes32, e, toSend, CltvExpiry(561), 1, Nil, Some(routeParams)) childPayFsm.expectMsgType[SendPaymentToRoute]
val fuzzParams = s"(sending $toSend with network capacity ${networkStats.capacity.percentile75.toMilliSatoshi}, fee base ${routeParams.maxFeeBase} and fee percentage ${routeParams.maxFeePct})"
val (remaining, payments) = splitPayment(f.nodeParams, toSend, testChannels.channels, Some(networkStats), request, randomize = true) val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toList
assert(remaining === 0.msat, fuzzParams) childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee, randomBytes32, Some(route.hops)))))
assert(payments.nonEmpty, fuzzParams) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage))
assert(payments.map(_.finalPayload.amount).sum === toSend, fuzzParams) awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED)
sender.watch(payFsm)
childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout)))))
val result = sender.expectMsgType[PaymentSent]
assert(result.parts.length === 1 && result.parts.head.id === childId)
assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount
assert(result.nonTrampolineFees === route.fee) // we paid the fee for only one of the partial payments
sender.expectTerminated(payFsm)
sender.expectNoMsg(100 millis)
router.expectNoMsg(100 millis)
childPayFsm.expectNoMsg(100 millis)
}
def fulfillPendingPayments(f: FixtureParam, childCount: Int): PaymentSent = {
import f._
sender.watch(payFsm)
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
assert(pending.size === childCount)
val partialPayments = pending.map {
case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee, randomBytes32, Some(route.hops))
} }
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp))))
sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage))
val result = sender.expectMsgType[PaymentSent]
assert(result.id === cfg.id)
assert(result.paymentHash === paymentHash)
assert(result.paymentPreimage === paymentPreimage)
assert(result.parts.toSet === partialPayments.toSet)
assert(result.recipientAmount === finalAmount)
assert(result.recipientNodeId === finalRecipient)
sender.expectTerminated(payFsm)
sender.expectNoMsg(100 millis)
router.expectNoMsg(100 millis)
childPayFsm.expectNoMsg(100 millis)
result
}
def abortAfterFailure(f: FixtureParam, childFailure: PaymentFailed): PaymentFailed = {
import f._
sender.watch(payFsm)
val pendingCount = payFsm.stateData.asInstanceOf[PaymentProgress].pending.size
childPayFsm.send(payFsm, childFailure) // this failure should trigger an abort
if (pendingCount > 1) {
awaitCond(payFsm.stateName === PAYMENT_ABORTED)
assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size === pendingCount - 1)
// Fail all remaining child payments.
payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId =>
childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, PaymentTimeout)))))
)
}
val result = sender.expectMsgType[PaymentFailed]
assert(result.id === cfg.id)
assert(result.paymentHash === paymentHash)
assert(result.failures.nonEmpty)
sender.expectTerminated(payFsm)
sender.expectNoMsg(100 millis)
router.expectNoMsg(100 millis)
childPayFsm.expectNoMsg(100 millis)
result
} }
} }
@ -513,6 +459,8 @@ object MultiPartPaymentLifecycleSpec {
val expiry = CltvExpiry(1105) val expiry = CltvExpiry(1105)
val finalAmount = 1000000 msat val finalAmount = 1000000 msat
val finalRecipient = randomKey.publicKey val finalRecipient = randomKey.publicKey
val routeParams = RouteParams(randomize = false, 15000 msat, 0.01, 6, CltvExpiryDelta(1008), None, MultiPartParams(1000 msat, 5))
val maxFee = 15000 msat // max fee for the defaultAmount
/** /**
* We simulate a multi-part-friendly network: * We simulate a multi-part-friendly network:
@ -527,35 +475,29 @@ object MultiPartPaymentLifecycleSpec {
val a :: b :: c :: d :: e :: Nil = Seq.fill(5)(randomKey.publicKey) val a :: b :: c :: d :: e :: Nil = Seq.fill(5)(randomKey.publicKey)
val channelId_ab_1 = ShortChannelId(1) val channelId_ab_1 = ShortChannelId(1)
val channelId_ab_2 = ShortChannelId(2) val channelId_ab_2 = ShortChannelId(2)
val channelId_be = ShortChannelId(3)
val channelId_ac_1 = ShortChannelId(11) val channelId_ac_1 = ShortChannelId(11)
val channelId_ac_2 = ShortChannelId(12) val channelId_ac_2 = ShortChannelId(12)
val channelId_ac_3 = ShortChannelId(13) val channelId_ce = ShortChannelId(13)
val channelId_ad_1 = ShortChannelId(21) val channelId_ad = ShortChannelId(21)
val defaultChannelUpdate = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, 1, ChannelFlags.AnnounceChannel, CltvExpiryDelta(12), 1 msat, 0 msat, 0, Some(2000 * 1000 msat)) val channelId_de = ShortChannelId(22)
val channelUpdate_ab_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_1, cltvExpiryDelta = CltvExpiryDelta(4), feeBaseMsat = 100 msat, feeProportionalMillionths = 70) val defaultChannelUpdate = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(0), 0, 1, ChannelFlags.AnnounceChannel, CltvExpiryDelta(12), 1 msat, 100 msat, 0, Some(2000000 msat))
val channelUpdate_ab_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_2, cltvExpiryDelta = CltvExpiryDelta(4), feeBaseMsat = 100 msat, feeProportionalMillionths = 70) val channelUpdate_ab_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_1)
val channelUpdate_ac_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_1, cltvExpiryDelta = CltvExpiryDelta(5), feeBaseMsat = 150 msat, feeProportionalMillionths = 40) val channelUpdate_ab_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ab_2)
val channelUpdate_ac_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_2, cltvExpiryDelta = CltvExpiryDelta(5), feeBaseMsat = 150 msat, feeProportionalMillionths = 40) val channelUpdate_be = defaultChannelUpdate.copy(shortChannelId = channelId_be)
val channelUpdate_ac_3 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_3, cltvExpiryDelta = CltvExpiryDelta(5), feeBaseMsat = 150 msat, feeProportionalMillionths = 40) val channelUpdate_ac_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_1)
val channelUpdate_ad_1 = defaultChannelUpdate.copy(shortChannelId = channelId_ad_1, cltvExpiryDelta = CltvExpiryDelta(6), feeBaseMsat = 200 msat, feeProportionalMillionths = 50) val channelUpdate_ac_2 = defaultChannelUpdate.copy(shortChannelId = channelId_ac_2)
val channelUpdate_ce = defaultChannelUpdate.copy(shortChannelId = channelId_ce)
// With a fee rate of 10, the fees for a single HTLC will be 10 * 172 / 1000 = 1 satoshi. val channelUpdate_ad = defaultChannelUpdate.copy(shortChannelId = channelId_ad)
def localChannels(feeRatePerKw: Long = 10): OutgoingChannels = OutgoingChannels(Seq( val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = channelId_de)
OutgoingChannel(b, channelUpdate_ab_1, makeCommitments(1000 * 1000 msat, feeRatePerKw)),
OutgoingChannel(b, channelUpdate_ab_2, makeCommitments(1500 * 1000 msat, feeRatePerKw)),
OutgoingChannel(c, channelUpdate_ac_1, makeCommitments(500 * 1000 msat, feeRatePerKw)),
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1000 * 1000 msat, feeRatePerKw)),
OutgoingChannel(c, channelUpdate_ac_3, makeCommitments(1500 * 1000 msat, feeRatePerKw)),
OutgoingChannel(d, channelUpdate_ad_1, makeCommitments(1000 * 1000 msat, feeRatePerKw))))
val hop_ab_1 = ChannelHop(a, b, channelUpdate_ab_1) val hop_ab_1 = ChannelHop(a, b, channelUpdate_ab_1)
val hop_ab_2 = ChannelHop(a, b, channelUpdate_ab_2) val hop_ab_2 = ChannelHop(a, b, channelUpdate_ab_2)
val hop_be = ChannelHop(b, e, channelUpdate_be)
val hop_ac_1 = ChannelHop(a, c, channelUpdate_ac_1) val hop_ac_1 = ChannelHop(a, c, channelUpdate_ac_1)
val hop_ac_2 = ChannelHop(a, c, channelUpdate_ac_2)
val emptyStats = NetworkStats(0, 0, Stats.generate(Seq(0), d => Satoshi(d.toLong)), Stats.generate(Seq(0), d => CltvExpiryDelta(d.toInt)), Stats.generate(Seq(0), d => MilliSatoshi(d.toLong)), Stats.generate(Seq(0), d => d.toLong)) val hop_ce = ChannelHop(c, e, channelUpdate_ce)
val hop_ad = ChannelHop(a, d, channelUpdate_ad)
// We are only interested in availableBalanceForSend so we can put dummy values for the rest. val hop_de = ChannelHop(d, e, channelUpdate_de)
def makeCommitments(canSend: MilliSatoshi, feeRatePerKw: Long, announceChannel: Boolean = true): Commitments =
CommitmentsSpec.makeCommitments(canSend, 0 msat, feeRatePerKw, 0 sat, announceChannel = announceChannel)
} }

View file

@ -26,12 +26,11 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer, Origin, Relayer} import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment}
import fr.acinq.eclair.payment.send.PaymentError
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound}
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, nodeFee, randomBytes, randomBytes32, randomKey} import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, nodeFee, randomBytes, randomBytes32, randomKey}
import org.scalatest.Outcome import org.scalatest.Outcome
@ -56,16 +55,16 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
within(30 seconds) { within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams val nodeParams = TestConstants.Bob.nodeParams
val outgoingPayFSM = TestProbe() val outgoingPayFSM = TestProbe()
val (relayer, router, commandBuffer, register, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe()) val (router, commandBuffer, register, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe())
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
class TestNodeRelayer extends NodeRelayer(nodeParams, relayer.ref, router.ref, commandBuffer.ref, register.ref) { class TestNodeRelayer extends NodeRelayer(nodeParams, router.ref, commandBuffer.ref, register.ref) {
override def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = { override def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
outgoingPayFSM.ref ! cfg outgoingPayFSM.ref ! cfg
outgoingPayFSM.ref outgoingPayFSM.ref
} }
} }
val nodeRelayer = TestActorRef(new TestNodeRelayer().asInstanceOf[NodeRelayer]) val nodeRelayer = TestActorRef(new TestNodeRelayer().asInstanceOf[NodeRelayer])
withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeRelayer, relayer, outgoingPayFSM, commandBuffer, eventListener))) withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeRelayer, TestProbe(), outgoingPayFSM, commandBuffer, eventListener)))
} }
} }
@ -219,7 +218,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id
outgoingPayFSM.expectMsgType[SendMultiPartPayment] outgoingPayFSM.expectMsgType[SendMultiPartPayment]
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, LocalFailure(Nil, PaymentError.BalanceTooLow) :: Nil)) outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, LocalFailure(Nil, BalanceTooLow) :: Nil))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true)))) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true))))
commandBuffer.expectNoMsg(100 millis) commandBuffer.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis) eventListener.expectNoMsg(100 millis)
@ -249,7 +248,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id
outgoingPayFSM.expectMsgType[SendMultiPartPayment] outgoingPayFSM.expectMsgType[SendMultiPartPayment]
val failures = RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(Nil) :: LocalFailure(Nil, RouteNotFound) :: Nil val failures = RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(Nil) :: Nil
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, failures)) outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, failures))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)))) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true))))
commandBuffer.expectNoMsg(100 millis) commandBuffer.expectNoMsg(100 millis)
@ -281,18 +280,12 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment] val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment) validateOutgoingPayment(outgoingPayment)
// A first downstream HTLC is fulfilled. // A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream.
val ff1 = createDownstreamFulfill(outgoingPayFSM.ref) outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
relayer.send(nodeRelayer, ff1)
outgoingPayFSM.expectMsg(ff1)
// We should immediately forward the fulfill upstream.
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)))) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
// A second downstream HTLC is fulfilled. // If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream.
val ff2 = createDownstreamFulfill(outgoingPayFSM.ref) outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
relayer.send(nodeRelayer, ff2)
outgoingPayFSM.expectMsg(ff2)
// We should not fulfill a second time upstream.
commandBuffer.expectNoMsg(100 millis) commandBuffer.expectNoMsg(100 millis)
// Once all the downstream payments have settled, we should emit the relayed event. // Once all the downstream payments have settled, we should emit the relayed event.
@ -315,9 +308,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment] val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment]
validateOutgoingPayment(outgoingPayment) validateOutgoingPayment(outgoingPayment)
val ff = createDownstreamFulfill(outgoingPayFSM.ref) outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
relayer.send(nodeRelayer, ff)
outgoingPayFSM.expectMsg(ff)
val incomingAdd = incomingSinglePart.add val incomingAdd = incomingSinglePart.add
commandBuffer.expectMsg(CommandBuffer.CommandSend(incomingAdd.channelId, CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true))) commandBuffer.expectMsg(CommandBuffer.CommandSend(incomingAdd.channelId, CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)))
@ -329,8 +320,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
commandBuffer.expectNoMsg(100 millis) commandBuffer.expectNoMsg(100 millis)
} }
// TODO: re-activate this test once we have better MPP split to remote legacy recipients test("relay to non-trampoline recipient supporting multi-part") { f =>
ignore("relay to non-trampoline recipient supporting multi-part") { f =>
import f._ import f._
// Receive an upstream multi-part payment. // Receive an upstream multi-part payment.
@ -352,9 +342,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
assert(outgoingPayment.routeParams.isDefined) assert(outgoingPayment.routeParams.isDefined)
assert(outgoingPayment.assistedRoutes === hints) assert(outgoingPayment.assistedRoutes === hints)
val ff = createDownstreamFulfill(outgoingPayFSM.ref) outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
relayer.send(nodeRelayer, ff)
outgoingPayFSM.expectMsg(ff)
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)))) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id)) outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
@ -378,16 +366,13 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig] val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add))) validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add)))
val outgoingPayment = outgoingPayFSM.expectMsgType[SendPayment] val outgoingPayment = outgoingPayFSM.expectMsgType[SendPayment]
assert(outgoingPayment.routePrefix === Nil)
assert(outgoingPayment.finalPayload.amount === outgoingAmount) assert(outgoingPayment.finalPayload.amount === outgoingAmount)
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry) assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
assert(outgoingPayment.targetNodeId === outgoingNodeId) assert(outgoingPayment.targetNodeId === outgoingNodeId)
assert(outgoingPayment.routeParams.isDefined) assert(outgoingPayment.routeParams.isDefined)
assert(outgoingPayment.assistedRoutes === hints) assert(outgoingPayment.assistedRoutes === hints)
val ff = createDownstreamFulfill(outgoingPayFSM.ref) outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
relayer.send(nodeRelayer, ff)
outgoingPayFSM.expectMsg(ff)
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)))) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id)) outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
@ -449,11 +434,6 @@ object NodeRelayerSpec {
val incomingSinglePart = val incomingSinglePart =
createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry) createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry)
def createDownstreamFulfill(payFSM: ActorRef): Relayer.ForwardFulfill = {
val origin = Origin.TrampolineRelayed(null, Some(payFSM))
Relayer.ForwardRemoteFulfill(UpdateFulfillHtlc(randomBytes32, Random.nextInt(100), paymentPreimage), origin, null)
}
def createSuccessEvent(id: UUID): PaymentSent = def createSuccessEvent(id: UUID): PaymentSent =
PaymentSent(id, paymentHash, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(id, outgoingAmount, 10 msat, randomBytes32, None))) PaymentSent(id, paymentHash, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(id, outgoingAmount, 10 msat, randomBytes32, None)))

View file

@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayme
import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.payment.send.PaymentInitiator._
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute} import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator} import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator}
import fr.acinq.eclair.router.RouteNotFound
import fr.acinq.eclair.router.Router.{MultiPartParams, NodeHop, RouteParams} import fr.acinq.eclair.router.Router.{MultiPartParams, NodeHop, RouteParams}
import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload} import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload}
import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv} import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv}
@ -69,7 +70,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe()) val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
val eventListener = TestProbe() val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
class TestPaymentInitiator extends PaymentInitiator(nodeParams, TestProbe().ref, TestProbe().ref, TestProbe().ref) { class TestPaymentInitiator extends PaymentInitiator(nodeParams, TestProbe().ref, TestProbe().ref) {
// @formatter:off // @formatter:off
override def spawnPaymentFsm(cfg: SendPaymentConfig): ActorRef = { override def spawnPaymentFsm(cfg: SendPaymentConfig): ActorRef = {
payFsm.ref ! cfg payFsm.ref ! cfg
@ -116,7 +117,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
sender.send(initiator, SendPaymentToRouteRequest(finalAmount, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil)) sender.send(initiator, SendPaymentToRouteRequest(finalAmount, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil))
val payment = sender.expectMsgType[SendPaymentToRouteResponse] val payment = sender.expectMsgType[SendPaymentToRouteResponse]
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil)) payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
payFsm.expectMsg(SendPaymentToRoute(Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)))) payFsm.expectMsg(SendPaymentToRoute(Left(Seq(a, b, c)), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
} }
test("forward legacy payment") { f => test("forward legacy payment") { f =>
@ -162,7 +163,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val payment = sender.expectMsgType[SendPaymentToRouteResponse] val payment = sender.expectMsgType[SendPaymentToRouteResponse]
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil)) payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
val msg = payFsm.expectMsgType[SendPaymentToRoute] val msg = payFsm.expectMsgType[SendPaymentToRoute]
assert(msg.hops === Seq(a, b, c)) assert(msg.route === Left(Seq(a, b, c)))
assert(msg.finalPayload.amount === finalAmount / 2) assert(msg.finalPayload.amount === finalAmount / 2)
assert(msg.finalPayload.paymentSecret === pr.paymentSecret) assert(msg.finalPayload.paymentSecret === pr.paymentSecret)
assert(msg.finalPayload.totalAmount === finalAmount) assert(msg.finalPayload.totalAmount === finalAmount)
@ -302,13 +303,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(msg1.totalAmount === finalAmount + 21000.msat) assert(msg1.totalAmount === finalAmount + 21000.msat)
// Simulate a failure which should trigger a retry. // Simulate a failure which should trigger a retry.
val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))))
multiPartPayFsm.send(initiator, failed)
multiPartPayFsm.expectMsgType[SendPaymentConfig] multiPartPayFsm.expectMsgType[SendPaymentConfig]
val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
assert(msg2.totalAmount === finalAmount + 25000.msat) assert(msg2.totalAmount === finalAmount + 25000.msat)
// Simulate a failure that exhausts payment attempts. // Simulate a failure that exhausts payment attempts.
val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))
multiPartPayFsm.send(initiator, failed) multiPartPayFsm.send(initiator, failed)
sender.expectMsg(failed) sender.expectMsg(failed)
eventListener.expectMsg(failed) eventListener.expectMsg(failed)
@ -316,6 +317,34 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
eventListener.expectNoMsg(100 millis) eventListener.expectNoMsg(100 millis)
} }
test("retry trampoline payment and fail (route not found)") { f =>
import f._
val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features))
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
sender.send(initiator, req)
sender.expectMsgType[UUID]
val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig]
val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
assert(msg1.totalAmount === finalAmount + 21000.msat)
// Trampoline node couldn't find a route for the given fee.
val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))
multiPartPayFsm.send(initiator, failed)
multiPartPayFsm.expectMsgType[SendPaymentConfig]
val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
assert(msg2.totalAmount === finalAmount + 25000.msat)
// Trampoline node couldn't find a route even with the increased fee.
multiPartPayFsm.send(initiator, failed)
val failure = sender.expectMsgType[PaymentFailed]
assert(failure.failures === Seq(LocalFailure(Seq(NodeHop(nodeParams.nodeId, b, nodeParams.expiryDeltaBlocks, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound)))
eventListener.expectMsg(failure)
sender.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
test("forward trampoline payment with pre-defined route") { f => test("forward trampoline payment with pre-defined route") { f =>
import f._ import f._
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice") val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice")
@ -326,7 +355,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
assert(payment.trampolineSecret.nonEmpty) assert(payment.trampolineSecret.nonEmpty)
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat))))
val msg = payFsm.expectMsgType[SendPaymentToRoute] val msg = payFsm.expectMsgType[SendPaymentToRoute]
assert(msg.hops === Seq(a, b)) assert(msg.route === Left(Seq(a, b)))
assert(msg.finalPayload.amount === finalAmount + trampolineFees) assert(msg.finalPayload.amount === finalAmount + trampolineFees)
assert(msg.finalPayload.paymentSecret === payment.trampolineSecret) assert(msg.finalPayload.paymentSecret === payment.trampolineSecret)
assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees) assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees)

View file

@ -62,10 +62,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val defaultExternalId = UUID.randomUUID().toString val defaultExternalId = UUID.randomUUID().toString
val defaultPaymentRequest = SendPaymentRequest(defaultAmountMsat, defaultPaymentHash, d, 1, externalId = Some(defaultExternalId)) val defaultPaymentRequest = SendPaymentRequest(defaultAmountMsat, defaultPaymentHash, d, 1, externalId = Some(defaultExternalId))
def defaultRouteRequest(source: PublicKey, target: PublicKey): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee) def defaultRouteRequest(source: PublicKey, target: PublicKey, cfg: SendPaymentConfig): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee, paymentContext = Some(cfg.paymentContext))
case class PaymentFixture(id: UUID, case class PaymentFixture(cfg: SendPaymentConfig,
parentId: UUID,
nodeParams: NodeParams, nodeParams: NodeParams,
paymentFSM: TestFSMRef[PaymentLifecycle.State, PaymentLifecycle.Data, PaymentLifecycle], paymentFSM: TestFSMRef[PaymentLifecycle.State, PaymentLifecycle.Data, PaymentLifecycle],
routerForwarder: TestProbe, routerForwarder: TestProbe,
@ -83,18 +82,43 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
paymentFSM ! SubscribeTransitionCallBack(monitor.ref) paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
PaymentFixture(id, parentId, nodeParams, paymentFSM, routerForwarder, register, sender, monitor, eventListener) PaymentFixture(cfg, nodeParams, paymentFSM, routerForwarder, register, sender, monitor, eventListener)
} }
test("send to route") { routerFixture => test("send to route") { _ =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
// pre-computed route going from A to D // pre-computed route going from A to D
val request = SendPaymentToRoute(Seq(a, b, c, d), FinalLegacyPayload(defaultAmountMsat, defaultExpiry)) val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil)
val request = SendPaymentToRoute(Right(route), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
sender.send(paymentFSM, request)
routerForwarder.expectNoMsg(100 millis) // we don't need the router, we have the pre-computed route
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[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
sender.send(paymentFSM, Relayer.ForwardRemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage), defaultOrigin, UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket)))
val ps = sender.expectMsgType[PaymentSent]
assert(ps.id === parentId)
assert(ps.parts.head.route === Some(route.hops))
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]))
}
test("send to route (node_id only)") { routerFixture =>
val payFixture = createPaymentLifecycle()
import payFixture._
import cfg._
// pre-computed route going from A to D
val request = SendPaymentToRoute(Left(Seq(a, b, c, d)), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, Seq(a, b, c, d))) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, Seq(a, b, c, d), paymentContext = Some(cfg.paymentContext)))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
@ -113,7 +137,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
val brokenRoute = SendPaymentToRoute(Seq(randomKey.publicKey, randomKey.publicKey, randomKey.publicKey), FinalLegacyPayload(defaultAmountMsat, defaultExpiry)) val brokenRoute = SendPaymentToRoute(Left(Seq(randomKey.publicKey, randomKey.publicKey, randomKey.publicKey)), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
sender.send(paymentFSM, brokenRoute) sender.send(paymentFSM, brokenRoute)
routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.expectMsgType[FinalizeRoute]
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
@ -125,13 +149,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("send to route (routing hints)") { routerFixture => test("send to route (routing hints)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val recipient = randomKey.publicKey val recipient = randomKey.publicKey
val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144)))) val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))))
val request = SendPaymentToRoute(Seq(a, b, c, recipient), FinalLegacyPayload(defaultAmountMsat, defaultExpiry), routingHint) val request = SendPaymentToRoute(Left(Seq(a, b, c, recipient)), FinalLegacyPayload(defaultAmountMsat, defaultExpiry), routingHint)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, Seq(a, b, c, recipient), routingHint)) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, Seq(a, b, c, recipient), routingHint, paymentContext = Some(cfg.paymentContext)))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
@ -145,54 +170,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]))
} }
test("send with route prefix") { _ =>
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(c, d).copy(ignoreNodes = Set(a, b)))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
routerForwarder.send(paymentFSM, RouteResponse(Route(defaultAmountMsat, Seq(ChannelHop(c, d, update_cd))) :: Nil))
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
}
test("send with whole route prefix") { _ =>
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(c, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
sender.send(paymentFSM, request)
routerForwarder.expectNoMsg(50 millis) // we don't need the router when we already have the whole route
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[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
}
test("send with route prefix and retry") { _ =>
val payFixture = createPaymentLifecycle()
import payFixture._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, update_ab), ChannelHop(b, c, update_bc)))
sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(c, d).copy(ignoreNodes = Set(a, b)))
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
routerForwarder.send(paymentFSM, RouteResponse(Route(defaultAmountMsat, Seq(ChannelHop(c, d, update_cd))) :: Nil))
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
sender.send(paymentFSM, UpdateFailHtlc(randomBytes32, 0, randomBytes(Sphinx.FailurePacket.PacketLength)))
routerForwarder.expectMsg(defaultRouteRequest(c, d).copy(ignoreNodes = Set(a, b, c)))
val Transition(_, WAITING_FOR_PAYMENT_COMPLETE, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
assert(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
}
test("payment failed (route not found)") { routerFixture => test("payment failed (route not found)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(f, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5) val request = SendPayment(f, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
@ -208,6 +189,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("payment failed (route too expensive)") { routerFixture => test("payment failed (route too expensive)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, routeParams = Some(RouteParams(randomize = false, 100 msat, 0.0, 20, CltvExpiryDelta(2016), None, MultiPartParams(10000 msat, 5)))) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, routeParams = Some(RouteParams(randomize = false, 100 msat, 0.0, 20, CltvExpiryDelta(2016), None, MultiPartParams(10000 msat, 5))))
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
@ -222,29 +204,30 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("payment failed (unparsable failure)") { routerFixture => test("payment failed (unparsable failure)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(a, d)) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, _, ignoreNodes1, _, route) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, _, ignore1, route) = paymentFSM.stateData
assert(ignoreNodes1.isEmpty) assert(ignore1.nodes.isEmpty)
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
sender.send(paymentFSM, Relayer.ForwardRemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32), defaultOrigin, UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket))) // unparsable message sender.send(paymentFSM, Relayer.ForwardRemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32), defaultOrigin, UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket))) // unparsable message
// then the payment lifecycle will ask for a new route excluding all intermediate nodes // then the payment lifecycle will ask for a new route excluding all intermediate nodes
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(ignoreNodes = Set(c))) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set(c), Set.empty)))
// let's simulate a response by the router with another route // let's simulate a response by the router with another route
sender.send(paymentFSM, RouteResponse(route :: Nil)) sender.send(paymentFSM, RouteResponse(route :: Nil))
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd2, _, _, ignoreNodes2, _, _) = paymentFSM.stateData val WaitingForComplete(_, _, cmd2, _, _, ignore2, _) = paymentFSM.stateData
assert(ignoreNodes2 === Set(c)) assert(ignore2.nodes === Set(c))
// and reply a 2nd time with an unparsable failure // and reply a 2nd time with an unparsable failure
register.expectMsg(ForwardShortId(channelId_ab, cmd2)) register.expectMsg(ForwardShortId(channelId_ab, cmd2))
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) // unparsable message sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) // unparsable message
@ -257,59 +240,62 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("payment failed (local error)") { routerFixture => test("payment failed (local error)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, defaultPaymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None))) sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, defaultPaymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None)))
// then the payment lifecycle will ask for a new route excluding the channel // then the payment lifecycle will ask for a new route excluding the channel
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b)))))
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable
} }
test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { routerFixture => test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32, FailureMessageCodecs.BADONION)) sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32, FailureMessageCodecs.BADONION))
// then the payment lifecycle will ask for a new route excluding the channel // then the payment lifecycle will ask for a new route excluding the channel
routerForwarder.expectMsg(defaultRouteRequest(a, d).copy(ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b)))))
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
} }
test("payment failed (TemporaryChannelFailure)") { routerFixture => test("payment failed (TemporaryChannelFailure)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, route) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
val failure = TemporaryChannelFailure(update_bc) val failure = TemporaryChannelFailure(update_bc)
@ -321,7 +307,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// payment lifecycle forwards the embedded channelUpdate to the router // payment lifecycle forwards the embedded channelUpdate to the router
routerForwarder.expectMsg(update_bc) routerForwarder.expectMsg(update_bc)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
routerForwarder.expectMsg(defaultRouteRequest(a, d)) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
// we allow 2 tries, so we send a 2nd request to the router // we allow 2 tries, so we send a 2nd request to the router
assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(Nil, RouteNotFound) :: Nil) assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(Nil, RouteNotFound) :: Nil)
@ -330,16 +316,17 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("payment failed (Update)") { routerFixture => test("payment failed (Update)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route1) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
// we change the cltv expiry // we change the cltv expiry
@ -351,12 +338,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// payment lifecycle forwards the embedded channelUpdate to the router // payment lifecycle forwards the embedded channelUpdate to the router
routerForwarder.expectMsg(channelUpdate_bc_modified) routerForwarder.expectMsg(channelUpdate_bc_modified)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
// router answers with a new route, taking into account the new update // router answers with a new route, taking into account the new update
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd2, _, sharedSecrets2, _, _, route2) = paymentFSM.stateData val WaitingForComplete(_, _, cmd2, _, sharedSecrets2, _, route2) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd2)) register.expectMsg(ForwardShortId(channelId_ab, cmd2))
// we change the cltv expiry one more time // we change the cltv expiry one more time
@ -371,7 +358,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
// but it will still forward the embedded channelUpdate to the router // but it will still forward the embedded channelUpdate to the router
routerForwarder.expectMsg(channelUpdate_bc_modified_2) routerForwarder.expectMsg(channelUpdate_bc_modified_2)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
// this time the router can't find a route: game over // this time the router can't find a route: game over
@ -379,9 +366,34 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
} }
test("payment failed (Update in last attempt)") { routerFixture =>
val payFixture = createPaymentLifecycle()
import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 1)
sender.send(paymentFSM, request)
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1))
// the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue)
val failure = TemporaryChannelFailure(update_bc)
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure)))
// we should temporarily exclude that channel
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c)))
routerForwarder.expectMsg(update_bc)
// this was a single attempt payment
sender.expectMsgType[PaymentFailed]
}
test("payment failed (Update in assisted route)") { routerFixture => test("payment failed (Update in assisted route)") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
// we build an assisted route for channel bc and cd // we build an assisted route for channel bc and cd
val assistedRoutes = Seq(Seq( val assistedRoutes = Seq(Seq(
@ -393,11 +405,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(assistedRoutes = assistedRoutes)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, _) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
// we change the cltv expiry // we change the cltv expiry
@ -413,36 +425,64 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta), ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta),
ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)
)) ))
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(assistedRoutes = assistedRoutes1)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes1))
routerForwarder.forward(routerFixture.router) routerForwarder.forward(routerFixture.router)
// router answers with a new route, taking into account the new update // router answers with a new route, taking into account the new update
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd2, _, _, _, _, _) = paymentFSM.stateData val WaitingForComplete(_, _, cmd2, _, _, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd2)) register.expectMsg(ForwardShortId(channelId_ab, cmd2))
assert(cmd2.cltvExpiry > cmd1.cltvExpiry) assert(cmd2.cltvExpiry > cmd1.cltvExpiry)
} }
test("payment failed (Update disabled in assisted route)") { routerFixture =>
val payFixture = createPaymentLifecycle()
import payFixture._
import cfg._
// we build an assisted route for channel cd
val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)))
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 1, assistedRoutes = assistedRoutes)
sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes))
routerForwarder.forward(routerFixture.router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1))
// we disable the channel
val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat.get, enable = false)
val failure = ChannelDisabled(channelUpdate_cd_disabled.messageFlags, channelUpdate_cd_disabled.channelFlags, channelUpdate_cd_disabled)
val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1)._1, failure), sharedSecrets1.head._1)
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion))
routerForwarder.expectMsg(channelUpdate_cd_disabled)
routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_cd.shortChannelId, c, d)))
}
def testPermanentFailure(router: ActorRef, failure: FailureMessage): Unit = { def testPermanentFailure(router: ActorRef, failure: FailureMessage): Unit = {
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData val WaitingForRoute(_, _, Nil, _) = paymentFSM.stateData
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg))
routerForwarder.forward(router) routerForwarder.forward(router)
awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE)
val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route1) = paymentFSM.stateData val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData
register.expectMsg(ForwardShortId(channelId_ab, cmd1)) register.expectMsg(ForwardShortId(channelId_ab, cmd1))
sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure)))
// payment lifecycle forwards the embedded channelUpdate to the router // payment lifecycle forwards the embedded channelUpdate to the router
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d).copy(ignoreChannels = Set(ChannelDesc(channelId_bc, b, c)))) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_bc, b, c)))))
routerForwarder.forward(router) routerForwarder.forward(router)
// we allow 2 tries, so we send a 2nd request to the router, which won't find another route // we allow 2 tries, so we send a 2nd request to the router, which won't find another route
@ -463,6 +503,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
test("payment succeeded") { routerFixture => test("payment succeeded") { routerFixture =>
val payFixture = createPaymentLifecycle() val payFixture = createPaymentLifecycle()
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)
@ -569,9 +610,9 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
) )
for ((failure, expectedNodes, expectedChannels) <- testCases) { for ((failure, expectedNodes, expectedChannels) <- testCases) {
val (ignoreNodes, ignoreChannels) = PaymentFailure.updateIgnored(failure, Set.empty[PublicKey], Set.empty[ChannelDesc]) val ignore = PaymentFailure.updateIgnored(failure, Ignore.empty)
assert(ignoreNodes === expectedNodes, failure) assert(ignore.nodes === expectedNodes, failure)
assert(ignoreChannels === expectedChannels, failure) assert(ignore.channels === expectedChannels, failure)
} }
val failures = Seq( val failures = Seq(
@ -579,14 +620,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(b, UnknownNextPeer)), RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(b, UnknownNextPeer)),
LocalFailure(route_abcd, new RuntimeException("fatal")) LocalFailure(route_abcd, new RuntimeException("fatal"))
) )
val (ignoreNodes, ignoreChannels) = PaymentFailure.updateIgnored(failures, Set.empty[PublicKey], Set.empty[ChannelDesc]) val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty)
assert(ignoreNodes === Set(c)) assert(ignore.nodes === Set(c))
assert(ignoreChannels === Set(ChannelDesc(channelId_ab, a, b), ChannelDesc(channelId_bc, b, c))) assert(ignore.channels === Set(ChannelDesc(channelId_ab, a, b), ChannelDesc(channelId_bc, b, c)))
} }
test("disable database and events") { routerFixture => test("disable database and events") { routerFixture =>
val payFixture = createPaymentLifecycle(storeInDb = false, publishEvent = false) val payFixture = createPaymentLifecycle(storeInDb = false, publishEvent = false)
import payFixture._ import payFixture._
import cfg._
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3) val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3)
sender.send(paymentFSM, request) sender.send(paymentFSM, request)

View file

@ -28,7 +28,8 @@ import fr.acinq.eclair.payment.OutgoingPacket.{buildCommand, buildOnion, buildPa
import fr.acinq.eclair.payment.relay.Origin._ import fr.acinq.eclair.payment.relay.Origin._
import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer} import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.router.Router.{ChannelHop, GetNetworkStats, GetNetworkStatsResponse, NodeHop, TickComputeNetworkStats} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.router.Router.{ChannelHop, Ignore, NodeHop}
import fr.acinq.eclair.router.{Announcements, _} import fr.acinq.eclair.router.{Announcements, _}
import fr.acinq.eclair.wire.Onion.{ChannelRelayTlvPayload, FinalLegacyPayload, FinalTlvPayload, PerHopPayload} import fr.acinq.eclair.wire.Onion.{ChannelRelayTlvPayload, FinalLegacyPayload, FinalTlvPayload, PerHopPayload}
import fr.acinq.eclair.wire._ import fr.acinq.eclair.wire._
@ -177,9 +178,13 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
sender.send(relayer, ForwardAdd(add_ab2)) sender.send(relayer, ForwardAdd(add_ab2))
// A multi-part payment FSM should start to relay the payment. // A multi-part payment FSM should start to relay the payment.
router.expectMsg(GetNetworkStats) val routeRequest1 = router.expectMsgType[Router.RouteRequest]
router.send(router.lastSender, GetNetworkStatsResponse(None)) assert(routeRequest1.source === b)
router.expectMsg(TickComputeNetworkStats) assert(routeRequest1.target === c)
assert(routeRequest1.amount === finalAmount)
assert(routeRequest1.allowMultiPart)
assert(routeRequest1.ignore === Ignore.empty)
router.send(router.lastSender, Router.RouteResponse(Router.Route(finalAmount, ChannelHop(b, c, channelUpdate_bc) :: Nil) :: Nil))
// first try // first try
val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
@ -191,6 +196,10 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcValueTooHighInFlight(channelId_bc, UInt64(1000000000L), 1516977616L msat), origin1, Some(channelUpdate_bc), originalCommand = Some(fwd1.message)))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, HtlcValueTooHighInFlight(channelId_bc, UInt64(1000000000L), 1516977616L msat), origin1, Some(channelUpdate_bc), originalCommand = Some(fwd1.message))))
// second try // second try
val routeRequest2 = router.expectMsgType[Router.RouteRequest]
assert(routeRequest2.ignore.channels.map(_.shortChannelId) === Set(channelUpdate_bc.shortChannelId))
router.send(router.lastSender, Router.RouteResponse(Router.Route(finalAmount, ChannelHop(b, c, channelUpdate_bc) :: Nil) :: Nil))
val fwd2 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] val fwd2 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
assert(fwd2.shortChannelId === channelUpdate_bc.shortChannelId) assert(fwd2.shortChannelId === channelUpdate_bc.shortChannelId)
assert(fwd2.message.upstream.asInstanceOf[Upstream.TrampolineRelayed].adds === Seq(add_ab1, add_ab2)) assert(fwd2.message.upstream.asInstanceOf[Upstream.TrampolineRelayed].adds === Seq(add_ab1, add_ab2))
@ -466,6 +475,9 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountOut, 100 sat, 0 sat, 0 sat), origin, Some(channelUpdate_bc), None))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, InsufficientFunds(channelId_bc, origin.amountOut, 100 sat, 0 sat, 0 sat), origin, Some(channelUpdate_bc), None)))
assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(TemporaryChannelFailure(channelUpdate_bc))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(TemporaryChannelFailure(channelUpdate_bc)))
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, FeerateTooDifferent(channelId_bc, 1000, 300), origin, Some(channelUpdate_bc), None)))
assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(TemporaryChannelFailure(channelUpdate_bc)))
val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = 2) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = 2)
sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ChannelUnavailable(channelId_bc), origin, Some(channelUpdate_bc_disabled), None))) sender.send(relayer, Status.Failure(AddHtlcFailed(channelId_bc, paymentHash, ChannelUnavailable(channelId_bc), origin, Some(channelUpdate_bc_disabled), None)))
assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ChannelDisabled(channelUpdate_bc_disabled.messageFlags, channelUpdate_bc_disabled.channelFlags, channelUpdate_bc_disabled))) assert(register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message.reason === Right(ChannelDisabled(channelUpdate_bc_disabled.messageFlags, channelUpdate_bc_disabled.channelFlags, channelUpdate_bc_disabled)))
@ -527,7 +539,10 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val add_ab2 = UpdateAddHtlc(channelId_ab, 565, cmd2.amount, cmd2.paymentHash, cmd2.cltvExpiry, cmd2.onion) val add_ab2 = UpdateAddHtlc(channelId_ab, 565, cmd2.amount, cmd2.paymentHash, cmd2.cltvExpiry, cmd2.onion)
sender.send(relayer, ForwardAdd(add_ab1)) sender.send(relayer, ForwardAdd(add_ab1))
sender.send(relayer, ForwardAdd(add_ab2)) sender.send(relayer, ForwardAdd(add_ab2))
router.expectMsg(GetNetworkStats) // A multi-part payment FSM is started to relay the payment downstream.
// A multi-part payment FSM is started to relay the payment downstream.
val routeRequest = router.expectMsgType[Router.RouteRequest]
assert(routeRequest.allowMultiPart)
// We simulate a fake htlc fulfill for the downstream channel. // We simulate a fake htlc fulfill for the downstream channel.
val payFSM = TestProbe() val payFSM = TestProbe()
@ -539,8 +554,9 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
} }
sender.send(relayer, forwardFulfill) sender.send(relayer, forwardFulfill)
// the FSM responsible for the payment should receive the fulfill. // the FSM responsible for the payment should receive the fulfill and emit a preimage event.
payFSM.expectMsg(forwardFulfill) payFSM.expectMsg(forwardFulfill)
system.actorSelection(relayer.path.child("node-relayer")).tell(PreimageReceived(paymentHash, preimage), payFSM.ref)
// the payment should be immediately fulfilled upstream. // the payment should be immediately fulfilled upstream.
val upstream1 = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val upstream1 = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]

View file

@ -402,19 +402,25 @@ class RouterSpec extends BaseRouterSpec {
val sender = TestProbe() val sender = TestProbe()
// Via private channels. // Via private channels.
sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE)) sender.send(router, RouteRequest(a, g, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
sender.expectMsgType[RouteResponse] sender.expectMsgType[RouteResponse]
sender.send(router, RouteRequest(a, h, 50000000 msat, Long.MaxValue.msat)) sender.send(router, RouteRequest(a, g, 50000000 msat, Long.MaxValue.msat))
sender.expectMsg(Failure(RouteNotFound)) sender.expectMsg(Failure(BalanceTooLow))
sender.send(router, RouteRequest(a, g, 50000000 msat, Long.MaxValue.msat, allowMultiPart = true))
sender.expectMsg(Failure(BalanceTooLow))
// Via public channels. // Via public channels.
sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE)) sender.send(router, RouteRequest(a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE))
sender.expectMsgType[RouteResponse] sender.expectMsgType[RouteResponse]
val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true)
sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1)) sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1))
sender.send(router, RouteRequest(a, d, 12000000 msat, Long.MaxValue.msat)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat))
sender.expectMsg(Failure(RouteNotFound)) sender.expectMsg(Failure(BalanceTooLow))
sender.send(router, RouteRequest(a, d, 5000000 msat, Long.MaxValue.msat)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, allowMultiPart = true))
sender.expectMsg(Failure(BalanceTooLow))
sender.send(router, RouteRequest(a, b, 5000000 msat, Long.MaxValue.msat))
sender.expectMsgType[RouteResponse]
sender.send(router, RouteRequest(a, b, 5000000 msat, Long.MaxValue.msat, allowMultiPart = true))
sender.expectMsgType[RouteResponse] sender.expectMsgType[RouteResponse]
} }
@ -589,6 +595,21 @@ class RouterSpec extends BaseRouterSpec {
assert(balances.contains(edge_ab.balance_opt)) assert(balances.contains(edge_ab.balance_opt))
assert(edge_ba.balance_opt === None) assert(edge_ba.balance_opt === None)
} }
{
// Private channels should also update the graph when HTLCs are relayed through them.
val balances = Set(33000000 msat, 5000000 msat)
val commitments = CommitmentsSpec.makeCommitments(33000000 msat, 5000000 msat, a, g, announceChannel = false)
sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ag, commitments))
sender.send(router, Symbol("data"))
val data = sender.expectMsgType[Data]
val channel_ag = data.privateChannels(channelId_ag)
assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) === balances)
// And the graph should be updated too.
val edge_ag = data.graph.getEdge(ChannelDesc(channelId_ag, a, g)).get
assert(edge_ag.capacity == channel_ag.capacity)
assert(edge_ag.balance_opt === Some(33000000 msat))
}
} }
} }

View file

@ -407,7 +407,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
"""[^$]+""").r """[^$]+""").r
// this regex extracts htlc direction and amounts // this regex extracts htlc direction and amounts
val htlcRegex = val htlcRegex =
""".*HTLC ([a-z]+) amount ([0-9]+).*""".r """.*HTLC [0-9] ([a-z]+) amount ([0-9]+).*""".r
val dustLimit = 546 sat val dustLimit = 546 sat
case class TestSetup(name: String, dustLimit: Satoshi, spec: CommitmentSpec, expectedFee: Satoshi) case class TestSetup(name: String, dustLimit: Satoshi, spec: CommitmentSpec, expectedFee: Satoshi)
@ -422,7 +422,7 @@ class TransactionsSpec extends AnyFunSuite with Logging {
} }
}).toSet }).toSet
TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocal = MilliSatoshi(to_local_msat.toLong), toRemote = MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) TestSetup(name, dustLimit, CommitmentSpec(htlcs = htlcs, feeratePerKw = feerate_per_kw.toLong, toLocal = MilliSatoshi(to_local_msat.toLong), toRemote = MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong))
}) }).toSeq
// simple non-reg test making sure we are not missing tests // simple non-reg test making sure we are not missing tests
assert(tests.size === 15, "there were 15 tests at ec99f893f320e8c88f564c1c8566f3454f0f1f5f") assert(tests.size === 15, "there were 15 tests at ec99f893f320e8c88f564c1c8566f3454f0f1f5f")

View file

@ -42,6 +42,7 @@ trait ExtraDirectives extends Directives {
val channelIdFormParam_opt = "channelId".as[Option[ByteVector32]](sha256HashUnmarshaller) val channelIdFormParam_opt = "channelId".as[Option[ByteVector32]](sha256HashUnmarshaller)
val channelIdsFormParam_opt = "channelIds".as[Option[List[ByteVector32]]](sha256HashesUnmarshaller) val channelIdsFormParam_opt = "channelIds".as[Option[List[ByteVector32]]](sha256HashesUnmarshaller)
val nodeIdFormParam_opt = "nodeId".as[Option[PublicKey]](publicKeyUnmarshaller) val nodeIdFormParam_opt = "nodeId".as[Option[PublicKey]](publicKeyUnmarshaller)
val nodeIdsFormParam_opt = "nodeIds".as[Option[Set[PublicKey]]](publicKeysUnmarshaller)
val paymentHashFormParam_opt = "paymentHash".as[Option[ByteVector32]](sha256HashUnmarshaller) val paymentHashFormParam_opt = "paymentHash".as[Option[ByteVector32]](sha256HashUnmarshaller)
val fromFormParam_opt = "from".as[Long] val fromFormParam_opt = "from".as[Long]
val toFormParam_opt = "to".as[Long] val toFormParam_opt = "to".as[Long]

View file

@ -38,6 +38,10 @@ object FormParamExtractors {
PublicKey(ByteVector.fromValidHex(str)) PublicKey(ByteVector.fromValidHex(str))
} }
implicit val publicKeysUnmarshaller: Deserializer[Option[String], Option[Set[PublicKey]]] = strictDeserializer { bin =>
bin.split(",").map(str => PublicKey(ByteVector.fromValidHex(str))).toSet
}
implicit val binaryDataUnmarshaller: Deserializer[Option[String], Option[ByteVector]] = strictDeserializer { str => implicit val binaryDataUnmarshaller: Deserializer[Option[String], Option[ByteVector]] = strictDeserializer { str =>
ByteVector.fromValidHex(str) ByteVector.fromValidHex(str)
} }

View file

@ -115,7 +115,12 @@ trait Service extends ExtraDirectives with Logging {
} }
} ~ } ~
path("peers") { path("peers") {
complete(eclairApi.peersInfo()) complete(eclairApi.peers())
} ~
path("nodes") {
formFields(nodeIdsFormParam_opt) { nodeIds_opt =>
complete(eclairApi.nodes(nodeIds_opt))
}
} ~ } ~
path("channels") { path("channels") {
formFields(nodeIdFormParam_opt) { toRemoteNodeId_opt => formFields(nodeIdFormParam_opt) { toRemoteNodeId_opt =>
@ -127,9 +132,6 @@ trait Service extends ExtraDirectives with Logging {
complete(eclairApi.channelInfo(channelIdentifier)) complete(eclairApi.channelInfo(channelIdentifier))
} }
} ~ } ~
path("allnodes") {
complete(eclairApi.allNodes())
} ~
path("allchannels") { path("allchannels") {
complete(eclairApi.allChannels()) complete(eclairApi.allChannels())
} ~ } ~
@ -228,7 +230,9 @@ trait Service extends ExtraDirectives with Logging {
} }
} ~ } ~
path("channelstats") { path("channelstats") {
complete(eclairApi.channelStats()) formFields(fromFormParam_opt.?, toFormParam_opt.?) { (from_opt, to_opt) =>
complete(eclairApi.channelStats(from_opt, to_opt))
}
} ~ } ~
path("usablebalances") { path("usablebalances") {
complete(eclairApi.usableBalances()) complete(eclairApi.usableBalances())

View file

@ -123,7 +123,7 @@ class ApiServiceSpec extends AnyFunSuiteLike with ScalatestRouteTest with RouteT
test("'peers' should ask the switchboard for current known peers") { test("'peers' should ask the switchboard for current known peers") {
val mockEclair = mock[Eclair] val mockEclair = mock[Eclair]
val service = new MockService(mockEclair) val service = new MockService(mockEclair)
mockEclair.peersInfo()(any[Timeout]) returns Future.successful(List( mockEclair.peers()(any[Timeout]) returns Future.successful(List(
PeerInfo( PeerInfo(
nodeId = aliceNodeId, nodeId = aliceNodeId,
state = "CONNECTED", state = "CONNECTED",
@ -142,7 +142,7 @@ class ApiServiceSpec extends AnyFunSuiteLike with ScalatestRouteTest with RouteT
assert(handled) assert(handled)
assert(status == OK) assert(status == OK)
val response = responseAs[String] val response = responseAs[String]
mockEclair.peersInfo()(any[Timeout]).wasCalled(once) mockEclair.peers()(any[Timeout]).wasCalled(once)
matchTestJson("peers", response) matchTestJson("peers", response)
} }
} }