From cb486dfa27fc94885a1c179decd42abb3e800738 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Mar 2019 12:26:37 +0100 Subject: [PATCH 01/83] add a payment identifier This is in preparation for a proper payments database. There is no unique identifier for payments in LN protocol. Critically, we can't use `payment_hash` as a unique id because there is no way to ensure unicity at the protocol level. Also, the generatl case for a "payment" is to be associated to multiple `update_add_htlc`s, because of automated retries. We also routinely retry payments, which means that the same `payment_hash` will be conceptually linked to a list of lists of `update_add_htlc`s. In order to address this, we introduce a payment id, which uniquely identifies a payment, as-in a set of sequential `update_add_htlc` managed by a single `PaymentLifecycle` that ends with a `PaymentSent` or `PaymentFailed` outcome. We can then query the api using either `payment_id` or `payment_hash`. The former will return a single payment status, the latter will return a set of payment statuses, each identified by their `payment_id`. NB: This is just the first step, there is no payment db yet and the ids are not actually used. Some db tests do not pass. --- .../fr/acinq/eclair/channel/Channel.scala | 10 ++--- .../acinq/eclair/channel/ChannelTypes.scala | 4 +- .../fr/acinq/eclair/channel/Helpers.scala | 9 ---- .../eclair/db/sqlite/SqliteAuditDb.scala | 5 ++- .../fr/acinq/eclair/payment/Autoprobe.scala | 2 +- .../acinq/eclair/payment/PaymentEvents.scala | 6 ++- .../eclair/payment/PaymentInitiator.scala | 7 ++- .../eclair/payment/PaymentLifecycle.scala | 28 ++++++------ .../fr/acinq/eclair/payment/Relayer.scala | 38 ++++++++-------- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 14 +++++- .../eclair/api/JsonSerializersSpec.scala | 3 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 3 +- .../acinq/eclair/channel/ThroughputSpec.scala | 1 + .../states/StateTestsHelperMethods.scala | 4 +- .../channel/states/e/NormalStateSpec.scala | 31 ++++++------- .../channel/states/f/ShutdownStateSpec.scala | 8 ++-- .../states/g/NegotiatingStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../fr/acinq/eclair/db/ChannelStateSpec.scala | 4 +- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 7 +-- .../eclair/payment/ChannelSelectionSpec.scala | 6 +-- .../eclair/payment/HtlcGenerationSpec.scala | 6 ++- .../eclair/payment/PaymentLifecycleSpec.scala | 45 ++++++++++++------- .../fr/acinq/eclair/payment/RelayerSpec.scala | 32 ++++++------- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 11 +++-- 25 files changed, 169 insertions(+), 119 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 220b6b9a10..3872b7dfb5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1252,8 +1252,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // for our outgoing payments, let's send events if we know that they will settle on chain Closing .onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) - .filter(add => Closing.isSentByLocal(add.id, d.commitments.originChannels)) // we only care about htlcs for which we were the original sender here - .foreach(add => context.system.eventStream.publish(PaymentSettlingOnChain(amount = MilliSatoshi(add.amountMsat), add.paymentHash))) + .map(add => (add, d.commitments.originChannels.get(add.id).collect { case Local(id, _) => id })) // we resolve the payment id if this was a local payment + .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = MilliSatoshi(add.amountMsat), add.paymentHash)) } // then let's see if any of the possible close scenarii can be considered done val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) @@ -2038,9 +2038,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } } - def origin(c: CMD_ADD_HTLC): Origin = c.upstream_opt match { - case None => Local(Some(sender)) - case Some(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) + def origin(c: CMD_ADD_HTLC): Origin = c.upstream match { + case Left(id) => Local(id, Some(sender)) // we were the origin of the payment + case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) // this is a relayed payment } def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: BinaryData) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 11cf76dabc..749f107df6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel +import java.util.UUID + import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.{Point, PublicKey} import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, OutPoint, Satoshi, Transaction} @@ -105,7 +107,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, cltvExpiry: Long, onion: BinaryData = Sphinx.LAST_PACKET.serialize, upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false, redirected: Boolean = false) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: BinaryData, cltvExpiry: Long, onion: BinaryData = Sphinx.LAST_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc] = Left(UUID.randomUUID()), commit: Boolean = false, redirected: Boolean = false) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: BinaryData, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[BinaryData, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: BinaryData, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 7b793fa5e0..5cbebb783c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -802,15 +802,6 @@ object Helpers { }).toSet.flatten } - /** - * Tells if we were the origin of this outgoing htlc - * - * @param htlcId - * @param originChannels - * @return - */ - def isSentByLocal(htlcId: Long, originChannels: Map[Long, Origin]) = (originChannels.get(htlcId) collect { case l: Local => l }).isDefined - /** * As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether * or not they actually have an output in the commitment tx). diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 044e65fb59..b687c397ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -17,12 +17,14 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection +import java.util.UUID import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi} -import fr.acinq.eclair.channel.{AvailableBalanceChanged, ChannelClosed, ChannelCreated, NetworkFeePaid} +import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent, NetworkFee, Stats} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} +import fr.acinq.eclair.wire.ChannelCodecs import scala.collection.immutable.Queue import scala.compat.Platform @@ -124,6 +126,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb { var q: Queue[PaymentSent] = Queue() while (rs.next()) { q = q :+ PaymentSent( + id = ChannelCodecs.UNKNOWN_UUID, // TODO: temporary! fix that amount = MilliSatoshi(rs.getLong("amount_msat")), feesPaid = MilliSatoshi(rs.getLong("fees_msat")), paymentHash = BinaryData(rs.getBytes("payment_hash")), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index 1ed5813da4..43d64c9ff9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -62,7 +62,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case paymentResult: PaymentResult => paymentResult match { - case PaymentFailed(_, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, UnknownPaymentHash))) => + case PaymentFailed(_, _, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, UnknownPaymentHash))) => log.info(s"payment probe successful to node=$targetNodeId") case _ => log.info(s"payment probe failed with paymentResult=$paymentResult") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 9afdd45eb8..ab9331687e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import fr.acinq.bitcoin.{BinaryData, MilliSatoshi} import scala.compat.Platform @@ -27,10 +29,10 @@ sealed trait PaymentEvent { val paymentHash: BinaryData } -case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: BinaryData, paymentPreimage: BinaryData, toChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentSent(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: BinaryData, paymentPreimage: BinaryData, toChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: BinaryData, fromChannelId: BinaryData, toChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent case class PaymentReceived(amount: MilliSatoshi, paymentHash: BinaryData, fromChannelId: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent -case class PaymentSettlingOnChain(amount: MilliSatoshi, paymentHash: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: BinaryData, timestamp: Long = Platform.currentTime) extends PaymentEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index 96fd5f8c81..6d99a602d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment @@ -27,7 +29,10 @@ class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: Acto override def receive: Receive = { case c: SendPayment => - val payFsm = context.actorOf(PaymentLifecycle.props(sourceNodeId, router, register)) + val paymentId = UUID.randomUUID() + val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register)) + // TODO: here we should probably send the payment id back to the sender so that it knows how to query the api later + //sender ! paymentId payFsm forward c } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index b511e31e2f..2245b6d158 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{ActorRef, FSM, Props, Status} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, MilliSatoshi} @@ -34,7 +36,7 @@ import scala.util.{Failure, Success} /** * Created by PM on 26/08/2016. */ -class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { +class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { startWith(WAITING_FOR_REQUEST, WaitingForRequest) @@ -51,12 +53,12 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto // we add one block in order to not have our htlc fail when a new block has just been found val finalExpiry = Globals.blockCount.get().toInt + c.finalCltvExpiry.toInt + 1 - val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops) + val (cmd, sharedSecrets) = buildCommand(id, c.amountMsat, finalExpiry, c.paymentHash, hops) register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops) case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => - reply(s, PaymentFailed(c.paymentHash, failures = failures :+ LocalFailure(t))) + reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) stop(FSM.Normal) } @@ -64,8 +66,8 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - reply(s, PaymentSucceeded(cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) - context.system.eventStream.publish(PaymentSent(MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) + context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => @@ -73,7 +75,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Success(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") - reply(s, PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) + reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -86,7 +88,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto UnreadableRemoteFailure(hops) } log.warning(s"too many failed attempts, failing the payment") - reply(s, PaymentFailed(c.paymentHash, failures = failures :+ failure)) + reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -151,7 +153,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { - reply(s, PaymentFailed(c.paymentHash, failures :+ LocalFailure(t))) + reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") @@ -176,7 +178,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto object PaymentLifecycle { - def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register) + def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register) // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None) @@ -196,12 +198,12 @@ object PaymentLifecycle { case class CheckPayment(paymentHash: BinaryData) sealed trait PaymentResult - case class PaymentSucceeded(amountMsat: Long, paymentHash: BinaryData, paymentPreimage: BinaryData, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees + case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: BinaryData, paymentPreimage: BinaryData, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees sealed trait PaymentFailure case class LocalFailure(t: Throwable) extends PaymentFailure case class RemoteFailure(route: Seq[Hop], e: ErrorPacket) extends PaymentFailure case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure - case class PaymentFailed(paymentHash: BinaryData, failures: Seq[PaymentFailure]) extends PaymentResult + case class PaymentFailed(id: UUID, paymentHash: BinaryData, failures: Seq[PaymentFailure]) extends PaymentResult sealed trait Data case object WaitingForRequest extends Data @@ -245,12 +247,12 @@ object PaymentLifecycle { (msat + nextFee, expiry + hop.lastUpdate.cltvExpiryDelta, PerHopPayload(hop.lastUpdate.shortChannelId, msat, expiry) +: payloads) } - def buildCommand(finalAmountMsat: Long, finalExpiry: Long, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = { + def buildCommand(id: UUID, finalAmountMsat: Long, finalExpiry: Long, paymentHash: BinaryData, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(BinaryData, PublicKey)]) = { val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = None, commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream = Left(id), commit = true) -> onion.sharedSecrets } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 4e3ade15a5..8d236db1d6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} @@ -36,7 +38,7 @@ import scala.util.{Failure, Success, Try} // @formatter:off sealed trait Origin -case class Local(sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors +case class Local(id: UUID, sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors case class Relayed(originChannelId: BinaryData, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin sealed trait ForwardMessage @@ -112,24 +114,24 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } - case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _, _)))) => + case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Right(add), _, _)))) => log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}") val cmdFail = CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true) commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) - case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(None), _, _)) => + case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(id, None), _, _)) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream - context.system.eventStream.publish(PaymentFailed(paymentHash, Nil)) + context.system.eventStream.publish(PaymentFailed(id, paymentHash, Nil)) - case Status.Failure(AddHtlcFailed(_, _, error, Local(Some(sender)), _, _)) => + case Status.Failure(AddHtlcFailed(_, _, error, Local(_, Some(sender)), _, _)) => sender ! Status.Failure(error) case Status.Failure(AddHtlcFailed(_, paymentHash, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt, originalCommand_opt)) => originalCommand_opt match { - case Some(cmd) if cmd.redirected && cmd.upstream_opt.isDefined => // cmd.upstream_opt.isDefined always true since origin = relayed + case Some(cmd) if cmd.redirected && cmd.upstream.isRight => // cmd.upstream_opt.isDefined always true since origin = relayed // if it was redirected, we give it one more try with the original requested channel (meaning that the error returned will always be for the requested channel) log.info(s"retrying htlc #$originHtlcId paymentHash=$paymentHash from channelId=$originChannelId") - self ! ForwardAdd(cmd.upstream_opt.get, canRedirect = false) + self ! ForwardAdd(cmd.upstream.right.get, canRedirect = false) case _ => // otherwise we just return a failure val failure = (error, channelUpdate_opt) match { @@ -147,13 +149,13 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmdFail) } - case ForwardFulfill(fulfill, Local(None), add) => + case ForwardFulfill(fulfill, Local(id, None), add) => val feesPaid = MilliSatoshi(0) - context.system.eventStream.publish(PaymentSent(MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the success on the event stream - context.system.eventStream.publish(PaymentSucceeded(add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // + context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // - case ForwardFulfill(fulfill, Local(Some(sender)), _) => + case ForwardFulfill(fulfill, Local(_, Some(sender)), _) => sender ! fulfill case ForwardFulfill(fulfill, Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut), add) => @@ -161,22 +163,22 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) - case ForwardFail(_, Local(None), add) => + case ForwardFail(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream - context.system.eventStream.publish(PaymentFailed(add.paymentHash, Nil)) + context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) - case ForwardFail(fail, Local(Some(sender)), _) => + case ForwardFail(fail, Local(_, Some(sender)), _) => sender ! fail case ForwardFail(fail, Relayed(originChannelId, originHtlcId, _, _), _) => val cmd = CMD_FAIL_HTLC(originHtlcId, Left(fail.reason), commit = true) commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) - case ForwardFailMalformed(_, Local(None), add) => + case ForwardFailMalformed(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream - context.system.eventStream.publish(PaymentFailed(add.paymentHash, Nil)) + context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) - case ForwardFailMalformed(fail, Local(Some(sender)), _) => + case ForwardFailMalformed(fail, Local(_, Some(sender)), _) => sender ! fail case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _), _) => @@ -270,7 +272,7 @@ object Relayer { Left(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => val isRedirected = (channelUpdate.shortChannelId != payload.shortChannelId) // we may decide to use another channel (to the same node) from the one requested - Right(CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true, redirected = isRedirected)) + Right(CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream = Right(add), commit = true, redirected = isRedirected)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index f1c31eb727..835894fa68 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -16,6 +16,9 @@ package fr.acinq.eclair.wire +import java.util.UUID + +import akka.actor.ActorRef import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{BinaryData, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ @@ -154,14 +157,23 @@ object ChannelCodecs extends Logging { ("sentAfterLocalCommitIndex" | uint64) :: ("reSignAsap" | bool)).as[WaitingForRevocation] + val localCodec: Codec[Local] = ( + ("id" | uuid) :: + ("sender" | provide(Option.empty[ActorRef])) + ).as[Local] + val relayedCodec: Codec[Relayed] = ( ("originChannelId" | binarydata(32)) :: ("originHtlcId" | int64) :: ("amountMsatIn" | uint64) :: ("amountMsatOut" | uint64)).as[Relayed] + // this is for backward compatibility to handle legacy payments that didn't have identifiers + val UNKNOWN_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") + val originCodec: Codec[Origin] = discriminated[Origin].by(uint16) - .typecase(0x01, provide(Local(None))) + .typecase(0x03, localCodec) // backward compatible + .typecase(0x01, provide(Local(UNKNOWN_UUID, None))) .typecase(0x02, relayedCodec) val originsListCodec: Codec[List[(Long, Origin)]] = listOfN(uint16, int64 ~ originCodec) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index a764339d47..ca05f8e729 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.api import java.net.InetAddress +import java.util.UUID import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, OutPoint} import fr.acinq.eclair._ @@ -76,7 +77,7 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("type hints") { implicit val formats = DefaultFormats.withTypeHintFieldName("type") + ShortTypeHints(List(classOf[PaymentSettlingOnChain])) + new BinaryDataSerializer + new MilliSatoshiSerializer - val e1 = PaymentSettlingOnChain(MilliSatoshi(42), randomBytes(32)) + val e1 = PaymentSettlingOnChain(UUID.randomUUID, MilliSatoshi(42), randomBytes(32)) println(Serialization.writePretty(e1)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index a3619e0868..705a790c91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.channel +import java.util.UUID import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} @@ -92,7 +93,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount) val expiry = Globals.blockCount.get().toInt + Channel.MIN_CLTV_EXPIRY + 1 - PaymentLifecycle.buildCommand(amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1 + PaymentLifecycle.buildCommand(UUID.randomUUID(), amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1 } def initiatePayment(stopping: Boolean) = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 83bdeabb32..7a0861798b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.channel +import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicLong diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 002c541ee9..1d7b9e4cac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states +import java.util.UUID + import akka.actor.Actor import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.{BinaryData, Crypto} @@ -116,7 +118,7 @@ trait StateTestsHelperMethods extends TestKitBase { val sender = TestProbe() val receiverPubkey = r.underlyingActor.nodeParams.nodeId val expiry = 400144 - val cmd = PaymentLifecycle.buildCommand(amountMsat, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) + val cmd = PaymentLifecycle.buildCommand(UUID.randomUUID, amountMsat, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) sender.send(s, cmd) sender.expectMsg("ok") val htlc = s2r.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 4669342c41..183ba023f3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -65,7 +65,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = BinaryData("42" * 32) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + val add = CMD_ADD_HTLC(50000000, h, 400144) + sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == 0 && htlc.paymentHash == h) @@ -73,7 +74,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { commitments = initialState.commitments.copy( localNextHtlcId = 1, localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), - originChannels = Map(0L -> Local(Some(sender.ref))) + originChannels = Map(0L -> Local(add.upstream.left.get, Some(sender.ref))) ))) } @@ -95,7 +96,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = BinaryData("42" * 32) val originHtlc = UpdateAddHtlc(channelId = "42" * 32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = "00" * 1254) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream_opt = Some(originHtlc)) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -115,7 +116,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "11" * 42, cltvExpiry = 400144) sender.send(alice, add) val error = InvalidPaymentHash(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -128,7 +129,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = expiryTooSmall) sender.send(alice, add) val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -141,7 +142,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = expiryTooBig) sender.send(alice, add) val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -152,7 +153,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(50, "11" * 32, 400144) sender.send(alice, add) val error = HtlcValueTooSmall(channelId(alice), 1000, 50) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -163,7 +164,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(Int.MaxValue, "11" * 32, 400144) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -183,7 +184,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(1000000, "44" * 32, 400144) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -200,7 +201,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "33" * 32, 400144) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -211,7 +212,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(151000000, "11" * 32, 400144) sender.send(bob, add) val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) - sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) bob2alice.expectNoMsg(200 millis) } @@ -228,7 +229,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(10000000, "33" * 32, 400144) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -247,7 +248,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "22" * 32, 400144) sender.send(alice, add2) val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) alice2bob.expectNoMsg(200 millis) } @@ -264,7 +265,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 400144) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -288,7 +289,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.forward(alice) sender.send(alice, add2) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) } test("recv UpdateAddHtlc") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 693aa411e6..9863f3462b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states.f +import java.util.UUID + import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.Scalar @@ -51,7 +53,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val h1: BinaryData = Crypto.sha256(r1) val amount1 = 300000000 val expiry1 = 400144 - val cmd1 = PaymentLifecycle.buildCommand(amount1, expiry1, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) + val cmd1 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount1, expiry1, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) sender.send(alice, cmd1) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -62,7 +64,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val h2: BinaryData = Crypto.sha256(r2) val amount2 = 200000000 val expiry2 = 400144 - val cmd2 = PaymentLifecycle.buildCommand(amount2, expiry2, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) + val cmd2 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount2, expiry2, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) sender.send(alice, cmd2) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -102,7 +104,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 1ebc75c8b6..d49216a133 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -72,7 +72,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index e6acdf2593..bde294f977 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -115,7 +115,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, "11" * 32, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala index 8fcbbbee59..3385b483e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.db +import java.util.UUID + import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar} import fr.acinq.bitcoin.{BinaryData, Block, Crypto, DeterministicWallet, MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.Helpers.Funding @@ -103,7 +105,7 @@ object ChannelStateSpec { val commitments = Commitments(localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, - originChannels = Map(42L -> Local(None), 15000L -> Relayed("42" * 32, 43, 11000000L, 10000000L)), + originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed("42" * 32, 43, 11000000L, 10000000L)), remoteNextCommitInfo = Right(randomKey.publicKey), commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = "00" * 32) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index fcfb88560b..8154a31341 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.db import java.sql.DriverManager +import java.util.UUID import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} @@ -42,12 +43,12 @@ class SqliteAuditDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteAuditDb(sqlite) - val e1 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32)) + val e1 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32)) val e2 = PaymentReceived(MilliSatoshi(42000), randomBytes(32), randomBytes(32)) val e3 = PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32)) val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes(32), Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual") - val e5 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32), timestamp = 0) - val e6 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32), timestamp = Platform.currentTime * 2) + val e5 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32), timestamp = 0) + val e6 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes(32), randomBytes(32), randomBytes(32), timestamp = Platform.currentTime * 2) val e7 = AvailableBalanceChanged(null, randomBytes(32), ShortChannelId(500000, 42, 1), 456123000, ChannelStateSpec.commitments) val e8 = ChannelLifecycleEvent(randomBytes(32), randomKey.publicKey, 456123000, true, false, "mutual") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 6fe5be331c..110a59a387 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -48,9 +48,9 @@ class ChannelSelectionSpec extends FunSuite { implicit val log = akka.event.NoLogging // nominal case - assert(Relayer.handleRelay(relayPayload, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Some(relayPayload.add), commit = true, redirected = false))) + assert(Relayer.handleRelay(relayPayload, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true, redirected = false))) // redirected to preferred channel - assert(Relayer.handleRelay(relayPayload, Some(channelUpdate.copy(shortChannelId = ShortChannelId(1111)))) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Some(relayPayload.add), commit = true, redirected = true))) + assert(Relayer.handleRelay(relayPayload, Some(channelUpdate.copy(shortChannelId = ShortChannelId(1111)))) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true, redirected = true))) // no channel_update assert(Relayer.handleRelay(relayPayload, channelUpdate_opt = None) === Left(CMD_FAIL_HTLC(relayPayload.add.id, Right(UnknownNextPeer), commit = true))) // channel disabled @@ -67,7 +67,7 @@ class ChannelSelectionSpec extends FunSuite { assert(Relayer.handleRelay(relayPayload_insufficientfee, Some(channelUpdate)) === Left(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) - assert(Relayer.handleRelay(relayPayload_highfee, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream_opt = Some(relayPayload.add), commit = true, redirected = false))) + assert(Relayer.handleRelay(relayPayload_highfee, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true, redirected = false))) } test("relay channel selection") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 08b1c40309..cdb0b12f61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{BinaryData, Block, Crypto, DeterministicWallet} import fr.acinq.eclair.channel.Channel @@ -96,7 +98,7 @@ class HtlcGenerationSpec extends FunSuite { test("build a command including the onion") { - val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (add, _) = buildCommand(UUID.randomUUID, finalAmountMsat, finalExpiry, paymentHash, hops) assert(add.amountMsat > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) @@ -130,7 +132,7 @@ class HtlcGenerationSpec extends FunSuite { } test("build a command with no hops") { - val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) + val (add, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) assert(add.amountMsat === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index e8dbec668d..1c51818333 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} @@ -44,7 +46,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (route not found)") { fixture => import fixture._ - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val id = UUID.randomUUID() + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -55,12 +58,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - sender.expectMsg(PaymentFailed(request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (route too expensive)") { fixture => import fixture._ - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val id = UUID.randomUUID() + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -78,7 +82,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -109,14 +114,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, defaultPaymentHash)) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) } test("payment failed (local error)") { fixture => import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -133,7 +139,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, Status.Failure(AddHtlcFailed("00" * 32, request.paymentHash, ChannelUnavailable("00" * 32), Local(Some(paymentFSM.underlying.self)), None, None))) + sender.send(paymentFSM, Status.Failure(AddHtlcFailed("00" * 32, request.paymentHash, ChannelUnavailable("00" * 32), Local(id, Some(paymentFSM.underlying.self)), None, None))) // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) @@ -144,7 +150,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -172,7 +179,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -202,14 +210,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (Update)") { fixture => import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -259,14 +268,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // this time the router can't find a route: game over - sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (PermanentChannelFailure)") { fixture => import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -293,12 +303,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment succeeded") { fixture => import fixture._ - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val id = UUID.randomUUID() + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -315,13 +326,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFulfillHtlc("00" * 32, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) } test("filter errors properly") { fixture => - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed("00" * 32, "00" * 32, ChannelUnavailable("00" * 32), Local(None), None, None)) :: LocalFailure(RouteNotFound) :: Nil + val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed("00" * 32, "00" * 32, ChannelUnavailable("00" * 32), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable("00" * 32)) :: Nil) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index 93f8efa685..14c4cc208f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{ActorRef, Status} import akka.testkit.TestProbe import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi} @@ -66,7 +68,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -75,7 +77,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd.message.upstream_opt === Some(add_ab)) + assert(fwd.message.upstream === Right(add_ab)) sender.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -86,7 +88,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) @@ -106,7 +108,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -115,7 +117,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd1.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd1.message.upstream_opt === Some(add_ab)) + assert(fwd1.message.upstream === Right(add_ab)) sender.send(relayer, Status.Failure(Register.ForwardShortIdFailure(fwd1))) @@ -133,7 +135,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // check that payments are sent properly - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -141,7 +143,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd.message.upstream_opt === Some(add_ab)) + assert(fwd.message.upstream === Right(add_ab)) sender.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -149,7 +151,7 @@ class RelayerSpec extends TestkitBaseClass { // now tell the relayer that the channel is down and try again relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) - val (cmd1, _) = buildCommand(finalAmountMsat, finalExpiry, "02" * 32, hops) + val (cmd1, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, "02" * 32, hops) val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) sender.send(relayer, ForwardAdd(add_ab)) @@ -166,7 +168,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = Announcements.makeChannelFlags(Announcements.isNode1(channelUpdate_bc.channelFlags), enable = false)) @@ -187,7 +189,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, "00" * Sphinx.PacketLength) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -207,7 +209,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) + val (cmd, _) = buildCommand(UUID.randomUUID(), channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -227,7 +229,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0))) - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -247,7 +249,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(feeBaseMsat = hops(1).lastUpdate.feeBaseMsat / 2))) - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -268,7 +270,7 @@ class RelayerSpec extends TestkitBaseClass { // to simulate this we use a zero-hop route A->B where A is the 'attacker' val hops1 = hops.head :: Nil - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc with a wrong expiry val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -289,7 +291,7 @@ class RelayerSpec extends TestkitBaseClass { // to simulate this we use a zero-hop route A->B where A is the 'attacker' val hops1 = hops.head :: Nil - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc with a wrong expiry val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry - 1, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 5e8b691573..34a6a824e6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -16,6 +16,9 @@ package fr.acinq.eclair.wire +import java.util.UUID + +import akka.actor.ActorSystem import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, OutPoint} import fr.acinq.eclair.channel._ @@ -146,19 +149,21 @@ class ChannelCodecsSpec extends FunSuite { } test("encode/decode origin") { - assert(originCodec.decodeValue(originCodec.encode(Local(None)).require).require === Local(None)) + val id = UUID.randomUUID() + assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("system").deadLetters))).require).require === Local(id, None)) + // TODO: add backward compatibility check val relayed = Relayed(randomBytes(32), 4324, 12000000L, 11000000L) assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed) } test("encode/decode map of origins") { val map = Map( - 1L -> Local(None), + 1L -> Local(UUID.randomUUID(), None), 42L -> Relayed(randomBytes(32), 4324, 12000000L, 11000000L), 130L -> Relayed(randomBytes(32), -45, 13000000L, 12000000L), 1000L -> Relayed(randomBytes(32), 10, 14000000L, 13000000L), -32L -> Relayed(randomBytes(32), 54, 15000000L, 14000000L), - -4L -> Local(None)) + -4L -> Local(UUID.randomUUID(), None)) assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map) } From 34a8ff178924c8389f174b6434b66cde7b2675c3 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Mar 2019 12:26:37 +0100 Subject: [PATCH 02/83] add a payment identifier This is in preparation for a proper payments database. There is no unique identifier for payments in LN protocol. Critically, we can't use `payment_hash` as a unique id because there is no way to ensure unicity at the protocol level. Also, the generatl case for a "payment" is to be associated to multiple `update_add_htlc`s, because of automated retries. We also routinely retry payments, which means that the same `payment_hash` will be conceptually linked to a list of lists of `update_add_htlc`s. In order to address this, we introduce a payment id, which uniquely identifies a payment, as-in a set of sequential `update_add_htlc` managed by a single `PaymentLifecycle` that ends with a `PaymentSent` or `PaymentFailed` outcome. We can then query the api using either `payment_id` or `payment_hash`. The former will return a single payment status, the latter will return a set of payment statuses, each identified by their `payment_id`. NB: This is just the first step, there is no payment db yet and the ids are not actually used. Some db tests do not pass. # Conflicts: # eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala # eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala # eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala # eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala # eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala # eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala # eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala # eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala # eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala # eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala # eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala # eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala # eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala --- .../fr/acinq/eclair/api/JsonSerializers.scala | 8 ++- .../fr/acinq/eclair/channel/Channel.scala | 8 +-- .../eclair/channel/ChannelExceptions.scala | 1 + .../acinq/eclair/channel/ChannelTypes.scala | 4 +- .../fr/acinq/eclair/channel/Helpers.scala | 9 ---- .../eclair/db/sqlite/SqliteAuditDb.scala | 3 ++ .../fr/acinq/eclair/payment/Autoprobe.scala | 2 +- .../acinq/eclair/payment/PaymentEvents.scala | 6 +-- .../eclair/payment/PaymentInitiator.scala | 7 ++- .../eclair/payment/PaymentLifecycle.scala | 28 ++++++----- .../fr/acinq/eclair/payment/Relayer.scala | 38 +++++++------- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 14 +++++- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 19 +++---- .../eclair/api/JsonSerializersSpec.scala | 3 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 3 +- .../acinq/eclair/channel/ThroughputSpec.scala | 1 + .../states/StateTestsHelperMethods.scala | 5 +- .../channel/states/e/NormalStateSpec.scala | 40 +++++++++------ .../channel/states/f/ShutdownStateSpec.scala | 8 +-- .../states/g/NegotiatingStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../fr/acinq/eclair/db/ChannelStateSpec.scala | 4 +- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 7 +-- .../eclair/integration/IntegrationSpec.scala | 4 +- .../eclair/payment/ChannelSelectionSpec.scala | 6 +-- .../eclair/payment/HtlcGenerationSpec.scala | 6 ++- .../eclair/payment/PaymentLifecycleSpec.scala | 49 ++++++++++++------- .../fr/acinq/eclair/payment/RelayerSpec.scala | 32 ++++++------ .../acinq/eclair/wire/ChannelCodecsSpec.scala | 11 +++-- 29 files changed, 198 insertions(+), 132 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index c40d73f474..2cbef11bab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.api import java.net.InetSocketAddress +import java.util.UUID import akka.http.scaladsl.model.MediaType import akka.http.scaladsl.model.MediaTypes._ @@ -154,6 +155,10 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format = Nil) })) +class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { + case id: UUID => JString(id.toString) +})) + object JsonSupport extends Json4sSupport { implicit val serialization = jackson.Serialization @@ -182,7 +187,8 @@ object JsonSupport extends Json4sSupport { new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer + - new PaymentRequestSerializer + new PaymentRequestSerializer + + new JavaUUIDSerializer implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 2cbbf777b6..f650130567 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1250,8 +1250,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // for our outgoing payments, let's send events if we know that they will settle on chain Closing .onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) - .filter(add => Closing.isSentByLocal(add.id, d.commitments.originChannels)) // we only care about htlcs for which we were the original sender here - .foreach(add => context.system.eventStream.publish(PaymentSettlingOnChain(amount = MilliSatoshi(add.amountMsat), add.paymentHash))) + .map(add => (add, d.commitments.originChannels.get(add.id).collect { case Local(id, _) => id })) // we resolve the payment id if this was a local payment + .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = MilliSatoshi(add.amountMsat), add.paymentHash)) } // then let's see if any of the possible close scenarii can be considered done val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) @@ -2047,8 +2047,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } def origin(c: CMD_ADD_HTLC): Origin = c.upstream_opt match { - case None => Local(Some(sender)) - case Some(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) + case Left(id) => Local(id, Some(sender)) // we were the origin of the payment + case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) // this is a relayed payment } def feePaid(fee: Satoshi, tx: Transaction, desc: String, channelId: ByteVector32) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 3cf1d713ca..94a99f92b2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -59,6 +59,7 @@ case class InvalidCloseFee (override val channelId: ByteVect case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual: $actual") case class ForcedLocalCommit (override val channelId: ByteVector32) extends ChannelException(channelId, s"forced local commit") case class UnexpectedHtlcId (override val channelId: ByteVector32, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual") +case class InvalidPaymentHash (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid payment hash") case class ExpiryTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockCount=$blockCount") case class ExpiryTooBig (override val channelId: ByteVector32, maximum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too big: maximum=$maximum actual=$actual blockCount=$blockCount") case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 544ae9a1f4..df35c9829c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel +import java.util.UUID + import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.{Point, PublicKey} import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} @@ -106,7 +108,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.LAST_PACKET.serialize, upstream_opt: Option[UpdateAddHtlc] = None, commit: Boolean = false, redirected: Boolean = false) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.LAST_PACKET.serialize, upstream_opt: Either[UUID, UpdateAddHtlc] = Left(UUID.randomUUID()), commit: Boolean = false, redirected: Boolean = false) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 022d3a5fad..1ffea2453f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -803,15 +803,6 @@ object Helpers { }).toSet.flatten } - /** - * Tells if we were the origin of this outgoing htlc - * - * @param htlcId - * @param originChannels - * @return - */ - def isSentByLocal(htlcId: Long, originChannels: Map[Long, Origin]) = (originChannels.get(htlcId) collect { case l: Local => l }).isDefined - /** * As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether * or not they actually have an output in the commitment tx). diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 64b5d87aed..a26e166c20 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -17,12 +17,14 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection +import java.util.UUID import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.MilliSatoshi import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent, NetworkFee, Stats} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} +import fr.acinq.eclair.wire.ChannelCodecs import scala.collection.immutable.Queue import scala.compat.Platform @@ -125,6 +127,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb { var q: Queue[PaymentSent] = Queue() while (rs.next()) { q = q :+ PaymentSent( + id = ChannelCodecs.UNKNOWN_UUID, // TODO: temporary! fix that amount = MilliSatoshi(rs.getLong("amount_msat")), feesPaid = MilliSatoshi(rs.getLong("fees_msat")), paymentHash = rs.getByteVector32("payment_hash"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index 2bd6999b2a..f87cf81c25 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -62,7 +62,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case paymentResult: PaymentResult => paymentResult match { - case PaymentFailed(_, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, UnknownPaymentHash))) => + case PaymentFailed(_, _, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, UnknownPaymentHash))) => log.info(s"payment probe successful to node=$targetNodeId") case _ => log.info(s"payment probe failed with paymentResult=$paymentResult") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index f426523f2f..1bb68fd7c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} - import scala.compat.Platform /** @@ -27,10 +27,10 @@ sealed trait PaymentEvent { val paymentHash: ByteVector32 } -case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: ByteVector32, paymentPreimage: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentSent(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: ByteVector32, paymentPreimage: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent case class PaymentReceived(amount: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent -case class PaymentSettlingOnChain(amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index 96fd5f8c81..6d99a602d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment @@ -27,7 +29,10 @@ class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: Acto override def receive: Receive = { case c: SendPayment => - val payFsm = context.actorOf(PaymentLifecycle.props(sourceNodeId, router, register)) + val paymentId = UUID.randomUUID() + val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register)) + // TODO: here we should probably send the payment id back to the sender so that it knows how to query the api later + //sender ! paymentId payFsm forward c } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 2449ecf168..9d51aa958a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{ActorRef, FSM, Props, Status} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} @@ -35,7 +37,7 @@ import scala.util.{Failure, Success} /** * Created by PM on 26/08/2016. */ -class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { +class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { startWith(WAITING_FOR_REQUEST, WaitingForRequest) @@ -52,12 +54,12 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto // we add one block in order to not have our htlc fail when a new block has just been found val finalExpiry = Globals.blockCount.get().toInt + c.finalCltvExpiry.toInt + 1 - val (cmd, sharedSecrets) = buildCommand(c.amountMsat, finalExpiry, c.paymentHash, hops) + val (cmd, sharedSecrets) = buildCommand(id, c.amountMsat, finalExpiry, c.paymentHash, hops) register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops) case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => - reply(s, PaymentFailed(c.paymentHash, failures = failures :+ LocalFailure(t))) + reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) stop(FSM.Normal) } @@ -65,8 +67,8 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - reply(s, PaymentSucceeded(cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) - context.system.eventStream.publish(PaymentSent(MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) + context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => @@ -74,7 +76,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Success(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") - reply(s, PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) + reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -87,7 +89,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto UnreadableRemoteFailure(hops) } log.warning(s"too many failed attempts, failing the payment") - reply(s, PaymentFailed(c.paymentHash, failures = failures :+ failure)) + reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -152,7 +154,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { - reply(s, PaymentFailed(c.paymentHash, failures :+ LocalFailure(t))) + reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") @@ -177,7 +179,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto object PaymentLifecycle { - def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register) + def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register) // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None) @@ -196,12 +198,12 @@ object PaymentLifecycle { case class CheckPayment(paymentHash: ByteVector32) sealed trait PaymentResult - case class PaymentSucceeded(amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees + case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees sealed trait PaymentFailure case class LocalFailure(t: Throwable) extends PaymentFailure case class RemoteFailure(route: Seq[Hop], e: ErrorPacket) extends PaymentFailure case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure - case class PaymentFailed(paymentHash: ByteVector32, failures: Seq[PaymentFailure]) extends PaymentResult + case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure]) extends PaymentResult sealed trait Data case object WaitingForRequest extends Data @@ -245,12 +247,12 @@ object PaymentLifecycle { (msat + nextFee, expiry + hop.lastUpdate.cltvExpiryDelta, PerHopPayload(hop.lastUpdate.shortChannelId, msat, expiry) +: payloads) } - def buildCommand(finalAmountMsat: Long, finalExpiry: Long, paymentHash: ByteVector32, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = { + def buildCommand(id: UUID, finalAmountMsat: Long, finalExpiry: Long, paymentHash: ByteVector32, hops: Seq[Hop]): (CMD_ADD_HTLC, Seq[(ByteVector32, PublicKey)]) = { val (firstAmountMsat, firstExpiry, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = None, commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = Left(id), commit = true) -> onion.sharedSecrets } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index d65d80fc79..dd079a0998 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} @@ -35,7 +37,7 @@ import scala.util.{Failure, Success, Try} // @formatter:off sealed trait Origin -case class Local(sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors +case class Local(id: UUID, sender: Option[ActorRef]) extends Origin // we don't persist reference to local actors case class Relayed(originChannelId: ByteVector32, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin sealed trait ForwardMessage @@ -111,24 +113,24 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } - case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Some(add), _, _)))) => + case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Right(add), _, _)))) => log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}") val cmdFail = CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true) commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) - case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(None), _, _)) => + case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(id, None), _, _)) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream - context.system.eventStream.publish(PaymentFailed(paymentHash, Nil)) + context.system.eventStream.publish(PaymentFailed(id, paymentHash, Nil)) - case Status.Failure(AddHtlcFailed(_, _, error, Local(Some(sender)), _, _)) => + case Status.Failure(AddHtlcFailed(_, _, error, Local(_, Some(sender)), _, _)) => sender ! Status.Failure(error) case Status.Failure(AddHtlcFailed(_, paymentHash, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt, originalCommand_opt)) => originalCommand_opt match { - case Some(cmd) if cmd.redirected && cmd.upstream_opt.isDefined => // cmd.upstream_opt.isDefined always true since origin = relayed + case Some(cmd) if cmd.redirected && cmd.upstream_opt.isRight => // cmd.upstream_opt.isDefined always true since origin = relayed // if it was redirected, we give it one more try with the original requested channel (meaning that the error returned will always be for the requested channel) log.info(s"retrying htlc #$originHtlcId paymentHash=$paymentHash from channelId=$originChannelId") - self ! ForwardAdd(cmd.upstream_opt.get, canRedirect = false) + self ! ForwardAdd(cmd.upstream_opt.right.get, canRedirect = false) case _ => // otherwise we just return a failure val failure = (error, channelUpdate_opt) match { @@ -146,13 +148,13 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmdFail) } - case ForwardFulfill(fulfill, Local(None), add) => + case ForwardFulfill(fulfill, Local(id, None), add) => val feesPaid = MilliSatoshi(0) - context.system.eventStream.publish(PaymentSent(MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) + context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the success on the event stream - context.system.eventStream.publish(PaymentSucceeded(add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // + context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // - case ForwardFulfill(fulfill, Local(Some(sender)), _) => + case ForwardFulfill(fulfill, Local(_, Some(sender)), _) => sender ! fulfill case ForwardFulfill(fulfill, Relayed(originChannelId, originHtlcId, amountMsatIn, amountMsatOut), add) => @@ -160,22 +162,22 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) - case ForwardFail(_, Local(None), add) => + case ForwardFail(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream - context.system.eventStream.publish(PaymentFailed(add.paymentHash, Nil)) + context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) - case ForwardFail(fail, Local(Some(sender)), _) => + case ForwardFail(fail, Local(_, Some(sender)), _) => sender ! fail case ForwardFail(fail, Relayed(originChannelId, originHtlcId, _, _), _) => val cmd = CMD_FAIL_HTLC(originHtlcId, Left(fail.reason), commit = true) commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) - case ForwardFailMalformed(_, Local(None), add) => + case ForwardFailMalformed(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream - context.system.eventStream.publish(PaymentFailed(add.paymentHash, Nil)) + context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) - case ForwardFailMalformed(fail, Local(Some(sender)), _) => + case ForwardFailMalformed(fail, Local(_, Some(sender)), _) => sender ! fail case ForwardFailMalformed(fail, Relayed(originChannelId, originHtlcId, _, _), _) => @@ -269,7 +271,7 @@ object Relayer { Left(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => val isRedirected = (channelUpdate.shortChannelId != payload.shortChannelId) // we may decide to use another channel (to the same node) from the one requested - Right(CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true, redirected = isRedirected)) + Right(CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Right(add), commit = true, redirected = isRedirected)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index 14b2028087..49ed5601cf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -16,6 +16,9 @@ package fr.acinq.eclair.wire +import java.util.UUID + +import akka.actor.ActorRef import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ @@ -154,14 +157,23 @@ object ChannelCodecs extends Logging { ("sentAfterLocalCommitIndex" | uint64) :: ("reSignAsap" | bool)).as[WaitingForRevocation] + val localCodec: Codec[Local] = ( + ("id" | uuid) :: + ("sender" | provide(Option.empty[ActorRef])) + ).as[Local] + val relayedCodec: Codec[Relayed] = ( ("originChannelId" | bytes32) :: ("originHtlcId" | int64) :: ("amountMsatIn" | uint64) :: ("amountMsatOut" | uint64)).as[Relayed] + // this is for backward compatibility to handle legacy payments that didn't have identifiers + val UNKNOWN_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000") + val originCodec: Codec[Origin] = discriminated[Origin].by(uint16) - .typecase(0x01, provide(Local(None))) + .typecase(0x03, localCodec) // backward compatible + .typecase(0x01, provide(Local(UNKNOWN_UUID, None))) .typecase(0x02, relayedCodec) val originsListCodec: Codec[List[(Long, Origin)]] = listOfN(uint16, int64 ~ originCodec) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 2b926bfb25..29b7c59727 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.api +import java.util.UUID import akka.actor.{Actor, ActorSystem, Props, Scheduler} import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ @@ -259,6 +260,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { test("the websocket should return typed objects") { val mockService = new MockService(new EclairMock {}) + val fixedUUID = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") val websocketRoute = Directives.path("ws") { Directives.handleWebSocketMessages(mockService.makeSocketHandler) @@ -269,38 +271,37 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { WS("/ws", wsClient.flow) ~> websocketRoute ~> check { - val pf = PaymentFailed(ByteVector32.Zeroes, failures = Seq.empty) - val expectedSerializedPf = """{"type":"payment-failed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[]}""" + val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty) + val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[]}""" Serialization.write(pf)(mockService.formatsWithTypeHint) === expectedSerializedPf system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) - val ps = PaymentSent(amount = MilliSatoshi(21), feesPaid = MilliSatoshi(1), paymentHash = ByteVector32.Zeroes, paymentPreimage = ByteVector32.One, toChannelId = ByteVector32.Zeroes, timestamp = 1553784337711L) - val expectedSerializedPs = """{"type":"payment-sent","amount":21,"feesPaid":1,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}""" + val ps = PaymentSent(fixedUUID, amount = MilliSatoshi(21), feesPaid = MilliSatoshi(1), paymentHash = ByteVector32.Zeroes, paymentPreimage = ByteVector32.One, toChannelId = ByteVector32.Zeroes, timestamp = 1553784337711L) + val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}""" Serialization.write(ps)(mockService.formatsWithTypeHint) === expectedSerializedPs system.eventStream.publish(ps) wsClient.expectMessage(expectedSerializedPs) val prel = PaymentRelayed(amountIn = MilliSatoshi(21), amountOut = MilliSatoshi(20), paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L) - val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" + val expectedSerializedPrel = """{"type":"payment-relayed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" Serialization.write(prel)(mockService.formatsWithTypeHint) === expectedSerializedPrel system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) val precv = PaymentReceived(amount = MilliSatoshi(21), paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.One, timestamp = 1553784963659L) - val expectedSerializedPrecv = """{"type":"payment-received","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" + val expectedSerializedPrecv = """{"type":"payment-received","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" Serialization.write(precv)(mockService.formatsWithTypeHint) === expectedSerializedPrecv system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) - val pset = PaymentSettlingOnChain(amount = MilliSatoshi(21), paymentHash = ByteVector32.One, timestamp = 1553785442676L) - val expectedSerializedPset = """{"type":"payment-settling-onchain","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553785442676}""" + val pset = PaymentSettlingOnChain(fixedUUID, amount = MilliSatoshi(21), paymentHash = ByteVector32.One, timestamp = 1553785442676L) + val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553785442676}""" Serialization.write(pset)(mockService.formatsWithTypeHint) === expectedSerializedPset system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset) } - } private def matchTestJson(apiName: String, response: String) = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index a61acdba05..60968bd599 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.api import java.net.InetAddress +import java.util.UUID import fr.acinq.bitcoin.{MilliSatoshi, OutPoint} import fr.acinq.eclair._ @@ -79,7 +80,7 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("type hints") { implicit val formats = DefaultFormats.withTypeHintFieldName("type") + CustomTypeHints(Map(classOf[PaymentSettlingOnChain] -> "payment-settling-onchain")) + new MilliSatoshiSerializer - val e1 = PaymentSettlingOnChain(MilliSatoshi(42), randomBytes32) + val e1 = PaymentSettlingOnChain(UUID.randomUUID, MilliSatoshi(42), randomBytes32) assert(Serialization.writePretty(e1).contains("\"type\" : \"payment-settling-onchain\"")) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 508b032f02..bea8dcd418 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.channel +import java.util.UUID import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} @@ -92,7 +93,7 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount) val expiry = Globals.blockCount.get().toInt + Channel.MIN_CLTV_EXPIRY + 1 - PaymentLifecycle.buildCommand(amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1 + PaymentLifecycle.buildCommand(UUID.randomUUID(), amount, expiry, paymentHash, Hop(null, dest, null) :: Nil)._1 } def initiatePayment(stopping: Boolean) = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 0c56ae69f7..817b4a8377 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.channel +import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicLong diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index ed310ca388..a6efd9f7cf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -16,6 +16,9 @@ package fr.acinq.eclair.channel.states +import java.util.UUID + +import akka.actor.Actor import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -112,7 +115,7 @@ trait StateTestsHelperMethods extends TestKitBase { val sender = TestProbe() val receiverPubkey = r.underlyingActor.nodeParams.nodeId val expiry = 400144 - val cmd = PaymentLifecycle.buildCommand(amountMsat, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) + val cmd = PaymentLifecycle.buildCommand(UUID.randomUUID, amountMsat, expiry, H, Hop(null, receiverPubkey, null) :: Nil)._1.copy(commit = false) sender.send(s, cmd) sender.expectMsg("ok") val htlc = s2r.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index dddc430100..8836d79e29 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -64,7 +64,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + val add = CMD_ADD_HTLC(50000000, h, 400144) + sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == 0 && htlc.paymentHash == h) @@ -72,7 +73,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { commitments = initialState.commitments.copy( localNextHtlcId = 1, localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), - originChannels = Map(0L -> Local(Some(sender.ref))) + originChannels = Map(0L -> Local(add.upstream_opt.left.get, Some(sender.ref))) ))) } @@ -94,7 +95,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = ByteVector.fill(1254)(0)) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream_opt = Some(originHtlc)) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream_opt = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -107,6 +108,17 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { ))) } + test("recv CMD_ADD_HTLC (invalid payment hash)") { f => + import f._ + val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] + val sender = TestProbe() + val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144) + sender.send(alice, add) + val error = InvalidPaymentHash(channelId(alice)) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + alice2bob.expectNoMsg(200 millis) + } + test("recv CMD_ADD_HTLC (expiry too small)") { f => import f._ val sender = TestProbe() @@ -116,7 +128,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooSmall) sender.send(alice, add) val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -129,7 +141,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooBig) sender.send(alice, add) val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -140,7 +152,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(50, randomBytes32, 400144) sender.send(alice, add) val error = HtlcValueTooSmall(channelId(alice), 1000, 50) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -151,7 +163,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -171,7 +183,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -188,7 +200,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -199,7 +211,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144) sender.send(bob, add) val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) - sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) bob2alice.expectNoMsg(200 millis) } @@ -216,7 +228,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -235,7 +247,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144) sender.send(alice, add2) val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) alice2bob.expectNoMsg(200 millis) } @@ -252,7 +264,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -276,7 +288,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.forward(alice) sender.send(alice, add2) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) } test("recv UpdateAddHtlc") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index b62cf37dd2..38806a7766 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states.f +import java.util.UUID + import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.Scalar @@ -54,7 +56,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val h1 = Crypto.sha256(r1) val amount1 = 300000000 val expiry1 = 400144 - val cmd1 = PaymentLifecycle.buildCommand(amount1, expiry1, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) + val cmd1 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount1, expiry1, h1, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) sender.send(alice, cmd1) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -64,7 +66,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val h2 = Crypto.sha256(r2) val amount2 = 200000000 val expiry2 = 400144 - val cmd2 = PaymentLifecycle.buildCommand(amount2, expiry2, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) + val cmd2 = PaymentLifecycle.buildCommand(UUID.randomUUID, amount2, expiry2, h2, Hop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil)._1.copy(commit = false) sender.send(alice, cmd2) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -104,7 +106,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 57505a09c2..e6aa946ad9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -73,7 +73,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index d68b63c48d..517f215dff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -116,7 +116,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala index e521424ebf..065f30b351 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.db +import java.util.UUID + import fr.acinq.bitcoin.Crypto.{PrivateKey, Scalar} import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.Helpers.Funding @@ -104,7 +106,7 @@ object ChannelStateSpec { val commitments = Commitments(localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, - originChannels = Map(42L -> Local(None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000L, 10000000L)), + originChannels = Map(42L -> Local(UUID.randomUUID, None), 15000L -> Relayed(ByteVector32(ByteVector.fill(32)(42)), 43, 11000000L, 10000000L)), remoteNextCommitInfo = Right(randomKey.publicKey), commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = ByteVector32.Zeroes) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index f23766b1e3..f27bddea71 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.db import java.sql.DriverManager +import java.util.UUID import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} @@ -42,12 +43,12 @@ class SqliteAuditDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteAuditDb(sqlite) - val e1 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) + val e1 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) val e2 = PaymentReceived(MilliSatoshi(42000), randomBytes32, randomBytes32) val e3 = PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual") - val e5 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) - val e6 = PaymentSent(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = Platform.currentTime * 2) + val e5 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) + val e6 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = Platform.currentTime * 2) val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000, ChannelStateSpec.commitments) val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000, true, false, "mutual") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 142f74a333..967a5badcb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -413,8 +413,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService awaitCond({ sender.expectMsgType[PaymentResult](10 seconds) match { - case PaymentFailed(_, failures) => failures == Seq.empty // if something went wrong fail with a hint - case PaymentSucceeded(_, _, _, route) => route.exists(_.nodeId == nodes("G").nodeParams.nodeId) + case PaymentFailed(_, _, failures) => failures == Seq.empty // if something went wrong fail with a hint + case PaymentSucceeded(_, _, _, _, route) => route.exists(_.nodeId == nodes("G").nodeParams.nodeId) } }, max = 30 seconds, interval = 10 seconds) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 5d4683c9c5..881ef70484 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -49,9 +49,9 @@ class ChannelSelectionSpec extends FunSuite { implicit val log = akka.event.NoLogging // nominal case - assert(Relayer.handleRelay(relayPayload, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Some(relayPayload.add), commit = true, redirected = false))) + assert(Relayer.handleRelay(relayPayload, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Right(relayPayload.add), commit = true, redirected = false))) // redirected to preferred channel - assert(Relayer.handleRelay(relayPayload, Some(channelUpdate.copy(shortChannelId = ShortChannelId(1111)))) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Some(relayPayload.add), commit = true, redirected = true))) + assert(Relayer.handleRelay(relayPayload, Some(channelUpdate.copy(shortChannelId = ShortChannelId(1111)))) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Right(relayPayload.add), commit = true, redirected = true))) // no channel_update assert(Relayer.handleRelay(relayPayload, channelUpdate_opt = None) === Left(CMD_FAIL_HTLC(relayPayload.add.id, Right(UnknownNextPeer), commit = true))) // channel disabled @@ -68,7 +68,7 @@ class ChannelSelectionSpec extends FunSuite { assert(Relayer.handleRelay(relayPayload_insufficientfee, Some(channelUpdate)) === Left(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) - assert(Relayer.handleRelay(relayPayload_highfee, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream_opt = Some(relayPayload.add), commit = true, redirected = false))) + assert(Relayer.handleRelay(relayPayload_highfee, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream_opt = Right(relayPayload.add), commit = true, redirected = false))) } test("relay channel selection") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 05d9461a29..cab7ce1152 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{Block, Crypto, DeterministicWallet} import fr.acinq.eclair.channel.Channel @@ -96,7 +98,7 @@ class HtlcGenerationSpec extends FunSuite { test("build a command including the onion") { - val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (add, _) = buildCommand(UUID.randomUUID, finalAmountMsat, finalExpiry, paymentHash, hops) assert(add.amountMsat > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) @@ -130,7 +132,7 @@ class HtlcGenerationSpec extends FunSuite { } test("build a command with no hops") { - val (add, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) + val (add, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops.take(1)) assert(add.amountMsat === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 45f8597bbf..b54c70122d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} @@ -48,7 +50,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (route not found)") { fixture => import fixture._ - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val id = UUID.randomUUID() + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -59,12 +62,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - sender.expectMsg(PaymentFailed(request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (route too expensive)") { fixture => import fixture._ - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val id = UUID.randomUUID() + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -82,7 +86,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -113,14 +118,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) } test("payment failed (local error)") { fixture => import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -137,7 +143,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, request.paymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(Some(paymentFSM.underlying.self)), None, None))) + sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, request.paymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None))) // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) @@ -148,7 +154,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -176,7 +183,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -206,14 +214,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (Update)") { fixture => import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -263,14 +272,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // this time the router can't find a route: game over - sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (PermanentChannelFailure)") { fixture => import fixture._ val relayer = TestProbe() val routerForwarder = TestProbe() - val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val id = UUID.randomUUID() + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -297,12 +307,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - sender.expectMsg(PaymentFailed(request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment succeeded") { fixture => import fixture._ - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val id = UUID.randomUUID() + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -319,7 +330,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) } @@ -349,7 +360,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { watcher.expectMsgType[WatchSpentBasic] // actual test begins - val paymentFSM = system.actorOf(PaymentLifecycle.props(a, router, TestProbe().ref)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(UUID.randomUUID(), a, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -369,7 +380,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] - val PaymentSent(MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] + val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] // during the route computation the fees were treated as if they were 1msat but when sending the onion we actually put zero // NB: A -> B doesn't pay fees because it's our direct neighbor @@ -379,7 +390,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { } test("filter errors properly") { fixture => - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(None), None, None)) :: LocalFailure(RouteNotFound) :: Nil + val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index ff01cd712c..a2118598ec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.payment +import java.util.UUID + import akka.actor.{ActorRef, Status} import akka.testkit.TestProbe import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} @@ -67,7 +69,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -76,7 +78,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd.message.upstream_opt === Some(add_ab)) + assert(fwd.message.upstream_opt === Right(add_ab)) sender.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -87,7 +89,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) @@ -107,7 +109,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -116,7 +118,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd1.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd1.message.upstream_opt === Some(add_ab)) + assert(fwd1.message.upstream_opt === Right(add_ab)) sender.send(relayer, Status.Failure(Register.ForwardShortIdFailure(fwd1))) @@ -134,7 +136,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // check that payments are sent properly - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -142,7 +144,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd.message.upstream_opt === Some(add_ab)) + assert(fwd.message.upstream_opt === Right(add_ab)) sender.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -150,7 +152,7 @@ class RelayerSpec extends TestkitBaseClass { // now tell the relayer that the channel is down and try again relayer ! LocalChannelDown(sender.ref, channelId = channelId_bc, shortChannelId = channelUpdate_bc.shortChannelId, remoteNodeId = TestConstants.Bob.nodeParams.nodeId) - val (cmd1, _) = buildCommand(finalAmountMsat, finalExpiry, randomBytes32, hops) + val (cmd1, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, randomBytes32, hops) val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) sender.send(relayer, ForwardAdd(add_ab)) @@ -167,7 +169,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) val channelUpdate_bc_disabled = channelUpdate_bc.copy(channelFlags = Announcements.makeChannelFlags(Announcements.isNode1(channelUpdate_bc.channelFlags), enable = false)) @@ -188,7 +190,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, ByteVector.fill(Sphinx.PacketLength)(0)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -208,7 +210,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we use this to build a valid onion - val (cmd, _) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) + val (cmd, _) = buildCommand(UUID.randomUUID(), channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0)))) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -228,7 +230,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0))) - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -248,7 +250,7 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(feeBaseMsat = hops(1).lastUpdate.feeBaseMsat / 2))) - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -269,7 +271,7 @@ class RelayerSpec extends TestkitBaseClass { // to simulate this we use a zero-hop route A->B where A is the 'attacker' val hops1 = hops.head :: Nil - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc with a wrong expiry val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) @@ -290,7 +292,7 @@ class RelayerSpec extends TestkitBaseClass { // to simulate this we use a zero-hop route A->B where A is the 'attacker' val hops1 = hops.head :: Nil - val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1) + val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops1) // and then manually build an htlc with a wrong expiry val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry - 1, cmd.onion) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 276e6db7a6..92b903ae00 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.wire +import java.util.UUID +import akka.actor.ActorSystem import fr.acinq.bitcoin.DeterministicWallet.KeyPath import fr.acinq.bitcoin.{DeterministicWallet, OutPoint} import fr.acinq.eclair.channel._ @@ -26,7 +28,6 @@ import fr.acinq.eclair.wire.ChannelCodecs._ import fr.acinq.eclair.{UInt64, randomBytes, randomBytes32, randomKey} import org.scalatest.FunSuite import scodec.bits._ - import scala.compat.Platform import scala.util.Random @@ -140,19 +141,21 @@ class ChannelCodecsSpec extends FunSuite { } test("encode/decode origin") { - assert(originCodec.decodeValue(originCodec.encode(Local(None)).require).require === Local(None)) + val id = UUID.randomUUID() + assert(originCodec.decodeValue(originCodec.encode(Local(id, Some(ActorSystem("system").deadLetters))).require).require === Local(id, None)) + // TODO: add backward compatibility check val relayed = Relayed(randomBytes32, 4324, 12000000L, 11000000L) assert(originCodec.decodeValue(originCodec.encode(relayed).require).require === relayed) } test("encode/decode map of origins") { val map = Map( - 1L -> Local(None), + 1L -> Local(UUID.randomUUID(), None), 42L -> Relayed(randomBytes32, 4324, 12000000L, 11000000L), 130L -> Relayed(randomBytes32, -45, 13000000L, 12000000L), 1000L -> Relayed(randomBytes32, 10, 14000000L, 13000000L), -32L -> Relayed(randomBytes32, 54, 15000000L, 14000000L), - -4L -> Local(None)) + -4L -> Local(UUID.randomUUID(), None)) assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map) } From 61774a3a7f78486bf0dd87431c219b5c9dec6a07 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 29 Mar 2019 12:14:13 +0100 Subject: [PATCH 03/83] Update websocket mocks to use the Id --- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 29b7c59727..e26178a146 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -284,13 +284,13 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { wsClient.expectMessage(expectedSerializedPs) val prel = PaymentRelayed(amountIn = MilliSatoshi(21), amountOut = MilliSatoshi(20), paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L) - val expectedSerializedPrel = """{"type":"payment-relayed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" + val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" Serialization.write(prel)(mockService.formatsWithTypeHint) === expectedSerializedPrel system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) val precv = PaymentReceived(amount = MilliSatoshi(21), paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.One, timestamp = 1553784963659L) - val expectedSerializedPrecv = """{"type":"payment-received","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" + val expectedSerializedPrecv = """{"type":"payment-received","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}""" Serialization.write(precv)(mockService.formatsWithTypeHint) === expectedSerializedPrecv system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) From deb8c5052c7bd1b0cc6c85a589b983185b4a1efc Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 29 Mar 2019 14:47:32 +0100 Subject: [PATCH 04/83] Use hardcoded uuid in auditDB test --- .../test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index f27bddea71..3512f4d32f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} +import fr.acinq.eclair.wire.ChannelCodecs import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey} import org.scalatest.FunSuite @@ -43,12 +44,12 @@ class SqliteAuditDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteAuditDb(sqlite) - val e1 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) + val e1 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) val e2 = PaymentReceived(MilliSatoshi(42000), randomBytes32, randomBytes32) val e3 = PaymentRelayed(MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), Satoshi(42), "mutual") - val e5 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) - val e6 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = Platform.currentTime * 2) + val e5 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = 0) + val e6 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32, timestamp = Platform.currentTime * 2) val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000, ChannelStateSpec.commitments) val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000, true, false, "mutual") From 46d749aef007b094d70497dc26452a428f245e2a Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 29 Mar 2019 16:00:05 +0100 Subject: [PATCH 05/83] Remove InvalidPaymentHash channel exception --- .../fr/acinq/eclair/channel/ChannelExceptions.scala | 1 - .../eclair/channel/states/e/NormalStateSpec.scala | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 94a99f92b2..3cf1d713ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -59,7 +59,6 @@ case class InvalidCloseFee (override val channelId: ByteVect case class HtlcSigCountMismatch (override val channelId: ByteVector32, expected: Int, actual: Int) extends ChannelException(channelId, s"htlc sig count mismatch: expected=$expected actual: $actual") case class ForcedLocalCommit (override val channelId: ByteVector32) extends ChannelException(channelId, s"forced local commit") case class UnexpectedHtlcId (override val channelId: ByteVector32, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual") -case class InvalidPaymentHash (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid payment hash") case class ExpiryTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: minimum=$minimum actual=$actual blockCount=$blockCount") case class ExpiryTooBig (override val channelId: ByteVector32, maximum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too big: maximum=$maximum actual=$actual blockCount=$blockCount") case class HtlcValueTooSmall (override val channelId: ByteVector32, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 8836d79e29..ac16d12e22 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -37,7 +37,6 @@ import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigne import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} import scodec.bits._ - import scala.concurrent.duration._ /** @@ -108,17 +107,6 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { ))) } - test("recv CMD_ADD_HTLC (invalid payment hash)") { f => - import f._ - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144) - sender.send(alice, add) - val error = InvalidPaymentHash(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) - alice2bob.expectNoMsg(200 millis) - } - test("recv CMD_ADD_HTLC (expiry too small)") { f => import f._ val sender = TestProbe() From 5b38adb50a9d94049e551e9e58ac5dffba466011 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 29 Mar 2019 16:18:21 +0100 Subject: [PATCH 06/83] Use correct serializer in test (and avoid nasty reflection access warning) --- .../test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 60968bd599..3aae5fe6d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -79,7 +79,7 @@ class JsonSerializersSpec extends FunSuite with Matchers { } test("type hints") { - implicit val formats = DefaultFormats.withTypeHintFieldName("type") + CustomTypeHints(Map(classOf[PaymentSettlingOnChain] -> "payment-settling-onchain")) + new MilliSatoshiSerializer + implicit val formats = JsonSupport.formats.withTypeHintFieldName("type") + CustomTypeHints(Map(classOf[PaymentSettlingOnChain] -> "payment-settling-onchain")) + new MilliSatoshiSerializer val e1 = PaymentSettlingOnChain(UUID.randomUUID, MilliSatoshi(42), randomBytes32) assert(Serialization.writePretty(e1).contains("\"type\" : \"payment-settling-onchain\"")) } From 29865e367d9ffe00b0f4415e5ef8b0ea2bbe3224 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 10:33:01 +0200 Subject: [PATCH 07/83] Remove unused 'close' from paymentsDb --- eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 2 -- .../main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 1 - 2 files changed, 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 7edb98d0a9..bf0008cfcf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -41,8 +41,6 @@ trait PaymentsDb { def listPayments(): Seq[Payment] - def close: Unit - } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 80467b925e..5630e88857 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -80,5 +80,4 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def close(): Unit = sqlite.close() } From b40611d374e99a6c493e6ceb2897ee7d0ec03e9c Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 11:31:30 +0200 Subject: [PATCH 08/83] Introduce sent_payments in PaymentDB, bump db version --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 30 +++++++-- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 65 ++++++++++++++----- .../eclair/payment/LocalPaymentHandler.scala | 6 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 34 +++++++--- 4 files changed, 100 insertions(+), 35 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index bf0008cfcf..63d788528b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -16,12 +16,14 @@ package fr.acinq.eclair.db +import java.util.UUID + import fr.acinq.bitcoin.ByteVector32 /** * Store the Lightning payments received by the node. Sent and relayed payments are not persisted. *

- * A payment is a [[Payment]] object. In the local context of a LN node, it is safe to consider that + * A payment is a [[ReceivedPayment]] object. In the local context of a LN node, it is safe to consider that * a payment is uniquely identified by its payment hash. As such, implementations of this database can use the payment * hash as a unique key and index. *

@@ -35,19 +37,33 @@ import fr.acinq.bitcoin.ByteVector32 */ trait PaymentsDb { - def addPayment(payment: Payment) + def addReceivedPayment(payment: ReceivedPayment) + + def addSentPayments(sent: SentPayment) - def findByPaymentHash(paymentHash: ByteVector32): Option[Payment] + def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] - def listPayments(): Seq[Payment] + def listReceived(): Seq[ReceivedPayment] + + def listSent(): Seq[SentPayment] } /** - * Payment object stored in DB. + * Received payment object stored in DB. + * + * @param paymentHash identifier of the payment + * @param amount_msat amount of the payment, in milli-satoshis + * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. + */ +case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) + +/** + * Sent payment object stored in DB. * - * @param payment_hash identifier of the payment + * @param id internal payment identifier + * @param payment_hash payment_hash * @param amount_msat amount of the payment, in milli-satoshis * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. */ -case class Payment(payment_hash: ByteVector32, amount_msat: Long, timestamp: Long) +case class SentPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 5630e88857..c0d81f1f8f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -17,10 +17,11 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection +import java.util.UUID import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import fr.acinq.eclair.db.{Payment, PaymentsDb} +import fr.acinq.eclair.db.{PaymentsDb, ReceivedPayment, SentPayment} import grizzled.slf4j.Logging import scala.collection.immutable.Queue @@ -40,41 +41,73 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { import SqliteUtils.ExtendedResultSet._ val DB_NAME = "payments" - val CURRENT_VERSION = 1 + val PREVIOUS_VERSION = 1 + val CURRENT_VERSION = 2 using(sqlite.createStatement()) { statement => - require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed - statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + getVersion(statement, DB_NAME, CURRENT_VERSION) match { + case PREVIOUS_VERSION => + statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL, payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + case CURRENT_VERSION => + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL, payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + case unknownVersion => + throw new RuntimeException(s"Unknown version of paymentsDB found, version=$unknownVersion") + } } - override def addPayment(payment: Payment): Unit = { - using(sqlite.prepareStatement("INSERT INTO payments VALUES (?, ?, ?)")) { statement => - statement.setBytes(1, payment.payment_hash.toArray) - statement.setLong(2, payment.amount_msat) + override def addReceivedPayment(payment: ReceivedPayment): Unit = { + using(sqlite.prepareStatement("INSERT INTO received_payments VALUES (?, ?, ?)")) { statement => + statement.setBytes(1, payment.paymentHash.toArray) + statement.setLong(2, payment.amountMsat) statement.setLong(3, payment.timestamp) val res = statement.executeUpdate() - logger.debug(s"inserted $res payment=${payment} in DB") + logger.debug(s"inserted $res payment=${payment.paymentHash} into payment DB") + } + } + + override def addSentPayments(sent: SentPayment): Unit = { + using(sqlite.prepareStatement("INSERT INTO sent_payments VALUES (?, ?, ?, ?)")) { statement => + statement.setBytes(1, sent.id.toString.getBytes) + statement.setBytes(2, sent.paymentHash.toArray) + statement.setLong(3, sent.amountMsat) + statement.setLong(4, sent.timestamp) + val res = statement.executeUpdate() + logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") } } - override def findByPaymentHash(paymentHash: ByteVector32): Option[Payment] = { - using(sqlite.prepareStatement("SELECT payment_hash, amount_msat, timestamp FROM payments WHERE payment_hash = ?")) { statement => + override def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] = { + using(sqlite.prepareStatement("SELECT payment_hash, amount_msat, timestamp FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(Payment(rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) + Some(ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) } else { None } } } - override def listPayments(): Seq[Payment] = { + override def listReceived(): Seq[ReceivedPayment] = { + using(sqlite.createStatement()) { statement => + val rs = statement.executeQuery("SELECT payment_hash, amount_msat, timestamp FROM received_payments") + var q: Queue[ReceivedPayment] = Queue() + while (rs.next()) { + q = q :+ ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp")) + } + q + } + } + + override def listSent(): Seq[SentPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_hash, amount_msat, timestamp FROM payments") - var q: Queue[Payment] = Queue() + val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, timestamp FROM sent_payments") + var q: Queue[SentPayment] = Queue() while (rs.next()) { - q = q :+ Payment(rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp")) + q = q :+ SentPayment(UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp")) } q } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 2fce5d5cb6..e10f839de9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, Props, Status} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel} -import fr.acinq.eclair.db.Payment +import fr.acinq.eclair.db.ReceivedPayment import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, randomBytes32} @@ -65,7 +65,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin } recover { case t => sender ! Status.Failure(t) } case CheckPayment(paymentHash) => - nodeParams.db.payments.findByPaymentHash(paymentHash) match { + nodeParams.db.payments.receivedByPaymentHash(paymentHash) match { case Some(_) => sender ! true case _ => sender ! false } @@ -92,7 +92,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case _ => log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") // amount is correct or was not specified in the payment request - nodeParams.db.payments.addPayment(Payment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime / 1000)) + nodeParams.db.payments.addReceivedPayment(ReceivedPayment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime / 1000)) sender ! CMD_FULFILL_HTLC(htlc.id, paymentPreimage, commit = true) context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash, htlc.channelId)) context.become(run(hash2preimage - htlc.paymentHash)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 52a7aba85c..af0f277c5f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.db import java.sql.DriverManager +import java.util.UUID import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb @@ -33,17 +34,32 @@ class SqlitePaymentsDbSpec extends FunSuite { val db2 = new SqlitePaymentsDb(sqlite) } - test("add/list payments/find 1 payment that exists/find 1 payment that does not exist") { + test("add/list received payments/find 1 payment that exists/find 1 payment that does not exist") { val sqlite = inmem val db = new SqlitePaymentsDb(sqlite) - val p1 = Payment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) - val p2 = Payment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345678, 1513871928275L) - assert(db.listPayments() === Nil) - db.addPayment(p1) - db.addPayment(p2) - assert(db.listPayments().toList === List(p1, p2)) - assert(db.findByPaymentHash(p1.payment_hash) === Some(p1)) - assert(db.findByPaymentHash(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) + val p1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) + val p2 = ReceivedPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345678, 1513871928275L) + assert(db.listReceived() === Nil) + db.addReceivedPayment(p1) + db.addReceivedPayment(p2) + assert(db.listReceived().toList === List(p1, p2)) + assert(db.receivedByPaymentHash(p1.paymentHash) === Some(p1)) + assert(db.receivedByPaymentHash(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) } + + test("add/retrieve sent payments") { + + val db = new SqlitePaymentsDb(inmem) + + val s1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928275L) + val s2 = SentPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928275L) + + assert(db.listSent().isEmpty) + db.addSentPayments(s1) + db.addSentPayments(s2) + + assert(db.listSent().toList == Seq(s1, s2)) + } + } From c2052ab1d957a2e098f77d3d75807e8b2a0b9710 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 11:41:07 +0200 Subject: [PATCH 09/83] Add query functions for payment DB sentPaymentById/sentPaymentByHash --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 ++++ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 24 +++++++++++++++++++ .../eclair/db/SqlitePaymentsDbSpec.scala | 4 ++++ 3 files changed, 32 insertions(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 63d788528b..0b247c755b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -43,6 +43,10 @@ trait PaymentsDb { def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] + def sentPaymentById(id: UUID): Option[SentPayment] + + def sentPaymentByHash(paymentHash: ByteVector32): Option[SentPayment] + def listReceived(): Seq[ReceivedPayment] def listSent(): Seq[SentPayment] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index c0d81f1f8f..3c31341ee6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -91,6 +91,30 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } + override def sentPaymentById(id: UUID): Option[SentPayment] = { + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp FROM sent_payments WHERE id = ?")) { statement => + statement.setBytes(1, id.toString.getBytes) + val rs = statement.executeQuery() + if (rs.next()) { + Some(SentPayment(UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) + } else { + None + } + } + } + + override def sentPaymentByHash(paymentHash: ByteVector32): Option[SentPayment] = { + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp FROM sent_payments WHERE payment_hash = ?")) { statement => + statement.setBytes(1, paymentHash.toArray) + val rs = statement.executeQuery() + if (rs.next()) { + Some(SentPayment(UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) + } else { + None + } + } + } + override def listReceived(): Seq[ReceivedPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT payment_hash, amount_msat, timestamp FROM received_payments") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index af0f277c5f..079f1563de 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -60,6 +60,10 @@ class SqlitePaymentsDbSpec extends FunSuite { db.addSentPayments(s2) assert(db.listSent().toList == Seq(s1, s2)) + assert(db.sentPaymentById(s1.id) === Some(s1)) + assert(db.sentPaymentById(UUID.randomUUID()) === None) + assert(db.sentPaymentByHash(s2.paymentHash) === Some(s2)) + assert(db.sentPaymentByHash(ByteVector32.Zeroes) === None) } } From 49197568e1918cdc1bab8498f5bc64a3498f186d Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 14:05:08 +0200 Subject: [PATCH 10/83] Update version of db with getVersion --- .../src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 9 ++++++--- .../fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 4 ++-- .../scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 0b247c755b..689a7d65a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -21,19 +21,22 @@ import java.util.UUID import fr.acinq.bitcoin.ByteVector32 /** - * Store the Lightning payments received by the node. Sent and relayed payments are not persisted. + * Store the Lightning payments received and sent by the node. Relayed payments are not persisted. *

- * A payment is a [[ReceivedPayment]] object. In the local context of a LN node, it is safe to consider that + * A received payment is a [[ReceivedPayment]] object. In the local context of a LN node, it is safe to consider that * a payment is uniquely identified by its payment hash. As such, implementations of this database can use the payment * hash as a unique key and index. *

+ * + *

+ * A sent payment is a [[SentPayment]] object. + *

* Basic operations on this DB are: *

    *
  • insertion *
  • find by payment hash *
  • list all *
- * Payments should not be updated nor deleted. */ trait PaymentsDb { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 3c31341ee6..dfcc8032b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -49,10 +49,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { case PREVIOUS_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL, payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL, payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of paymentsDB found, version=$unknownVersion") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 8d8e602e60..9bf6f36eca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -54,7 +54,7 @@ object SqliteUtils { def getVersion(statement: Statement, db_name: String, currentVersion: Int): Int = { statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") // if there was no version for the current db, then insert the current version - statement.executeUpdate(s"INSERT OR IGNORE INTO versions VALUES ('$db_name', $currentVersion)") + statement.executeUpdate(s"INSERT OR REPLACE INTO versions VALUES ('$db_name', $currentVersion)") // if there was a previous version installed, this will return a different value from current version val res = statement.executeQuery(s"SELECT version FROM versions WHERE db_name='$db_name'") res.getInt("version") From 2aeaa021ea444cd4caf7e477aebcc1ed052a7a95 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 14:11:48 +0200 Subject: [PATCH 11/83] Insert or update behavior on payment_db.addSentPayment --- .../scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 4 ++-- .../scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index dfcc8032b7..357f4b3959 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -69,7 +69,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def addSentPayments(sent: SentPayment): Unit = { - using(sqlite.prepareStatement("INSERT INTO sent_payments VALUES (?, ?, ?, ?)")) { statement => + using(sqlite.prepareStatement("INSERT OR REPLACE INTO sent_payments VALUES (?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) statement.setLong(3, sent.amountMsat) @@ -137,4 +137,4 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } -} +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 079f1563de..d7de7afda6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -48,7 +48,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.receivedByPaymentHash(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) } - test("add/retrieve sent payments") { + test("add/retrieve/update sent payments") { val db = new SqlitePaymentsDb(inmem) @@ -64,6 +64,10 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.sentPaymentById(UUID.randomUUID()) === None) assert(db.sentPaymentByHash(s2.paymentHash) === Some(s2)) assert(db.sentPaymentByHash(ByteVector32.Zeroes) === None) + + val s3 = s2.copy(amountMsat = 88776655) + db.addSentPayments(s3) + assert(db.sentPaymentById(s2.id).exists(_.amountMsat == s3.amountMsat)) } } From 18b8d72e6015a207f1ff7d07eca6a2b6a62f7a15 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 15:27:57 +0200 Subject: [PATCH 12/83] Add status to Outgoing payments in paymentDB --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 21 ++++--- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 57 ++++++++++--------- .../eclair/db/SqlitePaymentsDbSpec.scala | 8 +-- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 689a7d65a9..dfb58de70a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -29,7 +29,7 @@ import fr.acinq.bitcoin.ByteVector32 *

* *

- * A sent payment is a [[SentPayment]] object. + * A sent payment is a [[OutgoingPayment]] object. *

* Basic operations on this DB are: *

    @@ -42,17 +42,17 @@ trait PaymentsDb { def addReceivedPayment(payment: ReceivedPayment) - def addSentPayments(sent: SentPayment) + def addSentPayments(sent: OutgoingPayment) def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] - def sentPaymentById(id: UUID): Option[SentPayment] + def sentPaymentById(id: UUID): Option[OutgoingPayment] - def sentPaymentByHash(paymentHash: ByteVector32): Option[SentPayment] + def sentPaymentByHash(paymentHash: ByteVector32): Option[OutgoingPayment] def listReceived(): Seq[ReceivedPayment] - def listSent(): Seq[SentPayment] + def listSent(): Seq[OutgoingPayment] } @@ -66,11 +66,18 @@ trait PaymentsDb { case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) /** - * Sent payment object stored in DB. + * Outgoing payment is every payment that is sent by this node, they may not be finalized and + * when is final it can be failed or successful. * * @param id internal payment identifier * @param payment_hash payment_hash * @param amount_msat amount of the payment, in milli-satoshis * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. */ -case class SentPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) +case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, timestamp: Long, status: OutgoingPaymentStatus.Value) + +object OutgoingPaymentStatus extends Enumeration { + val PENDING = Value(1, "PENDING") + val SUCCEEDED = Value(2, "SUCCEEDED") + val FAILED = Value(3, "FAILED") +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 357f4b3959..6e2a859df9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -21,21 +21,11 @@ import java.util.UUID import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import fr.acinq.eclair.db.{PaymentsDb, ReceivedPayment, SentPayment} +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb, ReceivedPayment} import grizzled.slf4j.Logging import scala.collection.immutable.Queue -/** - * Payments are stored in the `payments` table. - * The primary key in this DB is the `payment_hash` column. Columns are not nullable. - *

    - * Types: - *

      - *
    • `payment_hash`: BLOB - *
    • `amount_msat`: INTEGER - *
    • `timestamp`: INTEGER (unix timestamp) - */ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { import SqliteUtils.ExtendedResultSet._ @@ -49,10 +39,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { case PREVIOUS_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL, status VARCHAR NOT NULL)") case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of paymentsDB found, version=$unknownVersion") } @@ -68,12 +58,13 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def addSentPayments(sent: SentPayment): Unit = { - using(sqlite.prepareStatement("INSERT OR REPLACE INTO sent_payments VALUES (?, ?, ?, ?)")) { statement => + override def addSentPayments(sent: OutgoingPayment): Unit = { + using(sqlite.prepareStatement("INSERT OR REPLACE INTO sent_payments VALUES (?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) statement.setLong(3, sent.amountMsat) statement.setLong(4, sent.timestamp) + statement.setString(5, sent.status.toString) val res = statement.executeUpdate() logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") } @@ -91,25 +82,34 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def sentPaymentById(id: UUID): Option[SentPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp FROM sent_payments WHERE id = ?")) { statement => + override def sentPaymentById(id: UUID): Option[OutgoingPayment] = { + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp, status FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() if (rs.next()) { - Some(SentPayment(UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) + Some(OutgoingPayment( + UUID.fromString(new String(rs.getBytes("id"))), + rs.getByteVector32("payment_hash"), + rs.getLong("amount_msat"), + rs.getLong("timestamp"), + OutgoingPaymentStatus.withName(rs.getString("status")))) } else { None } } } - override def sentPaymentByHash(paymentHash: ByteVector32): Option[SentPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp FROM sent_payments WHERE payment_hash = ?")) { statement => + override def sentPaymentByHash(paymentHash: ByteVector32): Option[OutgoingPayment] = { + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp, status FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(SentPayment(UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) - } else { + Some(OutgoingPayment( + UUID.fromString(new String(rs.getBytes("id"))), + rs.getByteVector32("payment_hash"), + rs.getLong("amount_msat"), + rs.getLong("timestamp"), + OutgoingPaymentStatus.withName(rs.getString("status")))) } else { None } } @@ -126,12 +126,17 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listSent(): Seq[SentPayment] = { + override def listSent(): Seq[OutgoingPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, timestamp FROM sent_payments") - var q: Queue[SentPayment] = Queue() + val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, timestamp, status FROM sent_payments") + var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { - q = q :+ SentPayment(UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp")) + q = q :+ OutgoingPayment( + UUID.fromString(new String(rs.getBytes("id"))), + rs.getByteVector32("payment_hash"), + rs.getLong("amount_msat"), + rs.getLong("timestamp"), + OutgoingPaymentStatus.withName(rs.getString("status"))) } q } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index d7de7afda6..5fe57b0022 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -52,8 +52,8 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(inmem) - val s1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928275L) - val s2 = SentPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928275L) + val s1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928275L, OutgoingPaymentStatus.PENDING) + val s2 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928275L, OutgoingPaymentStatus.PENDING) assert(db.listSent().isEmpty) db.addSentPayments(s1) @@ -65,9 +65,9 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.sentPaymentByHash(s2.paymentHash) === Some(s2)) assert(db.sentPaymentByHash(ByteVector32.Zeroes) === None) - val s3 = s2.copy(amountMsat = 88776655) + val s3 = s2.copy(amountMsat = 88776655, status = OutgoingPaymentStatus.SUCCEEDED) db.addSentPayments(s3) - assert(db.sentPaymentById(s2.id).exists(_.amountMsat == s3.amountMsat)) + assert(db.sentPaymentById(s2.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) } } From dc41eff1a245b950660b53bf13a87437b797087b Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 15:38:59 +0200 Subject: [PATCH 13/83] Bring paymentDb to PaymentInitiator/PaymentLifecycle --- .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../eclair/payment/PaymentInitiator.scala | 7 +++--- .../eclair/payment/PaymentLifecycle.scala | 5 ++-- .../eclair/payment/PaymentLifecycleSpec.scala | 24 ++++++++++--------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index a27a5cab28..85636c9a84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -245,7 +245,7 @@ class Setup(datadir: File, authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) - paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart)) + paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register, database.payments), "payment-initiator", SupervisorStrategy.Restart)) _ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart)) kit = Kit( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index 6d99a602d4..ec87b2afa2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -20,17 +20,18 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.db.PaymentsDb import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment /** * Created by PM on 29/08/2016. */ -class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends Actor with ActorLogging { +class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentDb: PaymentsDb) extends Actor with ActorLogging { override def receive: Receive = { case c: SendPayment => val paymentId = UUID.randomUUID() - val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register)) + val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register, paymentDb)) // TODO: here we should probably send the payment id back to the sender so that it knows how to query the api later //sender ! paymentId payFsm forward c @@ -39,5 +40,5 @@ class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: Acto } object PaymentInitiator { - def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], sourceNodeId, router, register) + def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentDb: PaymentsDb) = Props(classOf[PaymentInitiator], sourceNodeId, router, register, paymentDb) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 9d51aa958a..b78f6b6ecf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} +import fr.acinq.eclair.db.PaymentsDb import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router._ @@ -37,7 +38,7 @@ import scala.util.{Failure, Success} /** * Created by PM on 26/08/2016. */ -class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { +class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentsDb: PaymentsDb) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { startWith(WAITING_FOR_REQUEST, WaitingForRequest) @@ -179,7 +180,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi object PaymentLifecycle { - def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register) + def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentDb: PaymentsDb) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register, paymentDb) // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index b54c70122d..b446a60e37 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnounce import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, ShortChannelId, randomBytes32} +import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomBytes32} /** * Created by PM on 29/08/2016. @@ -48,10 +48,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val defaultAmountMsat = 142000000L val defaultPaymentHash = randomBytes32 + val paymentDb = TestConstants.inMemoryDb().payments + test("payment failed (route not found)") { fixture => import fixture._ val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -68,7 +70,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (route too expensive)") { fixture => import fixture._ val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -87,7 +89,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -126,7 +128,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -155,7 +157,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -184,7 +186,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -222,7 +224,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -280,7 +282,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() @@ -313,7 +315,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment succeeded") { fixture => import fixture._ val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -360,7 +362,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { watcher.expectMsgType[WatchSpentBasic] // actual test begins - val paymentFSM = system.actorOf(PaymentLifecycle.props(UUID.randomUUID(), a, router, TestProbe().ref)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(UUID.randomUUID(), a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() From 9d2b85b9491fea7170ad5b6b0645f0124f982980 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 17:09:34 +0200 Subject: [PATCH 14/83] Wire payment lifecycle events to paymentDB updates --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 6 ++-- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 28 +++++++++++++------ .../eclair/payment/PaymentLifecycle.scala | 8 +++++- .../eclair/db/SqlitePaymentsDbSpec.scala | 4 +++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index dfb58de70a..aa11d88910 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -44,6 +44,8 @@ trait PaymentsDb { def addSentPayments(sent: OutgoingPayment) + def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) + def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] def sentPaymentById(id: UUID): Option[OutgoingPayment] @@ -72,9 +74,9 @@ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestam * @param id internal payment identifier * @param payment_hash payment_hash * @param amount_msat amount of the payment, in milli-satoshis - * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. + * @param lastUpdate absolute time in seconds since UNIX epoch when the payment was last updated. */ -case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, timestamp: Long, status: OutgoingPaymentStatus.Value) +case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, lastUpdate: Long, status: OutgoingPaymentStatus.Value) object OutgoingPaymentStatus extends Enumeration { val PENDING = Value(1, "PENDING") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 6e2a859df9..7300ad8234 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb, R import grizzled.slf4j.Logging import scala.collection.immutable.Queue +import scala.compat.Platform class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { @@ -39,10 +40,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { case PREVIOUS_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of paymentsDB found, version=$unknownVersion") } @@ -58,12 +59,21 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } + override def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { + using(sqlite.prepareStatement(s"UPDATE sent_payments SET (status, updated_at) = (?, ?) WHERE id = ?")) { statement => + statement.setString(1, newStatus.toString) + statement.setLong(2, Platform.currentTime) + statement.setBytes(3, id.toString.getBytes) + statement.executeUpdate() + } + } + override def addSentPayments(sent: OutgoingPayment): Unit = { using(sqlite.prepareStatement("INSERT OR REPLACE INTO sent_payments VALUES (?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) statement.setLong(3, sent.amountMsat) - statement.setLong(4, sent.timestamp) + statement.setLong(4, sent.lastUpdate) statement.setString(5, sent.status.toString) val res = statement.executeUpdate() logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") @@ -83,7 +93,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def sentPaymentById(id: UUID): Option[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp, status FROM sent_payments WHERE id = ?")) { statement => + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, updated_at, status FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() if (rs.next()) { @@ -91,7 +101,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), - rs.getLong("timestamp"), + rs.getLong("updated_at"), OutgoingPaymentStatus.withName(rs.getString("status")))) } else { None @@ -100,7 +110,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def sentPaymentByHash(paymentHash: ByteVector32): Option[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, timestamp, status FROM sent_payments WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, updated_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { @@ -108,7 +118,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), - rs.getLong("timestamp"), + rs.getLong("updated_at"), OutgoingPaymentStatus.withName(rs.getString("status")))) } else { None } @@ -128,14 +138,14 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def listSent(): Seq[OutgoingPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, timestamp, status FROM sent_payments") + val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, updated_at, status FROM sent_payments") var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { q = q :+ OutgoingPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), - rs.getLong("timestamp"), + rs.getLong("updated_at"), OutgoingPaymentStatus.withName(rs.getString("status"))) } q diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index b78f6b6ecf..d5db2a01a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} -import fr.acinq.eclair.db.PaymentsDb +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router._ @@ -33,6 +33,7 @@ import fr.acinq.eclair.wire._ import scodec.Attempt import scodec.bits.ByteVector +import scala.compat.Platform import scala.util.{Failure, Success} /** @@ -45,6 +46,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi when(WAITING_FOR_REQUEST) { case Event(c: SendPayment, WaitingForRequest) => router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) + paymentsDb.addSentPayments(OutgoingPayment(id, c.paymentHash, c.amountMsat, Platform.currentTime, OutgoingPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -68,6 +70,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.SUCCEEDED) reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) @@ -78,6 +81,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -91,6 +95,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi } log.warning(s"too many failed attempts, failing the payment") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -155,6 +160,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 5fe57b0022..fe9720bf03 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -68,6 +68,10 @@ class SqlitePaymentsDbSpec extends FunSuite { val s3 = s2.copy(amountMsat = 88776655, status = OutgoingPaymentStatus.SUCCEEDED) db.addSentPayments(s3) assert(db.sentPaymentById(s2.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) + + db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.FAILED) + assert(db.sentPaymentById(s2.id).get.status == OutgoingPaymentStatus.FAILED) + } } From 5fd7db59caae0db47cddae5953c1343219e6b277 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 17:20:57 +0200 Subject: [PATCH 15/83] Return the UUID of the ongoing payment in /send API --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 12 +++++------- .../fr/acinq/eclair/payment/PaymentInitiator.scala | 3 +-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 729e00f070..f6f3a820e6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -1,5 +1,7 @@ package fr.acinq.eclair +import java.util.UUID + import akka.actor.ActorRef import akka.pattern._ import akka.util.Timeout @@ -15,7 +17,6 @@ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} import scodec.bits.ByteVector - import scala.concurrent.Future import scala.concurrent.duration._ @@ -47,7 +48,7 @@ trait Eclair { def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] - def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[PaymentResult] + def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] def checkpayment(paymentHash: ByteVector32): Future[Boolean] @@ -132,15 +133,12 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat, assistedRoutes)).mapTo[RouteResponse] } - override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[PaymentResult] = { + override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] = { val sendPayment = minFinalCltvExpiry match { case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv) case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) } - (appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map { - case s: PaymentSucceeded => s - case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures)) - } + (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index ec87b2afa2..1dc280105d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -32,9 +32,8 @@ class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: Acto case c: SendPayment => val paymentId = UUID.randomUUID() val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register, paymentDb)) - // TODO: here we should probably send the payment id back to the sender so that it knows how to query the api later - //sender ! paymentId payFsm forward c + sender ! paymentId } } From 3bb1482c32d7f31229eb1cff42f6612377ec41b3 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 18:13:33 +0200 Subject: [PATCH 16/83] Add api to query payments by ID --- .../main/scala/fr/acinq/eclair/Eclair.scala | 9 +++++- .../eclair/api/FormParamExtractors.scala | 6 ++++ .../fr/acinq/eclair/api/JsonSerializers.scala | 11 ++++--- .../scala/fr/acinq/eclair/api/Service.scala | 21 +++++++++++-- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 30 +++++++++++++++++-- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index f6f3a820e6..b99355c7a4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -9,7 +9,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.api.{AuditResponse, GetInfoResponse} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.{NetworkFee, Stats} +import fr.acinq.eclair.db.{NetworkFee, OutgoingPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ @@ -17,6 +17,7 @@ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest} import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} import scodec.bits.ByteVector + import scala.concurrent.Future import scala.concurrent.duration._ @@ -50,6 +51,8 @@ trait Eclair { def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] + def paymentInfo(id: UUID): Future[Option[OutgoingPayment]] + def checkpayment(paymentHash: ByteVector32): Future[Boolean] def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] @@ -141,6 +144,10 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } + override def paymentInfo(id: UUID): Future[Option[OutgoingPayment ]] = Future { + appKit.nodeParams.db.payments.sentPaymentById(id) + } + override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Boolean] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index 5e15506b84..bd9cd710ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -1,5 +1,7 @@ package fr.acinq.eclair.api +import java.util.UUID + import akka.http.scaladsl.unmarshalling.Unmarshaller import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey @@ -29,4 +31,8 @@ object FormParamExtractors { ShortChannelId(str) } + implicit val javaUUIDUnmarshaller: Unmarshaller[String, UUID] = Unmarshaller.strict { str => + UUID.fromString(str) + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 2cbef11bab..dd1db22172 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -18,9 +18,6 @@ package fr.acinq.eclair.api import java.net.InetSocketAddress import java.util.UUID - -import akka.http.scaladsl.model.MediaType -import akka.http.scaladsl.model.MediaTypes._ import com.google.common.net.HostAndPort import de.heikoseeberger.akkahttpjson4s.Json4sSupport import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty @@ -28,6 +25,7 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, OutPoint, Transaction} import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.router.RouteResponse import fr.acinq.eclair.transactions.Direction @@ -159,6 +157,10 @@ class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { case id: UUID => JString(id.toString) })) +class OutgoingPaymentStatusSerializer extends CustomSerializer[OutgoingPaymentStatus.Value](format => ({ null }, { + case el: OutgoingPaymentStatus.Value => JString(el.toString) +})) + object JsonSupport extends Json4sSupport { implicit val serialization = jackson.Serialization @@ -188,7 +190,8 @@ object JsonSupport extends Json4sSupport { new NodeAddressSerializer + new DirectionSerializer + new PaymentRequestSerializer + - new JavaUUIDSerializer + new JavaUUIDSerializer + + new OutgoingPaymentStatusSerializer implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 8fb91a7649..c34bcc0961 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.api +import java.util.UUID + import akka.http.scaladsl.server._ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} @@ -23,6 +25,7 @@ import fr.acinq.eclair.{Eclair, Kit, ShortChannelId} import FormParamExtractors._ import akka.NotUsed import akka.actor.{Actor, ActorRef, ActorSystem, Props} +import akka.http.scaladsl.marshalling.{Marshaller, ToResponseMarshaller} import akka.http.scaladsl.model.HttpMethods.POST import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} @@ -42,6 +45,7 @@ import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ +import scala.util.{Failure, Success} case class ErrorResponse(error: String) @@ -51,6 +55,7 @@ trait Service extends Directives with Logging { import JsonSupport.marshaller import JsonSupport.formats import JsonSupport.serialization + // used to send typed messages over the websocket val formatsWithTypeHint = formats.withTypeHintFieldName("type") + CustomTypeHints(Map( @@ -68,6 +73,13 @@ trait Service extends Directives with Logging { implicit val actorSystem: ActorSystem implicit val mat: ActorMaterializer + // custom directive to fail with HTTP 404 if the element was not found + def complete[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { + case Success(Some(t)) => complete(t) + case Success(None) => complete(StatusCodes.NotFound) + case Failure(thr) => throw thr + } + // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelId = "channelId".as[ByteVector32](sha256HashUnmarshaller) val nodeId = "nodeId".as[PublicKey] @@ -81,7 +93,7 @@ trait Service extends Directives with Logging { // map all the rejections to a JSON error object ErrorResponse val apiRejectionHandler = RejectionHandler.default.mapRejectionResponse { - case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) => + case res@HttpResponse(_, _, ent: HttpEntity.Strict, _) => res.copy(entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String)))) } @@ -127,7 +139,7 @@ trait Service extends Directives with Logging { val route: Route = { respondWithDefaultHeaders(customHeaders) { handleExceptions(apiExceptionHandler) { - handleRejections(apiRejectionHandler){ + handleRejections(apiRejectionHandler) { withRequestTimeoutResponse(timeoutResponse) { authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ => post { @@ -225,6 +237,11 @@ trait Service extends Directives with Logging { complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ + path("paymentinfo") { + formFields("id".as[UUID]) { id => + complete(eclairApi.paymentInfo(id)) + } + } ~ path("checkpayment") { formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => complete(eclairApi.checkpayment(paymentHash)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index e26178a146..0aefb24974 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.api import java.util.UUID + import akka.actor.{Actor, ActorSystem, Props, Scheduler} import org.scalatest.FunSuite import akka.http.scaladsl.model.StatusCodes._ @@ -30,13 +31,14 @@ import akka.stream.ActorMaterializer import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.RES_GETINFO -import fr.acinq.eclair.db.{NetworkFee, Stats} +import fr.acinq.eclair.db.{NetworkFee, OutgoingPayment, Stats} import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.{ChannelDesc, RouteResponse} import fr.acinq.eclair.wire.{ChannelUpdate, NodeAddress, NodeAnnouncement} import org.json4s.jackson.Serialization import scodec.bits.ByteVector + import scala.concurrent.Future import scala.concurrent.duration._ import scala.io.Source @@ -72,7 +74,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def findRoute(targetNodeId: Crypto.PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]]): Future[RouteResponse] = ??? - override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[PaymentLifecycle.PaymentResult] = ??? + override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[UUID] = ??? override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = ??? @@ -83,6 +85,8 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def channelStats(): Future[Seq[Stats]] = ??? override def getInfoResponse(): Future[GetInfoResponse] = ??? + + override def paymentInfo(id: UUID): Future[Option[OutgoingPayment]] = ??? } implicit val formats = JsonSupport.formats @@ -257,6 +261,28 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } } + test("'send' method should return the UUID of the outgoing payment") { + + val id = UUID.randomUUID() + val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" + + val mockService = new MockService(new EclairMock { + override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[UUID] = Future.successful( + id + ) + }) + + Post("/send", FormData("invoice" -> invoice).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == OK) + assert(entityAs[String] == "\""+id.toString+"\"") + } + } + + test("the websocket should return typed objects") { val mockService = new MockService(new EclairMock {}) From a52353d4ccc214898aac591bc644cf336740583a Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 1 Apr 2019 18:17:21 +0200 Subject: [PATCH 17/83] Support one option param for /audit API --- eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index b99355c7a4..c9fc873a55 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -155,6 +155,8 @@ class EclairImpl(appKit: Kit) extends Eclair { override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { val (from, to) = (from_opt, to_opt) match { case (Some(f), Some(t)) => (f, t) + case (None, Some(t)) => (0L, t) + case (Some(f), None) => (f, Long.MaxValue) case _ => (0L, Long.MaxValue) } @@ -168,6 +170,8 @@ class EclairImpl(appKit: Kit) extends Eclair { override def networkFees(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[NetworkFee]] = { val (from, to) = (from_opt, to_opt) match { case (Some(f), Some(t)) => (f, t) + case (None, Some(t)) => (0L, t) + case (Some(f), None) => (f, Long.MaxValue) case _ => (0L, Long.MaxValue) } From 76891c409b521d5cf8736a6136f7a3a8d0f2fbbc Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 2 Apr 2019 11:24:55 +0200 Subject: [PATCH 18/83] Adjust integration test to deal with the UUID from the payment initiator --- .../eclair/integration/IntegrationSpec.scala | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 967a5badcb..4e2f8beda8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.integration import java.io.{File, PrintWriter} -import java.util.Properties +import java.util.{Properties, UUID} import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{TestKit, TestProbe} @@ -262,7 +262,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // then we make the actual payment sender.send(nodes("A").paymentInitiator, SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams)) - sender.expectMsgType[PaymentSucceeded] + sender.expectMsgType[UUID] } test("send an HTLC A->D with an invalid expiry delta for B") { @@ -286,7 +286,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment - sender.expectMsgType[PaymentSucceeded](5 seconds) + val paymentId = sender.expectMsgType[UUID](5 seconds) + val ps = sender.expectMsgType[PaymentSucceeded](5 seconds) + assert(ps.id == paymentId) awaitCond({ // in the meantime, the router will have updated its state @@ -322,7 +324,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D - sender.expectMsgType[PaymentSucceeded](5 seconds) + sender.expectMsgType[UUID](5 seconds) } test("send an HTLC A->D with an unknown payment hash") { @@ -331,7 +333,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("A").paymentInitiator, pr) // A will receive an error from D and won't retry + val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] + assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, UnknownPaymentHash)) @@ -349,7 +353,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D + val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] + assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectPaymentAmount)) @@ -367,7 +373,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D + val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] + assert(paymentId == failed.id) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectPaymentAmount)) @@ -383,7 +391,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // A send payment of 3 mBTC, more than asked but it should still be accepted val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) - sender.expectMsgType[PaymentSucceeded] + sender.expectMsgType[UUID] } test("send multiple HTLCs A->D with a failover when a channel gets exhausted") { @@ -396,7 +404,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) - sender.expectMsgType[PaymentSucceeded] + sender.expectMsgType[UUID] } } @@ -411,6 +419,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService sender.send(nodes("A").paymentInitiator, SendPayment(amountMsat.amount, pr.paymentHash, nodes("C").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams.map(_.copy(ratios = Some(WeightRatios(0, 0, 1)))))) + sender.expectMsgType[UUID](max = 60 seconds) awaitCond({ sender.expectMsgType[PaymentResult](10 seconds) match { case PaymentFailed(_, _, failures) => failures == Seq.empty // if something went wrong fail with a hint @@ -455,6 +464,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) + paymentSender.expectMsgType[UUID](30 seconds) // F gets the htlc val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] // now that we have the channel id, we retrieve channels default final addresses @@ -534,6 +544,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) + paymentSender.expectMsgType[UUID](30 seconds) + // F gets the htlc val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] // now that we have the channel id, we retrieve channels default final addresses @@ -609,6 +621,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) + val paymentId = paymentSender.expectMsgType[UUID] // F gets the htlc val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] // now that we have the channel id, we retrieve channels default final addresses @@ -629,6 +642,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService }, max = 30 seconds, interval = 1 second) // this will fail the htlc val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds) + assert(failed.id == paymentId) assert(failed.paymentHash === paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) @@ -669,6 +683,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.nodeId, maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) + val paymentId = paymentSender.expectMsgType[UUID](30 seconds) + // F gets the htlc val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc] // now that we have the channel id, we retrieve channels default final addresses @@ -692,6 +708,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService }, max = 30 seconds, interval = 1 second) // this will fail the htlc val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds) + assert(failed.id == paymentId) assert(failed.paymentHash === paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) @@ -739,12 +756,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] val sendReq = SendPayment(300000000L, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) + val paymentId = sender.expectMsgType[UUID] // we forward the htlc to the payment handler forwardHandlerF.expectMsgType[UpdateAddHtlc] forwardHandlerF.forward(paymentHandlerF) sigListener.expectMsgType[ChannelSignatureReceived] sigListener.expectMsgType[ChannelSignatureReceived] - sender.expectMsgType[PaymentSucceeded] + sender.expectMsgType[PaymentSucceeded].id === paymentId // we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs def send(amountMsat: Long, paymentHandler: ActorRef, paymentInitiator: ActorRef) = { @@ -752,7 +770,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId, routeParams = integrationTestRouteParams) sender.send(paymentInitiator, sendReq) - sender.expectNoMsg() + sender.expectMsgType[UUID] } val buffer = TestProbe() From b43ec231e03e43dc505f609c582616ecd8ec8aeb Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 3 Apr 2019 14:08:24 +0200 Subject: [PATCH 19/83] Revert "Remove 'fallbackAddress' from /receive API" This reverts commit 5b423b1571ddc8aa664b4307e3e13239f52f9914. --- eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala | 7 ++++--- .../src/main/scala/fr/acinq/eclair/api/Service.scala | 4 ++-- .../test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 723016c7cd..b51501197e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -46,7 +46,7 @@ trait Eclair { def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] - def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] + def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] @@ -127,8 +127,9 @@ class EclairImpl(appKit: Kit) extends Eclair { case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } - override def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = { - (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire)).mapTo[PaymentRequest].map { pr => + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] = { + fallbackAddress.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception + (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire, fallbackAddress = fallbackAddress)).mapTo[PaymentRequest].map { pr => PaymentRequest.write(pr) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index c34bcc0961..86de7366b0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -203,8 +203,8 @@ trait Service extends Directives with Logging { } } ~ path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) => - complete(eclairApi.receive(desc, amountMsat, expire)) + formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) => + complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress)) } } ~ path("parseinvoice") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 0aefb24974..b0f4931977 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -70,7 +70,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def allupdates(nodeId: Option[Crypto.PublicKey]): Future[Iterable[ChannelUpdate]] = ??? - override def receive(description: String, amountMsat: Option[Long], expire: Option[Long]): Future[String] = ??? + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] = ??? override def findRoute(targetNodeId: Crypto.PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]]): Future[RouteResponse] = ??? From 2bce7a6a665a36977661fe9650394be9c95bb37d Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 3 Apr 2019 16:02:22 +0200 Subject: [PATCH 20/83] Use setVersion for db version migration, add test. --- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 4 +- .../acinq/eclair/db/sqlite/SqliteUtils.scala | 18 +++++++- .../eclair/db/SqlitePaymentsDbSpec.scala | 42 ++++++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 7300ad8234..83365f5953 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -20,7 +20,7 @@ import java.sql.Connection import java.util.UUID import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} +import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb, ReceivedPayment} import grizzled.slf4j.Logging @@ -38,9 +38,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { using(sqlite.createStatement()) { statement => getVersion(statement, DB_NAME, CURRENT_VERSION) match { case PREVIOUS_VERSION => + logger.warn(s"Performing db migration for paymentsDB, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 9bf6f36eca..fce229bdcd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -44,7 +44,8 @@ object SqliteUtils { /** * Several logical databases (channels, network, peers) may be stored in the same physical sqlite database. - * We keep track of their respective version using a dedicated table. + * We keep track of their respective version using a dedicated table. The version entry will be created if + * there is none but will never be updated here (use setVersion to do that). * * @param statement * @param db_name @@ -54,12 +55,25 @@ object SqliteUtils { def getVersion(statement: Statement, db_name: String, currentVersion: Int): Int = { statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") // if there was no version for the current db, then insert the current version - statement.executeUpdate(s"INSERT OR REPLACE INTO versions VALUES ('$db_name', $currentVersion)") + statement.executeUpdate(s"INSERT OR IGNORE INTO versions VALUES ('$db_name', $currentVersion)") // if there was a previous version installed, this will return a different value from current version val res = statement.executeQuery(s"SELECT version FROM versions WHERE db_name='$db_name'") res.getInt("version") } + /** + * Updates the version for a particular logical database, it will overwrite the previous version. + * @param statement + * @param db_name + * @param newVersion + * @return + */ + def setVersion(statement: Statement, db_name: String, newVersion: Int) = { + statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") + // overwrite the existing version + statement.executeUpdate(s"UPDATE versions SET version=$newVersion WHERE db_name='$db_name'") + } + /** * This helper assumes that there is a "data" column available, decodable with the provided codec * diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index fe9720bf03..f81465b10c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.db import java.sql.DriverManager import java.util.UUID - +import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import org.scalatest.FunSuite @@ -26,7 +26,7 @@ import scodec.bits._ class SqlitePaymentsDbSpec extends FunSuite { - def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") + def inmem() = DriverManager.getConnection("jdbc:sqlite::memory:") test("init sqlite 2 times in a row") { val sqlite = inmem @@ -34,6 +34,44 @@ class SqlitePaymentsDbSpec extends FunSuite { val db2 = new SqlitePaymentsDb(sqlite) } + test("handle version migration 1->2") { + + val connection = inmem() + + using(connection.createStatement()) { statement => + getVersion(statement, "payments", 1) + statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + } + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "payments", 1) == 1) // version 1 is deployed now + } + + val preMigrationDb = new SqlitePaymentsDb(connection) + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "payments", 1) == 2) // version has changed from 1 to 2! + } + + // add a few rows + val pr1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) + val ps1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928275L, OutgoingPaymentStatus.PENDING) + preMigrationDb.addReceivedPayment(pr1) + preMigrationDb.addSentPayments(ps1) + + assert(preMigrationDb.listReceived() == Seq(pr1)) + assert(preMigrationDb.listSent() == Seq(ps1)) + + val postMigrationDb = new SqlitePaymentsDb(connection) + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "payments", 2) == 2) // version still to 2 + } + + assert(postMigrationDb.listReceived() == Seq(pr1)) + assert(postMigrationDb.listSent() == Seq(ps1)) + } + test("add/list received payments/find 1 payment that exists/find 1 payment that does not exist") { val sqlite = inmem val db = new SqlitePaymentsDb(sqlite) From f722a34f0921a9d3541b34762eef667e0be5625a Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 4 Apr 2019 11:54:18 +0200 Subject: [PATCH 21/83] optimized imports, added license headers --- .../main/scala/fr/acinq/eclair/Eclair.scala | 16 ++++++++++++ .../eclair/api/FormParamExtractors.scala | 16 ++++++++++++ .../scala/fr/acinq/eclair/api/Service.scala | 25 ++++++++----------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index b51501197e..16e71d9485 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 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 import java.util.UUID diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala index bd9cd710ca..25767bc7e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/FormParamExtractors.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 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.api import java.util.UUID diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 86de7366b0..f786631421 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -18,32 +18,31 @@ package fr.acinq.eclair.api import java.util.UUID -import akka.http.scaladsl.server._ -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} -import fr.acinq.eclair.{Eclair, Kit, ShortChannelId} -import FormParamExtractors._ import akka.NotUsed -import akka.actor.{Actor, ActorRef, ActorSystem, Props} -import akka.http.scaladsl.marshalling.{Marshaller, ToResponseMarshaller} +import akka.actor.{Actor, ActorSystem, Props} +import akka.http.scaladsl.marshalling.ToResponseMarshaller import akka.http.scaladsl.model.HttpMethods.POST import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`} import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} -import akka.stream.{ActorMaterializer, OverflowStrategy} +import akka.http.scaladsl.server._ +import akka.http.scaladsl.server.directives.Credentials import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} +import akka.stream.{ActorMaterializer, OverflowStrategy} +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.api.FormParamExtractors._ import fr.acinq.eclair.api.JsonSupport.CustomTypeHints import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed import fr.acinq.eclair.payment._ +import fr.acinq.eclair.{Eclair, ShortChannelId} import grizzled.slf4j.Logging -import org.json4s.{ShortTypeHints, TypeHints} import org.json4s.jackson.Serialization import scodec.bits.ByteVector -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.{Failure, Success} @@ -52,9 +51,7 @@ case class ErrorResponse(error: String) trait Service extends Directives with Logging { // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 - import JsonSupport.marshaller - import JsonSupport.formats - import JsonSupport.serialization + import JsonSupport.{formats, marshaller, serialization} // used to send typed messages over the websocket val formatsWithTypeHint = formats.withTypeHintFieldName("type") + From 0746396fc87c922a9fbb01d346fa00a1a43736dd Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 14:00:38 +0200 Subject: [PATCH 22/83] Add test for paymentLifecycle and paymentDB --- .../eclair/payment/PaymentLifecycle.scala | 1 + .../eclair/payment/PaymentLifecycleSpec.scala | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index d5db2a01a9..baab55b773 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -63,6 +63,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index b446a60e37..5e2fc01cd2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -28,6 +28,7 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} @@ -48,10 +49,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val defaultAmountMsat = 142000000L val defaultPaymentHash = randomBytes32 - val paymentDb = TestConstants.inMemoryDb().payments test("payment failed (route not found)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val id = UUID.randomUUID() val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() @@ -63,12 +64,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) + awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (route too expensive)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val id = UUID.randomUUID() val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() @@ -80,12 +84,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, routeParams = Some(RouteParams(randomize = false, maxFeeBaseMsat = 100, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = 2016, ratios = None))) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures + awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (unparsable failure)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -99,6 +106,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -121,10 +130,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) + awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed } test("payment failed (local error)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -138,6 +149,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -150,10 +163,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -167,6 +182,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -179,10 +196,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -221,6 +240,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (Update)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -234,6 +254,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -250,6 +272,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -275,10 +298,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // this time the router can't find a route: game over sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) + awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (PermanentChannelFailure)") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -292,6 +317,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -310,10 +337,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router, which won't find another route sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment succeeded") { fixture => import fixture._ + val paymentDb = TestConstants.inMemoryDb().payments val id = UUID.randomUUID() val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) val monitor = TestProbe() @@ -328,18 +357,20 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) 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[_]]) - + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) + assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) } test("payment succeeded to a channel with fees=0") { fixture => import fixture._ import fr.acinq.eclair.randomKey + val paymentDb = TestConstants.inMemoryDb().payments // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) and b -> g has fees=0 // \ From b194b8e998680befefbbfc754415a23ef914069f Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 14:57:15 +0200 Subject: [PATCH 23/83] Extract future-option directive into its own file --- .../fr/acinq/eclair/api/ExtraDirectives.scala | 21 +++++++++++++++++++ .../scala/fr/acinq/eclair/api/Service.scala | 14 ++----------- 2 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala new file mode 100644 index 0000000000..8a4adb58a3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -0,0 +1,21 @@ +package fr.acinq.eclair.api + +import akka.http.scaladsl.marshalling.ToResponseMarshaller +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server.{Directives, Route} + +import scala.concurrent.Future +import scala.util.{Failure, Success} + +trait ExtraDirectives extends Directives { + + // custom directive to fail with HTTP 404 if the element was not found + def completeWithFutureOption[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { + case Success(Some(t)) => complete(t) + case Success(None) => complete(StatusCodes.NotFound) + case Failure(thr) => reject + } + + + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index f786631421..15ef4747ef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -20,7 +20,6 @@ import java.util.UUID import akka.NotUsed import akka.actor.{Actor, ActorSystem, Props} -import akka.http.scaladsl.marshalling.ToResponseMarshaller import akka.http.scaladsl.model.HttpMethods.POST import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} @@ -41,14 +40,12 @@ import fr.acinq.eclair.{Eclair, ShortChannelId} import grizzled.slf4j.Logging import org.json4s.jackson.Serialization import scodec.bits.ByteVector - import scala.concurrent.Future import scala.concurrent.duration._ -import scala.util.{Failure, Success} case class ErrorResponse(error: String) -trait Service extends Directives with Logging { +trait Service extends ExtraDirectives with Logging { // important! Must NOT import the unmarshaller as it is too generic...see https://github.com/akka/akka-http/issues/541 import JsonSupport.{formats, marshaller, serialization} @@ -70,13 +67,6 @@ trait Service extends Directives with Logging { implicit val actorSystem: ActorSystem implicit val mat: ActorMaterializer - // custom directive to fail with HTTP 404 if the element was not found - def complete[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { - case Success(Some(t)) => complete(t) - case Success(None) => complete(StatusCodes.NotFound) - case Failure(thr) => throw thr - } - // a named and typed URL parameter used across several routes, 32-bytes hex-encoded val channelId = "channelId".as[ByteVector32](sha256HashUnmarshaller) val nodeId = "nodeId".as[PublicKey] @@ -236,7 +226,7 @@ trait Service extends Directives with Logging { } ~ path("paymentinfo") { formFields("id".as[UUID]) { id => - complete(eclairApi.paymentInfo(id)) + completeWithFutureOption(eclairApi.paymentInfo(id)) } } ~ path("checkpayment") { From 8aea43f41b4525eb59eda673976c0e60e43a3074 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 16:29:42 +0200 Subject: [PATCH 24/83] Remove default UUID for CMD_ADD_HTLC, renaming --- .../fr/acinq/eclair/api/ExtraDirectives.scala | 2 - .../fr/acinq/eclair/channel/Channel.scala | 2 +- .../acinq/eclair/channel/ChannelTypes.scala | 2 +- .../eclair/payment/PaymentLifecycle.scala | 2 +- .../fr/acinq/eclair/payment/Relayer.scala | 6 +- .../acinq/eclair/channel/ThroughputSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 89 ++++++++++--------- .../channel/states/e/OfflineStateSpec.scala | 6 +- .../channel/states/f/ShutdownStateSpec.scala | 4 +- .../states/g/NegotiatingStateSpec.scala | 6 +- .../channel/states/h/ClosingStateSpec.scala | 6 +- .../rustytests/SynchronizationPipe.scala | 3 +- .../eclair/payment/ChannelSelectionSpec.scala | 6 +- .../fr/acinq/eclair/payment/RelayerSpec.scala | 6 +- 14 files changed, 75 insertions(+), 67 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala index 8a4adb58a3..92e4b239c0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -16,6 +16,4 @@ trait ExtraDirectives extends Directives { case Failure(thr) => reject } - - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index e899ce6fba..b090ebb169 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -2052,7 +2052,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } - def origin(c: CMD_ADD_HTLC): Origin = c.upstream_opt match { + def origin(c: CMD_ADD_HTLC): Origin = c.upstream match { case Left(id) => Local(id, Some(sender)) // we were the origin of the payment case Right(u) => Relayed(u.channelId, u.id, u.amountMsat, c.amountMsat) // this is a relayed payment } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index df35c9829c..1f687d198f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -108,7 +108,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.LAST_PACKET.serialize, upstream_opt: Either[UUID, UpdateAddHtlc] = Left(UUID.randomUUID()), commit: Boolean = false, redirected: Boolean = false) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.LAST_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, redirected: Boolean = false) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index baab55b773..70cd840b82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -260,7 +260,7 @@ object PaymentLifecycle { val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream_opt = Left(id), commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream = Left(id), commit = true) -> onion.sharedSecrets } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index dd079a0998..ab79a42060 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -127,10 +127,10 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Status.Failure(AddHtlcFailed(_, paymentHash, error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt, originalCommand_opt)) => originalCommand_opt match { - case Some(cmd) if cmd.redirected && cmd.upstream_opt.isRight => // cmd.upstream_opt.isDefined always true since origin = relayed + case Some(cmd) if cmd.redirected && cmd.upstream.isRight => // cmd.upstream_opt.isDefined always true since origin = relayed // if it was redirected, we give it one more try with the original requested channel (meaning that the error returned will always be for the requested channel) log.info(s"retrying htlc #$originHtlcId paymentHash=$paymentHash from channelId=$originChannelId") - self ! ForwardAdd(cmd.upstream_opt.right.get, canRedirect = false) + self ! ForwardAdd(cmd.upstream.right.get, canRedirect = false) case _ => // otherwise we just return a failure val failure = (error, channelUpdate_opt) match { @@ -271,7 +271,7 @@ object Relayer { Left(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => val isRedirected = (channelUpdate.shortChannelId != payload.shortChannelId) // we may decide to use another channel (to the same node) from the one requested - Right(CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Right(add), commit = true, redirected = isRedirected)) + Right(CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream = Right(add), commit = true, redirected = isRedirected)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 817b4a8377..d18ef5df83 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -51,7 +51,7 @@ class ThroughputSpec extends FunSuite { case ('add, tgt: ActorRef) => val r = randomBytes32 val h = Crypto.sha256(r) - tgt ! CMD_ADD_HTLC(1, h, 1) + tgt ! CMD_ADD_HTLC(1, h, 1, upstream = Left(UUID.randomUUID())) context.become(run(h2r + (h -> r))) case ('sig, tgt: ActorRef) => tgt ! CMD_SIGN diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index ac16d12e22..1a7c2b02c7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states.e +import java.util.UUID + import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe @@ -37,6 +39,7 @@ import fr.acinq.eclair.wire.{AnnouncementSignatures, ChannelUpdate, ClosingSigne import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32} import org.scalatest.{Outcome, Tag} import scodec.bits._ + import scala.concurrent.duration._ /** @@ -63,7 +66,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val add = CMD_ADD_HTLC(50000000, h, 400144) + val add = CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -72,7 +75,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { commitments = initialState.commitments.copy( localNextHtlcId = 1, localChanges = initialState.commitments.localChanges.copy(proposed = htlc :: Nil), - originChannels = Map(0L -> Local(add.upstream_opt.left.get, Some(sender.ref))) + originChannels = Map(0L -> Local(add.upstream.left.get, Some(sender.ref))) ))) } @@ -81,7 +84,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 for (i <- 0 until 10) { - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == i && htlc.paymentHash == h) @@ -94,7 +97,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = ByteVector.fill(1254)(0)) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream_opt = Right(originHtlc)) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -113,10 +116,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooSmall = currentBlockCount + 3 - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooSmall) + val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooSmall, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -126,10 +129,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooBig = currentBlockCount + Channel.MAX_CLTV_EXPIRY + 1 - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooBig) + val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooBig, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -137,10 +140,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(50, randomBytes32, 400144) + val add = CMD_ADD_HTLC(50, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = HtlcValueTooSmall(channelId(alice), 1000, 50) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -148,10 +151,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144) + val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -159,19 +162,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -179,16 +182,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -196,10 +199,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(bob, add) val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) - sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) bob2alice.expectNoMsg(200 millis) } @@ -209,14 +212,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -224,7 +227,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144) + val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -232,10 +235,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] // this is over channel-capacity - val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144) + val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add2) val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) alice2bob.expectNoMsg(200 millis) } @@ -249,10 +252,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && !alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isDefined) // actual test starts here - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144) + val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) alice2bob.expectNoMsg(200 millis) } @@ -261,14 +264,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144) + val add1 = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") // at the same time bob initiates a closing sender.send(bob, CMD_CLOSE(None)) sender.expectMsg("ok") // this command will be received by alice right after having received the shutdown - val add2 = CMD_ADD_HTLC(100000000, randomBytes32, cltvExpiry = 300000) + val add2 = CMD_ADD_HTLC(100000000, randomBytes32, cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -276,7 +279,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { bob2alice.forward(alice) sender.send(alice, add2) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream_opt.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) } test("recv UpdateAddHtlc") { f => @@ -415,7 +418,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (two identical htlcs in each direction)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -462,19 +465,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(a2b_2 > aliceMinOffer && a2b_2 > bobMinReceive) assert(b2a_1 > aliceMinReceive && b2a_1 > bobMinOffer) assert(b2a_2 < aliceMinReceive && b2a_2 > bobMinOffer) - sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144)) + sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) - sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144)) + sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) @@ -494,7 +497,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose val htlcCount = epsilons.size for (i <- epsilons) { @@ -691,12 +694,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val r = randomBytes32 val h = Crypto.sha256(r) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144)) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -1864,7 +1867,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // alice = 800 000 // bob = 200 000 - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index e1f7842d41..c2b29ac950 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states.e +import java.util.UUID + import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.Scalar import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} @@ -58,7 +60,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144)) + sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob) @@ -135,7 +137,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144)) + sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob, ab_add_0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 38806a7766..b662147f40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -103,10 +103,10 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000) + val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index e6aa946ad9..0f3fbf8b47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states.g +import java.util.UUID + import akka.actor.Status.Failure import akka.event.LoggingAdapter import akka.testkit.TestProbe @@ -70,10 +72,10 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods import f._ alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000) + val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 147338d961..ea278dccf3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.channel.states.h +import java.util.UUID + import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} @@ -113,10 +115,10 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000) + val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) - sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream_opt.left.get, Some(sender.ref)), None, Some(add)))) + sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) alice2bob.expectNoMsg(200 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index b33b7d6e02..04020e13b8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.interop.rustytests import java.io.{BufferedWriter, File, FileWriter} +import java.util.UUID import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} @@ -56,7 +57,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging script match { case offer(x, amount, rhash) :: rest => - resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144) + resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, upstream = Left(UUID.randomUUID())) exec(rest, a, b) case fulfill(x, id, r) :: rest => resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 881ef70484..b1b37c913a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -49,9 +49,9 @@ class ChannelSelectionSpec extends FunSuite { implicit val log = akka.event.NoLogging // nominal case - assert(Relayer.handleRelay(relayPayload, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Right(relayPayload.add), commit = true, redirected = false))) + assert(Relayer.handleRelay(relayPayload, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true, redirected = false))) // redirected to preferred channel - assert(Relayer.handleRelay(relayPayload, Some(channelUpdate.copy(shortChannelId = ShortChannelId(1111)))) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream_opt = Right(relayPayload.add), commit = true, redirected = true))) + assert(Relayer.handleRelay(relayPayload, Some(channelUpdate.copy(shortChannelId = ShortChannelId(1111)))) === Right(CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true, redirected = true))) // no channel_update assert(Relayer.handleRelay(relayPayload, channelUpdate_opt = None) === Left(CMD_FAIL_HTLC(relayPayload.add.id, Right(UnknownNextPeer), commit = true))) // channel disabled @@ -68,7 +68,7 @@ class ChannelSelectionSpec extends FunSuite { assert(Relayer.handleRelay(relayPayload_insufficientfee, Some(channelUpdate)) === Left(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) - assert(Relayer.handleRelay(relayPayload_highfee, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream_opt = Right(relayPayload.add), commit = true, redirected = false))) + assert(Relayer.handleRelay(relayPayload_highfee, Some(channelUpdate)) === Right(CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true, redirected = false))) } test("relay channel selection") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index a2118598ec..07aba1d414 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -78,7 +78,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd.message.upstream_opt === Right(add_ab)) + assert(fwd.message.upstream === Right(add_ab)) sender.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -118,7 +118,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd1 = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd1.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd1.message.upstream_opt === Right(add_ab)) + assert(fwd1.message.upstream === Right(add_ab)) sender.send(relayer, Status.Failure(Register.ForwardShortIdFailure(fwd1))) @@ -144,7 +144,7 @@ class RelayerSpec extends TestkitBaseClass { val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]] assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId) - assert(fwd.message.upstream_opt === Right(add_ab)) + assert(fwd.message.upstream === Right(add_ab)) sender.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) From 8856010123b63446435e31e1f7ce7e956a75f185 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 16:44:55 +0200 Subject: [PATCH 25/83] Move OutgoingPaymentStatus in companion object --- .../main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 13 +++++++++---- .../acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 3 ++- .../fr/acinq/eclair/payment/PaymentLifecycle.scala | 3 ++- .../fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 2 ++ .../acinq/eclair/payment/PaymentLifecycleSpec.scala | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index aa11d88910..d5b50633ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import java.util.UUID import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus /** * Store the Lightning payments received and sent by the node. Relayed payments are not persisted. @@ -78,8 +79,12 @@ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestam */ case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, lastUpdate: Long, status: OutgoingPaymentStatus.Value) -object OutgoingPaymentStatus extends Enumeration { - val PENDING = Value(1, "PENDING") - val SUCCEEDED = Value(2, "SUCCEEDED") - val FAILED = Value(3, "FAILED") +object OutgoingPayment { + + object OutgoingPaymentStatus extends Enumeration { + val PENDING = Value(1, "PENDING") + val SUCCEEDED = Value(2, "SUCCEEDED") + val FAILED = Value(3, "FAILED") + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 83365f5953..f29a2f632a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -20,8 +20,9 @@ import java.sql.Connection import java.util.UUID import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb, ReceivedPayment} +import fr.acinq.eclair.db.{OutgoingPayment, PaymentsDb, ReceivedPayment} import grizzled.slf4j.Logging import scala.collection.immutable.Queue diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 70cd840b82..11a4fef005 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -25,7 +25,8 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} -import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentsDb} +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.{OutgoingPayment, PaymentsDb} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index f81465b10c..abedc26131 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -18,8 +18,10 @@ package fr.acinq.eclair.db import java.sql.DriverManager import java.util.UUID + import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import org.scalatest.FunSuite import scodec.bits._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 5e2fc01cd2..f3eb68c8e8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.ErrorPacket -import fr.acinq.eclair.db.OutgoingPaymentStatus +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} From 5cf3da4b8ce3e543bb6882a36dfb656bdabc2741 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 16:59:57 +0200 Subject: [PATCH 26/83] Add createdAt timestamp to OutgoingPayment --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 ++-- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 20 +++++++++++-------- .../eclair/payment/PaymentLifecycle.scala | 3 ++- .../eclair/db/SqlitePaymentsDbSpec.scala | 12 +++++------ 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index d5b50633ea..2e5e83fc1f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -75,9 +75,9 @@ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestam * @param id internal payment identifier * @param payment_hash payment_hash * @param amount_msat amount of the payment, in milli-satoshis - * @param lastUpdate absolute time in seconds since UNIX epoch when the payment was last updated. + * @param updatedAt absolute time in seconds since UNIX epoch when the payment was last updated. */ -case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, lastUpdate: Long, status: OutgoingPaymentStatus.Value) +case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, updatedAt: Long, status: OutgoingPaymentStatus.Value) object OutgoingPayment { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index f29a2f632a..dde1f13219 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -42,11 +42,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { logger.warn(s"Performing db migration for paymentsDB, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of paymentsDB found, version=$unknownVersion") } @@ -72,12 +72,13 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def addSentPayments(sent: OutgoingPayment): Unit = { - using(sqlite.prepareStatement("INSERT OR REPLACE INTO sent_payments VALUES (?, ?, ?, ?, ?)")) { statement => + using(sqlite.prepareStatement("INSERT INTO sent_payments VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) statement.setLong(3, sent.amountMsat) - statement.setLong(4, sent.lastUpdate) - statement.setString(5, sent.status.toString) + statement.setLong(4, sent.createdAt) + statement.setLong(5, sent.updatedAt) + statement.setString(6, sent.status.toString) val res = statement.executeUpdate() logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") } @@ -96,7 +97,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def sentPaymentById(id: UUID): Option[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, updated_at, status FROM sent_payments WHERE id = ?")) { statement => + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() if (rs.next()) { @@ -104,6 +105,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), + rs.getLong("created_at"), rs.getLong("updated_at"), OutgoingPaymentStatus.withName(rs.getString("status")))) } else { @@ -113,7 +115,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def sentPaymentByHash(paymentHash: ByteVector32): Option[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, updated_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { @@ -121,6 +123,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), + rs.getLong("created_at"), rs.getLong("updated_at"), OutgoingPaymentStatus.withName(rs.getString("status")))) } else { None @@ -141,13 +144,14 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def listSent(): Seq[OutgoingPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, updated_at, status FROM sent_payments") + val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments") var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { q = q :+ OutgoingPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), + rs.getLong("created_at"), rs.getLong("updated_at"), OutgoingPaymentStatus.withName(rs.getString("status"))) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 11a4fef005..234c53262a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -47,7 +47,8 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi when(WAITING_FOR_REQUEST) { case Event(c: SendPayment, WaitingForRequest) => router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) - paymentsDb.addSentPayments(OutgoingPayment(id, c.paymentHash, c.amountMsat, Platform.currentTime, OutgoingPaymentStatus.PENDING)) + val currentTime = Platform.currentTime + paymentsDb.addSentPayments(OutgoingPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, OutgoingPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index abedc26131..50d4e71d96 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -57,7 +57,7 @@ class SqlitePaymentsDbSpec extends FunSuite { // add a few rows val pr1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) - val ps1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928275L, OutgoingPaymentStatus.PENDING) + val ps1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, OutgoingPaymentStatus.PENDING) preMigrationDb.addReceivedPayment(pr1) preMigrationDb.addSentPayments(ps1) @@ -92,8 +92,8 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(inmem) - val s1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928275L, OutgoingPaymentStatus.PENDING) - val s2 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928275L, OutgoingPaymentStatus.PENDING) + val s1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928273L, 1513871928275L, OutgoingPaymentStatus.PENDING) + val s2 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928272L, 1513871928275L, OutgoingPaymentStatus.PENDING) assert(db.listSent().isEmpty) db.addSentPayments(s1) @@ -105,12 +105,12 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.sentPaymentByHash(s2.paymentHash) === Some(s2)) assert(db.sentPaymentByHash(ByteVector32.Zeroes) === None) - val s3 = s2.copy(amountMsat = 88776655, status = OutgoingPaymentStatus.SUCCEEDED) + val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655, status = OutgoingPaymentStatus.SUCCEEDED) db.addSentPayments(s3) - assert(db.sentPaymentById(s2.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) + assert(db.sentPaymentById(s3.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.FAILED) - assert(db.sentPaymentById(s2.id).get.status == OutgoingPaymentStatus.FAILED) + assert(db.sentPaymentById(s3.id).get.status == OutgoingPaymentStatus.FAILED) } From 9b59b7e62a07be5500083864c0760028249d6d7d Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 17:07:44 +0200 Subject: [PATCH 27/83] Renaming in PaymentsDb --- .../main/scala/fr/acinq/eclair/Eclair.scala | 2 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 10 +++--- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 12 +++---- .../eclair/payment/LocalPaymentHandler.scala | 2 +- .../eclair/payment/PaymentLifecycle.scala | 12 +++---- .../eclair/db/SqlitePaymentsDbSpec.scala | 26 +++++++------- .../eclair/payment/PaymentLifecycleSpec.scala | 34 +++++++++---------- 7 files changed, 48 insertions(+), 50 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 16e71d9485..914b8e5848 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -163,7 +163,7 @@ class EclairImpl(appKit: Kit) extends Eclair { } override def paymentInfo(id: UUID): Future[Option[OutgoingPayment ]] = Future { - appKit.nodeParams.db.payments.sentPaymentById(id) + appKit.nodeParams.db.payments.getSent(id) } override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 2e5e83fc1f..69476d60af 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -43,15 +43,15 @@ trait PaymentsDb { def addReceivedPayment(payment: ReceivedPayment) - def addSentPayments(sent: OutgoingPayment) + def addSentPayment(sent: OutgoingPayment) - def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) + def updateSentStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) - def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] + def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] - def sentPaymentById(id: UUID): Option[OutgoingPayment] + def getSent(id: UUID): Option[OutgoingPayment] - def sentPaymentByHash(paymentHash: ByteVector32): Option[OutgoingPayment] + def getSent(paymentHash: ByteVector32): Option[OutgoingPayment] def listReceived(): Seq[ReceivedPayment] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index dde1f13219..9ef981d644 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -18,13 +18,11 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection import java.util.UUID - import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.db.{OutgoingPayment, PaymentsDb, ReceivedPayment} import grizzled.slf4j.Logging - import scala.collection.immutable.Queue import scala.compat.Platform @@ -62,7 +60,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { + override def updateSentStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { using(sqlite.prepareStatement(s"UPDATE sent_payments SET (status, updated_at) = (?, ?) WHERE id = ?")) { statement => statement.setString(1, newStatus.toString) statement.setLong(2, Platform.currentTime) @@ -71,7 +69,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def addSentPayments(sent: OutgoingPayment): Unit = { + override def addSentPayment(sent: OutgoingPayment): Unit = { using(sqlite.prepareStatement("INSERT INTO sent_payments VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) @@ -84,7 +82,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def receivedByPaymentHash(paymentHash: ByteVector32): Option[ReceivedPayment] = { + override def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] = { using(sqlite.prepareStatement("SELECT payment_hash, amount_msat, timestamp FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() @@ -96,7 +94,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def sentPaymentById(id: UUID): Option[OutgoingPayment] = { + override def getSent(id: UUID): Option[OutgoingPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() @@ -114,7 +112,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def sentPaymentByHash(paymentHash: ByteVector32): Option[OutgoingPayment] = { + override def getSent(paymentHash: ByteVector32): Option[OutgoingPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index e10f839de9..2a5e931366 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -65,7 +65,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin } recover { case t => sender ! Status.Failure(t) } case CheckPayment(paymentHash) => - nodeParams.db.payments.receivedByPaymentHash(paymentHash) match { + nodeParams.db.payments.getReceived(paymentHash) match { case Some(_) => sender ! true case _ => sender ! false } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 234c53262a..731b7e523b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -48,7 +48,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(c: SendPayment, WaitingForRequest) => router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) val currentTime = Platform.currentTime - paymentsDb.addSentPayments(OutgoingPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, OutgoingPaymentStatus.PENDING)) + paymentsDb.addSentPayment(OutgoingPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, OutgoingPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -65,7 +65,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) } @@ -73,7 +73,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.SUCCEEDED) + paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.SUCCEEDED) reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) @@ -84,7 +84,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -98,7 +98,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi } log.warning(s"too many failed attempts, failing the payment") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -163,7 +163,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 50d4e71d96..87d8249991 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -59,7 +59,7 @@ class SqlitePaymentsDbSpec extends FunSuite { val pr1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) val ps1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, OutgoingPaymentStatus.PENDING) preMigrationDb.addReceivedPayment(pr1) - preMigrationDb.addSentPayments(ps1) + preMigrationDb.addSentPayment(ps1) assert(preMigrationDb.listReceived() == Seq(pr1)) assert(preMigrationDb.listSent() == Seq(ps1)) @@ -84,8 +84,8 @@ class SqlitePaymentsDbSpec extends FunSuite { db.addReceivedPayment(p1) db.addReceivedPayment(p2) assert(db.listReceived().toList === List(p1, p2)) - assert(db.receivedByPaymentHash(p1.paymentHash) === Some(p1)) - assert(db.receivedByPaymentHash(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) + assert(db.getReceived(p1.paymentHash) === Some(p1)) + assert(db.getReceived(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) } test("add/retrieve/update sent payments") { @@ -96,21 +96,21 @@ class SqlitePaymentsDbSpec extends FunSuite { val s2 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928272L, 1513871928275L, OutgoingPaymentStatus.PENDING) assert(db.listSent().isEmpty) - db.addSentPayments(s1) - db.addSentPayments(s2) + db.addSentPayment(s1) + db.addSentPayment(s2) assert(db.listSent().toList == Seq(s1, s2)) - assert(db.sentPaymentById(s1.id) === Some(s1)) - assert(db.sentPaymentById(UUID.randomUUID()) === None) - assert(db.sentPaymentByHash(s2.paymentHash) === Some(s2)) - assert(db.sentPaymentByHash(ByteVector32.Zeroes) === None) + assert(db.getSent(s1.id) === Some(s1)) + assert(db.getSent(UUID.randomUUID()) === None) + assert(db.getSent(s2.paymentHash) === Some(s2)) + assert(db.getSent(ByteVector32.Zeroes) === None) val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655, status = OutgoingPaymentStatus.SUCCEEDED) - db.addSentPayments(s3) - assert(db.sentPaymentById(s3.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) + db.addSentPayment(s3) + assert(db.getSent(s3.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) - db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.FAILED) - assert(db.sentPaymentById(s3.id).get.status == OutgoingPaymentStatus.FAILED) + db.updateSentStatus(s3.id, OutgoingPaymentStatus.FAILED) + assert(db.getSent(s3.id).get.status == OutgoingPaymentStatus.FAILED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index f3eb68c8e8..19b345c784 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -64,10 +64,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (route too expensive)") { fixture => @@ -84,10 +84,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, routeParams = Some(RouteParams(randomize = false, maxFeeBaseMsat = 100, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = 2016, ratios = None))) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures - awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (unparsable failure)") { fixture => @@ -106,7 +106,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -130,7 +130,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) - awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed } test("payment failed (local error)") { fixture => @@ -149,7 +149,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -163,7 +163,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => @@ -182,7 +182,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -196,7 +196,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -254,7 +254,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -272,7 +272,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -298,7 +298,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // this time the router can't find a route: game over sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (PermanentChannelFailure)") { fixture => @@ -317,7 +317,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -337,7 +337,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router, which won't find another route sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment succeeded") { fixture => @@ -357,14 +357,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) 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[_]]) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) - assert(paymentDb.sentPaymentById(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) + assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) } test("payment succeeded to a channel with fees=0") { fixture => From bd0621e195daca6cf2e9ae103a094f8e670c5f8f Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 17:22:05 +0200 Subject: [PATCH 28/83] Fix compilation errors --- .../src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index dd1db22172..21ea116461 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.api import java.net.InetSocketAddress import java.util.UUID + import com.google.common.net.HostAndPort import de.heikoseeberger.akkahttpjson4s.Json4sSupport import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty @@ -25,7 +26,7 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, OutPoint, Transaction} import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.db.OutgoingPaymentStatus +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.router.RouteResponse import fr.acinq.eclair.transactions.Direction From 489e75a3b514b03f6e887a3752a68ddcc4b4f2a6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Apr 2019 17:35:47 +0200 Subject: [PATCH 29/83] Fix test in PaymentLifeCycleSpec --- .../fr/acinq/eclair/api/JsonSerializers.scala | 1 - .../eclair/payment/PaymentLifecycleSpec.scala | 24 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 21ea116461..db7afe1abc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.api import java.net.InetSocketAddress import java.util.UUID - import com.google.common.net.HostAndPort import de.heikoseeberger.akkahttpjson4s.Json4sSupport import de.heikoseeberger.akkahttpjson4s.Json4sSupport.ShouldWritePretty diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 19b345c784..ea9ead9d91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -36,6 +36,7 @@ import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomBytes32} +import scala.concurrent.duration._ /** * Created by PM on 29/08/2016. @@ -64,7 +65,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) @@ -84,7 +85,6 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, routeParams = Some(RouteParams(randomize = false, maxFeeBaseMsat = 100, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = 2016, ratios = None))) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) @@ -106,7 +106,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -149,7 +149,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -163,7 +163,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => @@ -182,7 +182,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -196,7 +196,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -254,7 +254,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -272,7 +272,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -317,7 +317,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -357,14 +357,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) 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[_]]) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) - assert(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) + awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) } test("payment succeeded to a channel with fees=0") { fixture => From f2d41ec8bfb63c1fa4582dbb3698b6f2080681da Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 10:08:52 +0200 Subject: [PATCH 30/83] Expose /paymentinfo by paymentHash --- eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala | 9 ++++++--- .../src/main/scala/fr/acinq/eclair/api/Service.scala | 9 ++++++--- .../test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 914b8e5848..26ee462774 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -68,7 +68,7 @@ trait Eclair { def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] - def paymentInfo(id: UUID): Future[Option[OutgoingPayment]] + def paymentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] def checkpayment(paymentHash: ByteVector32): Future[Boolean] @@ -162,8 +162,11 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } - override def paymentInfo(id: UUID): Future[Option[OutgoingPayment ]] = Future { - appKit.nodeParams.db.payments.getSent(id) + override def paymentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment ]] = Future { + id match { + case Left(uuid) => appKit.nodeParams.db.payments.getSent(uuid) + case Right(paymentHash) => appKit.nodeParams.db.payments.getSent(paymentHash) + } } override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 15ef4747ef..3312018272 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -71,6 +71,7 @@ trait Service extends ExtraDirectives with Logging { val channelId = "channelId".as[ByteVector32](sha256HashUnmarshaller) val nodeId = "nodeId".as[PublicKey] val shortChannelId = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) + val paymentHash = "paymentHash".as[ByteVector32](sha256HashUnmarshaller) val apiExceptionHandler = ExceptionHandler { case t: Throwable => @@ -220,17 +221,19 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("sendtonode") { - formFields("amountMsat".as[Long], "paymentHash".as[ByteVector32](sha256HashUnmarshaller), "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + formFields("amountMsat".as[Long], paymentHash, "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ path("paymentinfo") { formFields("id".as[UUID]) { id => - completeWithFutureOption(eclairApi.paymentInfo(id)) + completeWithFutureOption(eclairApi.paymentInfo(Left(id))) + } ~ formFields(paymentHash) { paymentHash => + completeWithFutureOption(eclairApi.paymentInfo(Right(paymentHash))) } } ~ path("checkpayment") { - formFields("paymentHash".as[ByteVector32](sha256HashUnmarshaller)) { paymentHash => + formFields(paymentHash) { paymentHash => complete(eclairApi.checkpayment(paymentHash)) } ~ formFields("invoice".as[PaymentRequest]) { invoice => complete(eclairApi.checkpayment(invoice.paymentHash)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index b0f4931977..e2ed4a91f3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -86,7 +86,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def getInfoResponse(): Future[GetInfoResponse] = ??? - override def paymentInfo(id: UUID): Future[Option[OutgoingPayment]] = ??? + override def paymentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = ??? } implicit val formats = JsonSupport.formats From 8e0255e0890d4e685bad384bcc6b1b1ed4cfd08e Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 11:04:32 +0200 Subject: [PATCH 31/83] Refactor inMemory Sqlite connection in database tests --- .../eclair/db/sqlite/SqliteAuditDb.scala | 75 +++++++++++++------ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 4 +- .../db/sqlite/SqliteWalletDbSpec.scala | 9 +-- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 13 +--- .../eclair/db/SqliteChannelsDbSpec.scala | 9 +-- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 16 ++-- .../eclair/db/SqlitePaymentsDbSpec.scala | 13 ++-- .../eclair/db/SqlitePendingRelayDbSpec.scala | 10 +-- 8 files changed, 77 insertions(+), 72 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index a26e166c20..ea08aa3cf7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -25,33 +25,59 @@ import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent, NetworkFee, Stats} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.wire.ChannelCodecs +import grizzled.slf4j.Logging import scala.collection.immutable.Queue import scala.compat.Platform -class SqliteAuditDb(sqlite: Connection) extends AuditDb { +class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { import SqliteUtils._ import ExtendedResultSet._ val DB_NAME = "audit" - val CURRENT_VERSION = 1 + val CURRENT_VERSION = 2 using(sqlite.createStatement()) { statement => - require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed - statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)") - - statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)") + getVersion(statement, DB_NAME, CURRENT_VERSION) match { + case 1 => // previous version let's migrate + logger.warn(s"Performing db migration for DB $DB_NAME, found version=1 current=$CURRENT_VERSION") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)") + + // add id + statement.executeUpdate("ALTER TABLE sent ADD id BLOB NOT NULL") + + statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)") + + // update version + setVersion(statement, DB_NAME, CURRENT_VERSION) + case CURRENT_VERSION => + statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (id BLOB NOT NULL, amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)") + + statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)") + + case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") + } } override def add(e: AvailableBalanceChanged): Unit = @@ -78,13 +104,14 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb { } override def add(e: PaymentSent): Unit = - using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setLong(1, e.amount.toLong) - statement.setLong(2, e.feesPaid.toLong) - statement.setBytes(3, e.paymentHash.toArray) - statement.setBytes(4, e.paymentPreimage.toArray) - statement.setBytes(5, e.toChannelId.toArray) - statement.setLong(6, e.timestamp) + using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setBytes(1, e.id.toString.getBytes) + statement.setLong(2, e.amount.toLong) + statement.setLong(3, e.feesPaid.toLong) + statement.setBytes(4, e.paymentHash.toArray) + statement.setBytes(5, e.paymentPreimage.toArray) + statement.setBytes(6, e.toChannelId.toArray) + statement.setLong(7, e.timestamp) statement.executeUpdate() } @@ -127,7 +154,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb { var q: Queue[PaymentSent] = Queue() while (rs.next()) { q = q :+ PaymentSent( - id = ChannelCodecs.UNKNOWN_UUID, // TODO: temporary! fix that + id = UUID.fromString(rs.getString("id")), amount = MilliSatoshi(rs.getLong("amount_msat")), feesPaid = MilliSatoshi(rs.getLong("fees_msat")), paymentHash = rs.getByteVector32("payment_hash"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 9ef981d644..8f6fbd2821 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -37,7 +37,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { using(sqlite.createStatement()) { statement => getVersion(statement, DB_NAME, CURRENT_VERSION) match { case PREVIOUS_VERSION => - logger.warn(s"Performing db migration for paymentsDB, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") + logger.warn(s"Performing db migration for DB $DB_NAME, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") @@ -46,7 +46,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => - throw new RuntimeException(s"Unknown version of paymentsDB found, version=$unknownVersion") + throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala index f4c7f167b5..d0468e8632 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala @@ -16,9 +16,8 @@ package fr.acinq.eclair.blockchain.electrum.db.sqlite -import java.sql.DriverManager - import fr.acinq.bitcoin.{Block, BlockHeader, OutPoint, Satoshi, Transaction, TxIn, TxOut} +import fr.acinq.eclair.TestConstants import fr.acinq.eclair.blockchain.electrum.ElectrumClient import fr.acinq.eclair.blockchain.electrum.ElectrumClient.GetMerkleResponse import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData @@ -29,8 +28,6 @@ import scala.util.Random class SqliteWalletDbSpec extends FunSuite { val random = new Random() - def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") - def makeChildHeader(header: BlockHeader): BlockHeader = header.copy(hashPreviousBlock = header.hash, nonce = random.nextLong() & 0xffffffffL) def makeHeaders(n: Int, acc: Seq[BlockHeader] = Seq(Block.RegtestGenesisBlock.header)): Seq[BlockHeader] = { @@ -38,7 +35,7 @@ class SqliteWalletDbSpec extends FunSuite { } test("add/get/list headers") { - val db = new SqliteWalletDb(inmem) + val db = new SqliteWalletDb(TestConstants.sqliteInMemory()) val headers = makeHeaders(100) db.addHeaders(2016, headers) @@ -61,7 +58,7 @@ class SqliteWalletDbSpec extends FunSuite { } test("serialize persistent data") { - val db = new SqliteWalletDb(inmem) + val db = new SqliteWalletDb(TestConstants.sqliteInMemory()) import fr.acinq.eclair.{randomBytes, randomBytes32} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index 3512f4d32f..ef038e5fbf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -16,15 +16,12 @@ package fr.acinq.eclair.db -import java.sql.DriverManager -import java.util.UUID - import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.wire.ChannelCodecs -import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey} +import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite import scala.compat.Platform @@ -32,16 +29,14 @@ import scala.compat.Platform class SqliteAuditDbSpec extends FunSuite { - def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") - test("init sqlite 2 times in a row") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db1 = new SqliteAuditDb(sqlite) val db2 = new SqliteAuditDb(sqlite) } test("add/list events") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteAuditDb(sqlite) val e1 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) @@ -71,7 +66,7 @@ class SqliteAuditDbSpec extends FunSuite { } test("stats") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteAuditDb(sqlite) val n1 = randomKey.publicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala index 9e218c70d7..76731dfcd0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala @@ -16,9 +16,8 @@ package fr.acinq.eclair.db -import java.sql.DriverManager - import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.TestConstants import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqlitePendingRelayDb} import org.scalatest.FunSuite import org.sqlite.SQLiteException @@ -27,16 +26,14 @@ import scodec.bits.ByteVector class SqliteChannelsDbSpec extends FunSuite { - def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") - test("init sqlite 2 times in a row") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db1 = new SqliteChannelsDb(sqlite) val db2 = new SqliteChannelsDb(sqlite) } test("add/remove/list channels") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteChannelsDb(sqlite) new SqlitePendingRelayDb(sqlite) // needed by db.removeChannel diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 7b4568532c..3282d22fe3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -16,31 +16,27 @@ package fr.acinq.eclair.db -import java.sql.DriverManager - import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.{Color, NodeAddress, Tor2} -import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey} +import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite import org.sqlite.SQLiteException class SqliteNetworkDbSpec extends FunSuite { - def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") - val shortChannelIds = (42 to (5000 + 42)).map(i => ShortChannelId(i)) test("init sqlite 2 times in a row") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db1 = new SqliteNetworkDb(sqlite) val db2 = new SqliteNetworkDb(sqlite) } test("add/remove/list nodes") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteNetworkDb(sqlite) val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) @@ -64,7 +60,7 @@ class SqliteNetworkDbSpec extends FunSuite { } test("add/remove/list channels and channel_updates") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteNetworkDb(sqlite) def sig = Crypto.encodeSignature(Crypto.sign(randomBytes32, randomKey)) :+ 1.toByte @@ -105,7 +101,7 @@ class SqliteNetworkDbSpec extends FunSuite { } test("remove many channels") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteNetworkDb(sqlite) val sig = Crypto.encodeSignature(Crypto.sign(randomBytes32, randomKey)) :+ 1.toByte val priv = randomKey @@ -128,7 +124,7 @@ class SqliteNetworkDbSpec extends FunSuite { } test("prune many channels") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqliteNetworkDb(sqlite) db.addToPruned(shortChannelIds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 87d8249991..1af3d374c1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -16,11 +16,10 @@ package fr.acinq.eclair.db -import java.sql.DriverManager import java.util.UUID - import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.TestConstants import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import org.scalatest.FunSuite @@ -28,17 +27,15 @@ import scodec.bits._ class SqlitePaymentsDbSpec extends FunSuite { - def inmem() = DriverManager.getConnection("jdbc:sqlite::memory:") - test("init sqlite 2 times in a row") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db1 = new SqlitePaymentsDb(sqlite) val db2 = new SqlitePaymentsDb(sqlite) } test("handle version migration 1->2") { - val connection = inmem() + val connection = TestConstants.sqliteInMemory() using(connection.createStatement()) { statement => getVersion(statement, "payments", 1) @@ -75,7 +72,7 @@ class SqlitePaymentsDbSpec extends FunSuite { } test("add/list received payments/find 1 payment that exists/find 1 payment that does not exist") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqlitePaymentsDb(sqlite) val p1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) @@ -90,7 +87,7 @@ class SqlitePaymentsDbSpec extends FunSuite { test("add/retrieve/update sent payments") { - val db = new SqlitePaymentsDb(inmem) + val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) val s1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928273L, 1513871928275L, OutgoingPaymentStatus.PENDING) val s2 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928272L, 1513871928275L, OutgoingPaymentStatus.PENDING) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala index 6f9d99f14d..dd270b958b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePendingRelayDbSpec.scala @@ -16,27 +16,23 @@ package fr.acinq.eclair.db -import java.sql.DriverManager - import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.sqlite.SqlitePendingRelayDb -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.FailureMessageCodecs import org.scalatest.FunSuite class SqlitePendingRelayDbSpec extends FunSuite { - def inmem = DriverManager.getConnection("jdbc:sqlite::memory:") - test("init sqlite 2 times in a row") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db1 = new SqlitePendingRelayDb(sqlite) val db2 = new SqlitePendingRelayDb(sqlite) } test("add/remove/list messages") { - val sqlite = inmem + val sqlite = TestConstants.sqliteInMemory() val db = new SqlitePendingRelayDb(sqlite) val channelId1 = randomBytes32 From 9134a663b463cdc3f8c86089364fa99b5aaf25f0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 12:25:52 +0200 Subject: [PATCH 32/83] Add id column to audit.sent table, add test for db migration --- .../eclair/db/sqlite/SqliteAuditDb.scala | 19 ++--- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 72 ++++++++++++++++++- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index ea08aa3cf7..7e608237e3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -50,7 +50,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)") // add id - statement.executeUpdate("ALTER TABLE sent ADD id BLOB NOT NULL") + statement.executeUpdate(s"ALTER TABLE sent ADD id BLOB DEFAULT '${ChannelCodecs.UNKNOWN_UUID.toString}' NOT NULL") statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)") statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)") @@ -63,7 +63,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (id BLOB NOT NULL, amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL, id BLOB NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") @@ -105,13 +105,14 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { override def add(e: PaymentSent): Unit = using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setBytes(1, e.id.toString.getBytes) - statement.setLong(2, e.amount.toLong) - statement.setLong(3, e.feesPaid.toLong) - statement.setBytes(4, e.paymentHash.toArray) - statement.setBytes(5, e.paymentPreimage.toArray) - statement.setBytes(6, e.toChannelId.toArray) - statement.setLong(7, e.timestamp) + statement.setLong(1, e.amount.toLong) + statement.setLong(2, e.feesPaid.toLong) + statement.setBytes(3, e.paymentHash.toArray) + statement.setBytes(4, e.paymentPreimage.toArray) + statement.setBytes(5, e.toChannelId.toArray) + statement.setLong(6, e.timestamp) + statement.setBytes(7, e.id.toString.getBytes) + statement.executeUpdate() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index ef038e5fbf..132da2f2af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -16,14 +16,19 @@ package fr.acinq.eclair.db +import java.util.UUID + import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} +import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteAuditDb +import fr.acinq.eclair.db.sqlite.SqliteUtils.{ExtendedResultSet, getVersion, using} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.wire.ChannelCodecs import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite +import scala.collection.immutable.Queue import scala.compat.Platform @@ -90,7 +95,72 @@ class SqliteAuditDbSpec extends FunSuite { assert(db.stats.toSet === Set( Stats(channelId = c1, avgPaymentAmountSatoshi = 42, paymentCount = 3, relayFeeSatoshi = 4, networkFeeSatoshi = 100), Stats(channelId = c2, avgPaymentAmountSatoshi = 40, paymentCount = 1, relayFeeSatoshi = 2, networkFeeSatoshi = 500), - Stats(channelId = c3, avgPaymentAmountSatoshi = 0, paymentCount = 0, relayFeeSatoshi = 0, networkFeeSatoshi = 400) + Stats(channelId = c3, avgPaymentAmountSatoshi = 0, paymentCount = 0, relayFeeSatoshi = 0, networkFeeSatoshi = 400) )) } + + test("handle migration version 1 -> 2") { + + import ExtendedResultSet._ + val connection = TestConstants.sqliteInMemory() + + // simulate existing previous version db + using(connection.createStatement()) { statement => + getVersion(statement, "audit", 1) + statement.executeUpdate("CREATE TABLE IF NOT EXISTS balance_updated (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, amount_msat INTEGER NOT NULL, capacity_sat INTEGER NOT NULL, reserve_sat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (amount_in_msat INTEGER NOT NULL, amount_out_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event STRING NOT NULL, timestamp INTEGER NOT NULL)") + + statement.executeUpdate("CREATE INDEX IF NOT EXISTS balance_updated_idx ON balance_updated(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)") + } + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "audit", 1) == 1) // version 1 is deployed now + } + + val ps = PaymentSent(UUID.randomUUID(), MilliSatoshi(42000), MilliSatoshi(1000), randomBytes32, randomBytes32, randomBytes32) + val ps1 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42001), MilliSatoshi(1001), randomBytes32, randomBytes32, randomBytes32) + val ps2 = PaymentSent(UUID.randomUUID(), MilliSatoshi(42002), MilliSatoshi(1002), randomBytes32, randomBytes32, randomBytes32) + + // add a row (no ID on sent) + using(connection.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?)")) { statement => + statement.setLong(1, ps.amount.toLong) + statement.setLong(2, ps.feesPaid.toLong) + statement.setBytes(3, ps.paymentHash.toArray) + statement.setBytes(4, ps.paymentPreimage.toArray) + statement.setBytes(5, ps.toChannelId.toArray) + statement.setLong(6, ps.timestamp) + statement.executeUpdate() + } + + val migratedDb = new SqliteAuditDb(connection) + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "audit", 1) == 2) // version changed from 1 -> 2 + } + + // existing rows will use 00000000-0000-0000-0000-000000000000 as default + assert(migratedDb.listSent(0, Long.MaxValue) == Seq(ps.copy(id = ChannelCodecs.UNKNOWN_UUID))) + + val postMigrationDb = new SqliteAuditDb(connection) + + using(connection.createStatement()) { statement => + assert(getVersion(statement, "audit", 2) == 2) // version 2 + } + + postMigrationDb.add(ps1) + postMigrationDb.add(ps2) + + // the old record will have the UNKNOWN_UUID but the new ones will have their actual id + assert(postMigrationDb.listSent(0, Long.MaxValue) == Seq(ps.copy(id = ChannelCodecs.UNKNOWN_UUID), ps1, ps2)) + } + } From d05a48cbe33e170e7bfccd8283bd0fabd8cf43bd Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 13:35:27 +0200 Subject: [PATCH 33/83] Pass NodeParams to actor props, complete javadoc for OutgoingPayment --- .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 5 +- .../eclair/payment/PaymentInitiator.scala | 8 +-- .../eclair/payment/PaymentLifecycle.scala | 6 ++- .../eclair/payment/PaymentLifecycleSpec.scala | 52 +++++++++++-------- 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 85636c9a84..6ea34dc94e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -245,7 +245,7 @@ class Setup(datadir: File, authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) - paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register, database.payments), "payment-initiator", SupervisorStrategy.Restart)) + paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register, nodeParams), "payment-initiator", SupervisorStrategy.Restart)) _ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart)) kit = Kit( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 69476d60af..bac5b2f9ed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -63,7 +63,7 @@ trait PaymentsDb { * Received payment object stored in DB. * * @param paymentHash identifier of the payment - * @param amount_msat amount of the payment, in milli-satoshis + * @param amountMsat amount of the payment, in milli-satoshis * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. */ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) @@ -75,7 +75,8 @@ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestam * @param id internal payment identifier * @param payment_hash payment_hash * @param amount_msat amount of the payment, in milli-satoshis - * @param updatedAt absolute time in seconds since UNIX epoch when the payment was last updated. + * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. + * @param updatedAt absolute time in seconds since UNIX epoch when the payment was last updated. */ case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, updatedAt: Long, status: OutgoingPaymentStatus.Value) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index 1dc280105d..597da2ac96 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -20,18 +20,18 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.db.PaymentsDb +import fr.acinq.eclair.NodeParams import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment /** * Created by PM on 29/08/2016. */ -class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentDb: PaymentsDb) extends Actor with ActorLogging { +class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) extends Actor with ActorLogging { override def receive: Receive = { case c: SendPayment => val paymentId = UUID.randomUUID() - val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register, paymentDb)) + val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register, nodeParams)) payFsm forward c sender ! paymentId } @@ -39,5 +39,5 @@ class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: Acto } object PaymentInitiator { - def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentDb: PaymentsDb) = Props(classOf[PaymentInitiator], sourceNodeId, router, register, paymentDb) + def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) = Props(classOf[PaymentInitiator], sourceNodeId, router, register, nodeParams) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 731b7e523b..94dc3b0b85 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -40,7 +40,9 @@ import scala.util.{Failure, Success} /** * Created by PM on 26/08/2016. */ -class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentsDb: PaymentsDb) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { +class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { + + lazy val paymentsDb = nodeParams.db.payments startWith(WAITING_FOR_REQUEST, WaitingForRequest) @@ -189,7 +191,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi object PaymentLifecycle { - def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, paymentDb: PaymentsDb) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register, paymentDb) + def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register, nodeParams) // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index ea9ead9d91..d53bf229cd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -35,8 +35,7 @@ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnounce import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomBytes32} -import scala.concurrent.duration._ +import fr.acinq.eclair._ /** * Created by PM on 29/08/2016. @@ -53,9 +52,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (route not found)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -73,9 +73,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (route too expensive)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -92,11 +93,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (unparsable failure)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -135,11 +137,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (local error)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -168,11 +171,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -201,11 +205,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (TemporaryChannelFailure)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -240,11 +244,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (Update)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -303,11 +308,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (PermanentChannelFailure)") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, paymentDb)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() @@ -342,9 +348,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment succeeded") { fixture => import fixture._ - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, paymentDb)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -370,7 +377,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment succeeded to a channel with fees=0") { fixture => import fixture._ import fr.acinq.eclair.randomKey - val paymentDb = TestConstants.inMemoryDb().payments + val nodeParams = TestConstants.Alice.nodeParams + val paymentDb = nodeParams.db.payments // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) and b -> g has fees=0 // \ @@ -393,7 +401,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { watcher.expectMsgType[WatchSpentBasic] // actual test begins - val paymentFSM = system.actorOf(PaymentLifecycle.props(UUID.randomUUID(), a, router, TestProbe().ref, paymentDb)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(UUID.randomUUID(), a, router, TestProbe().ref, nodeParams)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() From 1a2690637855486c40598bd501061f405d007c3a Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 13:52:50 +0200 Subject: [PATCH 34/83] Renaming, API and object --- .../main/scala/fr/acinq/eclair/Eclair.scala | 12 +++---- .../fr/acinq/eclair/api/JsonSerializers.scala | 6 ++-- .../scala/fr/acinq/eclair/api/Service.scala | 12 +++---- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 24 ++++++------- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 28 +++++++-------- .../eclair/payment/LocalPaymentHandler.scala | 6 ++-- .../eclair/payment/PaymentLifecycle.scala | 16 ++++----- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 8 ++--- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 2 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 16 ++++----- .../eclair/payment/PaymentHandlerSpec.scala | 17 +++++----- .../eclair/payment/PaymentLifecycleSpec.scala | 34 +++++++++---------- 12 files changed, 90 insertions(+), 91 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 26ee462774..718e410a71 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -23,7 +23,7 @@ import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.{NetworkFee, OutgoingPayment, Stats} +import fr.acinq.eclair.db.{NetworkFee, SentPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ @@ -68,9 +68,9 @@ trait Eclair { def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] - def paymentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] + def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] - def checkpayment(paymentHash: ByteVector32): Future[Boolean] + def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivePayment]] def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] @@ -162,15 +162,15 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } - override def paymentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment ]] = Future { + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment ]] = Future { id match { case Left(uuid) => appKit.nodeParams.db.payments.getSent(uuid) case Right(paymentHash) => appKit.nodeParams.db.payments.getSent(paymentHash) } } - override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = { - (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Boolean] + override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivePayment]] = { + (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Option[ReceivePayment]] } override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index db7afe1abc..1354b209ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, OutPoint, Transaction} import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.router.RouteResponse import fr.acinq.eclair.transactions.Direction @@ -157,8 +157,8 @@ class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { case id: UUID => JString(id.toString) })) -class OutgoingPaymentStatusSerializer extends CustomSerializer[OutgoingPaymentStatus.Value](format => ({ null }, { - case el: OutgoingPaymentStatus.Value => JString(el.toString) +class OutgoingPaymentStatusSerializer extends CustomSerializer[SentPaymentStatus.Value](format => ({ null }, { + case el: SentPaymentStatus.Value => JString(el.toString) })) object JsonSupport extends Json4sSupport { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 3312018272..0ab3d00de6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -225,18 +225,18 @@ trait Service extends ExtraDirectives with Logging { complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ - path("paymentinfo") { + path("sentinfo") { formFields("id".as[UUID]) { id => - completeWithFutureOption(eclairApi.paymentInfo(Left(id))) + completeWithFutureOption(eclairApi.sentInfo(Left(id))) } ~ formFields(paymentHash) { paymentHash => - completeWithFutureOption(eclairApi.paymentInfo(Right(paymentHash))) + completeWithFutureOption(eclairApi.sentInfo(Right(paymentHash))) } } ~ - path("checkpayment") { + path("receivedinfo") { formFields(paymentHash) { paymentHash => - complete(eclairApi.checkpayment(paymentHash)) + complete(eclairApi.receivedInfo(paymentHash)) } ~ formFields("invoice".as[PaymentRequest]) { invoice => - complete(eclairApi.checkpayment(invoice.paymentHash)) + complete(eclairApi.receivedInfo(invoice.paymentHash)) } } ~ path("audit") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index bac5b2f9ed..5c522c08bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.db import java.util.UUID import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus /** * Store the Lightning payments received and sent by the node. Relayed payments are not persisted. @@ -30,7 +30,7 @@ import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus *

      * *

      - * A sent payment is a [[OutgoingPayment]] object. + * A sent payment is a [[SentPayment]] object. *

      * Basic operations on this DB are: *

        @@ -43,19 +43,19 @@ trait PaymentsDb { def addReceivedPayment(payment: ReceivedPayment) - def addSentPayment(sent: OutgoingPayment) + def addSentPayment(sent: SentPayment) - def updateSentStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) + def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] - def getSent(id: UUID): Option[OutgoingPayment] + def getSent(id: UUID): Option[SentPayment] - def getSent(paymentHash: ByteVector32): Option[OutgoingPayment] + def getSent(paymentHash: ByteVector32): Option[SentPayment] def listReceived(): Seq[ReceivedPayment] - def listSent(): Seq[OutgoingPayment] + def listSent(): Seq[SentPayment] } @@ -63,13 +63,13 @@ trait PaymentsDb { * Received payment object stored in DB. * * @param paymentHash identifier of the payment - * @param amountMsat amount of the payment, in milli-satoshis + * @param amountMsat amount of the payment, in milli-satoshis * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. */ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) /** - * Outgoing payment is every payment that is sent by this node, they may not be finalized and + * Sent payment is every payment that is sent by this node, they may not be finalized and * when is final it can be failed or successful. * * @param id internal payment identifier @@ -78,11 +78,11 @@ case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestam * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. * @param updatedAt absolute time in seconds since UNIX epoch when the payment was last updated. */ -case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, updatedAt: Long, status: OutgoingPaymentStatus.Value) +case class SentPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, updatedAt: Long, status: SentPaymentStatus.Value) -object OutgoingPayment { +object SentPayment { - object OutgoingPaymentStatus extends Enumeration { + object SentPaymentStatus extends Enumeration { val PENDING = Value(1, "PENDING") val SUCCEEDED = Value(2, "SUCCEEDED") val FAILED = Value(3, "FAILED") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 8f6fbd2821..a2922c6ffc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection import java.util.UUID import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.db.{OutgoingPayment, PaymentsDb, ReceivedPayment} +import fr.acinq.eclair.db.{SentPayment, PaymentsDb, ReceivedPayment} import grizzled.slf4j.Logging import scala.collection.immutable.Queue import scala.compat.Platform @@ -60,7 +60,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def updateSentStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { + override def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) = { using(sqlite.prepareStatement(s"UPDATE sent_payments SET (status, updated_at) = (?, ?) WHERE id = ?")) { statement => statement.setString(1, newStatus.toString) statement.setLong(2, Platform.currentTime) @@ -69,7 +69,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def addSentPayment(sent: OutgoingPayment): Unit = { + override def addSentPayment(sent: SentPayment): Unit = { using(sqlite.prepareStatement("INSERT INTO sent_payments VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) @@ -94,36 +94,36 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getSent(id: UUID): Option[OutgoingPayment] = { + override def getSent(id: UUID): Option[SentPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() if (rs.next()) { - Some(OutgoingPayment( + Some(SentPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), rs.getLong("updated_at"), - OutgoingPaymentStatus.withName(rs.getString("status")))) + SentPaymentStatus.withName(rs.getString("status")))) } else { None } } } - override def getSent(paymentHash: ByteVector32): Option[OutgoingPayment] = { + override def getSent(paymentHash: ByteVector32): Option[SentPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(OutgoingPayment( + Some(SentPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), rs.getLong("updated_at"), - OutgoingPaymentStatus.withName(rs.getString("status")))) } else { + SentPaymentStatus.withName(rs.getString("status")))) } else { None } } @@ -140,18 +140,18 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listSent(): Seq[OutgoingPayment] = { + override def listSent(): Seq[SentPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments") - var q: Queue[OutgoingPayment] = Queue() + var q: Queue[SentPayment] = Queue() while (rs.next()) { - q = q :+ OutgoingPayment( + q = q :+ SentPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), rs.getLong("updated_at"), - OutgoingPaymentStatus.withName(rs.getString("status"))) + SentPaymentStatus.withName(rs.getString("status"))) } q } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 2a5e931366..10fe27d030 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -64,11 +64,9 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin context.become(run(hash2preimage + (paymentHash -> PendingPaymentRequest(paymentPreimage, paymentRequest)))) } recover { case t => sender ! Status.Failure(t) } + // if the payment hasn't been received it will reply with None case CheckPayment(paymentHash) => - nodeParams.db.payments.getReceived(paymentHash) match { - case Some(_) => sender ! true - case _ => sender ! false - } + sender ! nodeParams.db.payments.getReceived(paymentHash) case htlc: UpdateAddHtlc => hash2preimage diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 94dc3b0b85..c55bed2019 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -25,8 +25,8 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus -import fr.acinq.eclair.db.{OutgoingPayment, PaymentsDb} +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus +import fr.acinq.eclair.db.{SentPayment, PaymentsDb} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router._ @@ -50,7 +50,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(c: SendPayment, WaitingForRequest) => router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) val currentTime = Platform.currentTime - paymentsDb.addSentPayment(OutgoingPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, OutgoingPaymentStatus.PENDING)) + paymentsDb.addSentPayment(SentPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, SentPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -67,7 +67,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) - paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) stop(FSM.Normal) } @@ -75,7 +75,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.SUCCEEDED) + paymentsDb.updateSentStatus(id, SentPaymentStatus.SUCCEEDED) reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) @@ -86,7 +86,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) - paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -100,7 +100,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi } log.warning(s"too many failed attempts, failing the payment") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) - paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -165,7 +165,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { - paymentsDb.updateSentStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index e2ed4a91f3..5d6b0db6bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -31,8 +31,8 @@ import akka.stream.ActorMaterializer import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.RES_GETINFO -import fr.acinq.eclair.db.{NetworkFee, OutgoingPayment, Stats} -import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed +import fr.acinq.eclair.db.{NetworkFee, SentPayment, Stats} +import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, ReceivePayment} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.{ChannelDesc, RouteResponse} import fr.acinq.eclair.wire.{ChannelUpdate, NodeAddress, NodeAnnouncement} @@ -76,7 +76,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[UUID] = ??? - override def checkpayment(paymentHash: ByteVector32): Future[Boolean] = ??? + override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivePayment]] = ??? override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = ??? @@ -86,7 +86,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def getInfoResponse(): Future[GetInfoResponse] = ??? - override def paymentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = ??? + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] = ??? } implicit val formats = JsonSupport.formats diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index 132da2f2af..14449ce73e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -20,7 +20,7 @@ import java.util.UUID import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{ExtendedResultSet, getVersion, using} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 1af3d374c1..807daa07bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -20,7 +20,7 @@ import java.util.UUID import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.TestConstants -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import org.scalatest.FunSuite import scodec.bits._ @@ -54,7 +54,7 @@ class SqlitePaymentsDbSpec extends FunSuite { // add a few rows val pr1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) - val ps1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, OutgoingPaymentStatus.PENDING) + val ps1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, SentPaymentStatus.PENDING) preMigrationDb.addReceivedPayment(pr1) preMigrationDb.addSentPayment(ps1) @@ -89,8 +89,8 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) - val s1 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928273L, 1513871928275L, OutgoingPaymentStatus.PENDING) - val s2 = OutgoingPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928272L, 1513871928275L, OutgoingPaymentStatus.PENDING) + val s1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928273L, 1513871928275L, SentPaymentStatus.PENDING) + val s2 = SentPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928272L, 1513871928275L, SentPaymentStatus.PENDING) assert(db.listSent().isEmpty) db.addSentPayment(s1) @@ -102,12 +102,12 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.getSent(s2.paymentHash) === Some(s2)) assert(db.getSent(ByteVector32.Zeroes) === None) - val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655, status = OutgoingPaymentStatus.SUCCEEDED) + val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655, status = SentPaymentStatus.SUCCEEDED) db.addSentPayment(s3) - assert(db.getSent(s3.id).exists(el => el.amountMsat == s3.amountMsat && el.status == OutgoingPaymentStatus.SUCCEEDED)) + assert(db.getSent(s3.id).exists(el => el.amountMsat == s3.amountMsat && el.status == SentPaymentStatus.SUCCEEDED)) - db.updateSentStatus(s3.id, OutgoingPaymentStatus.FAILED) - assert(db.getSent(s3.id).get.status == OutgoingPaymentStatus.FAILED) + db.updateSentStatus(s3.id, SentPaymentStatus.FAILED) + assert(db.getSent(s3.id).get.status == SentPaymentStatus.FAILED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 2c842f6907..4274f7c629 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -22,6 +22,7 @@ import akka.testkit.{TestActorRef, TestKit, TestProbe} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} +import fr.acinq.eclair.db.ReceivedPayment import fr.acinq.eclair.payment.LocalPaymentHandler.PendingPaymentRequest import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop @@ -52,41 +53,41 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === false) + assert(sender.expectMsgType[Option[ReceivedPayment]] === None) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === true) + assert(sender.expectMsgType[Option[ReceivedPayment]].get.paymentHash === pr.paymentHash) } { sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee")) val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === false) + assert(sender.expectMsgType[Option[ReceivedPayment]] === None) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === true) + assert(sender.expectMsgType[Option[ReceivedPayment]].get.paymentHash === pr.paymentHash) } { sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === false) + assert(sender.expectMsgType[Option[ReceivedPayment]] === None) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === false) + assert(sender.expectMsgType[Option[ReceivedPayment]] === None) } { sender.send(handler, ReceivePayment(Some(amountMsat), "timeout expired", Some(1L))) @@ -94,14 +95,14 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike Thread.sleep(1001) val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === false) + assert(sender.expectMsgType[Option[ReceivedPayment]] === None) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(UnknownPaymentHash)) // We chose UnknownPaymentHash on purpose. So if you have expired by 1 second or 1 hour you get the same error message. eventListener.expectNoMsg(300 milliseconds) sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Boolean] === false) + assert(sender.expectMsgType[Option[ReceivedPayment]] === None) // make sure that the request is indeed pruned sender.send(handler, 'requests) sender.expectMsgType[Map[ByteVector, PendingPaymentRequest]].contains(pr.paymentHash) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index d53bf229cd..9c8a6305c2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.ErrorPacket -import fr.acinq.eclair.db.OutgoingPayment.OutgoingPaymentStatus +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} @@ -65,10 +65,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) } test("payment failed (route too expensive)") { fixture => @@ -88,7 +88,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) } test("payment failed (unparsable failure)") { fixture => @@ -108,7 +108,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -132,7 +132,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) // after last attempt the payment is failed } test("payment failed (local error)") { fixture => @@ -152,7 +152,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -166,7 +166,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => @@ -186,7 +186,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -200,7 +200,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -259,7 +259,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -277,7 +277,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -303,7 +303,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // this time the router can't find a route: game over sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) } test("payment failed (PermanentChannelFailure)") { fixture => @@ -323,7 +323,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -343,7 +343,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router, which won't find another route sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) } test("payment succeeded") { fixture => @@ -364,14 +364,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) 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(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) - awaitCond(paymentDb.getSent(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) + awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.SUCCEEDED)) } test("payment succeeded to a channel with fees=0") { fixture => From e0f981602c76dc785963cb51a40c71e00468b23b Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 14:54:07 +0200 Subject: [PATCH 35/83] Improve flaky behavior of PaymentLifecycleSpec --- .../eclair/payment/PaymentLifecycleSpec.scala | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 9c8a6305c2..0f63d931bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -65,7 +65,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + assert(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) @@ -107,8 +107,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -151,8 +150,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -165,8 +163,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => @@ -185,8 +182,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -199,8 +195,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -258,8 +253,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // router received the request and the payment is in status PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -276,8 +270,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -322,8 +315,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -378,8 +370,6 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import fixture._ import fr.acinq.eclair.randomKey val nodeParams = TestConstants.Alice.nodeParams - val paymentDb = nodeParams.db.payments - // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) and b -> g has fees=0 // \ // \--(5)--> g @@ -430,7 +420,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) } - test("filter errors properly") { fixture => + test("filter errors properly") { _ => val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) From 77e1934cf040e7110bed68d70625f4cc043f6824 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 15:40:02 +0200 Subject: [PATCH 36/83] Update paymentDB statuses even when the lifecycle is missing. --- .../scala/fr/acinq/eclair/payment/Relayer.scala | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index ab79a42060..d5590c9b1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -24,6 +24,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ @@ -119,7 +120,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(id, None), _, _)) => - // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream + // we sent the payment, but we probably restarted and the reference to the original sender was lost, + // we publish the failure on the event stream and update the status in paymentDb + nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, paymentHash, Nil)) case Status.Failure(AddHtlcFailed(_, _, error, Local(_, Some(sender)), _, _)) => @@ -151,7 +154,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardFulfill(fulfill, Local(id, None), add) => val feesPaid = MilliSatoshi(0) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) - // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the success on the event stream + // we sent the payment, but we probably restarted and the reference to the original sender was lost, + // we publish the failure on the event stream and update the status in paymentDb + nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.SUCCEEDED) context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // case ForwardFulfill(fulfill, Local(_, Some(sender)), _) => @@ -163,7 +168,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR context.system.eventStream.publish(PaymentRelayed(MilliSatoshi(amountMsatIn), MilliSatoshi(amountMsatOut), add.paymentHash, fromChannelId = originChannelId, toChannelId = fulfill.channelId)) case ForwardFail(_, Local(id, None), add) => - // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream + // we sent the payment, but we probably restarted and the reference to the original sender was lost + // we publish the failure on the event stream and update the status in paymentDb + nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) case ForwardFail(fail, Local(_, Some(sender)), _) => @@ -174,7 +181,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR commandBuffer ! CommandBuffer.CommandSend(originChannelId, originHtlcId, cmd) case ForwardFailMalformed(_, Local(id, None), add) => - // we sent the payment, but we probably restarted and the reference to the original sender was lost, we just publish the failure on the event stream + // we sent the payment, but we probably restarted and the reference to the original sender was lost + // we publish the failure on the event stream and update the status in paymentDb + nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) case ForwardFailMalformed(fail, Local(_, Some(sender)), _) => From 69deefe62d545d68a92a2d3e8c8ec8eac9f8c7cd Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 5 Apr 2019 16:18:58 +0200 Subject: [PATCH 37/83] Add invoices to payment DB --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 7 +++ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 44 ++++++++++++++++++- .../eclair/db/SqlitePaymentsDbSpec.scala | 24 +++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 5c522c08bd..97812aeb84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -20,6 +20,7 @@ import java.util.UUID import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.SentPayment.SentPaymentStatus +import fr.acinq.eclair.payment.PaymentRequest /** * Store the Lightning payments received and sent by the node. Relayed payments are not persisted. @@ -45,6 +46,8 @@ trait PaymentsDb { def addSentPayment(sent: SentPayment) + def addPaymentRequest(pr: PaymentRequest) + def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] @@ -53,10 +56,14 @@ trait PaymentsDb { def getSent(paymentHash: ByteVector32): Option[SentPayment] + def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] + def listReceived(): Seq[ReceivedPayment] def listSent(): Seq[SentPayment] + def listPaymentRequests(): Seq[PaymentRequest] + } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index a2922c6ffc..60df709b03 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -18,11 +18,14 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection import java.util.UUID + import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.db.{SentPayment, PaymentsDb, ReceivedPayment} +import fr.acinq.eclair.db.{PaymentsDb, ReceivedPayment, SentPayment} +import fr.acinq.eclair.payment.PaymentRequest import grizzled.slf4j.Logging + import scala.collection.immutable.Queue import scala.compat.Platform @@ -41,15 +44,26 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS payment_requests (payment_hash BLOB NOT NULL PRIMARY KEY, expiration INTEGER, payment_request BLOB NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS payment_requests (payment_hash BLOB NOT NULL PRIMARY KEY, expiration INTEGER, payment_request BLOB NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } } + override def addPaymentRequest(pr: PaymentRequest): Unit = { + using(sqlite.prepareStatement("INSERT INTO payment_requests VALUES (?, ?, ?)")) { statement => + statement.setBytes(1, pr.paymentHash.toArray) + pr.expiry.foreach { et => statement.setLong(2, et) } + statement.setBytes(3, PaymentRequest.write(pr).getBytes) + statement.executeUpdate() + } + } + override def addReceivedPayment(payment: ReceivedPayment): Unit = { using(sqlite.prepareStatement("INSERT INTO received_payments VALUES (?, ?, ?)")) { statement => statement.setBytes(1, payment.paymentHash.toArray) @@ -123,12 +137,27 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { rs.getLong("amount_msat"), rs.getLong("created_at"), rs.getLong("updated_at"), - SentPaymentStatus.withName(rs.getString("status")))) } else { + SentPaymentStatus.withName(rs.getString("status")))) + } else { + None + } + } + } + + + override def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] = { + using(sqlite.prepareStatement("SELECT payment_request FROM payment_requests WHERE payment_hash = ?")) { statement => + statement.setBytes(1, paymentHash.toArray) + val rs = statement.executeQuery() + if (rs.next()) { + Some(PaymentRequest.read(rs.getString("payment_request"))) + } else { None } } } + override def listReceived(): Seq[ReceivedPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT payment_hash, amount_msat, timestamp FROM received_payments") @@ -157,4 +186,15 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } + override def listPaymentRequests(): Seq[PaymentRequest] = { + using(sqlite.createStatement()) { statement => + val rs = statement.executeQuery("SELECT payment_request FROM payment_requests") + var q: Queue[PaymentRequest] = Queue() + while (rs.next()) { + q = q :+ PaymentRequest.read(rs.getString("payment_request")) + } + q + } + } + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 807daa07bb..ff3176c2a3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -17,11 +17,13 @@ package fr.acinq.eclair.db import java.util.UUID + import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.{TestConstants, payment} import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb +import fr.acinq.eclair.payment.PaymentRequest import org.scalatest.FunSuite import scodec.bits._ @@ -55,11 +57,15 @@ class SqlitePaymentsDbSpec extends FunSuite { // add a few rows val pr1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) val ps1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, SentPaymentStatus.PENDING) + val i1 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") + preMigrationDb.addReceivedPayment(pr1) preMigrationDb.addSentPayment(ps1) + preMigrationDb.addPaymentRequest(i1) assert(preMigrationDb.listReceived() == Seq(pr1)) assert(preMigrationDb.listSent() == Seq(ps1)) + assert(preMigrationDb.listPaymentRequests() == Seq(i1)) val postMigrationDb = new SqlitePaymentsDb(connection) @@ -69,6 +75,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(postMigrationDb.listReceived() == Seq(pr1)) assert(postMigrationDb.listSent() == Seq(ps1)) + assert(preMigrationDb.listPaymentRequests() == Seq(i1)) } test("add/list received payments/find 1 payment that exists/find 1 payment that does not exist") { @@ -111,4 +118,19 @@ class SqlitePaymentsDbSpec extends FunSuite { } + test("add/retrieve payment requests") { + + val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) + + val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") + val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") + + db.addPaymentRequest(i1) + db.addPaymentRequest(i2) + + assert(db.listPaymentRequests() == Seq(i1, i2)) + assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) + assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) + } + } From 6aff3ba2925aec49a7a1294ff435455d24423603 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 09:56:47 +0200 Subject: [PATCH 38/83] Add license header to ExtraDirective.scala --- .../fr/acinq/eclair/api/ExtraDirectives.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala index 92e4b239c0..4aa4314f25 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 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.api import akka.http.scaladsl.marshalling.ToResponseMarshaller From cfdfd700297d609bfe0078898b73155504ed1038 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 10:05:44 +0200 Subject: [PATCH 39/83] Formatting --- .../scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index a2922c6ffc..49ba4992f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -123,7 +123,8 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { rs.getLong("amount_msat"), rs.getLong("created_at"), rs.getLong("updated_at"), - SentPaymentStatus.withName(rs.getString("status")))) } else { + SentPaymentStatus.withName(rs.getString("status")))) + } else { None } } From 9e71507254957555cd6abf1cdf3ab738b5a55c7e Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 10:45:22 +0200 Subject: [PATCH 40/83] Respond with HTTP 404 if the corresponding invoice/paymentHash was not found. --- eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 0ab3d00de6..94cc2f7873 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -234,9 +234,9 @@ trait Service extends ExtraDirectives with Logging { } ~ path("receivedinfo") { formFields(paymentHash) { paymentHash => - complete(eclairApi.receivedInfo(paymentHash)) + completeWithFutureOption(eclairApi.receivedInfo(paymentHash)) } ~ formFields("invoice".as[PaymentRequest]) { invoice => - complete(eclairApi.receivedInfo(invoice.paymentHash)) + completeWithFutureOption(eclairApi.receivedInfo(invoice.paymentHash)) } } ~ path("audit") { From ab68bdb2f686aa682b884eb4558fe88b23fc4c3b Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 14:55:45 +0200 Subject: [PATCH 41/83] Add invoices data to received_payments table --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 68 +++++++++++++------ .../eclair/db/SqlitePaymentsDbSpec.scala | 41 ++++++++--- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 97812aeb84..9262b90202 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -46,7 +46,7 @@ trait PaymentsDb { def addSentPayment(sent: SentPayment) - def addPaymentRequest(pr: PaymentRequest) + def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) @@ -64,6 +64,8 @@ trait PaymentsDb { def listPaymentRequests(): Seq[PaymentRequest] + def listNonExpiredPaymentRequests(): Seq[PaymentRequest] + } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 60df709b03..b041a29c02 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -42,35 +42,53 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { case PREVIOUS_VERSION => logger.warn(s"Performing db migration for DB $DB_NAME, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("ALTER TABLE payments RENAME TO received_payments") + + // create the new table and copy data over it + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, received_msat INTEGER, received_at INTEGER)") + statement.executeUpdate("INSERT INTO received_payments (payment_hash, received_msat, received_at) SELECT payment_hash, amount_msat as received_msat, timestamp as received_at FROM payments") + + // drop old table + statement.executeUpdate("DROP TABLE payments") + + // now add columns for invoices + statement.executeUpdate("ALTER TABLE received_payments ADD COLUMN preimage BLOB") + statement.executeUpdate("ALTER TABLE received_payments ADD COLUMN expire_at INTEGER") + statement.executeUpdate("ALTER TABLE received_payments ADD COLUMN payment_request BLOB") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS payment_requests (payment_hash BLOB NOT NULL PRIMARY KEY, expiration INTEGER, payment_request BLOB NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, received_msat INTEGER, received_at INTEGER, preimage BLOB, expire_at INTEGER, payment_request BLOB)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS payment_requests (payment_hash BLOB NOT NULL PRIMARY KEY, expiration INTEGER, payment_request BLOB NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } } - override def addPaymentRequest(pr: PaymentRequest): Unit = { - using(sqlite.prepareStatement("INSERT INTO payment_requests VALUES (?, ?, ?)")) { statement => + override def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32): Unit = { + val insertStmt = pr.expiry match { + case Some(_) => "INSERT INTO received_payments (payment_hash, preimage, expire_at, payment_request) VALUES (?, ?, ?, ?)" + case None => "INSERT INTO received_payments (payment_hash, preimage, payment_request) VALUES (?, ?, ?)" + } + + using(sqlite.prepareStatement(insertStmt)) { statement => statement.setBytes(1, pr.paymentHash.toArray) - pr.expiry.foreach { et => statement.setLong(2, et) } - statement.setBytes(3, PaymentRequest.write(pr).getBytes) + // 2 received_msat + // 3 received_at + statement.setBytes(2, preimage.toArray) + pr.expiry.foreach { ex => statement.setLong(3, pr.timestamp + ex) } // we store "when" the invoice will expire + statement.setBytes(if(pr.expiry.isDefined) 4 else 3, PaymentRequest.write(pr).getBytes) statement.executeUpdate() } } override def addReceivedPayment(payment: ReceivedPayment): Unit = { - using(sqlite.prepareStatement("INSERT INTO received_payments VALUES (?, ?, ?)")) { statement => - statement.setBytes(1, payment.paymentHash.toArray) - statement.setLong(2, payment.amountMsat) - statement.setLong(3, payment.timestamp) + using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => + statement.setLong(1, payment.amountMsat) + statement.setLong(2, payment.timestamp) + statement.setBytes(3, payment.paymentHash.toArray) val res = statement.executeUpdate() - logger.debug(s"inserted $res payment=${payment.paymentHash} into payment DB") + if(res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") } } @@ -97,11 +115,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] = { - using(sqlite.prepareStatement("SELECT payment_hash, amount_msat, timestamp FROM received_payments WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp"))) + Some(ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) } else { None } @@ -146,7 +164,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM payment_requests WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { @@ -160,10 +178,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def listReceived(): Seq[ReceivedPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_hash, amount_msat, timestamp FROM received_payments") + val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") var q: Queue[ReceivedPayment] = Queue() while (rs.next()) { - q = q :+ ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("timestamp")) + q = q :+ ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) } q } @@ -188,7 +206,19 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def listPaymentRequests(): Seq[PaymentRequest] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_request FROM payment_requests") + val rs = statement.executeQuery("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL") + var q: Queue[PaymentRequest] = Queue() + while (rs.next()) { + q = q :+ PaymentRequest.read(rs.getString("payment_request")) + } + q + } + } + + override def listNonExpiredPaymentRequests(): Seq[PaymentRequest] = { + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL)")) { statement => + statement.setLong(1, Platform.currentTime / 1000) + val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() while (rs.next()) { q = q :+ PaymentRequest.read(rs.getString("payment_request")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index ff3176c2a3..d6f8c36b93 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -48,22 +48,35 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 1) == 1) // version 1 is deployed now } + val oldReceivedPayment = ReceivedPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 123, 1233322) + + // insert old type record + using(connection.prepareStatement("INSERT INTO payments VALUES (?, ?, ?)")) { statement => + statement.setBytes(1, oldReceivedPayment.paymentHash.toArray) + statement.setLong(2, oldReceivedPayment.amountMsat) + statement.setLong(3, oldReceivedPayment.timestamp) + statement.executeUpdate() + } + val preMigrationDb = new SqlitePaymentsDb(connection) using(connection.createStatement()) { statement => assert(getVersion(statement, "payments", 1) == 2) // version has changed from 1 to 2! } + // the existing received payment can still be queried + assert(preMigrationDb.getReceived(oldReceivedPayment.paymentHash) == Some(oldReceivedPayment)) + // add a few rows - val pr1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) val ps1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, SentPaymentStatus.PENDING) val i1 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") + val pr1 = ReceivedPayment(i1.paymentHash, 12345678, 1513871928275L) + preMigrationDb.addPaymentRequest(i1, ByteVector32.Zeroes) preMigrationDb.addReceivedPayment(pr1) preMigrationDb.addSentPayment(ps1) - preMigrationDb.addPaymentRequest(i1) - assert(preMigrationDb.listReceived() == Seq(pr1)) + assert(preMigrationDb.listReceived() == Seq(oldReceivedPayment, pr1)) assert(preMigrationDb.listSent() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests() == Seq(i1)) @@ -73,7 +86,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 2) == 2) // version still to 2 } - assert(postMigrationDb.listReceived() == Seq(pr1)) + assert(postMigrationDb.listReceived() == Seq(oldReceivedPayment, pr1)) assert(postMigrationDb.listSent() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests() == Seq(i1)) } @@ -82,8 +95,17 @@ class SqlitePaymentsDbSpec extends FunSuite { val sqlite = TestConstants.sqliteInMemory() val db = new SqlitePaymentsDb(sqlite) - val p1 = ReceivedPayment(ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 12345678, 1513871928275L) - val p2 = ReceivedPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345678, 1513871928275L) + // can't receive a payment without an invoice associated with it + assertThrows[IllegalArgumentException](db.addReceivedPayment(ReceivedPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad188"), 12345678, 1513871928275L))) + + val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") + val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") + + db.addPaymentRequest(i1, ByteVector32.Zeroes) + db.addPaymentRequest(i2, ByteVector32.Zeroes) + + val p1 = ReceivedPayment(i1.paymentHash, 12345678, 1513871928275L) + val p2 = ReceivedPayment(i2.paymentHash, 12345678, 1513871928275L) assert(db.listReceived() === Nil) db.addReceivedPayment(p1) db.addReceivedPayment(p2) @@ -125,12 +147,15 @@ class SqlitePaymentsDbSpec extends FunSuite { val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") - db.addPaymentRequest(i1) - db.addPaymentRequest(i2) + db.addPaymentRequest(i1, ByteVector32.Zeroes) + db.addPaymentRequest(i2, ByteVector32.One) assert(db.listPaymentRequests() == Seq(i1, i2)) assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) + + assert(db.listNonExpiredPaymentRequests() == Seq(i1, i2)) + } } From 8a7e39ec6847dfcc933deef2be8cb2fa2ff77523 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 16:14:22 +0200 Subject: [PATCH 42/83] WIP integrating payment db in local payment handler --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 8 +++ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 36 +++++++++++-- .../eclair/payment/LocalPaymentHandler.scala | 42 +++++----------- .../eclair/db/SqlitePaymentsDbSpec.scala | 24 +++++++-- .../eclair/payment/PaymentHandlerSpec.scala | 50 ++++++++++--------- .../eclair/payment/PaymentRequestSpec.scala | 9 ++-- 6 files changed, 103 insertions(+), 66 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 9262b90202..35735150a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -21,6 +21,7 @@ import java.util.UUID import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.PaymentRequest.PaymentHash /** * Store the Lightning payments received and sent by the node. Relayed payments are not persisted. @@ -58,14 +59,21 @@ trait PaymentsDb { def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] + // returns preimage + invoice if the request with the given paymentHash has not been paid yet (or expired) + def getActiveNonPaidPaymentRequest(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + def listReceived(): Seq[ReceivedPayment] def listSent(): Seq[SentPayment] def listPaymentRequests(): Seq[PaymentRequest] + // returns non expired payment requests def listNonExpiredPaymentRequests(): Seq[PaymentRequest] + // returns non paid, non expired payment requests + def listPendingPaymentRequests(): Seq[PaymentRequest] + } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index b041a29c02..e1513353a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -115,7 +115,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] = { - using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ? AND received_msat > 0")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { @@ -164,17 +164,35 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ? AND payment_request IS NOT NULL")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(PaymentRequest.read(rs.getString("payment_request"))) + val bytes = rs.getAsciiStream("payment_request").readAllBytes() + println(s"READ: ${new String(bytes)}") + Some(PaymentRequest.read(new String(bytes))) } else { None } } } + override def getActiveNonPaidPaymentRequest(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { + using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL) AND received_msat IS NULL AND payment_hash = ?")) { statement => + statement.setLong(1, Platform.currentTime / 1000) + statement.setBytes(2, paymentHash.toArray) + val rs = statement.executeQuery() + if (rs.next()) { + val preimage = rs.getByteVector32("preimage") + val pr = PaymentRequest.read(rs.getString("payment_request")) + Some(preimage, pr) + } else { + None + } + } + } + + override def listReceived(): Seq[ReceivedPayment] = { using(sqlite.createStatement()) { statement => @@ -227,4 +245,16 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } + override def listPendingPaymentRequests(): Seq[PaymentRequest] = { + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL) AND received_msat IS NULL")) { statement => + statement.setLong(1, Platform.currentTime / 1000) + val rs = statement.executeQuery() + var q: Queue[PaymentRequest] = Queue() + while (rs.next()) { + q = q :+ PaymentRequest.read(rs.getString("payment_request")) + } + q + } + } + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 10fe27d030..e1bc7cf762 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -41,18 +41,13 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin import LocalPaymentHandler._ implicit val ec: ExecutionContext = context.system.dispatcher - context.system.scheduler.schedule(10 minutes, 10 minutes)(self ! PurgeExpiredRequests) + lazy val paymentDb = nodeParams.db.payments - override def receive: Receive = run(Map.empty) - - def run(hash2preimage: Map[ByteVector32, PendingPaymentRequest]): Receive = { - - case PurgeExpiredRequests => - context.become(run(hash2preimage.filterNot { case (_, pr) => hasExpired(pr) })) + override def receive: Receive = { case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt) => Try { - if (hash2preimage.size > nodeParams.maxPendingPaymentRequests) { + if (paymentDb.listPendingPaymentRequests().size > nodeParams.maxPendingPaymentRequests) { throw new RuntimeException(s"too many pending payment requests (max=${nodeParams.maxPendingPaymentRequests})") } val paymentPreimage = randomBytes32 @@ -60,20 +55,19 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) log.debug(s"generated payment request=${PaymentRequest.write(paymentRequest)} from amount=$amount_opt") + paymentDb.addPaymentRequest(paymentRequest, paymentPreimage) sender ! paymentRequest - context.become(run(hash2preimage + (paymentHash -> PendingPaymentRequest(paymentPreimage, paymentRequest)))) - } recover { case t => sender ! Status.Failure(t) } + } recover { + case t => sender ! Status.Failure(t) + } // if the payment hasn't been received it will reply with None case CheckPayment(paymentHash) => - sender ! nodeParams.db.payments.getReceived(paymentHash) + sender ! paymentDb.getReceived(paymentHash) case htlc: UpdateAddHtlc => - hash2preimage - .get(htlc.paymentHash) // we retrieve the request - .filterNot(hasExpired) // and filter it out if it is expired (it will be purged independently) - match { - case Some(PendingPaymentRequest(paymentPreimage, paymentRequest)) => + paymentDb.getActiveNonPaidPaymentRequest(htlc.paymentHash) match { + case Some((paymentPreimage, paymentRequest)) => val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however // it must not be greater than two times the requested amount. @@ -93,15 +87,14 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin nodeParams.db.payments.addReceivedPayment(ReceivedPayment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime / 1000)) sender ! CMD_FULFILL_HTLC(htlc.id, paymentPreimage, commit = true) context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash, htlc.channelId)) - context.become(run(hash2preimage - htlc.paymentHash)) } case None => sender ! CMD_FAIL_HTLC(htlc.id, Right(UnknownPaymentHash), commit = true) } - case 'requests => - // this is just for testing - sender ! hash2preimage +// case 'requests => +// // this is just for testing +// sender ! hash2preimage } } @@ -110,13 +103,4 @@ object LocalPaymentHandler { def props(nodeParams: NodeParams): Props = Props(new LocalPaymentHandler(nodeParams)) - case object PurgeExpiredRequests - - case class PendingPaymentRequest(preimage: ByteVector32, paymentRequest: PaymentRequest) - - def hasExpired(pr: PendingPaymentRequest): Boolean = pr.paymentRequest.expiry match { - case Some(expiry) => pr.paymentRequest.timestamp + expiry <= Platform.currentTime / 1000 - case None => false // this request will never expire - } - } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index d6f8c36b93..a92c0645c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -19,7 +19,8 @@ package fr.acinq.eclair.db import java.util.UUID import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi} +import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.{TestConstants, payment} import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb @@ -144,18 +145,31 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) - val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") - val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") + val bob = Bob.keyManager + + val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = ByteVector32.One, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) + val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = ByteVector32.Zeroes, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = 12345) + + val serialized = PaymentRequest.write(i1) + val deserialized = PaymentRequest.read(serialized) + + assert(deserialized.expiry == i1.expiry) + + // i2 doesn't expire + assert(i1.expiry.isDefined && i2.expiry.isEmpty) + assert(i1.amount.isEmpty && i2.amount.isDefined) db.addPaymentRequest(i1, ByteVector32.Zeroes) db.addPaymentRequest(i2, ByteVector32.One) - assert(db.listPaymentRequests() == Seq(i1, i2)) + //assert(db.listPaymentRequests() == Seq(i1, i2)) + assert(db.getPaymentRequest(i1.paymentHash).get.expiry == i1.expiry) assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) assert(db.listNonExpiredPaymentRequests() == Seq(i1, i2)) - + assert(db.getActiveNonPaidPaymentRequest(i1.paymentHash) == Some((ByteVector32.Zeroes, i1))) + assert(db.getActiveNonPaidPaymentRequest(i2.paymentHash) == Some((ByteVector32.One, i2))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 4274f7c629..a26d419c3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -23,7 +23,6 @@ import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.ReceivedPayment -import fr.acinq.eclair.payment.LocalPaymentHandler.PendingPaymentRequest import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UnknownPaymentHash, UpdateAddHtlc} @@ -54,6 +53,9 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]] === None) + + assert(nodeParams.db.payments.getActiveNonPaidPaymentRequest(pr.paymentHash).isDefined) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] @@ -89,29 +91,29 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]] === None) } - { - sender.send(handler, ReceivePayment(Some(amountMsat), "timeout expired", Some(1L))) - //allow request to timeout - Thread.sleep(1001) - val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]] === None) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) - sender.send(handler, add) - assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(UnknownPaymentHash)) - // We chose UnknownPaymentHash on purpose. So if you have expired by 1 second or 1 hour you get the same error message. - eventListener.expectNoMsg(300 milliseconds) - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]] === None) - // make sure that the request is indeed pruned - sender.send(handler, 'requests) - sender.expectMsgType[Map[ByteVector, PendingPaymentRequest]].contains(pr.paymentHash) - sender.send(handler, LocalPaymentHandler.PurgeExpiredRequests) - awaitCond({ - sender.send(handler, 'requests) - sender.expectMsgType[Map[ByteVector32, PendingPaymentRequest]].contains(pr.paymentHash) == false - }) - } +// { +// sender.send(handler, ReceivePayment(Some(amountMsat), "timeout expired", Some(1L))) +// //allow request to timeout +// Thread.sleep(1001) +// val pr = sender.expectMsgType[PaymentRequest] +// sender.send(handler, CheckPayment(pr.paymentHash)) +// assert(sender.expectMsgType[Option[ReceivedPayment]] === None) +// val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) +// sender.send(handler, add) +// assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(UnknownPaymentHash)) +// // We chose UnknownPaymentHash on purpose. So if you have expired by 1 second or 1 hour you get the same error message. +// eventListener.expectNoMsg(300 milliseconds) +// sender.send(handler, CheckPayment(pr.paymentHash)) +// assert(sender.expectMsgType[Option[ReceivedPayment]] === None) +// // make sure that the request is indeed pruned +// sender.send(handler, 'requests) +// sender.expectMsgType[Map[ByteVector, PendingPaymentRequest]].contains(pr.paymentHash) +// //sender.send(handler, LocalPaymentHandler.PurgeExpiredRequests) +// awaitCond({ +// sender.send(handler, 'requests) +// sender.expectMsgType[Map[ByteVector32, PendingPaymentRequest]].contains(pr.paymentHash) == false +// }) +// } } test("Payment request generation should fail when the amount asked in not valid") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 9e4f93c7e8..3e0a4024ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -219,12 +219,11 @@ class PaymentRequestSpec extends FunSuite { } test("expiry is a variable-length unsigned value") { - val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(MilliSatoshi(100000L)), ByteVector32(hex"0001020304050607080900010203040506070809000102030405060708090102"), - priv, "test", fallbackAddress = None, expirySeconds = Some(21600), timestamp = System.currentTimeMillis() / 1000L) + val pr = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = ByteVector32.One, privateKey = priv, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) - val serialized = PaymentRequest write pr - val pr1 = PaymentRequest read serialized - assert(pr.expiry === Some(21600)) + val serialized = PaymentRequest.write(pr) + val pr1 = PaymentRequest.read(serialized) + assert(pr.expiry == Some(123456) && pr.expiry == pr1.expiry) } test("ignore unknown tags") { From c1ed5b9a21bfd4050a9f36a9903c1ec1119baf84 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 16:50:43 +0200 Subject: [PATCH 43/83] WIP integrating payment db in local payment handler /2 --- .../src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 2 +- .../scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 2 +- .../fr/acinq/eclair/payment/LocalPaymentHandler.scala | 4 +--- .../scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 7 +++---- .../scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala | 4 ++-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 35735150a0..6f7d4e2858 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -60,7 +60,7 @@ trait PaymentsDb { def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] // returns preimage + invoice if the request with the given paymentHash has not been paid yet (or expired) - def getActiveNonPaidPaymentRequest(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] def listReceived(): Seq[ReceivedPayment] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index e1513353a5..8ca36bc516 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -177,7 +177,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getActiveNonPaidPaymentRequest(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { + override def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL) AND received_msat IS NULL AND payment_hash = ?")) { statement => statement.setLong(1, Platform.currentTime / 1000) statement.setBytes(2, paymentHash.toArray) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index e1bc7cf762..4dec79892c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -38,8 +38,6 @@ import scala.util.Try */ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLogging { - import LocalPaymentHandler._ - implicit val ec: ExecutionContext = context.system.dispatcher lazy val paymentDb = nodeParams.db.payments @@ -66,7 +64,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin sender ! paymentDb.getReceived(paymentHash) case htlc: UpdateAddHtlc => - paymentDb.getActiveNonPaidPaymentRequest(htlc.paymentHash) match { + paymentDb.getPendingRequestAndPreimage(htlc.paymentHash) match { case Some((paymentPreimage, paymentRequest)) => val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index a92c0645c4..291aff2d2a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -162,14 +162,13 @@ class SqlitePaymentsDbSpec extends FunSuite { db.addPaymentRequest(i1, ByteVector32.Zeroes) db.addPaymentRequest(i2, ByteVector32.One) - //assert(db.listPaymentRequests() == Seq(i1, i2)) - assert(db.getPaymentRequest(i1.paymentHash).get.expiry == i1.expiry) + assert(db.listPaymentRequests() == Seq(i1, i2)) assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) assert(db.listNonExpiredPaymentRequests() == Seq(i1, i2)) - assert(db.getActiveNonPaidPaymentRequest(i1.paymentHash) == Some((ByteVector32.Zeroes, i1))) - assert(db.getActiveNonPaidPaymentRequest(i2.paymentHash) == Some((ByteVector32.One, i2))) + assert(db.getPendingRequestAndPreimage(i1.paymentHash) == Some((ByteVector32.Zeroes, i1))) + assert(db.getPendingRequestAndPreimage(i2.paymentHash) == Some((ByteVector32.One, i2))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index a26d419c3f..48546a9b99 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -53,8 +53,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]] === None) - - assert(nodeParams.db.payments.getActiveNonPaidPaymentRequest(pr.paymentHash).isDefined) + assert(nodeParams.db.payments.getPendingRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) @@ -63,6 +62,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]].get.paymentHash === pr.paymentHash) + assert(nodeParams.db.payments.getPendingRequestAndPreimage(pr.paymentHash).isEmpty) // empty because the invoice is not "pending" anymore } { From 7c509891d55b8510b124cfaf80d5c2f13cd66980 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 17:01:25 +0200 Subject: [PATCH 44/83] Add /listinvoice and /getinvoice internal APIs --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 12 ++++++++++++ .../fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 6 ++---- .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 5 +++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 718e410a71..cfd0e65592 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -80,6 +80,10 @@ trait Eclair { def getInfoResponse(): Future[GetInfoResponse] + def pendingInvoices(): Future[Seq[PaymentRequest]] + + def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] + } class EclairImpl(appKit: Kit) extends Eclair { @@ -193,6 +197,14 @@ class EclairImpl(appKit: Kit) extends Eclair { override def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.db.audit.stats) + override def pendingInvoices(): Future[Seq[PaymentRequest]] = Future { + appKit.nodeParams.db.payments.listPendingPaymentRequests() + } + + override def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] = Future { + appKit.nodeParams.db.payments.getPaymentRequest(paymentHash) + } + /** * Sends a request to a channel and expects a response * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 8ca36bc516..9c0804520a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -77,7 +77,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { // 3 received_at statement.setBytes(2, preimage.toArray) pr.expiry.foreach { ex => statement.setLong(3, pr.timestamp + ex) } // we store "when" the invoice will expire - statement.setBytes(if(pr.expiry.isDefined) 4 else 3, PaymentRequest.write(pr).getBytes) + statement.setBytes(if (pr.expiry.isDefined) 4 else 3, PaymentRequest.write(pr).getBytes) statement.executeUpdate() } } @@ -88,7 +88,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.setLong(2, payment.timestamp) statement.setBytes(3, payment.paymentHash.toArray) val res = statement.executeUpdate() - if(res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") + if (res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") } } @@ -192,8 +192,6 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - - override def listReceived(): Seq[ReceivedPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 5d6b0db6bb..7e3b42f1dd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -87,6 +87,11 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def getInfoResponse(): Future[GetInfoResponse] = ??? override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] = ??? + + override def pendingInvoices(): Future[Seq[PaymentRequest]] = ??? + + override def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] = ??? + } implicit val formats = JsonSupport.formats From 048f3563d81b7740560882da58f19f156d688fe6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 9 Apr 2019 16:07:56 +0200 Subject: [PATCH 45/83] Left-pad numeric bolt11 tagged fields to have a number of bits multiple of five (bech32 encoding). --- .../acinq/eclair/payment/PaymentRequest.scala | 9 +++-- .../eclair/payment/PaymentRequestSpec.scala | 35 ++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index 994fdbafe1..4c8c2f40da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -238,7 +238,8 @@ object PaymentRequest { } /** - * This returns a bitvector with the minimum size necessary to encode the long + * This returns a bitvector with the minimum size necessary to encode the long, left padded + * to have a length (in bits) multiples of 5 * @param l */ def long2bits(l: Long) = { @@ -247,7 +248,11 @@ object PaymentRequest { for (i <- 0 until bin.size.toInt) { if (highest == -1 && bin(i)) highest = i } - if (highest == -1) BitVector.empty else bin.drop(highest) + val nonPadded = if (highest == -1) BitVector.empty else bin.drop(highest) + nonPadded.size % 5 match { + case 0 => nonPadded + case remaining => BitVector.fill(5 - remaining)(false) ++ nonPadded + } } /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 9e4f93c7e8..e99ad6af8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -63,14 +63,14 @@ class PaymentRequestSpec extends FunSuite { assert(string2Bits("pz") === bin"0000100010") } - test("minimal length long") { + test("minimal length long, left-padded to be multiple of 5") { import scodec.bits._ assert(long2bits(0) == bin"") - assert(long2bits(1) == bin"1") - assert(long2bits(42) == bin"101010") - assert(long2bits(255) == bin"11111111") - assert(long2bits(256) == bin"100000000") - assert(long2bits(3600) == bin"111000010000") + assert(long2bits(1) == bin"00001") + assert(long2bits(42) == bin"0000101010") + assert(long2bits(255) == bin"0011111111") + assert(long2bits(256) == bin"0100000000") + assert(long2bits(3600) == bin"000111000010000") } test("verify that padding is zero") { @@ -218,13 +218,24 @@ class PaymentRequestSpec extends FunSuite { assert(PaymentRequest.write(pr.sign(priv)) == ref) } - test("expiry is a variable-length unsigned value") { - val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(MilliSatoshi(100000L)), ByteVector32(hex"0001020304050607080900010203040506070809000102030405060708090102"), - priv, "test", fallbackAddress = None, expirySeconds = Some(21600), timestamp = System.currentTimeMillis() / 1000L) + test("correctly serialize/deserialize variable-length tagged fields") { + val number = 123456 - val serialized = PaymentRequest write pr - val pr1 = PaymentRequest read serialized - assert(pr.expiry === Some(21600)) + val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits).as[PaymentRequest.Expiry] + val field = PaymentRequest.Expiry(number) + + assert(field.toLong == number) + + val serializedExpiry = codec.encode(field).require + val field1 = codec.decodeValue(serializedExpiry).require + assert(field1 == field) + + // Now with a payment request + val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) + + val serialized = PaymentRequest.write(pr) + val pr1 = PaymentRequest.read(serialized) + assert(pr == pr1) } test("ignore unknown tags") { From c4d6969dcb17bce70b9ba0add59fb179840446fd Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 10 Apr 2019 16:48:43 +0200 Subject: [PATCH 46/83] WIP do not filter on expire_at when retrieving invoice+preimage --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 5 +++++ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 9 +++------ .../eclair/payment/LocalPaymentHandler.scala | 3 ++- .../eclair/db/SqlitePaymentsDbSpec.scala | 20 ++++++++++--------- .../eclair/payment/PaymentHandlerSpec.scala | 4 ++-- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 6f7d4e2858..aedaf0e44c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -47,6 +47,7 @@ trait PaymentsDb { def addSentPayment(sent: SentPayment) + // adds a new payment request and stores its preimage with it def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) @@ -57,15 +58,19 @@ trait PaymentsDb { def getSent(paymentHash: ByteVector32): Option[SentPayment] + // return the payment request associated with this paymentHash def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] // returns preimage + invoice if the request with the given paymentHash has not been paid yet (or expired) def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + // returns all received payments def listReceived(): Seq[ReceivedPayment] + // returns all sent payments def listSent(): Seq[SentPayment] + // returns all payment request def listPaymentRequests(): Seq[PaymentRequest] // returns non expired payment requests diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 9c0804520a..d77fad253e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -168,9 +168,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - val bytes = rs.getAsciiStream("payment_request").readAllBytes() - println(s"READ: ${new String(bytes)}") - Some(PaymentRequest.read(new String(bytes))) + Some(PaymentRequest.read(rs.getString("payment_request"))) } else { None } @@ -178,9 +176,8 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { - using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL) AND received_msat IS NULL AND payment_hash = ?")) { statement => - statement.setLong(1, Platform.currentTime / 1000) - statement.setBytes(2, paymentHash.toArray) + using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_request IS NOT NULL AND preimage IS NOT NULL AND payment_hash = ?")) { statement => + statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { val preimage = rs.getByteVector32("preimage") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 4dec79892c..f64780202a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -52,8 +52,9 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val paymentHash = Crypto.sha256(paymentPreimage) val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) - log.debug(s"generated payment request=${PaymentRequest.write(paymentRequest)} from amount=$amount_opt") + log.debug(s"generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) paymentDb.addPaymentRequest(paymentRequest, paymentPreimage) + assert(paymentDb.getPendingRequestAndPreimage(paymentHash).isDefined) sender ! paymentRequest } recover { case t => sender ! Status.Failure(t) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 291aff2d2a..b725257280 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.db import java.util.UUID - import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi} import fr.acinq.eclair.TestConstants.Bob @@ -27,6 +26,7 @@ import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import fr.acinq.eclair.payment.PaymentRequest import org.scalatest.FunSuite import scodec.bits._ +import fr.acinq.eclair.randomBytes32 class SqlitePaymentsDbSpec extends FunSuite { @@ -147,13 +147,10 @@ class SqlitePaymentsDbSpec extends FunSuite { val bob = Bob.keyManager - val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = ByteVector32.One, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) - val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = ByteVector32.Zeroes, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = 12345) - - val serialized = PaymentRequest.write(i1) - val deserialized = PaymentRequest.read(serialized) + val (paymentHash1, paymentHash2) = (randomBytes32, randomBytes32) - assert(deserialized.expiry == i1.expiry) + val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) + val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = 12345) // i2 doesn't expire assert(i1.expiry.isDefined && i2.expiry.isEmpty) @@ -167,8 +164,13 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) assert(db.listNonExpiredPaymentRequests() == Seq(i1, i2)) - assert(db.getPendingRequestAndPreimage(i1.paymentHash) == Some((ByteVector32.Zeroes, i1))) - assert(db.getPendingRequestAndPreimage(i2.paymentHash) == Some((ByteVector32.One, i2))) + assert(db.listPendingPaymentRequests() == Seq(i1, i2)) + assert(db.getPendingRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) + assert(db.getPendingRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) + + // now invoice i1 is being paid + db.addReceivedPayment(ReceivedPayment(paymentHash1, 123, 222)) + assert(db.getPendingRequestAndPreimage(paymentHash1).isEmpty) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 48546a9b99..fafb27fbbf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.ReceivedPayment import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UnknownPaymentHash, UpdateAddHtlc} +import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} import org.scalatest.FunSuiteLike import scodec.bits.ByteVector @@ -62,7 +62,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]].get.paymentHash === pr.paymentHash) - assert(nodeParams.db.payments.getPendingRequestAndPreimage(pr.paymentHash).isEmpty) // empty because the invoice is not "pending" anymore + assert(nodeParams.db.payments.getReceived(pr.paymentHash).exists(_.amountMsat == add.amountMsat)) } { From 1514f88080dcb84dfc60a244c4e93fba6d7c7912 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 10 Apr 2019 16:58:26 +0200 Subject: [PATCH 47/83] Renaming, update test --- .../src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 ++-- .../fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 2 +- .../fr/acinq/eclair/payment/LocalPaymentHandler.scala | 4 ++-- .../scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 8 ++------ .../fr/acinq/eclair/payment/PaymentHandlerSpec.scala | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index aedaf0e44c..390ea55c12 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -61,8 +61,8 @@ trait PaymentsDb { // return the payment request associated with this paymentHash def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] - // returns preimage + invoice if the request with the given paymentHash has not been paid yet (or expired) - def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + // returns preimage + invoice + def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] // returns all received payments def listReceived(): Seq[ReceivedPayment] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index d77fad253e..9733f6604c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -175,7 +175,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { + override def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_request IS NOT NULL AND preimage IS NOT NULL AND payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index f64780202a..b1f86f410f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -54,7 +54,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) log.debug(s"generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) paymentDb.addPaymentRequest(paymentRequest, paymentPreimage) - assert(paymentDb.getPendingRequestAndPreimage(paymentHash).isDefined) + assert(paymentDb.getRequestAndPreimage(paymentHash).isDefined) sender ! paymentRequest } recover { case t => sender ! Status.Failure(t) @@ -65,7 +65,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin sender ! paymentDb.getReceived(paymentHash) case htlc: UpdateAddHtlc => - paymentDb.getPendingRequestAndPreimage(htlc.paymentHash) match { + paymentDb.getRequestAndPreimage(htlc.paymentHash) match { case Some((paymentPreimage, paymentRequest)) => val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index b725257280..5821ef2f67 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -165,12 +165,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.listNonExpiredPaymentRequests() == Seq(i1, i2)) assert(db.listPendingPaymentRequests() == Seq(i1, i2)) - assert(db.getPendingRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) - assert(db.getPendingRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) - - // now invoice i1 is being paid - db.addReceivedPayment(ReceivedPayment(paymentHash1, 123, 222)) - assert(db.getPendingRequestAndPreimage(paymentHash1).isEmpty) + assert(db.getRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) + assert(db.getRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index fafb27fbbf..dd14b23f15 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -53,7 +53,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]] === None) - assert(nodeParams.db.payments.getPendingRequestAndPreimage(pr.paymentHash).isDefined) + assert(nodeParams.db.payments.getRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) From ea75fcb756cdad40e183cdc8c91ce1ef42e89069 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 10 Apr 2019 17:52:19 +0200 Subject: [PATCH 48/83] Fix expire_at comparison, remove unused messages --- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 4 +-- .../eclair/payment/LocalPaymentHandler.scala | 5 ---- .../eclair/payment/PaymentHandlerSpec.scala | 25 +------------------ 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 9733f6604c..fa40def2b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -229,7 +229,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def listNonExpiredPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL)")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at > ? OR expire_at IS NULL)")) { statement => statement.setLong(1, Platform.currentTime / 1000) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() @@ -241,7 +241,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def listPendingPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at < ? OR expire_at IS NULL) AND received_msat IS NULL")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL")) { statement => statement.setLong(1, Platform.currentTime / 1000) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index b1f86f410f..cdb61b756e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -54,7 +54,6 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) log.debug(s"generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) paymentDb.addPaymentRequest(paymentRequest, paymentPreimage) - assert(paymentDb.getRequestAndPreimage(paymentHash).isDefined) sender ! paymentRequest } recover { case t => sender ! Status.Failure(t) @@ -90,10 +89,6 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case None => sender ! CMD_FAIL_HTLC(htlc.id, Right(UnknownPaymentHash), commit = true) } - -// case 'requests => -// // this is just for testing -// sender ! hash2preimage } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index dd14b23f15..293a6f44ae 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.ReceivedPayment import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop -import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} +import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UnknownPaymentHash, UpdateAddHtlc} import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} import org.scalatest.FunSuiteLike import scodec.bits.ByteVector @@ -91,29 +91,6 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, CheckPayment(pr.paymentHash)) assert(sender.expectMsgType[Option[ReceivedPayment]] === None) } -// { -// sender.send(handler, ReceivePayment(Some(amountMsat), "timeout expired", Some(1L))) -// //allow request to timeout -// Thread.sleep(1001) -// val pr = sender.expectMsgType[PaymentRequest] -// sender.send(handler, CheckPayment(pr.paymentHash)) -// assert(sender.expectMsgType[Option[ReceivedPayment]] === None) -// val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) -// sender.send(handler, add) -// assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(UnknownPaymentHash)) -// // We chose UnknownPaymentHash on purpose. So if you have expired by 1 second or 1 hour you get the same error message. -// eventListener.expectNoMsg(300 milliseconds) -// sender.send(handler, CheckPayment(pr.paymentHash)) -// assert(sender.expectMsgType[Option[ReceivedPayment]] === None) -// // make sure that the request is indeed pruned -// sender.send(handler, 'requests) -// sender.expectMsgType[Map[ByteVector, PendingPaymentRequest]].contains(pr.paymentHash) -// //sender.send(handler, LocalPaymentHandler.PurgeExpiredRequests) -// awaitCond({ -// sender.send(handler, 'requests) -// sender.expectMsgType[Map[ByteVector32, PendingPaymentRequest]].contains(pr.paymentHash) == false -// }) -// } } test("Payment request generation should fail when the amount asked in not valid") { From b33f0272b463bd9dc327539ca757686c0a29c28d Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 10 Apr 2019 18:03:04 +0200 Subject: [PATCH 49/83] Remove redundant nodeId parameter from payment life cycle --- .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../eclair/payment/PaymentInitiator.scala | 8 +++----- .../eclair/payment/PaymentLifecycle.scala | 18 ++++++++--------- .../eclair/payment/PaymentLifecycleSpec.scala | 20 +++++++++---------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 6ea34dc94e..7f9df23dad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -245,7 +245,7 @@ class Setup(datadir: File, authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) - paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register, nodeParams), "payment-initiator", SupervisorStrategy.Restart)) + paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams, router, register), "payment-initiator", SupervisorStrategy.Restart)) _ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart)) kit = Kit( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala index 597da2ac96..56aa5204bc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentInitiator.scala @@ -17,21 +17,19 @@ package fr.acinq.eclair.payment import java.util.UUID - import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.NodeParams import fr.acinq.eclair.payment.PaymentLifecycle.SendPayment /** * Created by PM on 29/08/2016. */ -class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) extends Actor with ActorLogging { +class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends Actor with ActorLogging { override def receive: Receive = { case c: SendPayment => val paymentId = UUID.randomUUID() - val payFsm = context.actorOf(PaymentLifecycle.props(paymentId, sourceNodeId, router, register, nodeParams)) + val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register)) payFsm forward c sender ! paymentId } @@ -39,5 +37,5 @@ class PaymentInitiator(sourceNodeId: PublicKey, router: ActorRef, register: Acto } object PaymentInitiator { - def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) = Props(classOf[PaymentInitiator], sourceNodeId, router, register, nodeParams) + def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], nodeParams, router, register) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index c55bed2019..4a1236b3d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -40,7 +40,7 @@ import scala.util.{Failure, Success} /** * Created by PM on 26/08/2016. */ -class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { +class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { lazy val paymentsDb = nodeParams.db.payments @@ -48,7 +48,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi when(WAITING_FOR_REQUEST) { case Event(c: SendPayment, WaitingForRequest) => - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) val currentTime = Platform.currentTime paymentsDb.addSentPayment(SentPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, SentPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) @@ -107,12 +107,12 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi // in that case we don't know which node is sending garbage, let's try to blacklist all nodes except the one we are directly connected to and the destination node val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) case Success(e@ErrorPacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) case Success(e@ErrorPacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") @@ -140,18 +140,18 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi // in any case, we forward the update to the router router ! failureMessage.update // let's try again, router will have updated its state - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels, c.routeParams) } else { // 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}") - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) case Success(e@ErrorPacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") // let's try again without the channel outgoing from nodeId val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)) - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) } @@ -171,7 +171,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi } else { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") val faultyChannel = ChannelDesc(hops.head.lastUpdate.shortChannelId, hops.head.nodeId, hops.head.nextNodeId) - router ! RouteRequest(sourceNodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t)) } @@ -191,7 +191,7 @@ class PaymentLifecycle(id: UUID, sourceNodeId: PublicKey, router: ActorRef, regi object PaymentLifecycle { - def props(id: UUID, sourceNodeId: PublicKey, router: ActorRef, register: ActorRef, nodeParams: NodeParams) = Props(classOf[PaymentLifecycle], id, sourceNodeId, router, register, nodeParams) + def props(nodeParams: NodeParams, id: UUID, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], nodeParams, id, router, register) // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 0f63d931bf..1db1daba37 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -55,7 +55,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val nodeParams = TestConstants.Alice.nodeParams val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, nodeParams)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -76,7 +76,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val nodeParams = TestConstants.Alice.nodeParams val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, nodeParams)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -98,7 +98,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -141,7 +141,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -173,7 +173,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -204,7 +204,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -244,7 +244,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -306,7 +306,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() - val paymentFSM = TestFSMRef(new PaymentLifecycle(id, a, routerForwarder.ref, relayer.ref, nodeParams)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, routerForwarder.ref, relayer.ref)) val monitor = TestProbe() val sender = TestProbe() @@ -343,7 +343,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val nodeParams = TestConstants.Alice.nodeParams val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(id, a, router, TestProbe().ref, nodeParams)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() @@ -391,7 +391,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { watcher.expectMsgType[WatchSpentBasic] // actual test begins - val paymentFSM = system.actorOf(PaymentLifecycle.props(UUID.randomUUID(), a, router, TestProbe().ref, nodeParams)) + val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, UUID.randomUUID(), router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() val eventListener = TestProbe() From 29bd9e4bc07d490ecb482041e8d3c32b65a0bcfd Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 11:17:55 +0200 Subject: [PATCH 50/83] Use actual timestamp during test to simulate invoice expiry --- .../test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 5821ef2f67..b5849c4c98 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.db import java.util.UUID + import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi} import fr.acinq.eclair.TestConstants.Bob @@ -28,6 +29,8 @@ import org.scalatest.FunSuite import scodec.bits._ import fr.acinq.eclair.randomBytes32 +import scala.compat.Platform + class SqlitePaymentsDbSpec extends FunSuite { test("init sqlite 2 times in a row") { @@ -149,7 +152,7 @@ class SqlitePaymentsDbSpec extends FunSuite { val (paymentHash1, paymentHash2) = (randomBytes32, randomBytes32) - val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = 12345) + val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = Platform.currentTime / 1000) val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = 12345) // i2 doesn't expire From bcd05518818d715092cb5b7ac42b484cd03bb548 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 12:12:40 +0200 Subject: [PATCH 51/83] Use custom keymanager in PaymentLifecycleSpec --- .../eclair/payment/PaymentLifecycleSpec.scala | 40 +++++++++---------- .../acinq/eclair/router/BaseRouterSpec.scala | 10 +++-- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 1db1daba37..d14119ef9e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi, Satoshi, Transaction import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} -import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.{KeyManager, Sphinx} import fr.acinq.eclair.crypto.Sphinx.ErrorPacket import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage @@ -73,7 +73,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (route too expensive)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) @@ -93,7 +93,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (unparsable failure)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() @@ -119,7 +119,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) // unparsable message // then the payment lifecycle will ask for a new route excluding all intermediate nodes - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set(c), ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, ignoreNodes = Set(c), ignoreChannels = Set.empty)) // let's simulate a response by the router with another route sender.send(paymentFSM, RouteResponse(hops, Set(c), Set.empty)) @@ -136,7 +136,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (local error)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() @@ -153,7 +153,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData @@ -162,13 +162,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, request.paymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() @@ -185,7 +185,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData @@ -200,7 +200,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (TemporaryChannelFailure)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val relayer = TestProbe() val routerForwarder = TestProbe() val id = UUID.randomUUID() @@ -215,7 +215,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData @@ -239,7 +239,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (Update)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() @@ -256,7 +256,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData @@ -271,7 +271,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // router answers with a new route, taking into account the new update @@ -291,7 +291,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // but it will still forward the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified_2) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // this time the router can't find a route: game over @@ -301,7 +301,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment failed (PermanentChannelFailure)") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val relayer = TestProbe() val routerForwarder = TestProbe() @@ -318,7 +318,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData @@ -330,7 +330,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_bc, b, c)))) + routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_bc, b, c)))) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route @@ -340,7 +340,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment succeeded") { fixture => import fixture._ - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) @@ -369,7 +369,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("payment succeeded to a channel with fees=0") { fixture => import fixture._ import fr.acinq.eclair.randomKey - val nodeParams = TestConstants.Alice.nodeParams + val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager) // the network will be a --(1)--> b ---(2)--> c --(3)--> d and e --(4)--> f (we are a) and b -> g has fees=0 // \ // \--(5)--> g diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 7d08d7add9..85aaee7d31 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} +import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.transactions.Scripts @@ -45,14 +46,15 @@ abstract class BaseRouterSpec extends TestkitBaseClass { val remoteNodeId = PrivateKey(ByteVector32(ByteVector.fill(32)(1)), compressed = true).publicKey - val (priv_a, priv_b, priv_c, priv_d, priv_e, priv_f) = (randomKey, randomKey, randomKey, randomKey, randomKey, randomKey) - val (a, b, c, d, e, f) = (priv_a.publicKey, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey, priv_f.publicKey) + val seed = ByteVector32(ByteVector.fill(32)(2)) + val testKeyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) + + val (priv_a, priv_b, priv_c, priv_d, priv_e, priv_f) = (testKeyManager.nodeKey.privateKey, randomKey, randomKey, randomKey, randomKey, randomKey) + val (a, b, c, d, e, f) = (testKeyManager.nodeId, priv_b.publicKey, priv_c.publicKey, priv_d.publicKey, priv_e.publicKey, priv_f.publicKey) val (priv_funding_a, priv_funding_b, priv_funding_c, priv_funding_d, priv_funding_e, priv_funding_f) = (randomKey, randomKey, randomKey, randomKey, randomKey, randomKey) val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey) - //val DUMMY_SIG = hex"3045022100e0a180fdd0fe38037cc878c03832861b40a29d32bd7b40b10c9e1efc8c1468a002205ae06d1624896d0d29f4b31e32772ea3cb1b4d7ed4e077e5da28dcc33c0e781201" - val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil) val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil) val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil) From 5f56cb9c8bf3ce5f9e11ee673619dee90e7be098 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 14:15:35 +0200 Subject: [PATCH 52/83] Do not migrate existing "payments" table in PaymentDB --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 21 +------------------ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 15 ++----------- .../eclair/db/SqlitePaymentsDbSpec.scala | 9 ++++---- 3 files changed, 7 insertions(+), 38 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 390ea55c12..b6b0686400 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -17,32 +17,13 @@ package fr.acinq.eclair.db import java.util.UUID - import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.payment.PaymentRequest -import fr.acinq.eclair.payment.PaymentRequest.PaymentHash -/** - * Store the Lightning payments received and sent by the node. Relayed payments are not persisted. - *

        - * A received payment is a [[ReceivedPayment]] object. In the local context of a LN node, it is safe to consider that - * a payment is uniquely identified by its payment hash. As such, implementations of this database can use the payment - * hash as a unique key and index. - *

        - * - *

        - * A sent payment is a [[SentPayment]] object. - *

        - * Basic operations on this DB are: - *

          - *
        • insertion - *
        • find by payment hash - *
        • list all - *
        - */ trait PaymentsDb { + // assumes there is already a payment request for it (the record for the given payment hash) def addReceivedPayment(payment: ReceivedPayment) def addSentPayment(sent: SentPayment) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index fa40def2b6..3a69358de1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -43,22 +43,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { logger.warn(s"Performing db migration for DB $DB_NAME, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - // create the new table and copy data over it - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, received_msat INTEGER, received_at INTEGER)") - statement.executeUpdate("INSERT INTO received_payments (payment_hash, received_msat, received_at) SELECT payment_hash, amount_msat as received_msat, timestamp as received_at FROM payments") - - // drop old table - statement.executeUpdate("DROP TABLE payments") - - // now add columns for invoices - statement.executeUpdate("ALTER TABLE received_payments ADD COLUMN preimage BLOB") - statement.executeUpdate("ALTER TABLE received_payments ADD COLUMN expire_at INTEGER") - statement.executeUpdate("ALTER TABLE received_payments ADD COLUMN payment_request BLOB") - + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request BLOB NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, received_msat INTEGER, received_at INTEGER, preimage BLOB, expire_at INTEGER, payment_request BLOB)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request BLOB NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index b5849c4c98..1488896fe4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -68,8 +68,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 1) == 2) // version has changed from 1 to 2! } - // the existing received payment can still be queried - assert(preMigrationDb.getReceived(oldReceivedPayment.paymentHash) == Some(oldReceivedPayment)) + // the existing received payment can NOT be queried anymore + assert(preMigrationDb.getReceived(oldReceivedPayment.paymentHash).isEmpty) // add a few rows val ps1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, SentPaymentStatus.PENDING) @@ -80,7 +80,7 @@ class SqlitePaymentsDbSpec extends FunSuite { preMigrationDb.addReceivedPayment(pr1) preMigrationDb.addSentPayment(ps1) - assert(preMigrationDb.listReceived() == Seq(oldReceivedPayment, pr1)) + assert(preMigrationDb.listReceived() == Seq(pr1)) assert(preMigrationDb.listSent() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests() == Seq(i1)) @@ -90,7 +90,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 2) == 2) // version still to 2 } - assert(postMigrationDb.listReceived() == Seq(oldReceivedPayment, pr1)) + assert(postMigrationDb.listReceived() == Seq(pr1)) assert(postMigrationDb.listSent() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests() == Seq(i1)) } @@ -141,7 +141,6 @@ class SqlitePaymentsDbSpec extends FunSuite { db.updateSentStatus(s3.id, SentPaymentStatus.FAILED) assert(db.getSent(s3.id).get.status == SentPaymentStatus.FAILED) - } test("add/retrieve payment requests") { From 35bebab24013508625f40255967aadb3708471a0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 14:19:59 +0200 Subject: [PATCH 53/83] Use VARCHAR to store payment requests --- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 3a69358de1..ca74d94aef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -43,11 +43,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { logger.warn(s"Performing db migration for DB $DB_NAME, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request BLOB NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request BLOB NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") @@ -66,7 +66,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { // 3 received_at statement.setBytes(2, preimage.toArray) pr.expiry.foreach { ex => statement.setLong(3, pr.timestamp + ex) } // we store "when" the invoice will expire - statement.setBytes(if (pr.expiry.isDefined) 4 else 3, PaymentRequest.write(pr).getBytes) + statement.setString(if (pr.expiry.isDefined) 4 else 3, PaymentRequest.write(pr)) statement.executeUpdate() } } @@ -153,7 +153,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ? AND payment_request IS NOT NULL")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { @@ -165,7 +165,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { - using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_request IS NOT NULL AND preimage IS NOT NULL AND payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { @@ -208,7 +208,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def listPaymentRequests(): Seq[PaymentRequest] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL") + val rs = statement.executeQuery("SELECT payment_request FROM received_payments") var q: Queue[PaymentRequest] = Queue() while (rs.next()) { q = q :+ PaymentRequest.read(rs.getString("payment_request")) @@ -218,7 +218,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def listNonExpiredPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at > ? OR expire_at IS NULL)")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE expire_at > ? OR expire_at IS NULL")) { statement => statement.setLong(1, Platform.currentTime / 1000) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() @@ -230,7 +230,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def listPendingPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_request IS NOT NULL AND (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL")) { statement => + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL")) { statement => statement.setLong(1, Platform.currentTime / 1000) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() From 77654926546dc104b04db445ddc6509b0be53861 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 14:44:04 +0200 Subject: [PATCH 54/83] Consume PaymentResult event from payment life cycle during IntegrationTest --- .../test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 4e2f8beda8..b61e148e7f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -405,6 +405,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.nodeId, routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] + sender.expectMsgType[PaymentSucceeded] // the payment FSM will also reply to the sender after the payment is completed } } From f48fa4483ad1077d4172503ccd36441e508a1040 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 16:08:25 +0200 Subject: [PATCH 55/83] Add invoices API --- .../main/scala/fr/acinq/eclair/Eclair.scala | 23 +++++++++---- .../scala/fr/acinq/eclair/api/Service.scala | 26 ++++++++++++--- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 5 +-- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 33 +++++++------------ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 ++- .../eclair/db/SqlitePaymentsDbSpec.scala | 7 ++-- 6 files changed, 55 insertions(+), 43 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index f1a09308d7..749f0453eb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -80,10 +80,12 @@ trait Eclair { def getInfoResponse(): Future[GetInfoResponse] - def pendingInvoices(): Future[Seq[PaymentRequest]] + def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] + def pendingInvoices(): Future[Seq[PaymentRequest]] + } class EclairImpl(appKit: Kit) extends Eclair { @@ -161,12 +163,12 @@ class EclairImpl(appKit: Kit) extends Eclair { override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] = { val sendPayment = minFinalCltvExpiry match { case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv) - case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) + case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes) } (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } - override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment ]] = Future { + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] = Future { id match { case Left(uuid) => appKit.nodeParams.db.payments.getSent(uuid) case Right(paymentHash) => appKit.nodeParams.db.payments.getSent(paymentHash) @@ -197,6 +199,13 @@ class EclairImpl(appKit: Kit) extends Eclair { override def channelStats(): Future[Seq[Stats]] = Future(appKit.nodeParams.db.audit.stats) + override def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] = Future { + val from = from_opt.getOrElse(0L) + val to = to_opt.getOrElse(Long.MaxValue) + + appKit.nodeParams.db.payments.listPaymentRequests(from, to) + } + override def pendingInvoices(): Future[Seq[PaymentRequest]] = Future { appKit.nodeParams.db.payments.listPendingPaymentRequests() } @@ -222,10 +231,10 @@ class EclairImpl(appKit: Kit) extends Eclair { override def getInfoResponse: Future[GetInfoResponse] = Future.successful( GetInfoResponse(nodeId = appKit.nodeParams.nodeId, - alias = appKit.nodeParams.alias, - chainHash = appKit.nodeParams.chainHash, - blockHeight = Globals.blockCount.intValue(), - publicAddresses = appKit.nodeParams.publicAddresses) + alias = appKit.nodeParams.alias, + chainHash = appKit.nodeParams.chainHash, + blockHeight = Globals.blockCount.intValue(), + publicAddresses = appKit.nodeParams.publicAddresses) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index a92a3bd281..45188b189b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -72,6 +72,8 @@ trait Service extends ExtraDirectives with Logging { val nodeId = "nodeId".as[PublicKey] val shortChannelId = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller) val paymentHash = "paymentHash".as[ByteVector32](sha256HashUnmarshaller) + val from = "from".as[Long] + val to = "to".as[Long] val apiExceptionHandler = ExceptionHandler { case t: Throwable => @@ -206,7 +208,8 @@ trait Service extends ExtraDirectives with Logging { case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) } - } ~ path("findroutetonode") { + } ~ + path("findroutetonode") { formFields(nodeId, "amountMsat".as[Long]) { (nodeId, amount) => complete(eclairApi.findRoute(nodeId, amount)) } @@ -240,17 +243,30 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("audit") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.audit(from, to)) + formFields(from.?, to.?) { (from_opt, to_opt) => + complete(eclairApi.audit(from_opt, to_opt)) } } ~ path("networkfees") { - formFields("from".as[Long].?, "to".as[Long].?) { (from, to) => - complete(eclairApi.networkFees(from, to)) + formFields(from.?, to.?) { (from_opt, to_opt) => + complete(eclairApi.networkFees(from_opt, to_opt)) } } ~ path("channelstats") { complete(eclairApi.channelStats()) + } ~ + path("invoice") { + formFields(paymentHash) { paymentHash => + complete(eclairApi.getInvoice(paymentHash)) + } + } ~ + path("allinvoices") { + formFields(from.?, to.?) { (from_opt, to_opt) => + complete(eclairApi.allInvoices(from_opt, to_opt)) + } + } + path("pendinginvoices") { + complete(eclairApi.pendingInvoices()) } } ~ get { path("ws") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index b6b0686400..1828184004 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -52,10 +52,7 @@ trait PaymentsDb { def listSent(): Seq[SentPayment] // returns all payment request - def listPaymentRequests(): Seq[PaymentRequest] - - // returns non expired payment requests - def listNonExpiredPaymentRequests(): Seq[PaymentRequest] + def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] // returns non paid, non expired payment requests def listPendingPaymentRequests(): Seq[PaymentRequest] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index ca74d94aef..60bcde702a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -43,11 +43,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { logger.warn(s"Performing db migration for DB $DB_NAME, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER, created_at INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER, created_at INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") @@ -56,17 +56,16 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32): Unit = { val insertStmt = pr.expiry match { - case Some(_) => "INSERT INTO received_payments (payment_hash, preimage, expire_at, payment_request) VALUES (?, ?, ?, ?)" - case None => "INSERT INTO received_payments (payment_hash, preimage, payment_request) VALUES (?, ?, ?)" + case Some(_) => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)" + case None => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at) VALUES (?, ?, ?, ?)" } using(sqlite.prepareStatement(insertStmt)) { statement => statement.setBytes(1, pr.paymentHash.toArray) - // 2 received_msat - // 3 received_at statement.setBytes(2, preimage.toArray) - pr.expiry.foreach { ex => statement.setLong(3, pr.timestamp + ex) } // we store "when" the invoice will expire - statement.setString(if (pr.expiry.isDefined) 4 else 3, PaymentRequest.write(pr)) + statement.setString(3, PaymentRequest.write(pr)) + statement.setLong(4, pr.timestamp) + pr.expiry.foreach { ex => statement.setLong(5, pr.timestamp + ex) } // we store "when" the invoice will expire statement.executeUpdate() } } @@ -206,20 +205,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_request FROM received_payments") - var q: Queue[PaymentRequest] = Queue() - while (rs.next()) { - q = q :+ PaymentRequest.read(rs.getString("payment_request")) - } - q - } - } - - override def listNonExpiredPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE expire_at > ? OR expire_at IS NULL")) { statement => - statement.setLong(1, Platform.currentTime / 1000) + override def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = { + using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ?")) { statement => + statement.setLong(1, from) + statement.setLong(2, to) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() while (rs.next()) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index b776e425ec..fac8066559 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -88,10 +88,12 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] = ??? - override def pendingInvoices(): Future[Seq[PaymentRequest]] = ??? + override def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] = ??? override def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] = ??? + override def pendingInvoices(): Future[Seq[PaymentRequest]] = ??? + } implicit val formats = JsonSupport.formats diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 1488896fe4..fa3f9efba3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -82,7 +82,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(preMigrationDb.listReceived() == Seq(pr1)) assert(preMigrationDb.listSent() == Seq(ps1)) - assert(preMigrationDb.listPaymentRequests() == Seq(i1)) + assert(preMigrationDb.listPaymentRequests(0, Long.MaxValue) == Seq(i1)) val postMigrationDb = new SqlitePaymentsDb(connection) @@ -92,7 +92,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(postMigrationDb.listReceived() == Seq(pr1)) assert(postMigrationDb.listSent() == Seq(ps1)) - assert(preMigrationDb.listPaymentRequests() == Seq(i1)) + assert(preMigrationDb.listPaymentRequests(0, Long.MaxValue) == Seq(i1)) } test("add/list received payments/find 1 payment that exists/find 1 payment that does not exist") { @@ -161,11 +161,10 @@ class SqlitePaymentsDbSpec extends FunSuite { db.addPaymentRequest(i1, ByteVector32.Zeroes) db.addPaymentRequest(i2, ByteVector32.One) - assert(db.listPaymentRequests() == Seq(i1, i2)) + assert(db.listPaymentRequests(0, Long.MaxValue) == Seq(i1, i2)) assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) - assert(db.listNonExpiredPaymentRequests() == Seq(i1, i2)) assert(db.listPendingPaymentRequests() == Seq(i1, i2)) assert(db.getRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) assert(db.getRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) From eebb691786d8b1d962a0d91a2d81b510327b8e7d Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 16:46:35 +0200 Subject: [PATCH 56/83] Fix service endpoint concatenation, print BOLT11 serialized invoice in json serialized invoice --- .../src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala | 1 + eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala | 2 +- .../src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 1354b209ae..4ede225588 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -150,6 +150,7 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format = JField("paymentHash", JString(p.paymentHash.toString())) :: JField("expiry", if (p.expiry.isDefined) JLong(p.expiry.get) else JNull) :: JField("minFinalCltvExpiry", if (p.minFinalCltvExpiry.isDefined) JLong(p.minFinalCltvExpiry.get) else JNull) :: + JField("serialized", JString(PaymentRequest.write(p))) :: Nil) })) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 45188b189b..a8dec14970 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -264,7 +264,7 @@ trait Service extends ExtraDirectives with Logging { formFields(from.?, to.?) { (from_opt, to_opt) => complete(eclairApi.allInvoices(from_opt, to_opt)) } - } + } ~ path("pendinginvoices") { complete(eclairApi.pendingInvoices()) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index fac8066559..26d5c1960d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -149,7 +149,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { assert(handled) assert(status == BadRequest) val resp = entityAs[ErrorResponse](JsonSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) - println(resp.error) assert(resp.error == "The form field 'channelId' was malformed:\nInvalid hexadecimal character 'h' at index 0") } @@ -263,7 +262,6 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { check { assert(handled) assert(status == OK) - println(entityAs[String]) assert(entityAs[String] == "\"connected\"") } } From 6e61248f9206c93a30fbb7a074efe70bcb4598e3 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 16:50:26 +0200 Subject: [PATCH 57/83] Add db test for [from, to] params in listInvoice --- .../test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index fa3f9efba3..e04d565a1a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -145,6 +145,7 @@ class SqlitePaymentsDbSpec extends FunSuite { test("add/retrieve payment requests") { + val someTimestamp = 12345 val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) val bob = Bob.keyManager @@ -152,7 +153,7 @@ class SqlitePaymentsDbSpec extends FunSuite { val (paymentHash1, paymentHash2) = (randomBytes32, randomBytes32) val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = Platform.currentTime / 1000) - val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = 12345) + val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = someTimestamp) // i2 doesn't expire assert(i1.expiry.isDefined && i2.expiry.isEmpty) @@ -168,6 +169,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.listPendingPaymentRequests() == Seq(i1, i2)) assert(db.getRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) assert(db.getRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) + + assert(db.listPaymentRequests(someTimestamp - 100, someTimestamp + 100) == Seq(i2)) } } From acfb7eb14ce11f699580f9756bf3eb225dba206b Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 11 Apr 2019 16:58:16 +0200 Subject: [PATCH 58/83] Renaming, remove CheckPayment message, update JsonSerializerSpec --- .../main/scala/fr/acinq/eclair/Eclair.scala | 22 ++++++++-------- .../fr/acinq/eclair/api/OldService.scala | 2 +- .../scala/fr/acinq/eclair/api/Service.scala | 14 +++++------ .../eclair/db/sqlite/SqlitePaymentsDb.scala | 2 -- .../eclair/payment/LocalPaymentHandler.scala | 8 +----- .../eclair/payment/PaymentLifecycle.scala | 1 - .../fr/acinq/eclair/api/ApiServiceSpec.scala | 10 ++++---- .../eclair/api/JsonSerializersSpec.scala | 2 +- .../eclair/payment/PaymentHandlerSpec.scala | 25 ++++++++----------- 9 files changed, 37 insertions(+), 49 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 749f0453eb..199a39266b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -17,18 +17,20 @@ package fr.acinq.eclair import java.util.UUID + import akka.actor.ActorRef import akka.pattern._ import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.{NetworkFee, SentPayment, Stats} +import fr.acinq.eclair.db.{NetworkFee, ReceivedPayment, SentPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import scodec.bits.ByteVector + import scala.concurrent.Future import scala.concurrent.duration._ import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent} @@ -56,11 +58,11 @@ trait Eclair { def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] - def allnodes(): Future[Iterable[NodeAnnouncement]] + def allNodes(): Future[Iterable[NodeAnnouncement]] - def allchannels(): Future[Iterable[ChannelDesc]] + def allChannels(): Future[Iterable[ChannelDesc]] - def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] + def allUpdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] @@ -70,7 +72,7 @@ trait Eclair { def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] - def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivePayment]] + def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] @@ -138,13 +140,13 @@ class EclairImpl(appKit: Kit) extends Eclair { sendToChannel(channelId.toString(), CMD_GETINFO).mapTo[RES_GETINFO] } - override def allnodes(): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]] + override def allNodes(): Future[Iterable[NodeAnnouncement]] = (appKit.router ? 'nodes).mapTo[Iterable[NodeAnnouncement]] - override def allchannels(): Future[Iterable[ChannelDesc]] = { + override def allChannels(): Future[Iterable[ChannelDesc]] = { (appKit.router ? 'channels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2))) } - override def allupdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] = nodeId match { + override def allUpdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] = nodeId match { case None => (appKit.router ? 'updates).mapTo[Iterable[ChannelUpdate]] case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } @@ -175,8 +177,8 @@ class EclairImpl(appKit: Kit) extends Eclair { } } - override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivePayment]] = { - (appKit.paymentHandler ? CheckPayment(paymentHash)).mapTo[Option[ReceivePayment]] + override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] = Future { + appKit.nodeParams.db.payments.getReceived(paymentHash) } override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala index 5a7cdc2f12..b9141bd473 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -307,7 +307,7 @@ trait OldService extends Logging { case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash")) } } - found <- (paymentHandler ? CheckPayment(ByteVector32.fromValidHex(identifier))).map(found => new JBool(found.asInstanceOf[Boolean])) + found <- Future(appKit.nodeParams.db.payments.getReceived(ByteVector32.fromValidHex(identifier)).map(_ => JBool(true)).getOrElse(JBool(false))) } yield found) case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index a8dec14970..1308b02c7f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -182,14 +182,14 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("allnodes") { - complete(eclairApi.allnodes()) + complete(eclairApi.allNodes()) } ~ path("allchannels") { - complete(eclairApi.allchannels()) + complete(eclairApi.allChannels()) } ~ path("allupdates") { formFields(nodeId.?) { nodeId_opt => - complete(eclairApi.allupdates(nodeId_opt)) + complete(eclairApi.allUpdates(nodeId_opt)) } } ~ path("receive") { @@ -210,10 +210,10 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("findroutetonode") { - formFields(nodeId, "amountMsat".as[Long]) { (nodeId, amount) => - complete(eclairApi.findRoute(nodeId, amount)) - } - } ~ + formFields(nodeId, "amountMsat".as[Long]) { (nodeId, amount) => + complete(eclairApi.findRoute(nodeId, amount)) + } + } ~ path("send") { formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 60bcde702a..54ffb939cd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -18,14 +18,12 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection import java.util.UUID - import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.db.{PaymentsDb, ReceivedPayment, SentPayment} import fr.acinq.eclair.payment.PaymentRequest import grizzled.slf4j.Logging - import scala.collection.immutable.Queue import scala.compat.Platform diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index cdb61b756e..5d54703a05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -20,13 +20,11 @@ import akka.actor.{Actor, ActorLogging, Props, Status} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel} import fr.acinq.eclair.db.ReceivedPayment -import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} +import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, randomBytes32} - import scala.compat.Platform import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ import scala.util.Try /** @@ -59,10 +57,6 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case t => sender ! Status.Failure(t) } - // if the payment hasn't been received it will reply with None - case CheckPayment(paymentHash) => - sender ! paymentDb.getReceived(paymentHash) - case htlc: UpdateAddHtlc => paymentDb.getRequestAndPreimage(htlc.paymentHash) match { case Some((paymentPreimage, paymentRequest)) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 4a1236b3d8..b5b1ffc7b0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -207,7 +207,6 @@ object PaymentLifecycle { routeParams: Option[RouteParams] = None) { require(amountMsat > 0, s"amountMsat must be > 0") } - case class CheckPayment(paymentHash: ByteVector32) sealed trait PaymentResult case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 26d5c1960d..eb126125cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -31,7 +31,7 @@ import akka.stream.ActorMaterializer import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.RES_GETINFO -import fr.acinq.eclair.db.{NetworkFee, SentPayment, Stats} +import fr.acinq.eclair.db.{NetworkFee, ReceivedPayment, SentPayment, Stats} import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, ReceivePayment} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.{ChannelDesc, RouteResponse} @@ -64,11 +64,11 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] = ??? - override def allnodes(): Future[Iterable[NodeAnnouncement]] = ??? + override def allNodes(): Future[Iterable[NodeAnnouncement]] = ??? - override def allchannels(): Future[Iterable[ChannelDesc]] = ??? + override def allChannels(): Future[Iterable[ChannelDesc]] = ??? - override def allupdates(nodeId: Option[Crypto.PublicKey]): Future[Iterable[ChannelUpdate]] = ??? + override def allUpdates(nodeId: Option[Crypto.PublicKey]): Future[Iterable[ChannelUpdate]] = ??? override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] = ??? @@ -76,7 +76,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[UUID] = ??? - override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivePayment]] = ??? + override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] = ??? override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = ??? diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 3aae5fe6d4..3d152371cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -75,7 +75,7 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("Payment Request") { val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" val pr = PaymentRequest.read(ref) - Serialization.write(pr)(org.json4s.DefaultFormats + new PaymentRequestSerializer) shouldBe """{"prefix":"lnbc","amount":250000000,"timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"minFinalCltvExpiry":null}""" + Serialization.write(pr)(org.json4s.DefaultFormats + new PaymentRequestSerializer) shouldBe """{"prefix":"lnbc","amount":250000000,"timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"minFinalCltvExpiry":null,"serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"}""" } test("type hints") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 293a6f44ae..a2c957c910 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -22,8 +22,7 @@ import akka.testkit.{TestActorRef, TestKit, TestProbe} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} -import fr.acinq.eclair.db.ReceivedPayment -import fr.acinq.eclair.payment.PaymentLifecycle.{CheckPayment, ReceivePayment} +import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UnknownPaymentHash, UpdateAddHtlc} import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} @@ -51,45 +50,41 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]] === None) + assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) assert(nodeParams.db.payments.getRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] + val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]].get.paymentHash === pr.paymentHash) - assert(nodeParams.db.payments.getReceived(pr.paymentHash).exists(_.amountMsat == add.amountMsat)) + assert(nodeParams.db.payments.getReceived(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee")) val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]] === None) + assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]].get.paymentHash === pr.paymentHash) + assert(nodeParams.db.payments.getReceived(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) val pr = sender.expectMsgType[PaymentRequest] - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]] === None) + assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) - sender.send(handler, CheckPayment(pr.paymentHash)) - assert(sender.expectMsgType[Option[ReceivedPayment]] === None) + assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) } } From 31db6338bd5217ea5a49711381262a848b53c436 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 11:17:42 +0200 Subject: [PATCH 59/83] GUI: consume UUID reply from payment initiator --- .../src/main/scala/fr/acinq/eclair/gui/Handlers.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala index f3d55232fb..9d0375183f 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/Handlers.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.gui +import java.util.UUID + import akka.pattern.{AskTimeoutException, ask} import akka.util.Timeout import fr.acinq.bitcoin.MilliSatoshi @@ -89,7 +91,7 @@ class Handlers(fKit: Future[Kit])(implicit ec: ExecutionContext = ExecutionConte case None => SendPayment(amountMsat, req.paymentHash, req.nodeId, req.routingInfo) case Some(minFinalCltvExpiry) => SendPayment(amountMsat, req.paymentHash, req.nodeId, req.routingInfo, finalCltvExpiry = minFinalCltvExpiry) } - res <- (kit.paymentInitiator ? sendPayment).mapTo[PaymentResult] + res <- (kit.paymentInitiator ? sendPayment).mapTo[UUID] } yield res).recover { // completed payment will be handled by the GUIUpdater by listening to PaymentSucceeded/PaymentFailed events case _: AskTimeoutException => From 22d328cc46fca69f0966fe1101c6fa24667c0260 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 11:55:37 +0200 Subject: [PATCH 60/83] API: reply with JSON encoded response if the queried element wasn't found --- .../fr/acinq/eclair/api/ExtraDirectives.scala | 14 ++++++++------ .../scala/fr/acinq/eclair/api/Service.scala | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala index 4aa4314f25..cc748d9f1c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -17,19 +17,21 @@ package fr.acinq.eclair.api import akka.http.scaladsl.marshalling.ToResponseMarshaller -import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.{ContentTypes, HttpResponse} +import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.server.{Directives, Route} - -import scala.concurrent.Future +import fr.acinq.eclair.api.JsonSupport._ +import scala.concurrent.{Future} import scala.util.{Failure, Success} trait ExtraDirectives extends Directives { - // custom directive to fail with HTTP 404 if the element was not found + // custom directive to fail with HTTP 404 (and JSON response) if the element was not found def completeWithFutureOption[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { case Success(Some(t)) => complete(t) - case Success(None) => complete(StatusCodes.NotFound) - case Failure(thr) => reject + case Success(None) => + complete(HttpResponse(NotFound).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("Not found")))) + case Failure(_) => reject } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 1308b02c7f..219f7a631d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -257,7 +257,7 @@ trait Service extends ExtraDirectives with Logging { } ~ path("invoice") { formFields(paymentHash) { paymentHash => - complete(eclairApi.getInvoice(paymentHash)) + completeWithFutureOption(eclairApi.getInvoice(paymentHash)) } } ~ path("allinvoices") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index eb126125cb..a8c12d765f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -287,6 +287,23 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { } } + test("'receivedinfo' method should respond HTTP 404 with a JSON encoded response if the element is not found") { + + val mockService = new MockService(new EclairMock { + override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] = Future.successful(None) // element not found + }) + + Post("/receivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> + addCredentials(BasicHttpCredentials("", mockService.password)) ~> + Route.seal(mockService.route) ~> + check { + assert(handled) + assert(status == NotFound) + val resp = entityAs[ErrorResponse](JsonSupport.unmarshaller, ClassTag(classOf[ErrorResponse])) + assert(resp == ErrorResponse("Not found")) + } + } + test("the websocket should return typed objects") { From c5a2ef2eefc938247913ce7bd5c8f51ea69f2727 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 11:57:14 +0200 Subject: [PATCH 61/83] Update eclair-cli help message to include new APIs --- eclair-core/eclair-cli | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 31d60109f5..948475492f 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -30,8 +30,8 @@ and COMMAND is one of: getinfo, connect, open, close, forceclose, updaterelayfee, peers, channels, channel, allnodes, allchannels, allupdates, receive, parseinvoice, findroute, findroutetonode, - send, sendtonode, checkpayment, - audit, networkfees, channelstats + send, sendtonode, receivedinfo, audit, networkfees, + channelstats, sentinfo, invoice, allinvoice, pendinginvoices Examples -------- From 8df955a7a89b78b08867ec6f46273783ea030911 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 13:26:03 +0200 Subject: [PATCH 62/83] PaymentLifecycleSpec: use TestFSRef and wait until state transition before checking payment in db --- .../scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index d14119ef9e..f2d7e25dbb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -55,7 +55,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val nodeParams = TestConstants.Alice.nodeParams val paymentDb = nodeParams.db.payments val id = UUID.randomUUID() - val paymentFSM = system.actorOf(PaymentLifecycle.props(nodeParams, id, router, TestProbe().ref)) + val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, id, router, TestProbe().ref)) val monitor = TestProbe() val sender = TestProbe() @@ -65,7 +65,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) From 13bb49ed36719bf7b06c26a9892b8a8e8a3bad8e Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 16:20:15 +0200 Subject: [PATCH 63/83] Renaming, reorg of 'successfulAt/failedAt' parameters on outgoing payments --- .../main/scala/fr/acinq/eclair/Eclair.scala | 20 ++--- .../fr/acinq/eclair/api/JsonSerializers.scala | 6 +- .../fr/acinq/eclair/api/OldService.scala | 2 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 59 ++++++++----- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 86 +++++++++++-------- .../eclair/payment/LocalPaymentHandler.scala | 4 +- .../eclair/payment/PaymentLifecycle.scala | 17 ++-- .../fr/acinq/eclair/payment/Relayer.scala | 10 +-- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 8 +- .../acinq/eclair/db/SqliteAuditDbSpec.scala | 3 - .../eclair/db/SqlitePaymentsDbSpec.scala | 72 ++++++++-------- .../eclair/payment/PaymentHandlerSpec.scala | 12 +-- .../eclair/payment/PaymentLifecycleSpec.scala | 34 ++++---- 13 files changed, 176 insertions(+), 157 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 199a39266b..d97521ded7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -17,23 +17,21 @@ package fr.acinq.eclair import java.util.UUID - import akka.actor.ActorRef import akka.pattern._ import akka.util.Timeout import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.db.{NetworkFee, ReceivedPayment, SentPayment, Stats} +import fr.acinq.eclair.db.{NetworkFee, IncomingPayment, OutgoingPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse} import scodec.bits.ByteVector - import scala.concurrent.Future import scala.concurrent.duration._ -import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent} +import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent} import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) @@ -70,9 +68,9 @@ trait Eclair { def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] - def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] + def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] - def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] + def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] @@ -170,15 +168,15 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } - override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] = Future { + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = Future { id match { - case Left(uuid) => appKit.nodeParams.db.payments.getSent(uuid) - case Right(paymentHash) => appKit.nodeParams.db.payments.getSent(paymentHash) + case Left(uuid) => appKit.nodeParams.db.payments.getOutgoing(uuid) + case Right(paymentHash) => appKit.nodeParams.db.payments.getOutgoing(paymentHash) } } - override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] = Future { - appKit.nodeParams.db.payments.getReceived(paymentHash) + override def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] = Future { + appKit.nodeParams.db.payments.getIncoming(paymentHash) } override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 4ede225588..0ac12a74b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, OutPoint, Transaction} import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus +import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.router.RouteResponse import fr.acinq.eclair.transactions.Direction @@ -158,8 +158,8 @@ class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { case id: UUID => JString(id.toString) })) -class OutgoingPaymentStatusSerializer extends CustomSerializer[SentPaymentStatus.Value](format => ({ null }, { - case el: SentPaymentStatus.Value => JString(el.toString) +class OutgoingPaymentStatusSerializer extends CustomSerializer[OutgoingPaymentStatus.Value](format => ({ null }, { + case el: OutgoingPaymentStatus.Value => JString(el.toString) })) object JsonSupport extends Json4sSupport { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala index b9141bd473..c91b525574 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -307,7 +307,7 @@ trait OldService extends Logging { case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash")) } } - found <- Future(appKit.nodeParams.db.payments.getReceived(ByteVector32.fromValidHex(identifier)).map(_ => JBool(true)).getOrElse(JBool(false))) + found <- Future(appKit.nodeParams.db.payments.getIncoming(ByteVector32.fromValidHex(identifier)).map(_ => JBool(true)).getOrElse(JBool(false))) } yield found) case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 1828184004..2b7d324238 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -18,26 +18,27 @@ package fr.acinq.eclair.db import java.util.UUID import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.payment.PaymentRequest trait PaymentsDb { // assumes there is already a payment request for it (the record for the given payment hash) - def addReceivedPayment(payment: ReceivedPayment) + def addIncomingPayment(payment: IncomingPayment) - def addSentPayment(sent: SentPayment) + // creates a record for a non yet finalized outgoing payment + def addOutgoingPayment(sent: OutgoingPayment) + + // updates the status of the payment, succeededAt OR failedAt + def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) // adds a new payment request and stores its preimage with it def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) - def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) - - def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] + def getIncoming(paymentHash: ByteVector32): Option[IncomingPayment] - def getSent(id: UUID): Option[SentPayment] + def getOutgoing(id: UUID): Option[OutgoingPayment] - def getSent(paymentHash: ByteVector32): Option[SentPayment] + def getOutgoing(paymentHash: ByteVector32): Option[OutgoingPayment] // return the payment request associated with this paymentHash def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] @@ -46,10 +47,10 @@ trait PaymentsDb { def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] // returns all received payments - def listReceived(): Seq[ReceivedPayment] + def listIncoming(): Seq[IncomingPayment] // returns all sent payments - def listSent(): Seq[SentPayment] + def listOutgoing(): Seq[OutgoingPayment] // returns all payment request def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] @@ -60,32 +61,44 @@ trait PaymentsDb { } /** - * Received payment object stored in DB. + * Incoming payment object stored in DB. * * @param paymentHash identifier of the payment * @param amountMsat amount of the payment, in milli-satoshis * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. */ -case class ReceivedPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) +case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) /** * Sent payment is every payment that is sent by this node, they may not be finalized and * when is final it can be failed or successful. * - * @param id internal payment identifier - * @param payment_hash payment_hash - * @param amount_msat amount of the payment, in milli-satoshis - * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. - * @param updatedAt absolute time in seconds since UNIX epoch when the payment was last updated. + * @param id internal payment identifier + * @param paymentHash payment_hash + * @param amountMsat amount of the payment, in milli-satoshis + * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. + * @param paidAt absolute time in seconds since UNIX epoch when the payment was last updated. */ -case class SentPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, updatedAt: Long, status: SentPaymentStatus.Value) +case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, succeededAt: Option[Long], failedAt: Option[Long], status: OutgoingPaymentStatus.Value) + +object OutgoingPaymentStatus extends Enumeration { + val PENDING = Value(1, "PENDING") + val SUCCEEDED = Value(2, "SUCCEEDED") + val FAILED = Value(3, "FAILED") +} + +object OutgoingPayment { -object SentPayment { + import OutgoingPaymentStatus._ - object SentPaymentStatus extends Enumeration { - val PENDING = Value(1, "PENDING") - val SUCCEEDED = Value(2, "SUCCEEDED") - val FAILED = Value(3, "FAILED") + def apply(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, succeededAt: Option[Long] = None, failedAt: Option[Long] = None): OutgoingPayment = { + val status = (succeededAt, failedAt) match { + case (None, None) => PENDING + case (Some(_), None) => SUCCEEDED + case (None, Some(_)) => FAILED + case (Some(_), Some(_)) => throw new RuntimeException(s"Invalid update field found for outgoing payment id=$id") + } + new OutgoingPayment(id, paymentHash, amountMsat, createdAt, succeededAt, failedAt, status = status) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 54ffb939cd..8529d01fc7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -16,16 +16,19 @@ package fr.acinq.eclair.db.sqlite -import java.sql.Connection +import java.sql.{Connection, ResultSet} +import java.time.Instant import java.util.UUID + import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.db.{PaymentsDb, ReceivedPayment, SentPayment} +import fr.acinq.eclair.db.{IncomingPayment, OutgoingPayment, OutgoingPaymentStatus, PaymentsDb} import fr.acinq.eclair.payment.PaymentRequest import grizzled.slf4j.Logging + import scala.collection.immutable.Queue import scala.compat.Platform +import scala.util.{Failure, Success, Try} class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { @@ -42,11 +45,11 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER, created_at INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, succeeded_at INTEGER, failed_at INTEGER)") setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER, created_at INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, succeeded_at INTEGER, failed_at INTEGER)") case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -68,7 +71,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def addReceivedPayment(payment: ReceivedPayment): Unit = { + override def addIncomingPayment(payment: IncomingPayment): Unit = { using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => statement.setLong(1, payment.amountMsat) statement.setLong(2, payment.timestamp) @@ -78,70 +81,72 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def updateSentStatus(id: UUID, newStatus: SentPaymentStatus.Value) = { - using(sqlite.prepareStatement(s"UPDATE sent_payments SET (status, updated_at) = (?, ?) WHERE id = ?")) { statement => - statement.setString(1, newStatus.toString) - statement.setLong(2, Platform.currentTime) - statement.setBytes(3, id.toString.getBytes) - statement.executeUpdate() + override def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { + val updateStmt = newStatus match { + case OutgoingPaymentStatus.SUCCEEDED => "UPDATE sent_payments SET succeeded_at = ? WHERE id = ? AND failed_at IS NULL" + case OutgoingPaymentStatus.FAILED => "UPDATE sent_payments SET failed_at = ? WHERE id = ? AND succeeded_at IS NULL" + } + + using(sqlite.prepareStatement(updateStmt)) { statement => + statement.setLong(1, Instant.now().getEpochSecond) + statement.setBytes(2, id.toString.getBytes) + if(statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to update an outgoing payment (id=$id) already in final status with=$newStatus") } } - override def addSentPayment(sent: SentPayment): Unit = { - using(sqlite.prepareStatement("INSERT INTO sent_payments VALUES (?, ?, ?, ?, ?, ?)")) { statement => + override def addOutgoingPayment(sent: OutgoingPayment): Unit = { + using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at) VALUES (?, ?, ?, ?)")) { statement => statement.setBytes(1, sent.id.toString.getBytes) statement.setBytes(2, sent.paymentHash.toArray) statement.setLong(3, sent.amountMsat) statement.setLong(4, sent.createdAt) - statement.setLong(5, sent.updatedAt) - statement.setString(6, sent.status.toString) val res = statement.executeUpdate() logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") } } - override def getReceived(paymentHash: ByteVector32): Option[ReceivedPayment] = { + override def getIncoming(paymentHash: ByteVector32): Option[IncomingPayment] = { using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ? AND received_msat > 0")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) + Some(IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) } else { None } } } - override def getSent(id: UUID): Option[SentPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE id = ?")) { statement => + override def getOutgoing(id: UUID): Option[OutgoingPayment] = { + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() if (rs.next()) { - Some(SentPayment( + Some(OutgoingPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), - rs.getLong("updated_at"), - SentPaymentStatus.withName(rs.getString("status")))) + getNullableTimestamp(rs, "succeeded_at"), + getNullableTimestamp(rs, "failed_at"))) } else { None } } } - override def getSent(paymentHash: ByteVector32): Option[SentPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => + override def getOutgoing(paymentHash: ByteVector32): Option[OutgoingPayment] = { + using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { - Some(SentPayment( + Some(OutgoingPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), - rs.getLong("updated_at"), - SentPaymentStatus.withName(rs.getString("status")))) + getNullableTimestamp(rs, "succeeded_at"), + getNullableTimestamp(rs, "failed_at"))) } else { None } @@ -175,34 +180,41 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listReceived(): Seq[ReceivedPayment] = { + override def listIncoming(): Seq[IncomingPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") - var q: Queue[ReceivedPayment] = Queue() + var q: Queue[IncomingPayment] = Queue() while (rs.next()) { - q = q :+ ReceivedPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) + q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) } q } } - override def listSent(): Seq[SentPayment] = { + override def listOutgoing(): Seq[OutgoingPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, updated_at, status FROM sent_payments") - var q: Queue[SentPayment] = Queue() + val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments") + var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { - q = q :+ SentPayment( + q = q :+ OutgoingPayment( UUID.fromString(new String(rs.getBytes("id"))), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), - rs.getLong("updated_at"), - SentPaymentStatus.withName(rs.getString("status"))) + getNullableTimestamp(rs, "succeeded_at"), + getNullableTimestamp(rs, "failed_at")) } q } } + def getNullableTimestamp(rs: ResultSet, column: String): Option[Long] = Try(rs.getLong(column)) match { + case Success(0) => None + case Success(timestamp) => Some(timestamp) + case Failure(exception) => None + } + + override def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = { using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ?")) { statement => statement.setLong(1, from) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 5d54703a05..1b71ca3cab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, Props, Status} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel} -import fr.acinq.eclair.db.ReceivedPayment +import fr.acinq.eclair.db.IncomingPayment import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment} import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, randomBytes32} @@ -76,7 +76,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case _ => log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}") // amount is correct or was not specified in the payment request - nodeParams.db.payments.addReceivedPayment(ReceivedPayment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime / 1000)) + nodeParams.db.payments.addIncomingPayment(IncomingPayment(htlc.paymentHash, htlc.amountMsat, Platform.currentTime / 1000)) sender ! CMD_FULFILL_HTLC(htlc.id, paymentPreimage, commit = true) context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash, htlc.channelId)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index b5b1ffc7b0..d0743c64db 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.payment +import java.time.Instant import java.util.UUID import akka.actor.{ActorRef, FSM, Props, Status} @@ -25,8 +26,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus -import fr.acinq.eclair.db.{SentPayment, PaymentsDb} +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router._ @@ -49,8 +49,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis when(WAITING_FOR_REQUEST) { case Event(c: SendPayment, WaitingForRequest) => router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) - val currentTime = Platform.currentTime - paymentsDb.addSentPayment(SentPayment(id, c.paymentHash, c.amountMsat, currentTime, currentTime, SentPaymentStatus.PENDING)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, c.amountMsat, Instant.now().getEpochSecond, succeededAt = None, failedAt = None)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -67,7 +66,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) - paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) } @@ -75,7 +74,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - paymentsDb.updateSentStatus(id, SentPaymentStatus.SUCCEEDED) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.SUCCEEDED) reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) @@ -86,7 +85,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) - paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -100,7 +99,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis } log.warning(s"too many failed attempts, failing the payment") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) - paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -165,7 +164,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { - paymentsDb.updateSentStatus(id, SentPaymentStatus.FAILED) + paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index d5590c9b1e..d6c132f9f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus +import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ @@ -122,7 +122,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(id, None), _, _)) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.FAILED) + nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, paymentHash, Nil)) case Status.Failure(AddHtlcFailed(_, _, error, Local(_, Some(sender)), _, _)) => @@ -156,7 +156,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.SUCCEEDED) + nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.SUCCEEDED) context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // case ForwardFulfill(fulfill, Local(_, Some(sender)), _) => @@ -170,7 +170,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardFail(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.FAILED) + nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) case ForwardFail(fail, Local(_, Some(sender)), _) => @@ -183,7 +183,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardFailMalformed(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateSentStatus(id, SentPaymentStatus.FAILED) + nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) case ForwardFailMalformed(fail, Local(_, Some(sender)), _) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index a8c12d765f..78e144cd9f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -31,7 +31,7 @@ import akka.stream.ActorMaterializer import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.RES_GETINFO -import fr.acinq.eclair.db.{NetworkFee, ReceivedPayment, SentPayment, Stats} +import fr.acinq.eclair.db.{NetworkFee, IncomingPayment, OutgoingPayment, Stats} import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, ReceivePayment} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.{ChannelDesc, RouteResponse} @@ -76,7 +76,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[UUID] = ??? - override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] = ??? + override def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] = ??? override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = ??? @@ -86,7 +86,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def getInfoResponse(): Future[GetInfoResponse] = ??? - override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[SentPayment]] = ??? + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = ??? override def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] = ??? @@ -290,7 +290,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { test("'receivedinfo' method should respond HTTP 404 with a JSON encoded response if the element is not found") { val mockService = new MockService(new EclairMock { - override def receivedInfo(paymentHash: ByteVector32): Future[Option[ReceivedPayment]] = Future.successful(None) // element not found + override def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] = Future.successful(None) // element not found }) Post("/receivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala index 14449ce73e..c5d62e5514 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteAuditDbSpec.scala @@ -20,15 +20,12 @@ import java.util.UUID import fr.acinq.bitcoin.{MilliSatoshi, Satoshi, Transaction} import fr.acinq.eclair.channel.{AvailableBalanceChanged, NetworkFeePaid} -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{ExtendedResultSet, getVersion, using} import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} import fr.acinq.eclair.wire.ChannelCodecs import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.FunSuite - -import scala.collection.immutable.Queue import scala.compat.Platform diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index e04d565a1a..20606d1277 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -22,13 +22,11 @@ import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi} import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.{TestConstants, payment} -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import fr.acinq.eclair.payment.PaymentRequest import org.scalatest.FunSuite import scodec.bits._ import fr.acinq.eclair.randomBytes32 - import scala.compat.Platform class SqlitePaymentsDbSpec extends FunSuite { @@ -52,7 +50,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 1) == 1) // version 1 is deployed now } - val oldReceivedPayment = ReceivedPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 123, 1233322) + val oldReceivedPayment = IncomingPayment(ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 123, 1233322) // insert old type record using(connection.prepareStatement("INSERT INTO payments VALUES (?, ?, ?)")) { statement => @@ -69,19 +67,19 @@ class SqlitePaymentsDbSpec extends FunSuite { } // the existing received payment can NOT be queried anymore - assert(preMigrationDb.getReceived(oldReceivedPayment.paymentHash).isEmpty) + assert(preMigrationDb.getIncoming(oldReceivedPayment.paymentHash).isEmpty) // add a few rows - val ps1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928274L, 1513871928275L, SentPaymentStatus.PENDING) + val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), amountMsat = 12345, createdAt = 12345) val i1 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") - val pr1 = ReceivedPayment(i1.paymentHash, 12345678, 1513871928275L) + val pr1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) preMigrationDb.addPaymentRequest(i1, ByteVector32.Zeroes) - preMigrationDb.addReceivedPayment(pr1) - preMigrationDb.addSentPayment(ps1) + preMigrationDb.addIncomingPayment(pr1) + preMigrationDb.addOutgoingPayment(ps1) - assert(preMigrationDb.listReceived() == Seq(pr1)) - assert(preMigrationDb.listSent() == Seq(ps1)) + assert(preMigrationDb.listIncoming() == Seq(pr1)) + assert(preMigrationDb.listOutgoing() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests(0, Long.MaxValue) == Seq(i1)) val postMigrationDb = new SqlitePaymentsDb(connection) @@ -90,8 +88,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 2) == 2) // version still to 2 } - assert(postMigrationDb.listReceived() == Seq(pr1)) - assert(postMigrationDb.listSent() == Seq(ps1)) + assert(postMigrationDb.listIncoming() == Seq(pr1)) + assert(postMigrationDb.listOutgoing() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests(0, Long.MaxValue) == Seq(i1)) } @@ -100,7 +98,7 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(sqlite) // can't receive a payment without an invoice associated with it - assertThrows[IllegalArgumentException](db.addReceivedPayment(ReceivedPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad188"), 12345678, 1513871928275L))) + assertThrows[IllegalArgumentException](db.addIncomingPayment(IncomingPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad188"), 12345678, 1513871928275L))) val i1 = PaymentRequest.read("lnbc5450n1pw2t4qdpp5vcrf6ylgpettyng4ac3vujsk0zpc25cj0q3zp7l7w44zvxmpzh8qdzz2pshjmt9de6zqen0wgsr2dp4ypcxj7r9d3ejqct5ypekzar0wd5xjuewwpkxzcm99cxqzjccqp2rzjqtspxelp67qc5l56p6999wkatsexzhs826xmupyhk6j8lxl038t27z9tsqqqgpgqqqqqqqlgqqqqqzsqpcz8z8hmy8g3ecunle4n3edn3zg2rly8g4klsk5md736vaqqy3ktxs30ht34rkfkqaffzxmjphvd0637dk2lp6skah2hq09z6lrjna3xqp3d4vyd") val i2 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") @@ -108,39 +106,41 @@ class SqlitePaymentsDbSpec extends FunSuite { db.addPaymentRequest(i1, ByteVector32.Zeroes) db.addPaymentRequest(i2, ByteVector32.Zeroes) - val p1 = ReceivedPayment(i1.paymentHash, 12345678, 1513871928275L) - val p2 = ReceivedPayment(i2.paymentHash, 12345678, 1513871928275L) - assert(db.listReceived() === Nil) - db.addReceivedPayment(p1) - db.addReceivedPayment(p2) - assert(db.listReceived().toList === List(p1, p2)) - assert(db.getReceived(p1.paymentHash) === Some(p1)) - assert(db.getReceived(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) + val p1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) + val p2 = IncomingPayment(i2.paymentHash, 12345678, 1513871928275L) + assert(db.listIncoming() === Nil) + db.addIncomingPayment(p1) + db.addIncomingPayment(p2) + assert(db.listIncoming().toList === List(p1, p2)) + assert(db.getIncoming(p1.paymentHash) === Some(p1)) + assert(db.getIncoming(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) } test("add/retrieve/update sent payments") { val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) - val s1 = SentPayment(UUID.randomUUID(), ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), 12345, 1513871928273L, 1513871928275L, SentPaymentStatus.PENDING) - val s2 = SentPayment(UUID.randomUUID(), ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), 54321, 1513871928272L, 1513871928275L, SentPaymentStatus.PENDING) + val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), amountMsat = 12345, createdAt = 12345) + val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), amountMsat = 12345, createdAt = 12345) + + assert(db.listOutgoing().isEmpty) + db.addOutgoingPayment(s1) + db.addOutgoingPayment(s2) - assert(db.listSent().isEmpty) - db.addSentPayment(s1) - db.addSentPayment(s2) + assert(db.listOutgoing().toList == Seq(s1, s2)) + assert(db.getOutgoing(s1.id) === Some(s1)) + assert(db.getOutgoing(UUID.randomUUID()) === None) + assert(db.getOutgoing(s2.paymentHash) === Some(s2)) + assert(db.getOutgoing(ByteVector32.Zeroes) === None) - assert(db.listSent().toList == Seq(s1, s2)) - assert(db.getSent(s1.id) === Some(s1)) - assert(db.getSent(UUID.randomUUID()) === None) - assert(db.getSent(s2.paymentHash) === Some(s2)) - assert(db.getSent(ByteVector32.Zeroes) === None) + val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655) + db.addOutgoingPayment(s3) - val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655, status = SentPaymentStatus.SUCCEEDED) - db.addSentPayment(s3) - assert(db.getSent(s3.id).exists(el => el.amountMsat == s3.amountMsat && el.status == SentPaymentStatus.SUCCEEDED)) + db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.FAILED) + assert(db.getOutgoing(s3.id).get.status == OutgoingPaymentStatus.FAILED) - db.updateSentStatus(s3.id, SentPaymentStatus.FAILED) - assert(db.getSent(s3.id).get.status == SentPaymentStatus.FAILED) + // can't update again once it's in a final state + assertThrows[IllegalArgumentException](db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.SUCCEEDED)) } test("add/retrieve payment requests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index a2c957c910..c5f8083e56 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -50,7 +50,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) assert(nodeParams.db.payments.getRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) @@ -59,32 +59,32 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - assert(nodeParams.db.payments.getReceived(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) + assert(nodeParams.db.payments.getIncoming(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - assert(nodeParams.db.payments.getReceived(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) + assert(nodeParams.db.payments.getIncoming(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) - assert(nodeParams.db.payments.getReceived(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index f2d7e25dbb..f2c9066558 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} import fr.acinq.eclair.crypto.{KeyManager, Sphinx} import fr.acinq.eclair.crypto.Sphinx.ErrorPacket -import fr.acinq.eclair.db.SentPayment.SentPaymentStatus +import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} @@ -65,10 +65,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (route too expensive)") { fixture => @@ -88,7 +88,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (unparsable failure)") { fixture => @@ -107,7 +107,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -131,7 +131,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) // after last attempt the payment is failed + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed } test("payment failed (local error)") { fixture => @@ -150,7 +150,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -163,7 +163,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => @@ -182,7 +182,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -195,7 +195,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -253,7 +253,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -270,7 +270,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -296,7 +296,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // this time the router can't find a route: game over sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (PermanentChannelFailure)") { fixture => @@ -315,7 +315,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -335,7 +335,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router, which won't find another route sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment succeeded") { fixture => @@ -356,14 +356,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) 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(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.PENDING)) + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) - awaitCond(paymentDb.getSent(id).exists(_.status == SentPaymentStatus.SUCCEEDED)) + awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) } test("payment succeeded to a channel with fees=0") { fixture => From 0afa57c421b0fa9fc4daab5b1bcc4b8e71dfe988 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 16:30:13 +0200 Subject: [PATCH 64/83] Return a payment request object in /receive --- .../main/scala/fr/acinq/eclair/Eclair.scala | 32 +++++++++---------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index d97521ded7..6c876799e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -50,27 +50,21 @@ trait Eclair { def updateRelayFee(channelId: String, feeBaseMsat: Long, feeProportionalMillionths: Long): Future[String] - def peersInfo(): Future[Iterable[PeerInfo]] - def channelsInfo(toRemoteNode: Option[PublicKey]): Future[Iterable[RES_GETINFO]] def channelInfo(channelId: ByteVector32): Future[RES_GETINFO] - def allNodes(): Future[Iterable[NodeAnnouncement]] - - def allChannels(): Future[Iterable[ChannelDesc]] - - def allUpdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] + def peersInfo(): Future[Iterable[PeerInfo]] - def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] + def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[PaymentRequest] - def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] + def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] - def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] + def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] @@ -78,13 +72,19 @@ trait Eclair { def channelStats(): Future[Seq[Stats]] - def getInfoResponse(): Future[GetInfoResponse] + def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] + + def pendingInvoices(): Future[Seq[PaymentRequest]] def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] - def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] + def allNodes(): Future[Iterable[NodeAnnouncement]] - def pendingInvoices(): Future[Seq[PaymentRequest]] + def allChannels(): Future[Iterable[ChannelDesc]] + + def allUpdates(nodeId: Option[PublicKey]): Future[Iterable[ChannelUpdate]] + + def getInfoResponse(): Future[GetInfoResponse] } @@ -149,11 +149,9 @@ class EclairImpl(appKit: Kit) extends Eclair { case Some(pk) => (appKit.router ? 'updatesMap).mapTo[Map[ChannelDesc, ChannelUpdate]].map(_.filter(e => e._1.a == pk || e._1.b == pk).values) } - override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] = { + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[PaymentRequest] = { fallbackAddress.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception - (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire, fallbackAddress = fallbackAddress)).mapTo[PaymentRequest].map { pr => - PaymentRequest.write(pr) - } + (appKit.paymentHandler ? ReceivePayment(description = description, amountMsat_opt = amountMsat.map(MilliSatoshi), expirySeconds_opt = expire, fallbackAddress = fallbackAddress)).mapTo[PaymentRequest] } override def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 78e144cd9f..478a65f913 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -70,7 +70,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def allUpdates(nodeId: Option[Crypto.PublicKey]): Future[Iterable[ChannelUpdate]] = ??? - override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[String] = ??? + override def receive(description: String, amountMsat: Option[Long], expire: Option[Long], fallbackAddress: Option[String]): Future[PaymentRequest] = ??? override def findRoute(targetNodeId: Crypto.PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]]): Future[RouteResponse] = ??? From 5710e9c0faeea1ab7ede734846b6593795e3cadd Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 12 Apr 2019 16:49:30 +0200 Subject: [PATCH 65/83] renamed PaymentDb methods --- .../main/scala/fr/acinq/eclair/Eclair.scala | 6 ++-- .../fr/acinq/eclair/api/OldService.scala | 2 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 34 ++++++++---------- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 12 +++---- .../eclair/payment/PaymentLifecycle.scala | 10 +++--- .../fr/acinq/eclair/payment/Relayer.scala | 8 ++--- .../eclair/db/SqlitePaymentsDbSpec.scala | 36 +++++++++---------- .../eclair/payment/PaymentHandlerSpec.scala | 12 +++---- .../eclair/payment/PaymentLifecycleSpec.scala | 32 ++++++++--------- 9 files changed, 74 insertions(+), 78 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index d97521ded7..e1ffd718c1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -170,13 +170,13 @@ class EclairImpl(appKit: Kit) extends Eclair { override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = Future { id match { - case Left(uuid) => appKit.nodeParams.db.payments.getOutgoing(uuid) - case Right(paymentHash) => appKit.nodeParams.db.payments.getOutgoing(paymentHash) + case Left(uuid) => appKit.nodeParams.db.payments.getOutgoingPayment(uuid) + case Right(paymentHash) => appKit.nodeParams.db.payments.getOutgoingPayment(paymentHash) } } override def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] = Future { - appKit.nodeParams.db.payments.getIncoming(paymentHash) + appKit.nodeParams.db.payments.getIncomingPayment(paymentHash) } override def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala index c91b525574..9e4007a55c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/OldService.scala @@ -307,7 +307,7 @@ trait OldService extends Logging { case _ => Future.failed(new IllegalArgumentException("payment identifier must be a payment request or a payment hash")) } } - found <- Future(appKit.nodeParams.db.payments.getIncoming(ByteVector32.fromValidHex(identifier)).map(_ => JBool(true)).getOrElse(JBool(false))) + found <- Future(appKit.nodeParams.db.payments.getIncomingPayment(ByteVector32.fromValidHex(identifier)).map(_ => JBool(true)).getOrElse(JBool(false))) } yield found) case _ => reject(UnknownParamsRejection(req.id, "[paymentHash] or [paymentRequest]")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 2b7d324238..74e9349317 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -22,42 +22,38 @@ import fr.acinq.eclair.payment.PaymentRequest trait PaymentsDb { - // assumes there is already a payment request for it (the record for the given payment hash) - def addIncomingPayment(payment: IncomingPayment) - // creates a record for a non yet finalized outgoing payment - def addOutgoingPayment(sent: OutgoingPayment) + def addOutgoingPayment(outgoingPayment: OutgoingPayment) // updates the status of the payment, succeededAt OR failedAt - def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) + def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value) - // adds a new payment request and stores its preimage with it - def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) + def getOutgoingPayment(id: UUID): Option[OutgoingPayment] - def getIncoming(paymentHash: ByteVector32): Option[IncomingPayment] + def getOutgoingPayment(paymentHash: ByteVector32): Option[OutgoingPayment] - def getOutgoing(id: UUID): Option[OutgoingPayment] + def listOutgoingPayments(): Seq[OutgoingPayment] - def getOutgoing(paymentHash: ByteVector32): Option[OutgoingPayment] - // return the payment request associated with this paymentHash + def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) + def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] - // returns preimage + invoice def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] - // returns all received payments - def listIncoming(): Seq[IncomingPayment] - - // returns all sent payments - def listOutgoing(): Seq[OutgoingPayment] - - // returns all payment request def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] // returns non paid, non expired payment requests def listPendingPaymentRequests(): Seq[PaymentRequest] + + // assumes there is already a payment request for it (the record for the given payment hash) + def addIncomingPayment(payment: IncomingPayment) + + def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] + + def listIncomingPayments(): Seq[IncomingPayment] + } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 8529d01fc7..5aff841e35 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -81,7 +81,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def updateOutgoingStatus(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { + override def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { val updateStmt = newStatus match { case OutgoingPaymentStatus.SUCCEEDED => "UPDATE sent_payments SET succeeded_at = ? WHERE id = ? AND failed_at IS NULL" case OutgoingPaymentStatus.FAILED => "UPDATE sent_payments SET failed_at = ? WHERE id = ? AND succeeded_at IS NULL" @@ -105,7 +105,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getIncoming(paymentHash: ByteVector32): Option[IncomingPayment] = { + override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ? AND received_msat > 0")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() @@ -117,7 +117,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getOutgoing(id: UUID): Option[OutgoingPayment] = { + override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE id = ?")) { statement => statement.setBytes(1, id.toString.getBytes) val rs = statement.executeQuery() @@ -135,7 +135,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getOutgoing(paymentHash: ByteVector32): Option[OutgoingPayment] = { + override def getOutgoingPayment(paymentHash: ByteVector32): Option[OutgoingPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() @@ -180,7 +180,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listIncoming(): Seq[IncomingPayment] = { + override def listIncomingPayments(): Seq[IncomingPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") var q: Queue[IncomingPayment] = Queue() @@ -191,7 +191,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listOutgoing(): Seq[OutgoingPayment] = { + override def listOutgoingPayments(): Seq[OutgoingPayment] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments") var q: Queue[OutgoingPayment] = Queue() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index d0743c64db..8e2ec55739 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -66,7 +66,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event(Status.Failure(t), WaitingForRoute(s, c, failures)) => reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ LocalFailure(t))) - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) } @@ -74,7 +74,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.SUCCEEDED) + paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED) reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) @@ -85,7 +85,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned @@ -99,7 +99,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis } log.warning(s"too many failed attempts, failing the payment") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ failure)) - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) stop(FSM.Normal) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") @@ -164,7 +164,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => if (failures.size + 1 >= c.maxAttempts) { - paymentsDb.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) reply(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t))) stop(FSM.Normal) } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index d6c132f9f9..a17e4ccbaa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -122,7 +122,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case Status.Failure(AddHtlcFailed(_, paymentHash, _, Local(id, None), _, _)) => // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, paymentHash, Nil)) case Status.Failure(AddHtlcFailed(_, _, error, Local(_, Some(sender)), _, _)) => @@ -156,7 +156,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.SUCCEEDED) + nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED) context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // case ForwardFulfill(fulfill, Local(_, Some(sender)), _) => @@ -170,7 +170,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardFail(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) case ForwardFail(fail, Local(_, Some(sender)), _) => @@ -183,7 +183,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardFailMalformed(_, Local(id, None), add) => // we sent the payment, but we probably restarted and the reference to the original sender was lost // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingStatus(id, OutgoingPaymentStatus.FAILED) + nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.FAILED) context.system.eventStream.publish(PaymentFailed(id, add.paymentHash, Nil)) case ForwardFailMalformed(fail, Local(_, Some(sender)), _) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 20606d1277..9ed8426e23 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -67,7 +67,7 @@ class SqlitePaymentsDbSpec extends FunSuite { } // the existing received payment can NOT be queried anymore - assert(preMigrationDb.getIncoming(oldReceivedPayment.paymentHash).isEmpty) + assert(preMigrationDb.getIncomingPayment(oldReceivedPayment.paymentHash).isEmpty) // add a few rows val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), amountMsat = 12345, createdAt = 12345) @@ -78,8 +78,8 @@ class SqlitePaymentsDbSpec extends FunSuite { preMigrationDb.addIncomingPayment(pr1) preMigrationDb.addOutgoingPayment(ps1) - assert(preMigrationDb.listIncoming() == Seq(pr1)) - assert(preMigrationDb.listOutgoing() == Seq(ps1)) + assert(preMigrationDb.listIncomingPayments() == Seq(pr1)) + assert(preMigrationDb.listOutgoingPayments() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests(0, Long.MaxValue) == Seq(i1)) val postMigrationDb = new SqlitePaymentsDb(connection) @@ -88,8 +88,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(getVersion(statement, "payments", 2) == 2) // version still to 2 } - assert(postMigrationDb.listIncoming() == Seq(pr1)) - assert(postMigrationDb.listOutgoing() == Seq(ps1)) + assert(postMigrationDb.listIncomingPayments() == Seq(pr1)) + assert(postMigrationDb.listOutgoingPayments() == Seq(ps1)) assert(preMigrationDb.listPaymentRequests(0, Long.MaxValue) == Seq(i1)) } @@ -108,12 +108,12 @@ class SqlitePaymentsDbSpec extends FunSuite { val p1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) val p2 = IncomingPayment(i2.paymentHash, 12345678, 1513871928275L) - assert(db.listIncoming() === Nil) + assert(db.listIncomingPayments() === Nil) db.addIncomingPayment(p1) db.addIncomingPayment(p2) - assert(db.listIncoming().toList === List(p1, p2)) - assert(db.getIncoming(p1.paymentHash) === Some(p1)) - assert(db.getIncoming(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) + assert(db.listIncomingPayments().toList === List(p1, p2)) + assert(db.getIncomingPayment(p1.paymentHash) === Some(p1)) + assert(db.getIncomingPayment(ByteVector32(hex"6e7e8018f05e169cf1d99e77dc22cb372d09f10b6a81f1eae410718c56cad187")) === None) } test("add/retrieve/update sent payments") { @@ -123,24 +123,24 @@ class SqlitePaymentsDbSpec extends FunSuite { val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), amountMsat = 12345, createdAt = 12345) val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), amountMsat = 12345, createdAt = 12345) - assert(db.listOutgoing().isEmpty) + assert(db.listOutgoingPayments().isEmpty) db.addOutgoingPayment(s1) db.addOutgoingPayment(s2) - assert(db.listOutgoing().toList == Seq(s1, s2)) - assert(db.getOutgoing(s1.id) === Some(s1)) - assert(db.getOutgoing(UUID.randomUUID()) === None) - assert(db.getOutgoing(s2.paymentHash) === Some(s2)) - assert(db.getOutgoing(ByteVector32.Zeroes) === None) + assert(db.listOutgoingPayments().toList == Seq(s1, s2)) + assert(db.getOutgoingPayment(s1.id) === Some(s1)) + assert(db.getOutgoingPayment(UUID.randomUUID()) === None) + assert(db.getOutgoingPayment(s2.paymentHash) === Some(s2)) + assert(db.getOutgoingPayment(ByteVector32.Zeroes) === None) val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655) db.addOutgoingPayment(s3) - db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.FAILED) - assert(db.getOutgoing(s3.id).get.status == OutgoingPaymentStatus.FAILED) + db.updateOutgoingPayment(s3.id, OutgoingPaymentStatus.FAILED) + assert(db.getOutgoingPayment(s3.id).get.status == OutgoingPaymentStatus.FAILED) // can't update again once it's in a final state - assertThrows[IllegalArgumentException](db.updateOutgoingStatus(s3.id, OutgoingPaymentStatus.SUCCEEDED)) + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(s3.id, OutgoingPaymentStatus.SUCCEEDED)) } test("add/retrieve payment requests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index c5f8083e56..0ad1bff2e7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -50,7 +50,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) assert(nodeParams.db.payments.getRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) @@ -59,32 +59,32 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - assert(nodeParams.db.payments.getIncoming(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "another coffee")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] assert(paymentRelayed.copy(timestamp = 0) === PaymentReceived(amountMsat, add.paymentHash, add.channelId, timestamp = 0)) - assert(nodeParams.db.payments.getIncoming(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.paymentHash == pr.paymentHash)) } { sender.send(handler, ReceivePayment(Some(amountMsat), "bad expiry")) val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) - assert(nodeParams.db.payments.getIncoming(pr.paymentHash).isEmpty) + assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index f2c9066558..4b58484e51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -65,10 +65,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (route too expensive)") { fixture => @@ -88,7 +88,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures - awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (unparsable failure)") { fixture => @@ -107,7 +107,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -131,7 +131,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router sender.expectMsg(PaymentFailed(id, request.paymentHash, UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil)) - awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) // after last attempt the payment is failed } test("payment failed (local error)") { fixture => @@ -150,7 +150,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -163,7 +163,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { fixture => @@ -182,7 +182,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -195,7 +195,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) } test("payment failed (TemporaryChannelFailure)") { fixture => @@ -253,7 +253,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 5) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -270,7 +270,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) @@ -296,7 +296,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // this time the router can't find a route: game over sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment failed (PermanentChannelFailure)") { fixture => @@ -315,7 +315,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, d, maxAttempts = 2) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val WaitingForRoute(_, _, Nil) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) @@ -335,7 +335,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we allow 2 tries, so we send a 2nd request to the router, which won't find another route sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) - awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.FAILED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } test("payment succeeded") { fixture => @@ -356,14 +356,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) 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(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.PENDING)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) val paymentOK = sender.expectMsgType[PaymentSucceeded] val PaymentSent(_, MilliSatoshi(request.amountMsat), fee, request.paymentHash, paymentOK.paymentPreimage, _, _) = eventListener.expectMsgType[PaymentSent] assert(fee > MilliSatoshi(0)) assert(fee === MilliSatoshi(paymentOK.amountMsat - request.amountMsat)) - awaitCond(paymentDb.getOutgoing(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.SUCCEEDED)) } test("payment succeeded to a channel with fees=0") { fixture => From 6fa4c222f543765697293264dfc7cae5f49ce804 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 16:52:35 +0200 Subject: [PATCH 66/83] Renaming, remove unnecessary "lazy" --- .../scala/fr/acinq/eclair/api/ExtraDirectives.scala | 2 +- .../src/main/scala/fr/acinq/eclair/api/Service.scala | 10 +++++----- .../fr/acinq/eclair/payment/LocalPaymentHandler.scala | 2 +- .../fr/acinq/eclair/payment/PaymentLifecycle.scala | 4 +--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala index cc748d9f1c..227d3e367f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala @@ -27,7 +27,7 @@ import scala.util.{Failure, Success} trait ExtraDirectives extends Directives { // custom directive to fail with HTTP 404 (and JSON response) if the element was not found - def completeWithFutureOption[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { + def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { case Success(Some(t)) => complete(t) case Success(None) => complete(HttpResponse(NotFound).withEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("Not found")))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 219f7a631d..9e68e91ffb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -230,16 +230,16 @@ trait Service extends ExtraDirectives with Logging { } ~ path("sentinfo") { formFields("id".as[UUID]) { id => - completeWithFutureOption(eclairApi.sentInfo(Left(id))) + completeOrNotFound(eclairApi.sentInfo(Left(id))) } ~ formFields(paymentHash) { paymentHash => - completeWithFutureOption(eclairApi.sentInfo(Right(paymentHash))) + completeOrNotFound(eclairApi.sentInfo(Right(paymentHash))) } } ~ path("receivedinfo") { formFields(paymentHash) { paymentHash => - completeWithFutureOption(eclairApi.receivedInfo(paymentHash)) + completeOrNotFound(eclairApi.receivedInfo(paymentHash)) } ~ formFields("invoice".as[PaymentRequest]) { invoice => - completeWithFutureOption(eclairApi.receivedInfo(invoice.paymentHash)) + completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash)) } } ~ path("audit") { @@ -257,7 +257,7 @@ trait Service extends ExtraDirectives with Logging { } ~ path("invoice") { formFields(paymentHash) { paymentHash => - completeWithFutureOption(eclairApi.getInvoice(paymentHash)) + completeOrNotFound(eclairApi.getInvoice(paymentHash)) } } ~ path("allinvoices") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 1b71ca3cab..114148a136 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -37,7 +37,7 @@ import scala.util.Try class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLogging { implicit val ec: ExecutionContext = context.system.dispatcher - lazy val paymentDb = nodeParams.db.payments + val paymentDb = nodeParams.db.payments override def receive: Receive = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 8e2ec55739..727914e5f6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -33,8 +33,6 @@ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import scodec.Attempt import scodec.bits.ByteVector - -import scala.compat.Platform import scala.util.{Failure, Success} /** @@ -42,7 +40,7 @@ import scala.util.{Failure, Success} */ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, register: ActorRef) extends FSM[PaymentLifecycle.State, PaymentLifecycle.Data] { - lazy val paymentsDb = nodeParams.db.payments + val paymentsDb = nodeParams.db.payments startWith(WAITING_FOR_REQUEST, WaitingForRequest) From 3de89217da138fc20cbca0168c9ed337f03b7dd6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 16:58:54 +0200 Subject: [PATCH 67/83] Factor out common named parameters --- .../main/scala/fr/acinq/eclair/api/Service.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 9e68e91ffb..e9dfe350e1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -74,6 +74,8 @@ trait Service extends ExtraDirectives with Logging { val paymentHash = "paymentHash".as[ByteVector32](sha256HashUnmarshaller) val from = "from".as[Long] val to = "to".as[Long] + val amountMsat = "amountMsat".as[Long] + val invoice = "invoice".as[PaymentRequest] val apiExceptionHandler = ExceptionHandler { case t: Throwable => @@ -193,29 +195,29 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("receive") { - formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) => + formFields("description".as[String], amountMsat.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) => complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress)) } } ~ path("parseinvoice") { - formFields("invoice".as[PaymentRequest]) { invoice => + formFields(invoice) { invoice => complete(invoice) } } ~ path("findroute") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + formFields(invoice, amountMsat.?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) case (invoice, Some(overrideAmount)) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, invoice.routingInfo)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'")) } } ~ path("findroutetonode") { - formFields(nodeId, "amountMsat".as[Long]) { (nodeId, amount) => + formFields(nodeId, amountMsat) { (nodeId, amount) => complete(eclairApi.findRoute(nodeId, amount)) } } ~ path("send") { - formFields("invoice".as[PaymentRequest], "amountMsat".as[Long].?) { + formFields(invoice, amountMsat.?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) case (invoice, Some(overrideAmount)) => @@ -224,7 +226,7 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("sendtonode") { - formFields("amountMsat".as[Long], paymentHash, "nodeId".as[PublicKey]) { (amountMsat, paymentHash, nodeId) => + formFields(amountMsat, paymentHash, nodeId) { (amountMsat, paymentHash, nodeId) => complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ @@ -238,7 +240,7 @@ trait Service extends ExtraDirectives with Logging { path("receivedinfo") { formFields(paymentHash) { paymentHash => completeOrNotFound(eclairApi.receivedInfo(paymentHash)) - } ~ formFields("invoice".as[PaymentRequest]) { invoice => + } ~ formFields(invoice) { invoice => completeOrNotFound(eclairApi.receivedInfo(invoice.paymentHash)) } } ~ From c51a09d1377ff1b2c109fd17a1ce593a58e7ca8e Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 17:02:12 +0200 Subject: [PATCH 68/83] Rename to 'getPendingRequestAndPreimage' --- .../src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 2 +- .../scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 2 +- .../scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala | 2 +- .../test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 4 ++-- .../scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 74e9349317..56c13adf6d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -39,7 +39,7 @@ trait PaymentsDb { def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] - def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 5aff841e35..b74c34c750 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -166,7 +166,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { + override def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 114148a136..f993ba504d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -58,7 +58,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin } case htlc: UpdateAddHtlc => - paymentDb.getRequestAndPreimage(htlc.paymentHash) match { + paymentDb.getPendingRequestAndPreimage(htlc.paymentHash) match { case Some((paymentPreimage, paymentRequest)) => val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 9ed8426e23..45dfd6392c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -167,8 +167,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) assert(db.listPendingPaymentRequests() == Seq(i1, i2)) - assert(db.getRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) - assert(db.getRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) + assert(db.getPendingRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) + assert(db.getPendingRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) assert(db.listPaymentRequests(someTimestamp - 100, someTimestamp + 100) == Seq(i2)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 0ad1bff2e7..5f9682eab2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -51,7 +51,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - assert(nodeParams.db.payments.getRequestAndPreimage(pr.paymentHash).isDefined) + assert(nodeParams.db.payments.getPendingRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) From 2ec7e290460d9aa870a30285fa692d47f3599ad0 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 17:11:53 +0200 Subject: [PATCH 69/83] ! remove limit of pending payment requests ! --- eclair-core/src/main/resources/reference.conf | 1 - .../main/scala/fr/acinq/eclair/NodeParams.scala | 2 -- .../eclair/payment/LocalPaymentHandler.scala | 3 --- .../scala/fr/acinq/eclair/TestConstants.scala | 2 -- .../acinq/eclair/payment/PaymentHandlerSpec.scala | 15 --------------- 5 files changed, 23 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 672cc0a2bd..a9adce66ae 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -87,7 +87,6 @@ eclair { payment-handler = "local" payment-request-expiry = 1 hour // default expiry for payment requests generated by this node - max-pending-payment-requests = 10000000 min-funding-satoshis = 100000 autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index a6b06c953d..b0fa68a513 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -73,7 +73,6 @@ case class NodeParams(keyManager: KeyManager, channelFlags: Byte, watcherType: WatcherType, paymentRequestExpiry: FiniteDuration, - maxPendingPaymentRequests: Int, minFundingSatoshis: Long, routerConf: RouterConf, socksProxy_opt: Option[Socks5ProxyParams]) { @@ -209,7 +208,6 @@ object NodeParams { channelFlags = config.getInt("channel-flags").toByte, watcherType = watcherType, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), - maxPendingPaymentRequests = config.getInt("max-pending-payment-requests"), minFundingSatoshis = config.getLong("min-funding-satoshis"), routerConf = RouterConf( channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index f993ba504d..4252be7ff7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -43,9 +43,6 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt) => Try { - if (paymentDb.listPendingPaymentRequests().size > nodeParams.maxPendingPaymentRequests) { - throw new RuntimeException(s"too many pending payment requests (max=${nodeParams.maxPendingPaymentRequests})") - } val paymentPreimage = randomBytes32 val paymentHash = Crypto.sha256(paymentPreimage) val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 4af4575694..1991c4299e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -81,7 +81,6 @@ object TestConstants { channelFlags = 1, watcherType = BITCOIND, paymentRequestExpiry = 1 hour, - maxPendingPaymentRequests = 10000000, minFundingSatoshis = 1000L, routerConf = RouterConf( randomizeRouteSelection = false, @@ -145,7 +144,6 @@ object TestConstants { channelFlags = 1, watcherType = BITCOIND, paymentRequestExpiry = 1 hour, - maxPendingPaymentRequests = 10000000, minFundingSatoshis = 1000L, routerConf = RouterConf( randomizeRouteSelection = false, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 5f9682eab2..29f0d0ff0f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -116,21 +116,6 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike assert(pr.amount.contains(MilliSatoshi(100000000L)) && pr.nodeId.toString == nodeParams.nodeId.toString) } - test("Payment request generation should fail when there are too many pending requests") { - val nodeParams = Alice.nodeParams.copy(maxPendingPaymentRequests = 42) - val handler = system.actorOf(LocalPaymentHandler.props(nodeParams)) - val sender = TestProbe() - - for (i <- 0 to nodeParams.maxPendingPaymentRequests) { - sender.send(handler, ReceivePayment(None, s"Request #$i")) - sender.expectMsgType[PaymentRequest] - } - - // over limit - sender.send(handler, ReceivePayment(None, "This one should fail")) - assert(sender.expectMsgType[Status.Failure].cause.getMessage === s"too many pending payment requests (max=${nodeParams.maxPendingPaymentRequests})") - } - test("Payment request generation should succeed when the amount is not set") { val handler = system.actorOf(LocalPaymentHandler.props(Alice.nodeParams)) val sender = TestProbe() From 42ecd6be7ecadaf176259df30b0073c76f1754e4 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 17:25:55 +0200 Subject: [PATCH 70/83] Use seconds in timestamp field for payment events --- .../scala/fr/acinq/eclair/payment/PaymentEvents.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 1bb68fd7c7..fe0289f720 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.payment +import java.time.Instant import java.util.UUID import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} -import scala.compat.Platform /** * Created by PM on 01/02/2017. @@ -27,10 +27,10 @@ sealed trait PaymentEvent { val paymentHash: ByteVector32 } -case class PaymentSent(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: ByteVector32, paymentPreimage: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentSent(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash: ByteVector32, paymentPreimage: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Instant.now().getEpochSecond) extends PaymentEvent -case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Instant.now().getEpochSecond) extends PaymentEvent -case class PaymentReceived(amount: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentReceived(amount: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, timestamp: Long = Instant.now().getEpochSecond) extends PaymentEvent -case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent +case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Instant.now().getEpochSecond) extends PaymentEvent From 7a815ec65e535d22aa1a93307867570675373326 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 12 Apr 2019 17:19:40 +0200 Subject: [PATCH 71/83] getRequestAndPreimage->getPendingPaymentRequestAndPreimage --- .../src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 2 +- .../scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 4 ++-- .../scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala | 2 +- .../test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 4 ++-- .../scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 56c13adf6d..5029977526 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -39,7 +39,7 @@ trait PaymentsDb { def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] - def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] + def getPendingPaymentRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index b74c34c750..c4ba38f1f8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -166,8 +166,8 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getPendingRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { - using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_hash = ?")) { statement => + override def getPendingPaymentRequestAndPreimage(paymentHash: ByteVector32): Option[(ByteVector32, PaymentRequest)] = { + using(sqlite.prepareStatement("SELECT payment_request, preimage FROM received_payments WHERE payment_hash = ? AND received_at IS NULL")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() if (rs.next()) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 4252be7ff7..9d56a61377 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -55,7 +55,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin } case htlc: UpdateAddHtlc => - paymentDb.getPendingRequestAndPreimage(htlc.paymentHash) match { + paymentDb.getPendingPaymentRequestAndPreimage(htlc.paymentHash) match { case Some((paymentPreimage, paymentRequest)) => val minFinalExpiry = Globals.blockCount.get() + paymentRequest.minFinalCltvExpiry.getOrElse(Channel.MIN_CLTV_EXPIRY) // The htlc amount must be equal or greater than the requested amount. A slight overpaying is permitted, however diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 45dfd6392c..a81ed9f4c3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -167,8 +167,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) assert(db.listPendingPaymentRequests() == Seq(i1, i2)) - assert(db.getPendingRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) - assert(db.getPendingRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) + assert(db.getPendingPaymentRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) + assert(db.getPendingPaymentRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) assert(db.listPaymentRequests(someTimestamp - 100, someTimestamp + 100) == Seq(i2)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 29f0d0ff0f..06a2725b45 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -51,7 +51,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, ReceivePayment(Some(amountMsat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - assert(nodeParams.db.payments.getPendingRequestAndPreimage(pr.paymentHash).isDefined) + assert(nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).isDefined) val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) sender.send(handler, add) From d662ee3c9b8a535f64a66ca99dba359cbe021c0a Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 12 Apr 2019 17:37:13 +0200 Subject: [PATCH 72/83] removed migration, changed column types/ordering --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 8 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 192 ++++++++---------- .../acinq/eclair/db/sqlite/SqliteUtils.scala | 13 ++ .../eclair/db/SqlitePaymentsDbSpec.scala | 2 +- 4 files changed, 105 insertions(+), 110 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 5029977526..d3f235ee7d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -61,9 +61,9 @@ trait PaymentsDb { * * @param paymentHash identifier of the payment * @param amountMsat amount of the payment, in milli-satoshis - * @param timestamp absolute time in seconds since UNIX epoch when the payment was created. + * @param receivedAt absolute time in seconds since UNIX epoch when the payment was received. */ -case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, timestamp: Long) +case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, receivedAt: Long) /** * Sent payment is every payment that is sent by this node, they may not be finalized and @@ -73,7 +73,9 @@ case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, timestam * @param paymentHash payment_hash * @param amountMsat amount of the payment, in milli-satoshis * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. - * @param paidAt absolute time in seconds since UNIX epoch when the payment was last updated. + * @param succeededAt absolute time in seconds since UNIX epoch when the payment succeeded. + * @param failedAt absolute time in seconds since UNIX epoch when the payment failed. + * @param status current status of the payment. */ case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, succeededAt: Option[Long], failedAt: Option[Long], status: OutgoingPaymentStatus.Value) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index c4ba38f1f8..62868b49db 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import java.sql.{Connection, ResultSet} +import java.sql.Connection import java.time.Instant import java.util.UUID @@ -28,56 +28,29 @@ import grizzled.slf4j.Logging import scala.collection.immutable.Queue import scala.compat.Platform -import scala.util.{Failure, Success, Try} class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { import SqliteUtils.ExtendedResultSet._ val DB_NAME = "payments" - val PREVIOUS_VERSION = 1 val CURRENT_VERSION = 2 using(sqlite.createStatement()) { statement => - getVersion(statement, DB_NAME, CURRENT_VERSION) match { - case PREVIOUS_VERSION => - logger.warn(s"Performing db migration for DB $DB_NAME, found version=$PREVIOUS_VERSION current=$CURRENT_VERSION") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS payments (payment_hash BLOB NOT NULL PRIMARY KEY, amount_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER, created_at INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, succeeded_at INTEGER, failed_at INTEGER)") - setVersion(statement, DB_NAME, CURRENT_VERSION) - case CURRENT_VERSION => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request VARCHAR NOT NULL, received_msat INTEGER, received_at INTEGER, expire_at INTEGER, created_at INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id BLOB NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, succeeded_at INTEGER, failed_at INTEGER)") - case unknownVersion => - throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") - } - } - - override def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32): Unit = { - val insertStmt = pr.expiry match { - case Some(_) => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)" - case None => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at) VALUES (?, ?, ?, ?)" - } - - using(sqlite.prepareStatement(insertStmt)) { statement => - statement.setBytes(1, pr.paymentHash.toArray) - statement.setBytes(2, preimage.toArray) - statement.setString(3, PaymentRequest.write(pr)) - statement.setLong(4, pr.timestamp) - pr.expiry.foreach { ex => statement.setLong(5, pr.timestamp + ex) } // we store "when" the invoice will expire - statement.executeUpdate() - } + require(getVersion(statement, DB_NAME, CURRENT_VERSION) <= CURRENT_VERSION) // version 2 is "backward compatible" in the sense that it uses separate tables from version 1. There is no migration though + statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, succeeded_at INTEGER, failed_at INTEGER)") + setVersion(statement, DB_NAME, CURRENT_VERSION) } - override def addIncomingPayment(payment: IncomingPayment): Unit = { - using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => - statement.setLong(1, payment.amountMsat) - statement.setLong(2, payment.timestamp) - statement.setBytes(3, payment.paymentHash.toArray) + override def addOutgoingPayment(sent: OutgoingPayment): Unit = { + using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at) VALUES (?, ?, ?, ?)")) { statement => + statement.setString(1, sent.id.toString) + statement.setBytes(2, sent.paymentHash.toArray) + statement.setLong(3, sent.amountMsat) + statement.setLong(4, sent.createdAt) val res = statement.executeUpdate() - if (res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") + logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") } } @@ -89,46 +62,23 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { using(sqlite.prepareStatement(updateStmt)) { statement => statement.setLong(1, Instant.now().getEpochSecond) - statement.setBytes(2, id.toString.getBytes) - if(statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to update an outgoing payment (id=$id) already in final status with=$newStatus") - } - } - - override def addOutgoingPayment(sent: OutgoingPayment): Unit = { - using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at) VALUES (?, ?, ?, ?)")) { statement => - statement.setBytes(1, sent.id.toString.getBytes) - statement.setBytes(2, sent.paymentHash.toArray) - statement.setLong(3, sent.amountMsat) - statement.setLong(4, sent.createdAt) - val res = statement.executeUpdate() - logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") - } - } - - override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { - using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ? AND received_msat > 0")) { statement => - statement.setBytes(1, paymentHash.toArray) - val rs = statement.executeQuery() - if (rs.next()) { - Some(IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) - } else { - None - } + statement.setString(2, id.toString) + if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to update an outgoing payment (id=$id) already in final status with=$newStatus") } } override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE id = ?")) { statement => - statement.setBytes(1, id.toString.getBytes) + statement.setString(1, id.toString) val rs = statement.executeQuery() if (rs.next()) { Some(OutgoingPayment( - UUID.fromString(new String(rs.getBytes("id"))), + UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), - getNullableTimestamp(rs, "succeeded_at"), - getNullableTimestamp(rs, "failed_at"))) + getNullableLong(rs, "succeeded_at"), + getNullableLong(rs, "failed_at"))) } else { None } @@ -141,18 +91,50 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { val rs = statement.executeQuery() if (rs.next()) { Some(OutgoingPayment( - UUID.fromString(new String(rs.getBytes("id"))), + UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), - getNullableTimestamp(rs, "succeeded_at"), - getNullableTimestamp(rs, "failed_at"))) + getNullableLong(rs, "succeeded_at"), + getNullableLong(rs, "failed_at"))) } else { None } } } + override def listOutgoingPayments(): Seq[OutgoingPayment] = { + using(sqlite.createStatement()) { statement => + val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments") + var q: Queue[OutgoingPayment] = Queue() + while (rs.next()) { + q = q :+ OutgoingPayment( + UUID.fromString(rs.getString("id")), + rs.getByteVector32("payment_hash"), + rs.getLong("amount_msat"), + rs.getLong("created_at"), + getNullableLong(rs, "succeeded_at"), + getNullableLong(rs, "failed_at")) + } + q + } + } + + override def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32): Unit = { + val insertStmt = pr.expiry match { + case Some(_) => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)" + case None => "INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at) VALUES (?, ?, ?, ?)" + } + + using(sqlite.prepareStatement(insertStmt)) { statement => + statement.setBytes(1, pr.paymentHash.toArray) + statement.setBytes(2, preimage.toArray) + statement.setString(3, PaymentRequest.write(pr)) + statement.setLong(4, pr.timestamp) + pr.expiry.foreach { ex => statement.setLong(5, pr.timestamp + ex) } // we store "when" the invoice will expire + statement.executeUpdate() + } + } override def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] = { using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE payment_hash = ?")) { statement => @@ -180,41 +162,6 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listIncomingPayments(): Seq[IncomingPayment] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") - var q: Queue[IncomingPayment] = Queue() - while (rs.next()) { - q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) - } - q - } - } - - override def listOutgoingPayments(): Seq[OutgoingPayment] = { - using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments") - var q: Queue[OutgoingPayment] = Queue() - while (rs.next()) { - q = q :+ OutgoingPayment( - UUID.fromString(new String(rs.getBytes("id"))), - rs.getByteVector32("payment_hash"), - rs.getLong("amount_msat"), - rs.getLong("created_at"), - getNullableTimestamp(rs, "succeeded_at"), - getNullableTimestamp(rs, "failed_at")) - } - q - } - } - - def getNullableTimestamp(rs: ResultSet, column: String): Option[Long] = Try(rs.getLong(column)) match { - case Success(0) => None - case Success(timestamp) => Some(timestamp) - case Failure(exception) => None - } - - override def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = { using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ?")) { statement => statement.setLong(1, from) @@ -240,4 +187,37 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } + override def addIncomingPayment(payment: IncomingPayment): Unit = { + using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (?, ?) WHERE payment_hash = ?")) { statement => + statement.setLong(1, payment.amountMsat) + statement.setLong(2, payment.receivedAt) + statement.setBytes(3, payment.paymentHash.toArray) + val res = statement.executeUpdate() + if (res == 0) throw new IllegalArgumentException("Inserted a received payment without having an invoice") + } + } + + override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { + using(sqlite.prepareStatement("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE payment_hash = ? AND received_msat > 0")) { statement => + statement.setBytes(1, paymentHash.toArray) + val rs = statement.executeQuery() + if (rs.next()) { + Some(IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at"))) + } else { + None + } + } + } + + override def listIncomingPayments(): Seq[IncomingPayment] = { + using(sqlite.createStatement()) { statement => + val rs = statement.executeQuery("SELECT payment_hash, received_msat, received_at FROM received_payments WHERE received_msat > 0") + var q: Queue[IncomingPayment] = Queue() + while (rs.next()) { + q = q :+ IncomingPayment(rs.getByteVector32("payment_hash"), rs.getLong("received_msat"), rs.getLong("received_at")) + } + q + } + } + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index fce229bdcd..3922e87d3d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -92,6 +92,19 @@ object SqliteUtils { q } + /** + * This helper retrieves the value from a nullable integer column and interprets it as an option. This is needed + * because `rs.getLong` would return `0` for a null value. + * It is used on Android only + * + * @param label + * @return + */ + def getNullableLong(rs: ResultSet, label: String) : Option[Long] = { + val result = rs.getLong(label) + if (rs.wasNull()) None else Some(result) + } + /** * Obtain an exclusive lock on a sqlite database. This is useful when we want to make sure that only one process * accesses the database file (see https://www.sqlite.org/pragma.html). diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index a81ed9f4c3..27bce3acc4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -56,7 +56,7 @@ class SqlitePaymentsDbSpec extends FunSuite { using(connection.prepareStatement("INSERT INTO payments VALUES (?, ?, ?)")) { statement => statement.setBytes(1, oldReceivedPayment.paymentHash.toArray) statement.setLong(2, oldReceivedPayment.amountMsat) - statement.setLong(3, oldReceivedPayment.timestamp) + statement.setLong(3, oldReceivedPayment.receivedAt) statement.executeUpdate() } From df7903376f09debc10718e8b26dec93248ad366f Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 18:00:43 +0200 Subject: [PATCH 73/83] Expose from/to filters in listPendingInvoices() --- .../main/scala/fr/acinq/eclair/Eclair.scala | 9 ++++--- .../scala/fr/acinq/eclair/api/Service.scala | 4 ++- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 3 +-- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 25 +++++++++---------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 2 +- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index fff6410221..854ce97be4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -74,7 +74,7 @@ trait Eclair { def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] - def pendingInvoices(): Future[Seq[PaymentRequest]] + def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] @@ -204,8 +204,11 @@ class EclairImpl(appKit: Kit) extends Eclair { appKit.nodeParams.db.payments.listPaymentRequests(from, to) } - override def pendingInvoices(): Future[Seq[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.listPendingPaymentRequests() + override def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] = Future { + val from = from_opt.getOrElse(0L) + val to = to_opt.getOrElse(Long.MaxValue) + + appKit.nodeParams.db.payments.listPendingPaymentRequests(from, to) } override def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] = Future { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index e9dfe350e1..26ad50d68a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -268,7 +268,9 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("pendinginvoices") { - complete(eclairApi.pendingInvoices()) + formFields(from.?, to.?) { (from_opt, to_opt) => + complete(eclairApi.pendingInvoices(from_opt, to_opt)) + } } } ~ get { path("ws") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index d3f235ee7d..49a55e78f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -44,8 +44,7 @@ trait PaymentsDb { def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] // returns non paid, non expired payment requests - def listPendingPaymentRequests(): Seq[PaymentRequest] - + def listPendingPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] // assumes there is already a payment request for it (the record for the given payment hash) def addIncomingPayment(payment: IncomingPayment) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 62868b49db..5db96eedde 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -162,22 +162,21 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ?")) { statement => + override def listPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = listPaymentRequests(from, to, pendingOnly = false) + + override def listPendingPaymentRequests(from: Long, to: Long): Seq[PaymentRequest] = listPaymentRequests(from, to, pendingOnly = true) + + def listPaymentRequests(from: Long, to: Long, pendingOnly: Boolean): Seq[PaymentRequest] = { + val queryStmt = pendingOnly match { + case true => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ? AND (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL" + case false => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ?" + } + + using(sqlite.prepareStatement(queryStmt)) { statement => statement.setLong(1, from) statement.setLong(2, to) - val rs = statement.executeQuery() - var q: Queue[PaymentRequest] = Queue() - while (rs.next()) { - q = q :+ PaymentRequest.read(rs.getString("payment_request")) - } - q - } - } + if(pendingOnly) statement.setLong(3, Instant.now().getEpochSecond) - override def listPendingPaymentRequests(): Seq[PaymentRequest] = { - using(sqlite.prepareStatement("SELECT payment_request FROM received_payments WHERE (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL")) { statement => - statement.setLong(1, Platform.currentTime / 1000) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() while (rs.next()) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 478a65f913..974adc9df4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -92,7 +92,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def getInvoice(paymentHash: ByteVector32): Future[Option[PaymentRequest]] = ??? - override def pendingInvoices(): Future[Seq[PaymentRequest]] = ??? + override def pendingInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] = ??? } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 27bce3acc4..1ca9c15119 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -166,7 +166,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) - assert(db.listPendingPaymentRequests() == Seq(i1, i2)) + assert(db.listPendingPaymentRequests(0, Long.MaxValue) == Seq(i1, i2)) assert(db.getPendingPaymentRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) assert(db.getPendingPaymentRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) From 260480063f2a97c1b37beb189aaacbce318f06a8 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 12 Apr 2019 18:10:01 +0200 Subject: [PATCH 74/83] Check condition before expecting the state transition in PaymentLifecycleSpec --- .../scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 4b58484e51..9d8edb24a1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -64,8 +64,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(defaultAmountMsat, defaultPaymentHash, f) sender.send(paymentFSM, request) + awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.PENDING)) sender.expectMsg(PaymentFailed(id, request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) From 3e8b514d373670e4be124217a67870d66cad7583 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 11:38:50 +0200 Subject: [PATCH 75/83] Avoid printing "null" fields when serializing an invoice to json --- .../fr/acinq/eclair/api/JsonSerializers.scala | 37 ++++++++++++------- .../eclair/api/JsonSerializersSpec.scala | 3 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 0ac12a74b8..86157ec531 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -138,20 +138,29 @@ class DirectionSerializer extends CustomSerializer[Direction](format => ({ null case d: Direction => JString(d.toString) })) -class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format => ({ null },{ - case p: PaymentRequest => JObject(JField("prefix", JString(p.prefix)) :: - JField("amount", if (p.amount.isDefined) JLong(p.amount.get.toLong) else JNull) :: - JField("timestamp", JLong(p.timestamp)) :: - JField("nodeId", JString(p.nodeId.toString())) :: - JField("description", JString(p.description match { - case Left(l) => l.toString() - case Right(r) => r.toString() - })) :: - JField("paymentHash", JString(p.paymentHash.toString())) :: - JField("expiry", if (p.expiry.isDefined) JLong(p.expiry.get) else JNull) :: - JField("minFinalCltvExpiry", if (p.minFinalCltvExpiry.isDefined) JLong(p.minFinalCltvExpiry.get) else JNull) :: - JField("serialized", JString(PaymentRequest.write(p))) :: - Nil) +class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format => ( { + null +}, { + case p: PaymentRequest => { + val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq + val minFinalCltvExpiry = p.minFinalCltvExpiry.map(mfce => JField("minFinalCltvExpiry", JLong(mfce))).toSeq + val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq + + val fieldList = List(JField("prefix", JString(p.prefix)), + JField("timestamp", JLong(p.timestamp)), + JField("nodeId", JString(p.nodeId.toString())), + JField("serialized", JString(PaymentRequest.write(p))), + JField("description", JString(p.description match { + case Left(l) => l.toString() + case Right(r) => r.toString() + })), + JField("paymentHash", JString(p.paymentHash.toString()))) ++ + expiry ++ + minFinalCltvExpiry ++ + amount + + JObject(fieldList) + } })) class JavaUUIDSerializer extends CustomSerializer[UUID](format => ({ null }, { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 3d152371cb..1dd8033013 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -28,7 +28,6 @@ import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} import org.json4s.jackson.Serialization -import org.json4s.{DefaultFormats, ShortTypeHints} import org.scalatest.{FunSuite, Matchers} import scodec.bits._ @@ -75,7 +74,7 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("Payment Request") { val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" val pr = PaymentRequest.read(ref) - Serialization.write(pr)(org.json4s.DefaultFormats + new PaymentRequestSerializer) shouldBe """{"prefix":"lnbc","amount":250000000,"timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"minFinalCltvExpiry":null,"serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp"}""" + JsonSupport.serialization.write(pr)(JsonSupport.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000}""" } test("type hints") { From 31a1fa1d51c0e48b2ad07e2c44ec595e91e8398f Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 13:30:00 +0200 Subject: [PATCH 76/83] Output all outgoing payments related to a payment hash in /sentinfo --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 8 ++++---- .../src/main/scala/fr/acinq/eclair/api/Service.scala | 4 ++-- .../main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 ++-- .../fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 12 ++++++------ .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- .../fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 854ce97be4..2b87ed0847 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -62,7 +62,7 @@ trait Eclair { def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry: Option[Long] = None): Future[UUID] - def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] + def sentInfo(id: Either[UUID, ByteVector32]): Future[Seq[OutgoingPayment]] def findRoute(targetNodeId: PublicKey, amountMsat: Long, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty): Future[RouteResponse] @@ -166,10 +166,10 @@ class EclairImpl(appKit: Kit) extends Eclair { (appKit.paymentInitiator ? sendPayment).mapTo[UUID] } - override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = Future { + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Seq[OutgoingPayment]] = Future { id match { - case Left(uuid) => appKit.nodeParams.db.payments.getOutgoingPayment(uuid) - case Right(paymentHash) => appKit.nodeParams.db.payments.getOutgoingPayment(paymentHash) + case Left(uuid) => appKit.nodeParams.db.payments.getOutgoingPayment(uuid).toSeq + case Right(paymentHash) => appKit.nodeParams.db.payments.getOutgoingPayments(paymentHash) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 26ad50d68a..cf7c389237 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -232,9 +232,9 @@ trait Service extends ExtraDirectives with Logging { } ~ path("sentinfo") { formFields("id".as[UUID]) { id => - completeOrNotFound(eclairApi.sentInfo(Left(id))) + complete(eclairApi.sentInfo(Left(id))) } ~ formFields(paymentHash) { paymentHash => - completeOrNotFound(eclairApi.sentInfo(Right(paymentHash))) + complete(eclairApi.sentInfo(Right(paymentHash))) } } ~ path("receivedinfo") { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 49a55e78f9..32a760cd27 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -30,11 +30,11 @@ trait PaymentsDb { def getOutgoingPayment(id: UUID): Option[OutgoingPayment] - def getOutgoingPayment(paymentHash: ByteVector32): Option[OutgoingPayment] + // all the outgoing payment (attempts) to pay the given paymentHash + def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] def listOutgoingPayments(): Seq[OutgoingPayment] - def addPaymentRequest(pr: PaymentRequest, preimage: ByteVector32) def getPaymentRequest(paymentHash: ByteVector32): Option[PaymentRequest] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 5db96eedde..a5d50675fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -85,21 +85,21 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } } - override def getOutgoingPayment(paymentHash: ByteVector32): Option[OutgoingPayment] = { + override def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = { using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() - if (rs.next()) { - Some(OutgoingPayment( + var q: Queue[OutgoingPayment] = Queue() + while (rs.next()) { + q = q :+ OutgoingPayment( UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), rs.getLong("amount_msat"), rs.getLong("created_at"), getNullableLong(rs, "succeeded_at"), - getNullableLong(rs, "failed_at"))) - } else { - None + getNullableLong(rs, "failed_at")) } + q } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 974adc9df4..a78d8ac679 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -86,7 +86,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def getInfoResponse(): Future[GetInfoResponse] = ??? - override def sentInfo(id: Either[UUID, ByteVector32]): Future[Option[OutgoingPayment]] = ??? + override def sentInfo(id: Either[UUID, ByteVector32]): Future[Seq[OutgoingPayment]] = ??? override def allInvoices(from_opt: Option[Long], to_opt: Option[Long]): Future[Seq[PaymentRequest]] = ??? diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 1ca9c15119..0eaf4aff94 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -130,8 +130,8 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.listOutgoingPayments().toList == Seq(s1, s2)) assert(db.getOutgoingPayment(s1.id) === Some(s1)) assert(db.getOutgoingPayment(UUID.randomUUID()) === None) - assert(db.getOutgoingPayment(s2.paymentHash) === Some(s2)) - assert(db.getOutgoingPayment(ByteVector32.Zeroes) === None) + assert(db.getOutgoingPayments(s2.paymentHash) === Seq(s2)) + assert(db.getOutgoingPayments(ByteVector32.Zeroes) === Seq.empty) val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655) db.addOutgoingPayment(s3) From 61357828d8f5b86d0b2cbc6f753a3c08d75312ab Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 15:19:32 +0200 Subject: [PATCH 77/83] Store preimage for successful sent payments, don't store successfulAt/failedAt --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 24 ++-------- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 48 ++++++++++--------- .../acinq/eclair/db/sqlite/SqliteUtils.scala | 5 ++ .../eclair/payment/PaymentLifecycle.scala | 4 +- .../fr/acinq/eclair/payment/Relayer.scala | 2 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 20 +++++--- 6 files changed, 52 insertions(+), 51 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 32a760cd27..ed1781a112 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -25,8 +25,8 @@ trait PaymentsDb { // creates a record for a non yet finalized outgoing payment def addOutgoingPayment(outgoingPayment: OutgoingPayment) - // updates the status of the payment, succeededAt OR failedAt - def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value) + // updates the status of the payment, if the newStatus is SUCCEEDED you must supply a preimage + def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value, preimage: Option[ByteVector32] = None) def getOutgoingPayment(id: UUID): Option[OutgoingPayment] @@ -76,26 +76,10 @@ case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, received * @param failedAt absolute time in seconds since UNIX epoch when the payment failed. * @param status current status of the payment. */ -case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, succeededAt: Option[Long], failedAt: Option[Long], status: OutgoingPaymentStatus.Value) +case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, preimage:Option[ByteVector32], amountMsat: Long, createdAt: Long, completedAt: Option[Long], status: OutgoingPaymentStatus.Value) object OutgoingPaymentStatus extends Enumeration { val PENDING = Value(1, "PENDING") val SUCCEEDED = Value(2, "SUCCEEDED") val FAILED = Value(3, "FAILED") -} - -object OutgoingPayment { - - import OutgoingPaymentStatus._ - - def apply(id: UUID, paymentHash: ByteVector32, amountMsat: Long, createdAt: Long, succeededAt: Option[Long] = None, failedAt: Option[Long] = None): OutgoingPayment = { - val status = (succeededAt, failedAt) match { - case (None, None) => PENDING - case (Some(_), None) => SUCCEEDED - case (None, Some(_)) => FAILED - case (Some(_), Some(_)) => throw new RuntimeException(s"Invalid update field found for outgoing payment id=$id") - } - new OutgoingPayment(id, paymentHash, amountMsat, createdAt, succeededAt, failedAt, status = status) - } - -} +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index a5d50675fc..3de924179d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -19,15 +19,13 @@ package fr.acinq.eclair.db.sqlite import java.sql.Connection import java.time.Instant import java.util.UUID - import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.db.{IncomingPayment, OutgoingPayment, OutgoingPaymentStatus, PaymentsDb} import fr.acinq.eclair.payment.PaymentRequest import grizzled.slf4j.Logging - import scala.collection.immutable.Queue -import scala.compat.Platform +import OutgoingPaymentStatus._ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { @@ -39,46 +37,48 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { using(sqlite.createStatement()) { statement => require(getVersion(statement, DB_NAME, CURRENT_VERSION) <= CURRENT_VERSION) // version 2 is "backward compatible" in the sense that it uses separate tables from version 1. There is no migration though statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, succeeded_at INTEGER, failed_at INTEGER)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL)") setVersion(statement, DB_NAME, CURRENT_VERSION) } override def addOutgoingPayment(sent: OutgoingPayment): Unit = { - using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at) VALUES (?, ?, ?, ?)")) { statement => + using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status) VALUES (?, ?, ?, ?, ?)")) { statement => statement.setString(1, sent.id.toString) statement.setBytes(2, sent.paymentHash.toArray) statement.setLong(3, sent.amountMsat) statement.setLong(4, sent.createdAt) + statement.setString(5, sent.status.toString) val res = statement.executeUpdate() logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB") } } - override def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value) = { - val updateStmt = newStatus match { - case OutgoingPaymentStatus.SUCCEEDED => "UPDATE sent_payments SET succeeded_at = ? WHERE id = ? AND failed_at IS NULL" - case OutgoingPaymentStatus.FAILED => "UPDATE sent_payments SET failed_at = ? WHERE id = ? AND succeeded_at IS NULL" - } + override def updateOutgoingPayment(id: UUID, newStatus: OutgoingPaymentStatus.Value, preimage: Option[ByteVector32] = None) = { + require((newStatus == SUCCEEDED && preimage.isDefined) || (newStatus == FAILED && preimage.isEmpty), "Wrong combination of state/preimage") - using(sqlite.prepareStatement(updateStmt)) { statement => + using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, preimage, status) = (?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement => statement.setLong(1, Instant.now().getEpochSecond) - statement.setString(2, id.toString) + statement.setBytes(2, if (preimage.isEmpty) null else preimage.get.toArray) + statement.setString(3, newStatus.toString) + statement.setString(4, id.toString) if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to update an outgoing payment (id=$id) already in final status with=$newStatus") } } override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE id = ?")) { statement => + using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments WHERE id = ?")) { statement => statement.setString(1, id.toString) val rs = statement.executeQuery() if (rs.next()) { Some(OutgoingPayment( UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), + rs.getByteVector32Nullable("preimage"), rs.getLong("amount_msat"), rs.getLong("created_at"), - getNullableLong(rs, "succeeded_at"), - getNullableLong(rs, "failed_at"))) + getNullableLong(rs, "completed_at"), + OutgoingPaymentStatus.withName(rs.getString("status")) + )) } else { None } @@ -86,7 +86,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { } override def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = { - using(sqlite.prepareStatement("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments WHERE payment_hash = ?")) { statement => + using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments WHERE payment_hash = ?")) { statement => statement.setBytes(1, paymentHash.toArray) val rs = statement.executeQuery() var q: Queue[OutgoingPayment] = Queue() @@ -94,10 +94,12 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { q = q :+ OutgoingPayment( UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), + rs.getByteVector32Nullable("preimage"), rs.getLong("amount_msat"), rs.getLong("created_at"), - getNullableLong(rs, "succeeded_at"), - getNullableLong(rs, "failed_at")) + getNullableLong(rs, "completed_at"), + OutgoingPaymentStatus.withName(rs.getString("status")) + ) } q } @@ -105,16 +107,18 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { override def listOutgoingPayments(): Seq[OutgoingPayment] = { using(sqlite.createStatement()) { statement => - val rs = statement.executeQuery("SELECT id, payment_hash, amount_msat, created_at, succeeded_at, failed_at FROM sent_payments") + val rs = statement.executeQuery("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments") var q: Queue[OutgoingPayment] = Queue() while (rs.next()) { q = q :+ OutgoingPayment( UUID.fromString(rs.getString("id")), rs.getByteVector32("payment_hash"), + rs.getByteVector32Nullable("preimage"), rs.getLong("amount_msat"), rs.getLong("created_at"), - getNullableLong(rs, "succeeded_at"), - getNullableLong(rs, "failed_at")) + getNullableLong(rs, "completed_at"), + OutgoingPaymentStatus.withName(rs.getString("status")) + ) } q } @@ -175,7 +179,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { using(sqlite.prepareStatement(queryStmt)) { statement => statement.setLong(1, from) statement.setLong(2, to) - if(pendingOnly) statement.setLong(3, Instant.now().getEpochSecond) + if (pendingOnly) statement.setLong(3, Instant.now().getEpochSecond) val rs = statement.executeQuery() var q: Queue[PaymentRequest] = Queue() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 3922e87d3d..e14611243b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -126,6 +126,11 @@ object SqliteUtils { def getByteVector(columnLabel: String): ByteVector = ByteVector(rs.getBytes(columnLabel)) def getByteVector32(columnLabel: String): ByteVector32 = ByteVector32(ByteVector(rs.getBytes(columnLabel))) + + def getByteVector32Nullable(columnLabel: String): Option[ByteVector32] = { + val bytes = rs.getBytes(columnLabel) + if(rs.wasNull()) None else Some(ByteVector32(ByteVector(bytes))) + } } object ExtendedResultSet { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 727914e5f6..909645a5c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -47,7 +47,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis when(WAITING_FOR_REQUEST) { case Event(c: SendPayment, WaitingForRequest) => router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, routeParams = c.routeParams) - paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, c.amountMsat, Instant.now().getEpochSecond, succeededAt = None, failedAt = None)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, c.paymentHash, None, c.amountMsat, Instant.now().getEpochSecond, None, OutgoingPaymentStatus.PENDING)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) } @@ -72,7 +72,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case Event("ok", _) => stay() case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, hops)) => - paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED) + paymentsDb.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED, preimage = Some(fulfill.paymentPreimage)) reply(s, PaymentSucceeded(id, cmd.amountMsat, c.paymentHash, fulfill.paymentPreimage, hops)) context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(c.amountMsat), MilliSatoshi(cmd.amountMsat - c.amountMsat), cmd.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) stop(FSM.Normal) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index a17e4ccbaa..517ffad57a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -156,7 +156,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR context.system.eventStream.publish(PaymentSent(id, MilliSatoshi(add.amountMsat), feesPaid, add.paymentHash, fulfill.paymentPreimage, fulfill.channelId)) // we sent the payment, but we probably restarted and the reference to the original sender was lost, // we publish the failure on the event stream and update the status in paymentDb - nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED) + nodeParams.db.payments.updateOutgoingPayment(id, OutgoingPaymentStatus.SUCCEEDED, Some(fulfill.paymentPreimage)) context.system.eventStream.publish(PaymentSucceeded(id, add.amountMsat, add.paymentHash, fulfill.paymentPreimage, Nil)) // case ForwardFulfill(fulfill, Local(_, Some(sender)), _) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 0eaf4aff94..11e2205875 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -28,6 +28,7 @@ import org.scalatest.FunSuite import scodec.bits._ import fr.acinq.eclair.randomBytes32 import scala.compat.Platform +import OutgoingPaymentStatus._ class SqlitePaymentsDbSpec extends FunSuite { @@ -70,7 +71,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(preMigrationDb.getIncomingPayment(oldReceivedPayment.paymentHash).isEmpty) // add a few rows - val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), amountMsat = 12345, createdAt = 12345) + val ps1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) val i1 = PaymentRequest.read("lnbc10u1pw2t4phpp5ezwm2gdccydhnphfyepklc0wjkxhz0r4tctg9paunh2lxgeqhcmsdqlxycrqvpqwdshgueqvfjhggr0dcsry7qcqzpgfa4ecv7447p9t5hkujy9qgrxvkkf396p9zar9p87rv2htmeuunkhydl40r64n5s2k0u7uelzc8twxmp37nkcch6m0wg5tvvx69yjz8qpk94qf3") val pr1 = IncomingPayment(i1.paymentHash, 12345678, 1513871928275L) @@ -120,8 +121,8 @@ class SqlitePaymentsDbSpec extends FunSuite { val db = new SqlitePaymentsDb(TestConstants.sqliteInMemory()) - val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), amountMsat = 12345, createdAt = 12345) - val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), amountMsat = 12345, createdAt = 12345) + val s1 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"0f059ef9b55bb70cc09069ee4df854bf0fab650eee6f2b87ba26d1ad08ab114f"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) + val s2 = OutgoingPayment(id = UUID.randomUUID(), paymentHash = ByteVector32(hex"08d47d5f7164d4b696e8f6b62a03094d4f1c65f16e9d7b11c4a98854707e55cf"), None, amountMsat = 12345, createdAt = 12345, None, PENDING) assert(db.listOutgoingPayments().isEmpty) db.addOutgoingPayment(s1) @@ -129,6 +130,7 @@ class SqlitePaymentsDbSpec extends FunSuite { assert(db.listOutgoingPayments().toList == Seq(s1, s2)) assert(db.getOutgoingPayment(s1.id) === Some(s1)) + assert(db.getOutgoingPayment(s1.id).get.completedAt.isEmpty) assert(db.getOutgoingPayment(UUID.randomUUID()) === None) assert(db.getOutgoingPayments(s2.paymentHash) === Seq(s2)) assert(db.getOutgoingPayments(ByteVector32.Zeroes) === Seq.empty) @@ -136,11 +138,17 @@ class SqlitePaymentsDbSpec extends FunSuite { val s3 = s2.copy(id = UUID.randomUUID(), amountMsat = 88776655) db.addOutgoingPayment(s3) - db.updateOutgoingPayment(s3.id, OutgoingPaymentStatus.FAILED) - assert(db.getOutgoingPayment(s3.id).get.status == OutgoingPaymentStatus.FAILED) + db.updateOutgoingPayment(s3.id, FAILED) + assert(db.getOutgoingPayment(s3.id).get.status == FAILED) + assert(db.getOutgoingPayment(s3.id).get.preimage.isEmpty) // failed sent payments don't have a preimage + assert(db.getOutgoingPayment(s3.id).get.completedAt.isDefined) // can't update again once it's in a final state - assertThrows[IllegalArgumentException](db.updateOutgoingPayment(s3.id, OutgoingPaymentStatus.SUCCEEDED)) + assertThrows[IllegalArgumentException](db.updateOutgoingPayment(s3.id, SUCCEEDED)) + + db.updateOutgoingPayment(s1.id, SUCCEEDED, Some(ByteVector32.One)) + assert(db.getOutgoingPayment(s1.id).get.preimage.isDefined) + assert(db.getOutgoingPayment(s1.id).get.completedAt.isDefined) } test("add/retrieve payment requests") { From 64c94d7260a9d1cddff013e3e7c2141f5b50e0ae Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 15:21:44 +0200 Subject: [PATCH 78/83] Update javadoc for OutgoingPayment --- .../src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index ed1781a112..a72f151016 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -70,10 +70,10 @@ case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, received * * @param id internal payment identifier * @param paymentHash payment_hash + * @param preimage the preimage of the payment_hash, known if the outgoing payment was successful * @param amountMsat amount of the payment, in milli-satoshis * @param createdAt absolute time in seconds since UNIX epoch when the payment was created. - * @param succeededAt absolute time in seconds since UNIX epoch when the payment succeeded. - * @param failedAt absolute time in seconds since UNIX epoch when the payment failed. + * @param completedAt absolute time in seconds since UNIX epoch when the payment succeeded. * @param status current status of the payment. */ case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, preimage:Option[ByteVector32], amountMsat: Long, createdAt: Long, completedAt: Option[Long], status: OutgoingPaymentStatus.Value) From 03125beb59725f2862d06907dfd05c55565940e9 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 15:39:34 +0200 Subject: [PATCH 79/83] Rename API enpoints, reorg endpoint definitions in Service --- eclair-core/eclair-cli | 4 +- .../scala/fr/acinq/eclair/api/Service.scala | 66 +++++++++---------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 948475492f..8472c37cc3 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -30,8 +30,8 @@ and COMMAND is one of: getinfo, connect, open, close, forceclose, updaterelayfee, peers, channels, channel, allnodes, allchannels, allupdates, receive, parseinvoice, findroute, findroutetonode, - send, sendtonode, receivedinfo, audit, networkfees, - channelstats, sentinfo, invoice, allinvoice, pendinginvoices + payinvoice, sendtonode, getreceivedinfo, audit, networkfees, + channelstats, getsentinfo, getinvoice, allinvoice, listpendinginvoices Examples -------- diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index cf7c389237..82dc696061 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -151,6 +151,11 @@ trait Service extends ExtraDirectives with Logging { complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags)) } } ~ + path("updaterelayfee") { + formFields(channelId, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => + complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) + } + } ~ path("close") { formFields(channelId, "scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) => complete(eclairApi.close(Left(channelId), scriptPubKey_opt)) @@ -165,11 +170,6 @@ trait Service extends ExtraDirectives with Logging { complete(eclairApi.forceClose(Right(shortChannelId))) } } ~ - path("updaterelayfee") { - formFields(channelId, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channelId.toString, feeBase, feeProportional)) - } - } ~ path("peers") { complete(eclairApi.peersInfo()) } ~ @@ -194,16 +194,6 @@ trait Service extends ExtraDirectives with Logging { complete(eclairApi.allUpdates(nodeId_opt)) } } ~ - path("receive") { - formFields("description".as[String], amountMsat.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) => - complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress)) - } - } ~ - path("parseinvoice") { - formFields(invoice) { invoice => - complete(invoice) - } - } ~ path("findroute") { formFields(invoice, amountMsat.?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.findRoute(nodeId, amount.toLong, invoice.routingInfo)) @@ -216,7 +206,12 @@ trait Service extends ExtraDirectives with Logging { complete(eclairApi.findRoute(nodeId, amount)) } } ~ - path("send") { + path("parseinvoice") { + formFields(invoice) { invoice => + complete(invoice) + } + } ~ + path("payinvoice") { formFields(invoice, amountMsat.?) { case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) @@ -230,14 +225,34 @@ trait Service extends ExtraDirectives with Logging { complete(eclairApi.send(nodeId, amountMsat, paymentHash)) } } ~ - path("sentinfo") { + path("getsentinfo") { formFields("id".as[UUID]) { id => complete(eclairApi.sentInfo(Left(id))) } ~ formFields(paymentHash) { paymentHash => complete(eclairApi.sentInfo(Right(paymentHash))) } } ~ - path("receivedinfo") { + path("createinvoice") { + formFields("description".as[String], amountMsat.?, "expireIn".as[Long].?, "fallbackAddress".as[String].?) { (desc, amountMsat, expire, fallBackAddress) => + complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress)) + } + } ~ + path("getinvoice") { + formFields(paymentHash) { paymentHash => + completeOrNotFound(eclairApi.getInvoice(paymentHash)) + } + } ~ + path("allinvoices") { + formFields(from.?, to.?) { (from_opt, to_opt) => + complete(eclairApi.allInvoices(from_opt, to_opt)) + } + } ~ + path("listpendinginvoices") { + formFields(from.?, to.?) { (from_opt, to_opt) => + complete(eclairApi.pendingInvoices(from_opt, to_opt)) + } + } ~ + path("getreceivedinfo") { formFields(paymentHash) { paymentHash => completeOrNotFound(eclairApi.receivedInfo(paymentHash)) } ~ formFields(invoice) { invoice => @@ -256,21 +271,6 @@ trait Service extends ExtraDirectives with Logging { } ~ path("channelstats") { complete(eclairApi.channelStats()) - } ~ - path("invoice") { - formFields(paymentHash) { paymentHash => - completeOrNotFound(eclairApi.getInvoice(paymentHash)) - } - } ~ - path("allinvoices") { - formFields(from.?, to.?) { (from_opt, to_opt) => - complete(eclairApi.allInvoices(from_opt, to_opt)) - } - } ~ - path("pendinginvoices") { - formFields(from.?, to.?) { (from_opt, to_opt) => - complete(eclairApi.pendingInvoices(from_opt, to_opt)) - } } } ~ get { path("ws") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index a78d8ac679..06a7db960a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -277,7 +277,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { ) }) - Post("/send", FormData("invoice" -> invoice).toEntity) ~> + Post("/payinvoice", FormData("invoice" -> invoice).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> check { @@ -293,7 +293,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { override def receivedInfo(paymentHash: ByteVector32): Future[Option[IncomingPayment]] = Future.successful(None) // element not found }) - Post("/receivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> + Post("/getreceivedinfo", FormData("paymentHash" -> ByteVector32.Zeroes.toHex).toEntity) ~> addCredentials(BasicHttpCredentials("", mockService.password)) ~> Route.seal(mockService.route) ~> check { From 1db4399e4d75b9860bee612326df6411cccf46d5 Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 16:00:48 +0200 Subject: [PATCH 80/83] Finish merging master --- .../main/scala/fr/acinq/eclair/api/Service.scala | 16 ++++++++-------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index d24a89bb8f..fde183ec0b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -217,17 +217,17 @@ trait Service extends ExtraDirectives with Logging { } } ~ path("payinvoice") { - formFields(invoice, amountMsat.?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None) => - complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) - case (invoice, Some(overrideAmount)) => - complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry)) + formFields(invoice, amountMsat.?, "maxAttempts".as[Int].?) { + case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts) => + complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts)) + case (invoice, Some(overrideAmount), maxAttempts) => + complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts)) case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'")) } } ~ path("sendtonode") { - formFields(amountMsat, paymentHash, nodeId) { (amountMsat, paymentHash, nodeId) => - complete(eclairApi.send(nodeId, amountMsat, paymentHash)) + formFields(amountMsat, paymentHash, nodeId, "maxAttempts".as[Int].?) { (amountMsat, paymentHash, nodeId, maxAttempts) => + complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts = maxAttempts)) } } ~ path("getsentinfo") { @@ -247,7 +247,7 @@ trait Service extends ExtraDirectives with Logging { completeOrNotFound(eclairApi.getInvoice(paymentHash)) } } ~ - path("allinvoices") { + path("listinvoices") { formFields(from.?, to.?) { (from_opt, to_opt) => complete(eclairApi.allInvoices(from_opt, to_opt)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 195ffbdb60..f4ca4f3a05 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -272,7 +272,7 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest { val invoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" val mockService = new MockService(new EclairMock { - override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long]): Future[UUID] = Future.successful( + override def send(recipientNodeId: Crypto.PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry: Option[Long], maxAttempts: Option[Int] = None): Future[UUID] = Future.successful( id ) }) From 6f8fdd0fd1405ffbcc88723ff4d37cd8318aaa3d Mon Sep 17 00:00:00 2001 From: Andrea Date: Mon, 15 Apr 2019 16:28:24 +0200 Subject: [PATCH 81/83] More elegant Try/Match in LocalPaymentHandler --- .../acinq/eclair/payment/LocalPaymentHandler.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala index 9d56a61377..985e381610 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/LocalPaymentHandler.scala @@ -20,12 +20,13 @@ import akka.actor.{Actor, ActorLogging, Props, Status} import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel} import fr.acinq.eclair.db.IncomingPayment -import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment} +import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, randomBytes32} + import scala.compat.Platform import scala.concurrent.ExecutionContext -import scala.util.Try +import scala.util.{Failure, Success, Try} /** * Simple payment handler that generates payment requests and fulfills incoming htlcs. @@ -49,9 +50,10 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops) log.debug(s"generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) paymentDb.addPaymentRequest(paymentRequest, paymentPreimage) - sender ! paymentRequest - } recover { - case t => sender ! Status.Failure(t) + paymentRequest + } match { + case Success(paymentRequest) => sender ! paymentRequest + case Failure(exception) => sender ! Status.Failure(exception) } case htlc: UpdateAddHtlc => From f7fc22c7a716fa74575e3fa87eb3e238207438c9 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 16 Apr 2019 15:10:56 +0200 Subject: [PATCH 82/83] Add index on paymentDb.sent_payments.payment_hash --- .../main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 3de924179d..03183b7f47 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -38,6 +38,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { require(getVersion(statement, DB_NAME, CURRENT_VERSION) <= CURRENT_VERSION) // version 2 is "backward compatible" in the sense that it uses separate tables from version 1. There is no migration though statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL)") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS payment_hash_idx ON sent_payments(payment_hash)") setVersion(statement, DB_NAME, CURRENT_VERSION) } From 153b62c4bf18474dab2567f3718d239c7e57d492 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 16 Apr 2019 15:23:42 +0200 Subject: [PATCH 83/83] Order results in descending order in listPaymentRequest --- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 4 ++-- .../acinq/eclair/db/SqlitePaymentsDbSpec.scala | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 03183b7f47..a491f5cc1c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -173,8 +173,8 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { def listPaymentRequests(from: Long, to: Long, pendingOnly: Boolean): Seq[PaymentRequest] = { val queryStmt = pendingOnly match { - case true => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ? AND (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL" - case false => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ?" + case true => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ? AND (expire_at > ? OR expire_at IS NULL) AND received_msat IS NULL ORDER BY created_at DESC" + case false => "SELECT payment_request FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at DESC" } using(sqlite.prepareStatement(queryStmt)) { statement => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 11e2205875..67377b7d2c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.db +import java.time.Instant import java.util.UUID import fr.acinq.eclair.db.sqlite.SqliteUtils._ @@ -27,6 +28,7 @@ import fr.acinq.eclair.payment.PaymentRequest import org.scalatest.FunSuite import scodec.bits._ import fr.acinq.eclair.randomBytes32 + import scala.compat.Platform import OutgoingPaymentStatus._ @@ -160,25 +162,26 @@ class SqlitePaymentsDbSpec extends FunSuite { val (paymentHash1, paymentHash2) = (randomBytes32, randomBytes32) - val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = Platform.currentTime / 1000) - val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = someTimestamp) + val i1 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = Some(MilliSatoshi(123)), paymentHash = paymentHash1, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = None, timestamp = someTimestamp) + val i2 = PaymentRequest(chainHash = Block.TestnetGenesisBlock.hash, amount = None, paymentHash = paymentHash2, privateKey = bob.nodeKey.privateKey, description = "Some invoice", expirySeconds = Some(123456), timestamp = Instant.now().getEpochSecond) // i2 doesn't expire - assert(i1.expiry.isDefined && i2.expiry.isEmpty) - assert(i1.amount.isEmpty && i2.amount.isDefined) + assert(i1.expiry.isEmpty && i2.expiry.isDefined) + assert(i1.amount.isDefined && i2.amount.isEmpty) db.addPaymentRequest(i1, ByteVector32.Zeroes) db.addPaymentRequest(i2, ByteVector32.One) - assert(db.listPaymentRequests(0, Long.MaxValue) == Seq(i1, i2)) + // order matters, i2 has a more recent timestamp than i1 + assert(db.listPaymentRequests(0, Long.MaxValue) == Seq(i2, i1)) assert(db.getPaymentRequest(i1.paymentHash) == Some(i1)) assert(db.getPaymentRequest(i2.paymentHash) == Some(i2)) - assert(db.listPendingPaymentRequests(0, Long.MaxValue) == Seq(i1, i2)) + assert(db.listPendingPaymentRequests(0, Long.MaxValue) == Seq(i2, i1)) assert(db.getPendingPaymentRequestAndPreimage(paymentHash1) == Some((ByteVector32.Zeroes, i1))) assert(db.getPendingPaymentRequestAndPreimage(paymentHash2) == Some((ByteVector32.One, i2))) - assert(db.listPaymentRequests(someTimestamp - 100, someTimestamp + 100) == Seq(i2)) + assert(db.listPaymentRequests(someTimestamp - 100, someTimestamp + 100) == Seq(i1)) } }