mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-14 03:48:13 +01:00
Merge branch 'android' into android-phoenix
This commit is contained in:
commit
a74dd6c08f
62 changed files with 2186 additions and 973 deletions
5
BUILD.md
5
BUILD.md
|
@ -8,6 +8,11 @@
|
|||
|
||||
## Build
|
||||
|
||||
Eclair supports deterministic builds for the eclair-core submodule, this is the 'core' of the eclair application
|
||||
and its artifact can be deterministically built achieving byte-to-byte equality for each build. To build the exact
|
||||
same artifacts that we release, you must use the build environment (OS, JDK, maven...) that we specify in our
|
||||
release notes.
|
||||
|
||||
To build the project and run the tests, simply run:
|
||||
|
||||
```shell
|
||||
|
|
|
@ -48,8 +48,9 @@ You will find detailed guides and frequently asked questions there.
|
|||
:warning: Eclair requires Bitcoin Core 0.17.1 or higher. If you are upgrading an existing wallet, you need to create a new address and send all your funds to that address.
|
||||
|
||||
Eclair needs a _synchronized_, _segwit-ready_, **_zeromq-enabled_**, _wallet-enabled_, _non-pruning_, _tx-indexing_ [Bitcoin Core](https://github.com/bitcoin/bitcoin) node.
|
||||
Eclair will use any BTC it finds in the Bitcoin Core wallet to fund any channels you choose to open. Eclair will return BTC from closed channels to this wallet.
|
||||
Eclair will use any BTC it finds in the Bitcoin Core wallet to fund any channels you choose to open. Eclair will return BTC from closed channels to this wallet.
|
||||
You can configure your Bitcoin Node to use either `p2sh-segwit` addresses or `bech32` addresses, Eclair is compatible with both modes.
|
||||
If your Bitcoin Core wallet has "non-segwit UTXOs" (outputs that are neither `p2sh-segwit` or `bech32`), you must send them to a `p2sh-segwit` or `bech32` address.
|
||||
|
||||
Run bitcoind with the following minimal `bitcoin.conf`:
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ import fr.acinq.eclair.io.{NodeURI, Peer}
|
|||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
||||
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendTrampolinePaymentRequest}
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -88,13 +88,11 @@ trait Eclair {
|
|||
|
||||
def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
|
||||
|
||||
def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID]
|
||||
|
||||
def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]
|
||||
|
||||
def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]
|
||||
|
||||
def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta, invoice_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID]
|
||||
def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse]
|
||||
|
||||
def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]
|
||||
|
||||
|
@ -214,10 +212,21 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
|||
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse]
|
||||
}
|
||||
|
||||
override def sendToRoute(externalId_opt: Option[String], route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta, invoice_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID] = {
|
||||
externalId_opt match {
|
||||
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters"))
|
||||
case _ => (appKit.paymentInitiator ? SendPaymentRequest(amount, paymentHash, route.last, 1, finalCltvExpiryDelta, invoice_opt, externalId_opt, route)).mapTo[UUID]
|
||||
override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: Seq[PublicKey], trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = {
|
||||
val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount))
|
||||
val sendPayment = SendPaymentToRouteRequest(amount, recipientAmount, externalId_opt, parentId_opt, invoice, finalCltvExpiryDelta, route, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
|
||||
if (invoice.isExpired) {
|
||||
Future.failed(new IllegalArgumentException("invoice has expired"))
|
||||
} else if (route.isEmpty) {
|
||||
Future.failed(new IllegalArgumentException("missing payment route"))
|
||||
} else if (externalId_opt.exists(_.length > externalIdMaxLength)) {
|
||||
Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
|
||||
} else if (trampolineNodes_opt.nonEmpty && (trampolineFees_opt.isEmpty || trampolineExpiryDelta_opt.isEmpty)) {
|
||||
Future.failed(new IllegalArgumentException("trampoline payments must specify a trampoline fee and cltv delta"))
|
||||
} else if (trampolineNodes_opt.nonEmpty && trampolineNodes_opt.length != 2) {
|
||||
Future.failed(new IllegalArgumentException("trampoline payments currently only support paying a trampoline node via a single other trampoline node"))
|
||||
} else {
|
||||
(appKit.paymentInitiator ? sendPayment).mapTo[SendPaymentToRouteResponse]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,7 +239,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
|||
)
|
||||
|
||||
externalId_opt match {
|
||||
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException("externalId is too long: cannot exceed 66 characters"))
|
||||
case Some(externalId) if externalId.length > externalIdMaxLength => Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
|
||||
case _ => invoice_opt match {
|
||||
case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired"))
|
||||
case Some(invoice) =>
|
||||
|
@ -246,12 +255,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
|
|||
}
|
||||
}
|
||||
|
||||
override def sendToTrampoline(invoice: PaymentRequest, trampolineId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] = {
|
||||
val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
|
||||
val sendPayment = SendTrampolinePaymentRequest(invoice.amount.get, trampolineFees, invoice, trampolineId, invoice.minFinalCltvExpiryDelta.getOrElse(Channel.MIN_CLTV_EXPIRY_DELTA), trampolineExpiryDelta, Some(defaultRouteParams))
|
||||
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
|
||||
}
|
||||
|
||||
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
|
||||
id match {
|
||||
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)
|
||||
|
|
|
@ -90,7 +90,9 @@ object Features {
|
|||
// Features may depend on other features, as specified in Bolt 9.
|
||||
private val featuresDependency = Map(
|
||||
ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil),
|
||||
PaymentSecret -> (VariableLengthOnion :: Nil),
|
||||
// This dependency requirement was added to the spec after the Phoenix release, which means Phoenix users have "invalid"
|
||||
// invoices in their payment history. We choose to treat such invoices as valid; this is a harmless spec violation.
|
||||
// PaymentSecret -> (VariableLengthOnion :: Nil),
|
||||
BasicMultiPartPayment -> (PaymentSecret :: Nil),
|
||||
TrampolinePayment -> (PaymentSecret :: Nil)
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ import fr.acinq.eclair.crypto.ShaChain
|
|||
import fr.acinq.eclair.payment.relay.Origin
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelUpdate, ClosingSigned, CommitSig, FundingCreated, FundingLocked, FundingSigned, Init, NodeAddress, OnionRoutingPacket, OpenChannel, OpenTlv, Shutdown, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, UpdateMessage}
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelUpdate, ClosingSigned, CommitSig, FundingCreated, FundingLocked, FundingSigned, GenericTlv, Init, NodeAddress, OnionRoutingPacket, OpenChannel, OpenTlv, Shutdown, Tlv, TlvStream, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, UpdateMessage}
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
|
||||
object JsonSerializers {
|
||||
|
@ -61,6 +61,11 @@ object JsonSerializers {
|
|||
implicit val relayedOriginReadWriter: ReadWriter[Origin.Relayed] = macroRW
|
||||
implicit val paymentOriginReadWriter: ReadWriter[Origin] = ReadWriter.merge(localOriginReadWriter, relayedOriginReadWriter)
|
||||
implicit val remoteChangesReadWriter: ReadWriter[RemoteChanges] = macroRW
|
||||
implicit val openTlvPlaceholderReadWriter: ReadWriter[OpenTlv.ChannelVersionTlv] = macroRW
|
||||
implicit val openTlvReadWriter: ReadWriter[OpenTlv] = ReadWriter.merge(openTlvPlaceholderReadWriter)
|
||||
implicit val genericTlvReadWriter: ReadWriter[GenericTlv] = macroRW
|
||||
implicit val tlvReadWriter: ReadWriter[Tlv] = ReadWriter.merge(genericTlvReadWriter)
|
||||
implicit val tlvStreamOpenTlvReadWriter: ReadWriter[TlvStream[OpenTlv]] = readwriter[String].bimap[TlvStream[OpenTlv]](s => "N/A", s2 => TlvStream(List.empty[OpenTlv]))
|
||||
|
||||
case class ShaChain2(knownHashes: Map[Long, ByteVector32], lastIndex: Option[Long] = None) {
|
||||
def toShaChain = ShaChain(knownHashes.map { case (k, v) => ShaChain.moves(k) -> v }, lastIndex)
|
||||
|
@ -75,9 +80,7 @@ object JsonSerializers {
|
|||
implicit val actorRefReadWriter: ReadWriter[ActorRef] = readwriter[String].bimap[ActorRef](_.toString, _ => ActorRef.noSender)
|
||||
implicit val shortChannelIdReadWriter: ReadWriter[ShortChannelId] = readwriter[String].bimap[ShortChannelId](_.toString, s => ShortChannelId(s))
|
||||
|
||||
implicit val optionalTlvStreamReadWriter: ReadWriter[Option[TlvStream[OpenTlv]]] = readwriter[String].bimap[Option[TlvStream[OpenTlv]]](_ => "", _ => null)
|
||||
|
||||
implicit val initReadWriter: ReadWriter[Init] = macroRW
|
||||
implicit val initReadWriter: ReadWriter[Init] = readwriter[ByteVector].bimap[Init](_.features, s => Init(s))
|
||||
implicit val openChannelReadWriter: ReadWriter[OpenChannel] = macroRW
|
||||
implicit val acceptChannelReadWriter: ReadWriter[AcceptChannel] = macroRW
|
||||
implicit val fundingCreatedReadWriter: ReadWriter[FundingCreated] = macroRW
|
||||
|
|
|
@ -139,12 +139,14 @@ class Setup(datadir: File,
|
|||
case true =>
|
||||
val host = config.getString("electrum.host")
|
||||
val port = config.getInt("electrum.port")
|
||||
val address = InetSocketAddress.createUnresolved(host, port)
|
||||
val ssl = config.getString("electrum.ssl") match {
|
||||
case _ if address.getHostName.endsWith(".onion") => SSL.OFF // Tor already adds end-to-end encryption, adding TLS on top doesn't add anything
|
||||
case "off" => SSL.OFF
|
||||
case "loose" => SSL.LOOSE
|
||||
case _ => SSL.STRICT // strict mode is the default when we specify a custom electrum server, we don't want to be MITMed
|
||||
}
|
||||
val address = InetSocketAddress.createUnresolved(host, port)
|
||||
|
||||
logger.info(s"override electrum default with server=$address ssl=$ssl")
|
||||
Set(ElectrumServerAddress(address, ssl))
|
||||
case false =>
|
||||
|
|
|
@ -626,6 +626,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
Try(Commitments.sendAdd(d.commitments, c, origin(c), nodeParams.currentBlockHeight)) match {
|
||||
case Success(Right((commitments1, add))) =>
|
||||
if (c.commit) self ! CMD_SIGN
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending add
|
||||
case Success(Left(error)) => handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, error, origin(c), Some(d.channelUpdate), Some(c)), c)
|
||||
case Failure(cause) => handleCommandError(AddHtlcFailed(d.channelId, c.paymentHash, cause, origin(c), Some(d.channelUpdate), Some(c)), c)
|
||||
|
@ -641,6 +642,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
Try(Commitments.sendFulfill(d.commitments, c)) match {
|
||||
case Success((commitments1, fulfill)) =>
|
||||
if (c.commit) self ! CMD_SIGN
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fulfill
|
||||
case Failure(cause) =>
|
||||
// we can clean up the command right away in case of failure
|
||||
|
@ -662,6 +664,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
Try(Commitments.sendFail(d.commitments, c, nodeParams.privateKey)) match {
|
||||
case Success((commitments1, fail)) =>
|
||||
if (c.commit) self ! CMD_SIGN
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fail
|
||||
case Failure(cause) =>
|
||||
// we can clean up the command right away in case of failure
|
||||
|
@ -673,6 +676,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
Try(Commitments.sendFailMalformed(d.commitments, c)) match {
|
||||
case Success((commitments1, fail)) =>
|
||||
if (c.commit) self ! CMD_SIGN
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fail
|
||||
case Failure(cause) =>
|
||||
// we can clean up the command right away in case of failure
|
||||
|
@ -700,6 +704,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
Try(Commitments.sendFee(d.commitments, c)) match {
|
||||
case Success((commitments1, fee)) =>
|
||||
if (c.commit) self ! CMD_SIGN
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fee
|
||||
case Failure(cause) => handleCommandError(cause, c)
|
||||
}
|
||||
|
@ -728,7 +733,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
trimmedHtlcs collect {
|
||||
case DirectedHtlc(_, u) =>
|
||||
log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber")
|
||||
nodeParams.db.channels.addOrUpdateHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.cltvExpiry)
|
||||
nodeParams.db.channels.addHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.cltvExpiry)
|
||||
}
|
||||
if (!Helpers.aboveReserve(d.commitments) && Helpers.aboveReserve(commitments1)) {
|
||||
// we just went above reserve (can't go below), let's refresh our channel_update to enable/disable it accordingly
|
||||
|
@ -736,10 +741,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
self ! BroadcastChannelUpdate(AboveReserve)
|
||||
}
|
||||
context.system.eventStream.publish(ChannelSignatureSent(self, commitments1))
|
||||
if (nextRemoteCommit.spec.toRemote != d.commitments.remoteCommit.spec.toRemote) {
|
||||
// we send this event only when our balance changes (note that remoteCommit.toRemote == toLocal)
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemote, commitments1))
|
||||
}
|
||||
// we expect a quick response from our peer
|
||||
setTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer = context.parent), timeout = nodeParams.revocationTimeout, repeat = false)
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) storing() sending commit
|
||||
|
@ -759,6 +760,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
// if we have newly acknowledged changes let's sign them
|
||||
self ! CMD_SIGN
|
||||
}
|
||||
if (d.commitments.availableBalanceForSend != commitments1.availableBalanceForSend) {
|
||||
// we send this event only when our balance changes
|
||||
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1))
|
||||
}
|
||||
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1))
|
||||
stay using d.copy(commitments = commitments1) storing() sending revocation
|
||||
case Failure(cause) => handleLocalError(cause, d, Some(commit))
|
||||
|
@ -871,8 +876,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
|
||||
case Event(c: CurrentBlockCount, d: DATA_NORMAL) => handleNewBlock(c, d)
|
||||
|
||||
case Event(c@CurrentFeerates(feeratesPerKw), d: DATA_NORMAL) =>
|
||||
handleCurrentFeerate(c, d)
|
||||
case Event(c: CurrentFeerates, d: DATA_NORMAL) => handleCurrentFeerate(c, d)
|
||||
|
||||
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex, _), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty =>
|
||||
val shortChannelId = ShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt)
|
||||
|
@ -1142,8 +1146,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
|
||||
case Event(c: CurrentBlockCount, d: DATA_SHUTDOWN) => handleNewBlock(c, d)
|
||||
|
||||
case Event(c@CurrentFeerates(feerates), d: DATA_SHUTDOWN) =>
|
||||
handleCurrentFeerate(c, d)
|
||||
case Event(c: CurrentFeerates, d: DATA_SHUTDOWN) => handleCurrentFeerate(c, d)
|
||||
|
||||
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
|
||||
|
||||
|
@ -1213,7 +1216,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
log.info(s"got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain")
|
||||
val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets))
|
||||
val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets))
|
||||
val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets))
|
||||
val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => {
|
||||
require(commitments1.remoteNextCommitInfo.isLeft, "next remote commit must be defined")
|
||||
val remoteCommit = commitments1.remoteNextCommitInfo.left.get.nextRemoteCommit
|
||||
Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, remoteCommit, remoteCommitPublished.commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
|
||||
})
|
||||
|
||||
def republish(): Unit = {
|
||||
localCommitPublished1.foreach(doPublish)
|
||||
|
@ -2331,4 +2338,3 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remo
|
|||
case class NetworkFeePaid(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, tx: Transaction, fee: Satoshi, txType: String) extends ChannelEvent
|
||||
|
||||
// NB: this event is only sent when the channel is available
|
||||
case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, localBalance: MilliSatoshi, commitments: Commitments) extends ChannelEvent
|
||||
case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: Commitments) extends ChannelEvent
|
||||
|
||||
case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: Data) extends ChannelEvent
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256}
|
|||
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto}
|
||||
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets}
|
||||
import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.payment.relay.{Origin, Relayer}
|
||||
import fr.acinq.eclair.transactions.Transactions._
|
||||
import fr.acinq.eclair.transactions._
|
||||
|
@ -94,7 +93,7 @@ case class Commitments(channelVersion: ChannelVersion,
|
|||
val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat)
|
||||
if (localParams.isFunder) {
|
||||
// The funder always pays the on-chain fees, so we must subtract that from the amount we can send.
|
||||
val commitFees = commitTxFee(remoteParams.dustLimit, reduced).toMilliSatoshi
|
||||
val commitFees = commitTxFeeMsat(remoteParams.dustLimit, reduced)
|
||||
val htlcFees = htlcOutputFee(reduced.feeratePerKw)
|
||||
if (balanceNoFees - commitFees < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced)) {
|
||||
// htlc will be trimmed
|
||||
|
@ -117,7 +116,7 @@ case class Commitments(channelVersion: ChannelVersion,
|
|||
balanceNoFees
|
||||
} else {
|
||||
// The funder always pays the on-chain fees, so we must subtract that from the amount we can receive.
|
||||
val commitFees = commitTxFee(localParams.dustLimit, reduced).toMilliSatoshi
|
||||
val commitFees = commitTxFeeMsat(localParams.dustLimit, reduced)
|
||||
val htlcFees = htlcOutputFee(reduced.feeratePerKw)
|
||||
if (balanceNoFees - commitFees < receivedHtlcTrimThreshold(localParams.dustLimit, reduced)) {
|
||||
// htlc will be trimmed
|
||||
|
|
|
@ -25,8 +25,6 @@ import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent}
|
|||
|
||||
trait AuditDb extends Closeable {
|
||||
|
||||
def add(availableBalanceChanged: AvailableBalanceChanged)
|
||||
|
||||
def add(channelLifecycle: ChannelLifecycleEvent)
|
||||
|
||||
def add(paymentSent: PaymentSent)
|
||||
|
|
|
@ -30,7 +30,7 @@ trait ChannelsDb extends Closeable {
|
|||
|
||||
def listLocalChannels(): Seq[HasCommitments]
|
||||
|
||||
def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry)
|
||||
def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry)
|
||||
|
||||
def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)]
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.util.UUID
|
|||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.ChannelHop
|
||||
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop}
|
||||
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
|
||||
|
||||
import scala.compat.Platform
|
||||
|
@ -31,7 +31,7 @@ trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with Payment
|
|||
|
||||
trait IncomingPaymentsDb {
|
||||
/** Add a new expected incoming payment (not yet received). */
|
||||
def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32): Unit
|
||||
def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit
|
||||
|
||||
/**
|
||||
* Mark an incoming payment as received (paid). The received amount may exceed the payment request amount.
|
||||
|
@ -80,6 +80,12 @@ trait OutgoingPaymentsDb {
|
|||
|
||||
}
|
||||
|
||||
case object PaymentType {
|
||||
val Standard = "Standard"
|
||||
val SwapIn = "SwapIn"
|
||||
val SwapOut = "SwapOut"
|
||||
}
|
||||
|
||||
/**
|
||||
* An incoming payment received by this node.
|
||||
* At first it is in a pending state once the payment request has been generated, then will become either a success (if
|
||||
|
@ -87,11 +93,13 @@ trait OutgoingPaymentsDb {
|
|||
*
|
||||
* @param paymentRequest Bolt 11 payment request.
|
||||
* @param paymentPreimage pre-image associated with the payment request's payment_hash.
|
||||
* @param paymentType distinguish different payment types (standard, swaps, etc).
|
||||
* @param createdAt absolute time in milli-seconds since UNIX epoch when the payment request was generated.
|
||||
* @param status current status of the payment.
|
||||
*/
|
||||
case class IncomingPayment(paymentRequest: PaymentRequest,
|
||||
paymentPreimage: ByteVector32,
|
||||
paymentType: String,
|
||||
createdAt: Long,
|
||||
status: IncomingPaymentStatus)
|
||||
|
||||
|
@ -119,22 +127,26 @@ object IncomingPaymentStatus {
|
|||
* An outgoing payment sent by this node.
|
||||
* At first it is in a pending state, then will become either a success or a failure.
|
||||
*
|
||||
* @param id internal payment identifier.
|
||||
* @param parentId internal identifier of a parent payment, or [[id]] if single-part payment.
|
||||
* @param externalId external payment identifier: lets lightning applications reconcile payments with their own db.
|
||||
* @param paymentHash payment_hash.
|
||||
* @param amount amount of the payment, in milli-satoshis.
|
||||
* @param targetNodeId node ID of the payment recipient.
|
||||
* @param createdAt absolute time in milli-seconds since UNIX epoch when the payment was created.
|
||||
* @param paymentRequest Bolt 11 payment request (if paying from an invoice).
|
||||
* @param status current status of the payment.
|
||||
* @param id internal payment identifier.
|
||||
* @param parentId internal identifier of a parent payment, or [[id]] if single-part payment.
|
||||
* @param externalId external payment identifier: lets lightning applications reconcile payments with their own db.
|
||||
* @param paymentHash payment_hash.
|
||||
* @param paymentType distinguish different payment types (standard, swaps, etc).
|
||||
* @param amount amount that will be received by the target node, will be different from recipientAmount for trampoline payments.
|
||||
* @param recipientAmount amount that will be received by the final recipient.
|
||||
* @param recipientNodeId id of the final recipient.
|
||||
* @param createdAt absolute time in milli-seconds since UNIX epoch when the payment was created.
|
||||
* @param paymentRequest Bolt 11 payment request (if paying from an invoice).
|
||||
* @param status current status of the payment.
|
||||
*/
|
||||
case class OutgoingPayment(id: UUID,
|
||||
parentId: UUID,
|
||||
externalId: Option[String],
|
||||
paymentHash: ByteVector32,
|
||||
paymentType: String,
|
||||
amount: MilliSatoshi,
|
||||
targetNodeId: PublicKey,
|
||||
recipientAmount: MilliSatoshi,
|
||||
recipientNodeId: PublicKey,
|
||||
createdAt: Long,
|
||||
paymentRequest: Option[PaymentRequest],
|
||||
status: OutgoingPaymentStatus)
|
||||
|
@ -151,8 +163,9 @@ object OutgoingPaymentStatus {
|
|||
* We now have a valid proof-of-payment.
|
||||
*
|
||||
* @param paymentPreimage the preimage of the payment_hash.
|
||||
* @param feesPaid total amount of fees paid to intermediate routing nodes.
|
||||
* @param route payment route.
|
||||
* @param feesPaid fees paid to route to the target node (which not necessarily the final recipient, e.g. when
|
||||
* trampoline is used).
|
||||
* @param route payment route used.
|
||||
* @param completedAt absolute time in milli-seconds since UNIX epoch when the payment was completed.
|
||||
*/
|
||||
case class Succeeded(paymentPreimage: ByteVector32, feesPaid: MilliSatoshi, route: Seq[HopSummary], completedAt: Long) extends OutgoingPaymentStatus
|
||||
|
@ -176,7 +189,13 @@ case class HopSummary(nodeId: PublicKey, nextNodeId: PublicKey, shortChannelId:
|
|||
}
|
||||
|
||||
object HopSummary {
|
||||
def apply(h: ChannelHop): HopSummary = HopSummary(h.nodeId, h.nextNodeId, Some(h.lastUpdate.shortChannelId))
|
||||
def apply(h: Hop): HopSummary = {
|
||||
val shortChannelId = h match {
|
||||
case ChannelHop(_, _, channelUpdate) => Some(channelUpdate.shortChannelId)
|
||||
case _: NodeHop => None
|
||||
}
|
||||
HopSummary(h.nodeId, h.nextNodeId, shortChannelId)
|
||||
}
|
||||
}
|
||||
|
||||
/** A minimal representation of a payment failure (suitable to store in a database). */
|
||||
|
@ -210,6 +229,7 @@ trait PaymentsOverviewDb {
|
|||
*/
|
||||
sealed trait PlainPayment {
|
||||
val paymentHash: ByteVector32
|
||||
val paymentType: String
|
||||
val paymentRequest: Option[String]
|
||||
val finalAmount: Option[MilliSatoshi]
|
||||
val createdAt: Long
|
||||
|
@ -217,6 +237,7 @@ sealed trait PlainPayment {
|
|||
}
|
||||
|
||||
case class PlainIncomingPayment(paymentHash: ByteVector32,
|
||||
paymentType: String,
|
||||
finalAmount: Option[MilliSatoshi],
|
||||
paymentRequest: Option[String],
|
||||
status: IncomingPaymentStatus,
|
||||
|
@ -227,6 +248,7 @@ case class PlainIncomingPayment(paymentHash: ByteVector32,
|
|||
case class PlainOutgoingPayment(parentId: Option[UUID],
|
||||
externalId: Option[String],
|
||||
paymentHash: ByteVector32,
|
||||
paymentType: String,
|
||||
finalAmount: Option[MilliSatoshi],
|
||||
paymentRequest: Option[String],
|
||||
status: OutgoingPaymentStatus,
|
||||
|
|
|
@ -19,13 +19,13 @@ package fr.acinq.eclair.db.sqlite
|
|||
import java.sql.{Connection, Statement}
|
||||
import java.util.UUID
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Satoshi
|
||||
import fr.acinq.eclair.MilliSatoshi
|
||||
import fr.acinq.eclair.channel.{AvailableBalanceChanged, Channel, ChannelErrorOccurred, NetworkFeePaid}
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
|
||||
import fr.acinq.eclair.channel.{Channel, ChannelErrorOccurred, NetworkFeePaid}
|
||||
import fr.acinq.eclair.db._
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.wire.ChannelCodecs
|
||||
import fr.acinq.eclair.{LongToBtcAmount, MilliSatoshi}
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.collection.immutable.Queue
|
||||
|
@ -37,42 +37,70 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
|||
import ExtendedResultSet._
|
||||
|
||||
val DB_NAME = "audit"
|
||||
val CURRENT_VERSION = 3
|
||||
val CURRENT_VERSION = 4
|
||||
|
||||
case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long)
|
||||
|
||||
using(sqlite.createStatement(), inTransaction = true) { statement =>
|
||||
|
||||
def migration12(statement: Statement) = {
|
||||
def migration12(statement: Statement): Int = {
|
||||
statement.executeUpdate(s"ALTER TABLE sent ADD id BLOB DEFAULT '${ChannelCodecs.UNKNOWN_UUID.toString}' NOT NULL")
|
||||
}
|
||||
|
||||
def migration23(statement: Statement) = {
|
||||
def migration23(statement: Statement): Int = {
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_errors (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal INTEGER NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_errors_timestamp_idx ON channel_errors(timestamp)")
|
||||
}
|
||||
|
||||
def migration34(statement: Statement): Int = {
|
||||
statement.executeUpdate("DROP index sent_timestamp_idx")
|
||||
statement.executeUpdate("ALTER TABLE sent RENAME TO _sent_old")
|
||||
statement.executeUpdate("CREATE TABLE sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, recipient_amount_msat INTEGER NOT NULL, payment_id TEXT NOT NULL, parent_payment_id TEXT NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, recipient_node_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
// Old rows will be missing a recipient node id, so we use an easy-to-spot default value.
|
||||
val defaultRecipientNodeId = PrivateKey(ByteVector32.One).publicKey
|
||||
statement.executeUpdate(s"INSERT INTO sent (amount_msat, fees_msat, recipient_amount_msat, payment_id, parent_payment_id, payment_hash, payment_preimage, recipient_node_id, to_channel_id, timestamp) SELECT amount_msat, fees_msat, amount_msat, id, id, payment_hash, payment_preimage, X'${defaultRecipientNodeId.toString}', to_channel_id, timestamp FROM _sent_old")
|
||||
statement.executeUpdate("DROP table _sent_old")
|
||||
|
||||
statement.executeUpdate("DROP INDEX relayed_timestamp_idx")
|
||||
statement.executeUpdate("ALTER TABLE relayed RENAME TO _relayed_old")
|
||||
statement.executeUpdate("CREATE TABLE relayed (payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, channel_id BLOB NOT NULL, direction TEXT NOT NULL, relay_type TEXT NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) SELECT payment_hash, amount_in_msat, from_channel_id, 'IN', 'channel', timestamp FROM _relayed_old")
|
||||
statement.executeUpdate("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) SELECT payment_hash, amount_out_msat, to_channel_id, 'OUT', 'channel', timestamp FROM _relayed_old")
|
||||
statement.executeUpdate("DROP table _relayed_old")
|
||||
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_timestamp_idx ON sent(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_payment_hash_idx ON relayed(payment_hash)")
|
||||
}
|
||||
|
||||
getVersion(statement, DB_NAME, CURRENT_VERSION) match {
|
||||
case 1 => // previous version let's migrate
|
||||
logger.warn(s"migrating db $DB_NAME, found version=1 current=$CURRENT_VERSION")
|
||||
migration12(statement)
|
||||
migration23(statement)
|
||||
migration34(statement)
|
||||
setVersion(statement, DB_NAME, CURRENT_VERSION)
|
||||
case 2 =>
|
||||
logger.warn(s"migrating db $DB_NAME, found version=2 current=$CURRENT_VERSION")
|
||||
migration23(statement)
|
||||
migration34(statement)
|
||||
setVersion(statement, DB_NAME, CURRENT_VERSION)
|
||||
case 3 =>
|
||||
logger.warn(s"migrating db $DB_NAME, found version=3 current=$CURRENT_VERSION")
|
||||
migration34(statement)
|
||||
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 (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 sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, recipient_amount_msat INTEGER NOT NULL, payment_id TEXT NOT NULL, parent_payment_id TEXT NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, recipient_node_id 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 relayed (payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, channel_id BLOB NOT NULL, direction TEXT NOT NULL, relay_type TEXT 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 TEXT NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_errors (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal INTEGER 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 relayed_payment_hash_idx ON relayed(payment_hash)")
|
||||
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)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_errors_timestamp_idx ON channel_errors(timestamp)")
|
||||
|
@ -80,17 +108,6 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
|||
}
|
||||
}
|
||||
|
||||
override def add(e: AvailableBalanceChanged): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO balance_updated VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, e.channelId.toArray)
|
||||
statement.setBytes(2, e.commitments.remoteParams.nodeId.value.toArray)
|
||||
statement.setLong(3, e.localBalance.toLong)
|
||||
statement.setLong(4, e.commitments.commitInput.txOut.amount.toLong)
|
||||
statement.setLong(5, e.commitments.remoteParams.channelReserve.toLong) // remote decides what our reserve should be
|
||||
statement.setLong(6, Platform.currentTime)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
override def add(e: ChannelLifecycleEvent): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO channel_events VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, e.channelId.toArray)
|
||||
|
@ -104,15 +121,18 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
|||
}
|
||||
|
||||
override def add(e: PaymentSent): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
e.parts.foreach(p => {
|
||||
statement.setLong(1, p.amount.toLong)
|
||||
statement.setLong(2, p.feesPaid.toLong)
|
||||
statement.setBytes(3, e.paymentHash.toArray)
|
||||
statement.setBytes(4, e.paymentPreimage.toArray)
|
||||
statement.setBytes(5, p.toChannelId.toArray)
|
||||
statement.setLong(6, p.timestamp)
|
||||
statement.setBytes(7, p.id.toString.getBytes)
|
||||
statement.setLong(3, e.recipientAmount.toLong)
|
||||
statement.setString(4, p.id.toString)
|
||||
statement.setString(5, e.id.toString)
|
||||
statement.setBytes(6, e.paymentHash.toArray)
|
||||
statement.setBytes(7, e.paymentPreimage.toArray)
|
||||
statement.setBytes(8, e.recipientNodeId.value.toArray)
|
||||
statement.setBytes(9, p.toChannelId.toArray)
|
||||
statement.setLong(10, p.timestamp)
|
||||
statement.addBatch()
|
||||
})
|
||||
statement.executeBatch()
|
||||
|
@ -130,23 +150,27 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
|||
statement.executeBatch()
|
||||
}
|
||||
|
||||
override def add(e: PaymentRelayed): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setLong(1, e.amountIn.toLong)
|
||||
statement.setLong(2, e.amountOut.toLong)
|
||||
statement.setBytes(3, e.paymentHash.toArray)
|
||||
e match {
|
||||
case ChannelPaymentRelayed(_, _, _, fromChannelId, toChannelId, _) =>
|
||||
statement.setBytes(4, fromChannelId.toArray)
|
||||
statement.setBytes(5, toChannelId.toArray)
|
||||
case TrampolinePaymentRelayed(_, _, _, _, fromChannelIds, toChannelIds, _) =>
|
||||
// TODO: @t-bast: we should change the DB schema to allow accurate Trampoline reporting
|
||||
statement.setBytes(4, fromChannelIds.head.toArray)
|
||||
statement.setBytes(5, toChannelIds.head.toArray)
|
||||
}
|
||||
statement.setLong(6, e.timestamp)
|
||||
statement.executeUpdate()
|
||||
override def add(e: PaymentRelayed): Unit = {
|
||||
val payments = e match {
|
||||
case ChannelPaymentRelayed(amountIn, amountOut, _, fromChannelId, toChannelId, ts) =>
|
||||
// non-trampoline relayed payments have one input and one output
|
||||
Seq(RelayedPart(fromChannelId, amountIn, "IN", "channel", ts), RelayedPart(toChannelId, amountOut, "OUT", "channel", ts))
|
||||
case TrampolinePaymentRelayed(_, incoming, outgoing, ts) =>
|
||||
// trampoline relayed payments do MPP aggregation and may have M inputs and N outputs
|
||||
incoming.map(i => RelayedPart(i.channelId, i.amount, "IN", "trampoline", ts)) ++ outgoing.map(o => RelayedPart(o.channelId, o.amount, "OUT", "trampoline", ts))
|
||||
}
|
||||
for (p <- payments) {
|
||||
using(sqlite.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, e.paymentHash.toArray)
|
||||
statement.setLong(2, p.amount.toLong)
|
||||
statement.setBytes(3, p.channelId.toArray)
|
||||
statement.setString(4, p.direction)
|
||||
statement.setString(5, p.relayType)
|
||||
statement.setLong(6, e.timestamp)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def add(e: NetworkFeePaid): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO network_fees VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
|
@ -175,61 +199,86 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
|||
}
|
||||
|
||||
override def listSent(from: Long, to: Long): Seq[PaymentSent] =
|
||||
using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement =>
|
||||
using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ?")) { statement =>
|
||||
statement.setLong(1, from)
|
||||
statement.setLong(2, to)
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[PaymentSent] = Queue()
|
||||
var sentByParentId = Map.empty[UUID, PaymentSent]
|
||||
while (rs.next()) {
|
||||
q = q :+ PaymentSent(
|
||||
UUID.fromString(rs.getString("id")),
|
||||
rs.getByteVector32("payment_hash"),
|
||||
rs.getByteVector32("payment_preimage"),
|
||||
Seq(PaymentSent.PartialPayment(
|
||||
UUID.fromString(rs.getString("id")),
|
||||
MilliSatoshi(rs.getLong("amount_msat")),
|
||||
MilliSatoshi(rs.getLong("fees_msat")),
|
||||
rs.getByteVector32("to_channel_id"),
|
||||
None, // we don't store the route
|
||||
rs.getLong("timestamp"))))
|
||||
val parentId = UUID.fromString(rs.getString("parent_payment_id"))
|
||||
val part = PaymentSent.PartialPayment(
|
||||
UUID.fromString(rs.getString("payment_id")),
|
||||
MilliSatoshi(rs.getLong("amount_msat")),
|
||||
MilliSatoshi(rs.getLong("fees_msat")),
|
||||
rs.getByteVector32("to_channel_id"),
|
||||
None, // we don't store the route in the audit DB
|
||||
rs.getLong("timestamp"))
|
||||
val sent = sentByParentId.get(parentId) match {
|
||||
case Some(s) => s.copy(parts = s.parts :+ part)
|
||||
case None => PaymentSent(
|
||||
parentId,
|
||||
rs.getByteVector32("payment_hash"),
|
||||
rs.getByteVector32("payment_preimage"),
|
||||
MilliSatoshi(rs.getLong("recipient_amount_msat")),
|
||||
PublicKey(rs.getByteVector("recipient_node_id")),
|
||||
Seq(part))
|
||||
}
|
||||
sentByParentId = sentByParentId + (parentId -> sent)
|
||||
}
|
||||
q
|
||||
sentByParentId.values.toSeq.sortBy(_.timestamp)
|
||||
}
|
||||
|
||||
override def listReceived(from: Long, to: Long): Seq[PaymentReceived] =
|
||||
using(sqlite.prepareStatement("SELECT * FROM received WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement =>
|
||||
using(sqlite.prepareStatement("SELECT * FROM received WHERE timestamp >= ? AND timestamp < ?")) { statement =>
|
||||
statement.setLong(1, from)
|
||||
statement.setLong(2, to)
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[PaymentReceived] = Queue()
|
||||
var receivedByHash = Map.empty[ByteVector32, PaymentReceived]
|
||||
while (rs.next()) {
|
||||
q = q :+ PaymentReceived(
|
||||
rs.getByteVector32("payment_hash"),
|
||||
Seq(PaymentReceived.PartialPayment(
|
||||
MilliSatoshi(rs.getLong("amount_msat")),
|
||||
rs.getByteVector32("from_channel_id"),
|
||||
rs.getLong("timestamp")
|
||||
)))
|
||||
val paymentHash = rs.getByteVector32("payment_hash")
|
||||
val part = PaymentReceived.PartialPayment(
|
||||
MilliSatoshi(rs.getLong("amount_msat")),
|
||||
rs.getByteVector32("from_channel_id"),
|
||||
rs.getLong("timestamp"))
|
||||
val received = receivedByHash.get(paymentHash) match {
|
||||
case Some(r) => r.copy(parts = r.parts :+ part)
|
||||
case None => PaymentReceived(paymentHash, Seq(part))
|
||||
}
|
||||
receivedByHash = receivedByHash + (paymentHash -> received)
|
||||
}
|
||||
q
|
||||
receivedByHash.values.toSeq.sortBy(_.timestamp)
|
||||
}
|
||||
|
||||
override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] =
|
||||
using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement =>
|
||||
using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ?")) { statement =>
|
||||
statement.setLong(1, from)
|
||||
statement.setLong(2, to)
|
||||
val rs = statement.executeQuery()
|
||||
var q: Queue[PaymentRelayed] = Queue()
|
||||
var relayedByHash = Map.empty[ByteVector32, Seq[RelayedPart]]
|
||||
while (rs.next()) {
|
||||
q = q :+ ChannelPaymentRelayed(
|
||||
amountIn = MilliSatoshi(rs.getLong("amount_in_msat")),
|
||||
amountOut = MilliSatoshi(rs.getLong("amount_out_msat")),
|
||||
paymentHash = rs.getByteVector32("payment_hash"),
|
||||
fromChannelId = rs.getByteVector32("from_channel_id"),
|
||||
toChannelId = rs.getByteVector32("to_channel_id"),
|
||||
timestamp = rs.getLong("timestamp"))
|
||||
val paymentHash = rs.getByteVector32("payment_hash")
|
||||
val part = RelayedPart(
|
||||
rs.getByteVector32("channel_id"),
|
||||
MilliSatoshi(rs.getLong("amount_msat")),
|
||||
rs.getString("direction"),
|
||||
rs.getString("relay_type"),
|
||||
rs.getLong("timestamp"))
|
||||
relayedByHash = relayedByHash + (paymentHash -> (relayedByHash.getOrElse(paymentHash, Nil) :+ part))
|
||||
}
|
||||
q
|
||||
relayedByHash.flatMap {
|
||||
case (paymentHash, parts) =>
|
||||
// We may have been routing multiple payments for the same payment_hash (MPP) in both cases (trampoline and channel).
|
||||
// NB: we may link the wrong in-out parts, but the overall sum will be correct: we sort by amounts to minimize the risk of mismatch.
|
||||
val incoming = parts.filter(_.direction == "IN").map(p => PaymentRelayed.Part(p.amount, p.channelId)).sortBy(_.amount)
|
||||
val outgoing = parts.filter(_.direction == "OUT").map(p => PaymentRelayed.Part(p.amount, p.channelId)).sortBy(_.amount)
|
||||
parts.headOption match {
|
||||
case Some(RelayedPart(_, _, _, "channel", timestamp)) => incoming.zip(outgoing).map {
|
||||
case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, timestamp)
|
||||
}
|
||||
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, timestamp) :: Nil
|
||||
case _ => Nil
|
||||
}
|
||||
}.toSeq.sortBy(_.timestamp)
|
||||
}
|
||||
|
||||
override def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] =
|
||||
|
@ -250,48 +299,34 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
|
|||
q
|
||||
}
|
||||
|
||||
override def stats: Seq[Stats] =
|
||||
using(sqlite.createStatement()) { statement =>
|
||||
val rs = statement.executeQuery(
|
||||
"""
|
||||
|SELECT
|
||||
| channel_id,
|
||||
| sum(avg_payment_amount_sat) AS avg_payment_amount_sat,
|
||||
| sum(payment_count) AS payment_count,
|
||||
| sum(relay_fee_sat) AS relay_fee_sat,
|
||||
| sum(network_fee_sat) AS network_fee_sat
|
||||
|FROM (
|
||||
| SELECT
|
||||
| to_channel_id AS channel_id,
|
||||
| avg(amount_out_msat) / 1000 AS avg_payment_amount_sat,
|
||||
| count(*) AS payment_count,
|
||||
| sum(amount_in_msat - amount_out_msat) / 1000 AS relay_fee_sat,
|
||||
| 0 AS network_fee_sat
|
||||
| FROM relayed
|
||||
| GROUP BY 1
|
||||
| UNION
|
||||
| SELECT
|
||||
| channel_id,
|
||||
| 0 AS avg_payment_amount_sat,
|
||||
| 0 AS payment_count,
|
||||
| 0 AS relay_fee_sat,
|
||||
| sum(fee_sat) AS network_fee_sat
|
||||
| FROM network_fees
|
||||
| GROUP BY 1
|
||||
|)
|
||||
|GROUP BY 1
|
||||
""".stripMargin)
|
||||
var q: Queue[Stats] = Queue()
|
||||
while (rs.next()) {
|
||||
q = q :+ Stats(
|
||||
channelId = rs.getByteVector32("channel_id"),
|
||||
avgPaymentAmount = Satoshi(rs.getLong("avg_payment_amount_sat")),
|
||||
paymentCount = rs.getInt("payment_count"),
|
||||
relayFee = Satoshi(rs.getLong("relay_fee_sat")),
|
||||
networkFee = Satoshi(rs.getLong("network_fee_sat")))
|
||||
}
|
||||
q
|
||||
override def stats: Seq[Stats] = {
|
||||
val networkFees = listNetworkFees(0, Platform.currentTime + 1).foldLeft(Map.empty[ByteVector32, Satoshi]) { case (feeByChannelId, f) =>
|
||||
feeByChannelId + (f.channelId -> (feeByChannelId.getOrElse(f.channelId, 0 sat) + f.fee))
|
||||
}
|
||||
val relayed = listRelayed(0, Platform.currentTime + 1).foldLeft(Map.empty[ByteVector32, Seq[PaymentRelayed]]) { case (relayedByChannelId, e) =>
|
||||
val relayedTo = e match {
|
||||
case c: ChannelPaymentRelayed => Set(c.toChannelId)
|
||||
case t: TrampolinePaymentRelayed => t.outgoing.map(_.channelId).toSet
|
||||
}
|
||||
val updated = relayedTo.map(channelId => (channelId, relayedByChannelId.getOrElse(channelId, Nil) :+ e)).toMap
|
||||
relayedByChannelId ++ updated
|
||||
}
|
||||
networkFees.map {
|
||||
case (channelId, networkFee) =>
|
||||
val r = relayed.getOrElse(channelId, Nil)
|
||||
val paymentCount = r.length
|
||||
if (paymentCount == 0) {
|
||||
Stats(channelId, 0 sat, 0, 0 sat, networkFee)
|
||||
} else {
|
||||
val avgPaymentAmount = r.map(_.amountOut).sum / paymentCount
|
||||
val relayFee = r.map {
|
||||
case c: ChannelPaymentRelayed => c.amountIn - c.amountOut
|
||||
case t: TrampolinePaymentRelayed => (t.amountIn - t.amountOut) * t.outgoing.count(_.channelId == channelId) / t.outgoing.length
|
||||
}.sum
|
||||
Stats(channelId, avgPaymentAmount.truncateToSatoshi, paymentCount, relayFee.truncateToSatoshi, networkFee)
|
||||
}
|
||||
}.toSeq
|
||||
}
|
||||
|
||||
// used by mobile apps
|
||||
override def close(): Unit = sqlite.close()
|
||||
|
|
|
@ -101,8 +101,8 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
|
|||
}
|
||||
}
|
||||
|
||||
def addOrUpdateHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT OR IGNORE INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
|
||||
def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = {
|
||||
using(sqlite.prepareStatement("INSERT INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, channelId.toArray)
|
||||
statement.setLong(2, commitmentNumber)
|
||||
statement.setBytes(3, paymentHash.toArray)
|
||||
|
|
|
@ -37,28 +37,22 @@ import scala.concurrent.duration._
|
|||
|
||||
class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
||||
|
||||
import SqlitePaymentsDb._
|
||||
import SqliteUtils.ExtendedResultSet._
|
||||
|
||||
val DB_NAME = "payments"
|
||||
val CURRENT_VERSION = 3
|
||||
|
||||
private val hopSummaryCodec = (("node_id" | CommonCodecs.publicKey) :: ("next_node_id" | CommonCodecs.publicKey) :: ("short_channel_id" | optional(bool, CommonCodecs.shortchannelid))).as[HopSummary]
|
||||
private val paymentRouteCodec = discriminated[List[HopSummary]].by(byte)
|
||||
.typecase(0x01, listOfN(uint8, hopSummaryCodec))
|
||||
private val failureSummaryCodec = (("type" | enumerated(uint8, FailureType)) :: ("message" | ascii32) :: paymentRouteCodec).as[FailureSummary]
|
||||
private val paymentFailuresCodec = discriminated[List[FailureSummary]].by(byte)
|
||||
.typecase(0x01, listOfN(uint8, failureSummaryCodec))
|
||||
val CURRENT_VERSION = 4
|
||||
|
||||
using(sqlite.createStatement(), inTransaction = true) { statement =>
|
||||
|
||||
def migration12(statement: Statement) = {
|
||||
def migration12(statement: Statement): Int = {
|
||||
// Version 2 is "backwards compatible" in the sense that it uses separate tables from version 1 (which used a single "payments" table).
|
||||
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)")
|
||||
}
|
||||
|
||||
def migration23(statement: Statement) = {
|
||||
def migration23(statement: Statement): Int = {
|
||||
// We add many more columns to the sent_payments table.
|
||||
statement.executeUpdate("DROP index payment_hash_idx")
|
||||
statement.executeUpdate("ALTER TABLE sent_payments RENAME TO _sent_payments_old")
|
||||
|
@ -82,19 +76,47 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_created_idx ON received_payments(created_at)")
|
||||
}
|
||||
|
||||
def migration34(statement: Statement): Int = {
|
||||
// We add a recipient_amount_msat and payment_type columns, rename some columns and change column order.
|
||||
statement.executeUpdate("DROP index sent_parent_id_idx")
|
||||
statement.executeUpdate("DROP index sent_payment_hash_idx")
|
||||
statement.executeUpdate("DROP index sent_created_idx")
|
||||
statement.executeUpdate("ALTER TABLE sent_payments RENAME TO _sent_payments_old")
|
||||
statement.executeUpdate("CREATE TABLE sent_payments (id TEXT NOT NULL PRIMARY KEY, parent_id TEXT NOT NULL, external_id TEXT, payment_hash BLOB NOT NULL, payment_preimage BLOB, payment_type TEXT NOT NULL, amount_msat INTEGER NOT NULL, fees_msat INTEGER, recipient_amount_msat INTEGER NOT NULL, recipient_node_id BLOB NOT NULL, payment_request TEXT, payment_route BLOB, failures BLOB, created_at INTEGER NOT NULL, completed_at INTEGER)")
|
||||
statement.executeUpdate("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, payment_preimage, payment_type, amount_msat, fees_msat, recipient_amount_msat, recipient_node_id, payment_request, payment_route, failures, created_at, completed_at) SELECT id, parent_id, external_id, payment_hash, payment_preimage, 'Standard', amount_msat, fees_msat, amount_msat, target_node_id, payment_request, payment_route, failures, created_at, completed_at FROM _sent_payments_old")
|
||||
statement.executeUpdate("DROP table _sent_payments_old")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_parent_id_idx ON sent_payments(parent_id)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_payment_hash_idx ON sent_payments(payment_hash)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_created_idx ON sent_payments(created_at)")
|
||||
|
||||
// We add payment_type column.
|
||||
statement.executeUpdate("DROP index received_created_idx")
|
||||
statement.executeUpdate("ALTER TABLE received_payments RENAME TO _received_payments_old")
|
||||
statement.executeUpdate("CREATE TABLE received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, payment_type TEXT NOT NULL, payment_preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, received_at INTEGER)")
|
||||
statement.executeUpdate("INSERT INTO received_payments (payment_hash, payment_type, payment_preimage, payment_request, received_msat, created_at, expire_at, received_at) SELECT payment_hash, 'Standard', payment_preimage, payment_request, received_msat, created_at, expire_at, received_at FROM _received_payments_old")
|
||||
statement.executeUpdate("DROP table _received_payments_old")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_created_idx ON received_payments(created_at)")
|
||||
}
|
||||
|
||||
getVersion(statement, DB_NAME, CURRENT_VERSION) match {
|
||||
case 1 =>
|
||||
logger.warn(s"migrating db $DB_NAME, found version=1 current=$CURRENT_VERSION")
|
||||
migration12(statement)
|
||||
migration23(statement)
|
||||
migration34(statement)
|
||||
setVersion(statement, DB_NAME, CURRENT_VERSION)
|
||||
case 2 =>
|
||||
logger.warn(s"migrating db $DB_NAME, found version=2 current=$CURRENT_VERSION")
|
||||
migration23(statement)
|
||||
migration34(statement)
|
||||
setVersion(statement, DB_NAME, CURRENT_VERSION)
|
||||
case 3 =>
|
||||
logger.warn(s"migrating db $DB_NAME, found version=3 current=$CURRENT_VERSION")
|
||||
migration34(statement)
|
||||
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, payment_preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, received_at INTEGER)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, parent_id TEXT NOT NULL, external_id TEXT, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, target_node_id BLOB NOT NULL, created_at INTEGER NOT NULL, payment_request TEXT, completed_at INTEGER, payment_preimage BLOB, fees_msat INTEGER, payment_route BLOB, failures BLOB)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, payment_type TEXT NOT NULL, payment_preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, received_at INTEGER)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, parent_id TEXT NOT NULL, external_id TEXT, payment_hash BLOB NOT NULL, payment_preimage BLOB, payment_type TEXT NOT NULL, amount_msat INTEGER NOT NULL, fees_msat INTEGER, recipient_amount_msat INTEGER NOT NULL, recipient_node_id BLOB NOT NULL, payment_request TEXT, payment_route BLOB, failures BLOB, created_at INTEGER NOT NULL, completed_at INTEGER)")
|
||||
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_parent_id_idx ON sent_payments(parent_id)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_payment_hash_idx ON sent_payments(payment_hash)")
|
||||
|
@ -107,15 +129,17 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
|
||||
override def addOutgoingPayment(sent: OutgoingPayment): Unit = {
|
||||
require(sent.status == OutgoingPaymentStatus.Pending, s"outgoing payment isn't pending (${sent.status.getClass.getSimpleName})")
|
||||
using(sqlite.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, amount_msat, target_node_id, created_at, payment_request) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
using(sqlite.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, payment_type, amount_msat, recipient_amount_msat, recipient_node_id, created_at, payment_request) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setString(1, sent.id.toString)
|
||||
statement.setString(2, sent.parentId.toString)
|
||||
statement.setString(3, sent.externalId.orNull)
|
||||
statement.setBytes(4, sent.paymentHash.toArray)
|
||||
statement.setLong(5, sent.amount.toLong)
|
||||
statement.setBytes(6, sent.targetNodeId.value.toArray)
|
||||
statement.setLong(7, sent.createdAt)
|
||||
statement.setString(8, sent.paymentRequest.map(PaymentRequest.write).orNull)
|
||||
statement.setString(5, sent.paymentType)
|
||||
statement.setLong(6, sent.amount.toLong)
|
||||
statement.setLong(7, sent.recipientAmount.toLong)
|
||||
statement.setBytes(8, sent.recipientNodeId.value.toArray)
|
||||
statement.setLong(9, sent.createdAt)
|
||||
statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
@ -154,8 +178,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
UUID.fromString(rs.getString("parent_id")),
|
||||
rs.getStringNullable("external_id"),
|
||||
rs.getByteVector32("payment_hash"),
|
||||
rs.getString("payment_type"),
|
||||
MilliSatoshi(rs.getLong("amount_msat")),
|
||||
PublicKey(rs.getByteVector("target_node_id")),
|
||||
MilliSatoshi(rs.getLong("recipient_amount_msat")),
|
||||
PublicKey(rs.getByteVector("recipient_node_id")),
|
||||
rs.getLong("created_at"),
|
||||
rs.getStringNullable("payment_request").map(PaymentRequest.read),
|
||||
status
|
||||
|
@ -232,13 +258,14 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
q
|
||||
}
|
||||
|
||||
override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)")) { statement =>
|
||||
override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit =
|
||||
using(sqlite.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, pr.paymentHash.toArray)
|
||||
statement.setBytes(2, preimage.toArray)
|
||||
statement.setString(3, PaymentRequest.write(pr))
|
||||
statement.setLong(4, pr.timestamp.seconds.toMillis) // BOLT11 timestamp is in seconds
|
||||
statement.setLong(5, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)).seconds.toMillis)
|
||||
statement.setString(3, paymentType)
|
||||
statement.setString(4, PaymentRequest.write(pr))
|
||||
statement.setLong(5, pr.timestamp.seconds.toMillis) // BOLT11 timestamp is in seconds
|
||||
statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)).seconds.toMillis)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
|
@ -255,8 +282,10 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
|
||||
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
|
||||
val paymentRequest = rs.getString("payment_request")
|
||||
IncomingPayment(PaymentRequest.read(paymentRequest),
|
||||
IncomingPayment(
|
||||
PaymentRequest.read(paymentRequest),
|
||||
rs.getByteVector32("payment_preimage"),
|
||||
rs.getString("payment_type"),
|
||||
rs.getLong("created_at"),
|
||||
buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getLongNullable("received_at")))
|
||||
}
|
||||
|
@ -344,9 +373,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
| NULL as external_id,
|
||||
| payment_hash,
|
||||
| payment_preimage,
|
||||
| payment_type,
|
||||
| received_msat as final_amount,
|
||||
| payment_request,
|
||||
| NULL as target_node_id,
|
||||
| created_at,
|
||||
| received_at as completed_at,
|
||||
| expire_at,
|
||||
|
@ -359,9 +388,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
| external_id,
|
||||
| payment_hash,
|
||||
| payment_preimage,
|
||||
| payment_type,
|
||||
| sum(amount_msat + fees_msat) as final_amount,
|
||||
| payment_request,
|
||||
| target_node_id,
|
||||
| created_at,
|
||||
| completed_at,
|
||||
| NULL as expire_at,
|
||||
|
@ -380,6 +409,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
val parentId = rs.getUUIDNullable("parent_id")
|
||||
val externalId_opt = rs.getStringNullable("external_id")
|
||||
val paymentHash = rs.getByteVector32("payment_hash")
|
||||
val paymentType = rs.getString("payment_type")
|
||||
val paymentRequest_opt = rs.getStringNullable("payment_request")
|
||||
val amount_opt = rs.getMilliSatoshiNullable("final_amount")
|
||||
val createdAt = rs.getLong("created_at")
|
||||
|
@ -388,12 +418,12 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
|
||||
val p = if (rs.getString("type") == "received") {
|
||||
val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt)
|
||||
PlainIncomingPayment(paymentHash, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt, expireAt_opt)
|
||||
PlainIncomingPayment(paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt, expireAt_opt)
|
||||
} else {
|
||||
val preimage_opt = rs.getByteVector32Nullable("payment_preimage")
|
||||
// note that the resulting status will not contain any details (routes, failures...)
|
||||
val status: OutgoingPaymentStatus = buildOutgoingPaymentStatus(preimage_opt, None, None, completedAt_opt, None)
|
||||
PlainOutgoingPayment(parentId, externalId_opt, paymentHash, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt)
|
||||
PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt)
|
||||
}
|
||||
q = q :+ p
|
||||
}
|
||||
|
@ -403,4 +433,16 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
|
|||
|
||||
// used by mobile apps
|
||||
override def close(): Unit = sqlite.close()
|
||||
|
||||
}
|
||||
|
||||
object SqlitePaymentsDb {
|
||||
|
||||
private val hopSummaryCodec = (("node_id" | CommonCodecs.publicKey) :: ("next_node_id" | CommonCodecs.publicKey) :: ("short_channel_id" | optional(bool, CommonCodecs.shortchannelid))).as[HopSummary]
|
||||
val paymentRouteCodec = discriminated[List[HopSummary]].by(byte)
|
||||
.typecase(0x01, listOfN(uint8, hopSummaryCodec))
|
||||
private val failureSummaryCodec = (("type" | enumerated(uint8, FailureType)) :: ("message" | ascii32) :: paymentRouteCodec).as[FailureSummary]
|
||||
val paymentFailuresCodec = discriminated[List[FailureSummary]].by(byte)
|
||||
.typecase(0x01, listOfN(uint8, failureSummaryCodec))
|
||||
|
||||
}
|
|
@ -100,8 +100,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
log.debug(s"got authenticated connection to $remoteNodeId@${address.getHostString}:${address.getPort}")
|
||||
transport ! TransportHandler.Listener(self)
|
||||
context watch transport
|
||||
val localInit = nodeParams.overrideFeatures.get(remoteNodeId) match {
|
||||
case Some(f) => wire.Init(f)
|
||||
val localFeatures = nodeParams.overrideFeatures.get(remoteNodeId) match {
|
||||
case Some(f) => f
|
||||
case None =>
|
||||
// Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask
|
||||
// off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue.
|
||||
|
@ -116,9 +116,10 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
// ... and leave the others untouched
|
||||
case (value, _) => value
|
||||
}).reverse.bytes.dropWhile(_ == 0)
|
||||
wire.Init(tweakedFeatures)
|
||||
tweakedFeatures
|
||||
}
|
||||
log.info(s"using features=${localInit.features.toBin}")
|
||||
log.info(s"using features=${localFeatures.toBin}")
|
||||
val localInit = wire.Init(localFeatures, TlvStream(InitTlv.Networks(nodeParams.chainHash :: Nil)))
|
||||
transport ! localInit
|
||||
|
||||
val address_opt = if (outgoing) {
|
||||
|
@ -144,9 +145,19 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
case Event(remoteInit: wire.Init, d: InitializingData) =>
|
||||
d.transport ! TransportHandler.ReadAck(remoteInit)
|
||||
|
||||
log.info(s"peer is using features=${remoteInit.features.toBin}")
|
||||
log.info(s"peer is using features=${remoteInit.features.toBin}, networks=${remoteInit.networks.mkString(",")}")
|
||||
|
||||
if (Features.areSupported(remoteInit.features)) {
|
||||
if (remoteInit.networks.nonEmpty && !remoteInit.networks.contains(nodeParams.chainHash)) {
|
||||
log.warning(s"incompatible networks (${remoteInit.networks}), disconnecting")
|
||||
d.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible networks")))
|
||||
d.transport ! PoisonPill
|
||||
stay
|
||||
} else if (!Features.areSupported(remoteInit.features)) {
|
||||
log.warning("incompatible features, disconnecting")
|
||||
d.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible features")))
|
||||
d.transport ! PoisonPill
|
||||
stay
|
||||
} else {
|
||||
d.origin_opt.foreach(origin => origin ! "connected")
|
||||
|
||||
def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f)
|
||||
|
@ -177,11 +188,6 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
val rebroadcastDelay = Random.nextInt(nodeParams.routerConf.routerBroadcastInterval.toSeconds.toInt).seconds
|
||||
log.info(s"rebroadcast will be delayed by $rebroadcastDelay")
|
||||
goto(CONNECTED) using ConnectedData(d.address_opt, d.transport, d.localInit, remoteInit, d.channels.map { case (k: ChannelId, v) => (k, v) }, rebroadcastDelay) forMax (30 seconds) // forMax will trigger a StateTimeout
|
||||
} else {
|
||||
log.warning(s"incompatible features, disconnecting")
|
||||
d.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible features")))
|
||||
d.transport ! PoisonPill
|
||||
stay
|
||||
}
|
||||
|
||||
case Event(Authenticator.Authenticated(connection, _, _, _, _, origin_opt), _) =>
|
||||
|
@ -440,11 +446,11 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
/**
|
||||
* Send and count in a single iteration
|
||||
*/
|
||||
def sendAndCount(msgs: Map[_ <: RoutingMessage, Set[ActorRef]]): Int = msgs.foldLeft(0) {
|
||||
case (count, (_, origins)) if origins.contains(self) =>
|
||||
def sendAndCount(msgs: Map[_ <: RoutingMessage, Set[GossipOrigin]]): Int = msgs.foldLeft(0) {
|
||||
case (count, (_, origins)) if origins.contains(RemoteGossip(self)) =>
|
||||
// the announcement came from this peer, we don't send it back
|
||||
count
|
||||
case (count, (msg, _)) if !timestampInRange(msg, d.gossipTimestampFilter) =>
|
||||
case (count, (msg, origins)) if !timestampInRange(msg, origins, d.gossipTimestampFilter) =>
|
||||
// the peer has set up a filter on timestamp and this message is out of range
|
||||
count
|
||||
case (count, (msg, _)) =>
|
||||
|
@ -673,6 +679,31 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Peer may want to filter announcements based on timestamp.
|
||||
*
|
||||
* @param gossipTimestampFilter_opt optional gossip timestamp range
|
||||
* @return
|
||||
* - true if this is our own gossip
|
||||
* - true if there is a filter and msg has no timestamp, or has one that matches the filter
|
||||
* - false otherwise
|
||||
*/
|
||||
def timestampInRange(msg: RoutingMessage, origins: Set[GossipOrigin], gossipTimestampFilter_opt: Option[GossipTimestampFilter]): Boolean = {
|
||||
// For our own gossip, we should ignore the peer's timestamp filter.
|
||||
val isOurGossip = msg match {
|
||||
case n: NodeAnnouncement if n.nodeId == nodeParams.nodeId => true
|
||||
case _ if origins.contains(LocalGossip) => true
|
||||
case _ => false
|
||||
}
|
||||
// Otherwise we check if this message has a timestamp that matches the timestamp filter.
|
||||
val matchesFilter = (msg, gossipTimestampFilter_opt) match {
|
||||
case (_, None) => false // BOLT 7: A node which wants any gossip messages would have to send this, otherwise [...] no gossip messages would be received.
|
||||
case (hasTs: HasTimestamp, Some(GossipTimestampFilter(_, firstTimestamp, timestampRange))) => hasTs.timestamp >= firstTimestamp && hasTs.timestamp <= firstTimestamp + timestampRange
|
||||
case _ => true // if there is a filter and message doesn't have a timestamp (e.g. channel_announcement), then we send it
|
||||
}
|
||||
isOurGossip || matchesFilter
|
||||
}
|
||||
|
||||
// a failing channel won't be restarted, it should handle its states
|
||||
override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Stop }
|
||||
|
||||
|
@ -788,23 +819,6 @@ object Peer {
|
|||
features = nodeParams.features)
|
||||
}
|
||||
|
||||
/**
|
||||
* Peer may want to filter announcements based on timestamp
|
||||
*
|
||||
* @param gossipTimestampFilter_opt optional gossip timestamp range
|
||||
* @return
|
||||
* - true if there is a filter and msg has no timestamp, or has one that matches the filter
|
||||
* - false otherwise
|
||||
*/
|
||||
def timestampInRange(msg: RoutingMessage, gossipTimestampFilter_opt: Option[GossipTimestampFilter]): Boolean = {
|
||||
// check if this message has a timestamp that matches our timestamp filter
|
||||
(msg, gossipTimestampFilter_opt) match {
|
||||
case (_, None) => false // BOLT 7: A node which wants any gossip messages would have to send this, otherwise [...] no gossip messages would be received.
|
||||
case (hasTs: HasTimestamp, Some(GossipTimestampFilter(_, firstTimestamp, timestampRange))) => hasTs.timestamp >= firstTimestamp && hasTs.timestamp <= firstTimestamp + timestampRange
|
||||
case _ => true // if there is a filter and message doesn't have a timestamp (e.g. channel_announcement), then we send it
|
||||
}
|
||||
}
|
||||
|
||||
def hostAndPort2InetSocketAddress(hostAndPort: HostAndPort): InetSocketAddress = new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort)
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,30 +17,23 @@
|
|||
package fr.acinq.eclair.payment
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Props}
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.NodeParams
|
||||
import fr.acinq.eclair.channel.Channel.{LocalError, RemoteError}
|
||||
import fr.acinq.eclair.channel.Helpers.Closing._
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.db.{AuditDb, ChannelLifecycleEvent}
|
||||
import fr.acinq.eclair.db.ChannelLifecycleEvent
|
||||
import kamon.Kamon
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
|
||||
|
||||
val db = nodeParams.db.audit
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[PaymentEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[NetworkFeePaid])
|
||||
context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged])
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelErrorOccurred])
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
|
||||
context.system.eventStream.subscribe(self, classOf[ChannelClosed])
|
||||
|
||||
val balanceEventThrottler = context.actorOf(Props(new BalanceEventThrottler(db)))
|
||||
|
||||
override def receive: Receive = {
|
||||
|
||||
case e: PaymentSent =>
|
||||
|
@ -48,7 +41,7 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
|
|||
.histogram("payment.hist")
|
||||
.withTag("direction", "sent")
|
||||
.withTag("type", "amount")
|
||||
.record(e.amount.truncateToSatoshi.toLong)
|
||||
.record(e.recipientAmount.truncateToSatoshi.toLong)
|
||||
Kamon
|
||||
.histogram("payment.hist")
|
||||
.withTag("direction", "sent")
|
||||
|
@ -101,8 +94,6 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
|
|||
|
||||
case e: NetworkFeePaid => db.add(e)
|
||||
|
||||
case e: AvailableBalanceChanged => balanceEventThrottler ! e
|
||||
|
||||
case e: ChannelErrorOccurred =>
|
||||
val metric = Kamon.counter("channels.errors")
|
||||
e.error match {
|
||||
|
@ -139,54 +130,6 @@ class Auditor(nodeParams: NodeParams) extends Actor with ActorLogging {
|
|||
}
|
||||
|
||||
override def unhandled(message: Any): Unit = log.warning(s"unhandled msg=$message")
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to log every tiny payment, and we don't want to log probing events.
|
||||
*/
|
||||
class BalanceEventThrottler(db: AuditDb) extends Actor with ActorLogging {
|
||||
|
||||
import ExecutionContext.Implicits.global
|
||||
|
||||
val delay = 30 seconds
|
||||
|
||||
case class BalanceUpdate(first: AvailableBalanceChanged, last: AvailableBalanceChanged)
|
||||
|
||||
case class ProcessEvent(channelId: ByteVector32)
|
||||
|
||||
override def receive: Receive = run(Map.empty)
|
||||
|
||||
def run(pending: Map[ByteVector32, BalanceUpdate]): Receive = {
|
||||
|
||||
case e: AvailableBalanceChanged =>
|
||||
pending.get(e.channelId) match {
|
||||
case None =>
|
||||
// we delay the processing of the event in order to smooth variations
|
||||
log.info(s"will log balance event in $delay for channelId=${e.channelId}")
|
||||
context.system.scheduler.scheduleOnce(delay, self, ProcessEvent(e.channelId))
|
||||
context.become(run(pending + (e.channelId -> BalanceUpdate(e, e))))
|
||||
case Some(BalanceUpdate(first, _)) =>
|
||||
// we already are about to log a balance event, let's update the data we have
|
||||
log.info(s"updating balance data for channelId=${e.channelId}")
|
||||
context.become(run(pending + (e.channelId -> BalanceUpdate(first, e))))
|
||||
}
|
||||
|
||||
case ProcessEvent(channelId) =>
|
||||
pending.get(channelId) match {
|
||||
case Some(BalanceUpdate(first, last)) =>
|
||||
if (first.commitments.remoteCommit.spec.toRemote == last.localBalance) {
|
||||
// we don't log anything if the balance didn't change (e.g. it was a probe payment)
|
||||
log.info(s"ignoring balance event for channelId=$channelId (changed was discarded)")
|
||||
} else {
|
||||
log.info(s"processing balance event for channelId=$channelId balance=${first.localBalance}->${last.localBalance}")
|
||||
// we log the last event, which contains the most up to date balance
|
||||
db.add(last)
|
||||
context.become(run(pending - channelId))
|
||||
}
|
||||
case None => () // wtf?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import fr.acinq.bitcoin.ByteVector32
|
|||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.MilliSatoshi
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.router.ChannelHop
|
||||
import fr.acinq.eclair.router.Hop
|
||||
|
||||
import scala.compat.Platform
|
||||
|
||||
|
@ -35,19 +35,41 @@ sealed trait PaymentEvent {
|
|||
val timestamp: Long
|
||||
}
|
||||
|
||||
case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) extends PaymentEvent {
|
||||
require(parts.nonEmpty, "must have at least one subpayment")
|
||||
val amount: MilliSatoshi = parts.map(_.amount).sum
|
||||
val feesPaid: MilliSatoshi = parts.map(_.feesPaid).sum
|
||||
/**
|
||||
* A payment was successfully sent and fulfilled.
|
||||
*
|
||||
* @param id id of the whole payment attempt (if using multi-part, there will be multiple parts, each with
|
||||
* a different id).
|
||||
* @param paymentHash payment hash.
|
||||
* @param paymentPreimage payment preimage (proof of payment).
|
||||
* @param recipientAmount amount that has been received by the final recipient.
|
||||
* @param recipientNodeId id of the final recipient.
|
||||
* @param parts child payments (actual outgoing HTLCs).
|
||||
*/
|
||||
case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment]) extends PaymentEvent {
|
||||
require(parts.nonEmpty, "must have at least one payment part")
|
||||
val amountWithFees: MilliSatoshi = parts.map(_.amountWithFees).sum
|
||||
val feesPaid: MilliSatoshi = amountWithFees - recipientAmount // overall fees for this payment (routing + trampoline)
|
||||
val trampolineFees: MilliSatoshi = parts.map(_.amount).sum - recipientAmount
|
||||
val nonTrampolineFees: MilliSatoshi = feesPaid - trampolineFees // routing fees to reach the first trampoline node, or the recipient if not using trampoline
|
||||
val timestamp: Long = parts.map(_.timestamp).min // we use min here because we receive the proof of payment as soon as the first partial payment is fulfilled
|
||||
}
|
||||
|
||||
// TODO: @t-bast: the route fields should be a Seq[Hop], not Seq[ChannelHop]
|
||||
|
||||
object PaymentSent {
|
||||
|
||||
case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[ChannelHop]], timestamp: Long = Platform.currentTime) {
|
||||
/**
|
||||
* A successfully sent partial payment (single outgoing HTLC).
|
||||
*
|
||||
* @param id id of the outgoing payment.
|
||||
* @param amount amount received by the target node.
|
||||
* @param feesPaid fees paid to route to the target node.
|
||||
* @param toChannelId id of the channel used.
|
||||
* @param route payment route used.
|
||||
* @param timestamp absolute time in milli-seconds since UNIX epoch when the payment was fulfilled.
|
||||
*/
|
||||
case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: Long = Platform.currentTime) {
|
||||
require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop")
|
||||
val amountWithFees: MilliSatoshi = amount + feesPaid
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -57,14 +79,27 @@ case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[Paym
|
|||
sealed trait PaymentRelayed extends PaymentEvent {
|
||||
val amountIn: MilliSatoshi
|
||||
val amountOut: MilliSatoshi
|
||||
val timestamp: Long
|
||||
}
|
||||
|
||||
case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentRelayed
|
||||
|
||||
case class TrampolinePaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, toNodeId: PublicKey, fromChannelIds: Seq[ByteVector32], toChannelIds: Seq[ByteVector32], timestamp: Long = Platform.currentTime) extends PaymentRelayed
|
||||
case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, timestamp: Long = Platform.currentTime) extends PaymentRelayed {
|
||||
override val amountIn: MilliSatoshi = incoming.map(_.amount).sum
|
||||
override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum
|
||||
}
|
||||
|
||||
object PaymentRelayed {
|
||||
|
||||
case class Part(amount: MilliSatoshi, channelId: ByteVector32)
|
||||
|
||||
type Incoming = Seq[Part]
|
||||
type Outgoing = Seq[Part]
|
||||
|
||||
}
|
||||
|
||||
case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent {
|
||||
require(parts.nonEmpty, "must have at least one subpayment")
|
||||
require(parts.nonEmpty, "must have at least one payment part")
|
||||
val amount: MilliSatoshi = parts.map(_.amount).sum
|
||||
val timestamp: Long = parts.map(_.timestamp).max // we use max here because we fulfill the payment only once we received all the parts
|
||||
}
|
||||
|
@ -83,14 +118,13 @@ sealed trait PaymentFailure
|
|||
case class LocalFailure(t: Throwable) extends PaymentFailure
|
||||
|
||||
/** A remote node failed the payment and we were able to decrypt the onion failure packet. */
|
||||
case class RemoteFailure(route: Seq[ChannelHop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure
|
||||
case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure
|
||||
|
||||
/** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */
|
||||
case class UnreadableRemoteFailure(route: Seq[ChannelHop]) extends PaymentFailure
|
||||
case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure
|
||||
|
||||
object PaymentFailure {
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel.AddHtlcFailed
|
||||
import fr.acinq.eclair.router.RouteNotFound
|
||||
import fr.acinq.eclair.wire.Update
|
||||
|
|
|
@ -495,15 +495,14 @@ object PaymentRequest {
|
|||
timestamp = bolt11Data.timestamp,
|
||||
nodeId = pub,
|
||||
tags = bolt11Data.taggedFields,
|
||||
signature = bolt11Data.signature
|
||||
)
|
||||
signature = bolt11Data.signature)
|
||||
}
|
||||
|
||||
private def readBoltData(input: String): Bolt11Data = {
|
||||
val lowercaseInput = input.toLowerCase
|
||||
val separatorIndex = lowercaseInput.lastIndexOf('1')
|
||||
val hrp = lowercaseInput.take(separatorIndex)
|
||||
val prefix: String = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix"))
|
||||
if (!prefixes.values.exists(prefix => hrp.startsWith(prefix))) throw new RuntimeException("unknown prefix")
|
||||
val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size
|
||||
Codecs.bolt11DataCodec.decode(data).require.value
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import akka.actor.{ActorContext, ActorRef}
|
|||
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.{NodeParams, _}
|
||||
import fr.acinq.eclair.db.{IncomingPayment, PaymentsDb}
|
||||
import fr.acinq.eclair.db.{IncomingPayment, PaymentType, PaymentsDb}
|
||||
import fr.acinq.eclair.io.PayToOpenRequestEvent
|
||||
import fr.acinq.eclair.payment.PaymentReceived
|
||||
import fr.acinq.eclair.payment.receive.PayToOpenHandler._
|
||||
|
@ -37,7 +37,7 @@ class PayToOpenHandler(nodeParams: NodeParams, commandBuffer: ActorRef) extends
|
|||
|
||||
override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = {
|
||||
case payToOpenRequest: PayToOpenRequest => nodeParams.db.payments.getIncomingPayment(payToOpenRequest.paymentHash) match {
|
||||
case Some(record@IncomingPayment(paymentRequest, _, _, _)) if paymentRequest.features.allowMultiPart && paymentRequest.amount.isDefined && payToOpenRequest.amountMsat < paymentRequest.amount.get =>
|
||||
case Some(record@IncomingPayment(paymentRequest, _, PaymentType.Standard, _, _)) if paymentRequest.features.allowMultiPart && paymentRequest.amount.isDefined && payToOpenRequest.amountMsat < paymentRequest.amount.get =>
|
||||
// this is a chunk for a multipart payment, do we already have the rest?
|
||||
log.info(s"received chunk for a multipart payment payToOpenRequest=$payToOpenRequest")
|
||||
val chunks = payToOpenRequest +: pendingPayToOpenReqs.getOrElse(payToOpenRequest.paymentHash, Nil)
|
||||
|
@ -74,7 +74,7 @@ object PayToOpenHandler {
|
|||
paymentHash = paymentHash,
|
||||
paymentPreimage = ByteVector32.Zeroes) // preimage all-zero means we say no to the pay-to-open request
|
||||
record_opt match {
|
||||
case Some(IncomingPayment(paymentRequest, paymentPreimage, _, _)) =>
|
||||
case Some(IncomingPayment(paymentRequest, paymentPreimage, _, _, _)) =>
|
||||
paymentRequest.amount match {
|
||||
case _ if paymentRequest.isExpired =>
|
||||
log.warning(s"received payment for an expired payment request paymentHash=${paymentHash}")
|
||||
|
|
|
@ -21,14 +21,15 @@ import java.util.UUID
|
|||
import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, PoisonPill, Props}
|
||||
import akka.event.Logging.MDC
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
|
||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
|
||||
import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentLifecycle}
|
||||
import fr.acinq.eclair.payment.{IncomingPacket, PaymentFailed, PaymentSent, TrampolinePaymentRelayed}
|
||||
import fr.acinq.eclair.router.{RouteParams, Router}
|
||||
import fr.acinq.eclair.router.{RouteNotFound, RouteParams, Router}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, Logs, MilliSatoshi, NodeParams, nodeFee, randomBytes32}
|
||||
|
||||
|
@ -45,9 +46,6 @@ import scala.collection.immutable.Queue
|
|||
*/
|
||||
class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging {
|
||||
|
||||
// TODO: @t-bast: if fees/cltv insufficient (could not find route) send special error (sender should retry with higher fees/cltv)?
|
||||
// TODO: @t-bast: add Kamon counters to monitor the size of pendingIncoming/Outgoing?
|
||||
|
||||
import NodeRelayer._
|
||||
|
||||
override def receive: Receive = main(Map.empty, Map.empty)
|
||||
|
@ -93,6 +91,7 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
|
|||
case Some(failure) =>
|
||||
log.warning(s"rejecting trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv} htlcCount=${parts.length} reason=$failure)")
|
||||
rejectPayment(upstream, Some(failure))
|
||||
context become main(pendingIncoming - paymentHash, pendingOutgoing)
|
||||
case None =>
|
||||
log.info(s"relaying trampoline payment (amountIn=${upstream.amountIn} expiryIn=${upstream.expiryIn} amountOut=${nextPayload.amountToForward} expiryOut=${nextPayload.outgoingCltv} htlcCount=${parts.length})")
|
||||
val paymentId = relay(paymentHash, upstream, nextPayload, nextPacket)
|
||||
|
@ -101,23 +100,20 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
|
|||
case None => throw new RuntimeException(s"could not find pending incoming payment (paymentHash=$paymentHash)")
|
||||
}
|
||||
|
||||
case PaymentSent(id, paymentHash, paymentPreimage, parts) =>
|
||||
case PaymentSent(id, paymentHash, paymentPreimage, _, _, parts) =>
|
||||
log.debug("trampoline payment successfully relayed")
|
||||
pendingOutgoing.get(id).foreach {
|
||||
case PendingResult(upstream, nextPayload) =>
|
||||
case PendingResult(upstream, _) =>
|
||||
fulfillPayment(upstream, paymentPreimage)
|
||||
val fromChannelIds = upstream.adds.map(_.channelId)
|
||||
val toChannelIds = parts.map(_.toChannelId)
|
||||
context.system.eventStream.publish(TrampolinePaymentRelayed(upstream.amountIn, nextPayload.amountToForward, paymentHash, nextPayload.outgoingNodeId, fromChannelIds, toChannelIds))
|
||||
val incoming = upstream.adds.map(add => PaymentRelayed.Part(add.amountMsat, add.channelId))
|
||||
val outgoing = parts.map(part => PaymentRelayed.Part(part.amountWithFees, part.toChannelId))
|
||||
context.system.eventStream.publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing))
|
||||
}
|
||||
context become main(pendingIncoming, pendingOutgoing - id)
|
||||
|
||||
case PaymentFailed(id, _, _, _) =>
|
||||
// TODO: @t-bast: try to extract the most meaningful error to return upstream (from the downstream failures)
|
||||
// - if local failure because balance too low: we should send a TEMPORARY failure upstream (they should retry when we have more balance available)
|
||||
// - if local failure because route not found: sender probably need to raise fees/cltv?
|
||||
case PaymentFailed(id, _, failures, _) =>
|
||||
log.debug("trampoline payment failed")
|
||||
pendingOutgoing.get(id).foreach { case PendingResult(upstream, _) => rejectPayment(upstream) }
|
||||
pendingOutgoing.get(id).foreach { case PendingResult(upstream, nextPayload) => rejectPayment(upstream, translateError(failures, nextPayload.outgoingNodeId)) }
|
||||
context become main(pendingIncoming, pendingOutgoing - id)
|
||||
|
||||
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
|
||||
|
@ -134,7 +130,7 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
|
|||
|
||||
private def relay(paymentHash: ByteVector32, upstream: Upstream.TrampolineRelayed, payloadOut: Onion.NodeRelayPayload, packetOut: OnionRoutingPacket): UUID = {
|
||||
val paymentId = UUID.randomUUID()
|
||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, paymentHash, payloadOut.outgoingNodeId, upstream, None, storeInDb = false, publishEvent = false)
|
||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, paymentHash, payloadOut.amountToForward, payloadOut.outgoingNodeId, upstream, None, storeInDb = false, publishEvent = false, Nil)
|
||||
val routeParams = computeRouteParams(nodeParams, upstream.amountIn, upstream.expiryIn, payloadOut.amountToForward, payloadOut.outgoingCltv)
|
||||
payloadOut.invoiceFeatures match {
|
||||
case Some(_) =>
|
||||
|
@ -143,13 +139,13 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
|
|||
// TODO: @t-bast: MPP is disabled for trampoline to non-trampoline payments until we improve the splitting algorithm for nodes with a lot of channels.
|
||||
val payFSM = spawnOutgoingPayFSM(paymentCfg, multiPart = false)
|
||||
val finalPayload = Onion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, payloadOut.paymentSecret)
|
||||
val payment = SendPayment(paymentHash, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
|
||||
val payment = SendPayment(payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, Some(routeParams))
|
||||
payFSM ! payment
|
||||
case None =>
|
||||
log.debug("relaying trampoline payment to next trampoline node")
|
||||
val payFSM = spawnOutgoingPayFSM(paymentCfg, multiPart = true)
|
||||
val paymentSecret = randomBytes32 // we generate a new secret to protect against probing attacks
|
||||
val payment = SendMultiPartPayment(paymentHash, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = Some(routeParams), additionalTlvs = Seq(OnionTlv.TrampolineOnion(packetOut)))
|
||||
val payment = SendMultiPartPayment(paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = Some(routeParams), additionalTlvs = Seq(OnionTlv.TrampolineOnion(packetOut)))
|
||||
payFSM ! payment
|
||||
}
|
||||
paymentId
|
||||
|
@ -170,12 +166,12 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c
|
|||
|
||||
override def mdc(currentMessage: Any): MDC = {
|
||||
val paymentHash_opt = currentMessage match {
|
||||
case IncomingPacket.NodeRelayPacket(add, _, _, _) => Some(add.paymentHash)
|
||||
case MultiPartPaymentFSM.MultiPartHtlcFailed(paymentHash, _, _) => Some(paymentHash)
|
||||
case MultiPartPaymentFSM.MultiPartHtlcSucceeded(paymentHash, _) => Some(paymentHash)
|
||||
case MultiPartPaymentFSM.ExtraHtlcReceived(paymentHash, _, _) => Some(paymentHash)
|
||||
case PaymentFailed(_, paymentHash, _, _) => Some(paymentHash)
|
||||
case PaymentSent(_, paymentHash, _, _) => Some(paymentHash)
|
||||
case m: IncomingPacket.NodeRelayPacket => Some(m.add.paymentHash)
|
||||
case m: MultiPartPaymentFSM.MultiPartHtlcFailed => Some(m.paymentHash)
|
||||
case m: MultiPartPaymentFSM.MultiPartHtlcSucceeded => Some(m.paymentHash)
|
||||
case m: MultiPartPaymentFSM.ExtraHtlcReceived => Some(m.paymentHash)
|
||||
case m: PaymentFailed => Some(m.paymentHash)
|
||||
case m: PaymentSent => Some(m.paymentHash)
|
||||
case _ => None
|
||||
}
|
||||
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), paymentHash_opt = paymentHash_opt)
|
||||
|
@ -207,21 +203,19 @@ object NodeRelayer {
|
|||
*/
|
||||
case class PendingResult(upstream: Upstream.TrampolineRelayed, nextPayload: Onion.NodeRelayPayload)
|
||||
|
||||
def validateRelay(nodeParams: NodeParams, upstream: Upstream.TrampolineRelayed, payloadOut: Onion.NodeRelayPayload): Option[FailureMessage] = {
|
||||
private def validateRelay(nodeParams: NodeParams, upstream: Upstream.TrampolineRelayed, payloadOut: Onion.NodeRelayPayload): Option[FailureMessage] = {
|
||||
val fee = nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, payloadOut.amountToForward)
|
||||
if (upstream.amountIn - payloadOut.amountToForward < fee) {
|
||||
// TODO: @t-bast: should be a TrampolineFeeInsufficient(upstream.amountIn, myLatestNodeUpdate)
|
||||
Some(IncorrectOrUnknownPaymentDetails(upstream.amountIn, nodeParams.currentBlockHeight))
|
||||
Some(TrampolineFeeInsufficient)
|
||||
} else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.expiryDeltaBlocks) {
|
||||
// TODO: @t-bast: should be a TrampolineExpiryTooSoon(myLatestNodeUpdate)
|
||||
Some(IncorrectOrUnknownPaymentDetails(upstream.amountIn, nodeParams.currentBlockHeight))
|
||||
Some(TrampolineExpiryTooSoon)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute route params that honor our fee and cltv requirements. */
|
||||
def computeRouteParams(nodeParams: NodeParams, amountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): RouteParams = {
|
||||
private def computeRouteParams(nodeParams: NodeParams, amountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): RouteParams = {
|
||||
val routeMaxCltv = expiryIn - expiryOut - nodeParams.expiryDeltaBlocks
|
||||
val routeMaxFee = amountIn - amountOut - nodeFee(nodeParams.feeBase, nodeParams.feeProportionalMillionth, amountOut)
|
||||
Router.getDefaultRouteParams(nodeParams.routerConf).copy(
|
||||
|
@ -231,4 +225,27 @@ object NodeRelayer {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper method translates relaying errors (returned by the downstream nodes) to a BOLT 4 standard error that we
|
||||
* should return upstream.
|
||||
*/
|
||||
private def translateError(failures: Seq[PaymentFailure], outgoingNodeId: PublicKey): Option[FailureMessage] = {
|
||||
def tooManyRouteNotFound(failures: Seq[PaymentFailure]): Boolean = {
|
||||
val routeNotFoundCount = failures.count(_ == LocalFailure(RouteNotFound))
|
||||
routeNotFoundCount > failures.length / 2
|
||||
}
|
||||
|
||||
failures match {
|
||||
case Nil => None
|
||||
case LocalFailure(MultiPartPaymentLifecycle.BalanceTooLow) :: Nil => Some(TemporaryNodeFailure) // we don't have enough outgoing liquidity at the moment
|
||||
case _ if tooManyRouteNotFound(failures) => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient
|
||||
case _ =>
|
||||
// Otherwise, we try to find a downstream error that we could decrypt.
|
||||
val outgoingNodeFailure = failures.collectFirst { case RemoteFailure(_, e) if e.originNode == outgoingNodeId => e.failureMessage }
|
||||
val otherNodeFailure = failures.collectFirst { case RemoteFailure(_, e) => e.failureMessage }
|
||||
val failure = outgoingNodeFailure.getOrElse(otherNodeFailure.getOrElse(TemporaryNodeFailure))
|
||||
Some(failure)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc}
|
|||
import fr.acinq.eclair.{LongToBtcAmount, NodeParams}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Try
|
||||
|
||||
|
@ -110,20 +111,29 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in
|
|||
case Some(relayedOut) => origin match {
|
||||
case Origin.Local(id, _) =>
|
||||
val feesPaid = 0.msat // fees are unknown since we lost the reference to the payment
|
||||
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil))
|
||||
// If all downstream HTLCs are now resolved, we can emit the payment event.
|
||||
nodeParams.db.payments.getOutgoingPayment(id).foreach(p => {
|
||||
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
|
||||
if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) {
|
||||
val succeeded = payments.collect {
|
||||
case OutgoingPayment(id, _, _, _, amount, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
|
||||
PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt)
|
||||
nodeParams.db.payments.getOutgoingPayment(id) match {
|
||||
case Some(p) =>
|
||||
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil))
|
||||
// If all downstream HTLCs are now resolved, we can emit the payment event.
|
||||
val payments = nodeParams.db.payments.listOutgoingPayments(p.parentId)
|
||||
if (!payments.exists(p => p.status == OutgoingPaymentStatus.Pending)) {
|
||||
val succeeded = payments.collect {
|
||||
case OutgoingPayment(id, _, _, _, _, amount, _, _, _, _, OutgoingPaymentStatus.Succeeded(_, feesPaid, _, completedAt)) =>
|
||||
PaymentSent.PartialPayment(id, amount, feesPaid, ByteVector32.Zeroes, None, completedAt)
|
||||
}
|
||||
val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, p.recipientAmount, p.recipientNodeId, succeeded)
|
||||
log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.recipientAmount})")
|
||||
context.system.eventStream.publish(sent)
|
||||
}
|
||||
val sent = PaymentSent(p.parentId, fulfilledHtlc.paymentHash, paymentPreimage, succeeded)
|
||||
log.info(s"payment id=${sent.id} paymentHash=${sent.paymentHash} successfully sent (amount=${sent.amount})")
|
||||
context.system.eventStream.publish(sent)
|
||||
}
|
||||
})
|
||||
case None =>
|
||||
log.warning(s"database inconsistency detected: payment $id is fulfilled but doesn't have a corresponding database entry")
|
||||
// Since we don't have a matching DB entry, we've lost the payment recipient and total amount, so we put
|
||||
// dummy values in the DB (to make sure we store the preimage) but we don't emit an event.
|
||||
val dummyFinalAmount = fulfilledHtlc.amountMsat
|
||||
val dummyNodeId = nodeParams.nodeId
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id, id, None, fulfilledHtlc.paymentHash, PaymentType.Standard, fulfilledHtlc.amountMsat, dummyFinalAmount, dummyNodeId, Platform.currentTime, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.payments.updateOutgoingPayment(PaymentSent(id, fulfilledHtlc.paymentHash, paymentPreimage, dummyFinalAmount, dummyNodeId, PaymentSent.PartialPayment(id, fulfilledHtlc.amountMsat, feesPaid, fulfilledHtlc.channelId, None) :: Nil))
|
||||
}
|
||||
// There can never be more than one pending downstream HTLC for a given local origin (a multi-part payment is
|
||||
// instead spread across multiple local origins) so we can now forget this origin.
|
||||
context become main(brokenHtlcs.copy(relayedOut = brokenHtlcs.relayedOut - origin))
|
||||
|
@ -229,7 +239,7 @@ object PostRestartHtlcCleaner {
|
|||
*/
|
||||
private def shouldFulfill(finalPacket: IncomingPacket.FinalPacket, paymentsDb: IncomingPaymentsDb): Option[ByteVector32] =
|
||||
paymentsDb.getIncomingPayment(finalPacket.add.paymentHash) match {
|
||||
case Some(IncomingPayment(_, preimage, _, IncomingPaymentStatus.Received(_, _))) => Some(preimage)
|
||||
case Some(IncomingPayment(_, preimage, _, _, IncomingPaymentStatus.Received(_, _))) => Some(preimage)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
|
|||
context.system.eventStream.publish(OutgoingChannels(channelUpdates1.values.toSeq))
|
||||
context become main(channelUpdates1, node2channels.removeBinding(remoteNodeId, shortChannelId))
|
||||
|
||||
case AvailableBalanceChanged(_, _, shortChannelId, _, commitments) =>
|
||||
case AvailableBalanceChanged(_, _, shortChannelId, commitments) =>
|
||||
val channelUpdates1 = channelUpdates.get(shortChannelId) match {
|
||||
case Some(c: OutgoingChannel) => channelUpdates + (shortChannelId -> c.copy(commitments = commitments))
|
||||
case None => channelUpdates // we only consider the balance if we have the channel_update
|
||||
|
|
|
@ -55,11 +55,13 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
require(cfg.id == cfg.parentId, "multi-part payment cannot have a parent payment")
|
||||
|
||||
val id = cfg.id
|
||||
val paymentHash = cfg.paymentHash
|
||||
|
||||
private val span = Kamon.spanBuilder("multi-part-payment")
|
||||
.tag("parentPaymentId", cfg.parentId.toString)
|
||||
.tag("paymentHash", cfg.paymentHash.toHex)
|
||||
.tag("targetNodeId", cfg.targetNodeId.toString())
|
||||
.tag("paymentHash", paymentHash.toHex)
|
||||
.tag("recipientNodeId", cfg.recipientNodeId.toString())
|
||||
.tag("recipientAmount", cfg.recipientAmount.toLong)
|
||||
.start()
|
||||
|
||||
startWith(WAIT_FOR_PAYMENT_REQUEST, WaitingForRequest)
|
||||
|
@ -94,8 +96,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
val pending = setFees(d.request.routeParams, payments, payments.size)
|
||||
Kamon.runWithContextEntry(parentPaymentIdKey, cfg.parentId) {
|
||||
Kamon.runWithSpan(span, finishSpan = true) {
|
||||
pending.headOption.foreach { case (childId, payment) => spawnChildPaymentFsm(childId, includeTrampolineFees = true) ! payment }
|
||||
pending.tail.foreach { case (childId, payment) => spawnChildPaymentFsm(childId, includeTrampolineFees = false) ! payment }
|
||||
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment }
|
||||
}
|
||||
}
|
||||
goto(PAYMENT_IN_PROGRESS) using PaymentProgress(d.sender, d.request, d.networkStats, channels.length, 0 msat, d.request.maxAttempts - 1, pending, Set.empty, Nil)
|
||||
|
@ -138,7 +139,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
case Event(ps: PaymentSent, d: PaymentProgress) =>
|
||||
require(ps.parts.length == 1, "child payment must contain only one part")
|
||||
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
|
||||
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.id)
|
||||
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id)
|
||||
}
|
||||
|
||||
when(RETRY_WITH_UPDATED_BALANCES) {
|
||||
|
@ -151,7 +152,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, d.failures :+ LocalFailure(BalanceTooLow), d.pending.keySet)
|
||||
} else {
|
||||
val pending = setFees(d.request.routeParams, payments, payments.size + d.pending.size)
|
||||
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId, includeTrampolineFees = false) ! payment }
|
||||
pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment }
|
||||
goto(PAYMENT_IN_PROGRESS) using d.copy(toSend = 0 msat, remainingAttempts = d.remainingAttempts - 1, pending = d.pending ++ pending, channelsCount = channels.length)
|
||||
}
|
||||
|
||||
|
@ -167,7 +168,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
case Event(ps: PaymentSent, d: PaymentProgress) =>
|
||||
require(ps.parts.length == 1, "child payment must contain only one part")
|
||||
// As soon as we get the preimage we can consider that the whole payment succeeded (we have a proof of payment).
|
||||
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.id)
|
||||
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending.keySet - ps.parts.head.id)
|
||||
}
|
||||
|
||||
when(PAYMENT_ABORTED) {
|
||||
|
@ -175,7 +176,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
val failures = d.failures ++ pf.failures
|
||||
val pending = d.pending - pf.id
|
||||
if (pending.isEmpty) {
|
||||
myStop(d.sender, Left(PaymentFailed(id, d.request.paymentHash, failures)))
|
||||
myStop(d.sender, Left(PaymentFailed(id, paymentHash, failures)))
|
||||
} else {
|
||||
stay using d.copy(failures = failures, pending = pending)
|
||||
}
|
||||
|
@ -184,17 +185,17 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
// This is a spec violation and is too bad for them, we obtained a proof of payment without paying the full amount.
|
||||
case Event(ps: PaymentSent, d: PaymentAborted) =>
|
||||
require(ps.parts.length == 1, "child payment must contain only one part")
|
||||
log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.id})")
|
||||
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending - ps.id)
|
||||
log.warning(s"payment recipient fulfilled incomplete multi-part payment (id=${ps.parts.head.id})")
|
||||
goto(PAYMENT_SUCCEEDED) using PaymentSucceeded(d.sender, d.request, ps.paymentPreimage, ps.parts, d.pending - ps.parts.head.id)
|
||||
}
|
||||
|
||||
when(PAYMENT_SUCCEEDED) {
|
||||
case Event(ps: PaymentSent, d: PaymentSucceeded) =>
|
||||
require(ps.parts.length == 1, "child payment must contain only one part")
|
||||
val parts = d.parts ++ ps.parts
|
||||
val pending = d.pending - ps.id
|
||||
val pending = d.pending - ps.parts.head.id
|
||||
if (pending.isEmpty) {
|
||||
myStop(d.sender, Right(PaymentSent(id, d.request.paymentHash, d.preimage, parts)))
|
||||
myStop(d.sender, Right(cfg.createPaymentSent(d.preimage, parts)))
|
||||
} else {
|
||||
stay using d.copy(parts = parts, pending = pending)
|
||||
}
|
||||
|
@ -205,7 +206,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
log.warning(s"payment succeeded but partial payment failed (id=${pf.id})")
|
||||
val pending = d.pending - pf.id
|
||||
if (pending.isEmpty) {
|
||||
myStop(d.sender, Right(PaymentSent(id, d.request.paymentHash, d.preimage, d.parts)))
|
||||
myStop(d.sender, Right(cfg.createPaymentSent(d.preimage, d.parts)))
|
||||
} else {
|
||||
stay using d.copy(pending = pending)
|
||||
}
|
||||
|
@ -214,28 +215,23 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
onTransition {
|
||||
case _ -> PAYMENT_ABORTED => nextStateData match {
|
||||
case d: PaymentAborted if d.pending.isEmpty =>
|
||||
myStop(d.sender, Left(PaymentFailed(id, d.request.paymentHash, d.failures)))
|
||||
myStop(d.sender, Left(PaymentFailed(id, paymentHash, d.failures)))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
case _ -> PAYMENT_SUCCEEDED => nextStateData match {
|
||||
case d: PaymentSucceeded if d.pending.isEmpty =>
|
||||
myStop(d.sender, Right(PaymentSent(id, d.request.paymentHash, d.preimage, d.parts)))
|
||||
myStop(d.sender, Right(cfg.createPaymentSent(d.preimage, d.parts)))
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
def spawnChildPaymentFsm(childId: UUID, includeTrampolineFees: Boolean): ActorRef = {
|
||||
def spawnChildPaymentFsm(childId: UUID): ActorRef = {
|
||||
val upstream = cfg.upstream match {
|
||||
case Upstream.Local(_) => Upstream.Local(childId)
|
||||
case _ => cfg.upstream
|
||||
}
|
||||
// We attach the trampoline fees to the first child in order to account for them in the DB.
|
||||
// This is hackish and won't work if the first child payment fails and is retried, but it's okay-ish for an MVP.
|
||||
// We will update the DB schema to contain accurate Trampoline reporting, which will fix that in the future.
|
||||
// TODO: @t-bast: fix that once the DB schema is updated
|
||||
val trampolineData = if (includeTrampolineFees) cfg.trampolineData else cfg.trampolineData.map(_.copy(trampolineFees = 0 msat))
|
||||
val childCfg = cfg.copy(id = childId, publishEvent = false, upstream = upstream, trampolineData = trampolineData)
|
||||
val childCfg = cfg.copy(id = childId, publishEvent = false, upstream = upstream)
|
||||
context.actorOf(PaymentLifecycle.props(nodeParams, childCfg, router, register))
|
||||
}
|
||||
|
||||
|
@ -271,7 +267,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig,
|
|||
}
|
||||
|
||||
override def mdc(currentMessage: Any): MDC = {
|
||||
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(cfg.paymentHash))
|
||||
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(paymentHash))
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
@ -284,11 +280,24 @@ object MultiPartPaymentLifecycle {
|
|||
|
||||
def props(nodeParams: NodeParams, cfg: SendPaymentConfig, relayer: ActorRef, router: ActorRef, register: ActorRef) = Props(new MultiPartPaymentLifecycle(nodeParams, cfg, relayer, router, register))
|
||||
|
||||
case class SendMultiPartPayment(paymentHash: ByteVector32,
|
||||
paymentSecret: ByteVector32,
|
||||
/**
|
||||
* Send a payment to a given node. The payment may be split into multiple child payments, for which a path-finding
|
||||
* algorithm will run to find suitable payment routes.
|
||||
*
|
||||
* @param paymentSecret payment secret to protect against probing (usually from a Bolt 11 invoice).
|
||||
* @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline
|
||||
* node when using trampoline).
|
||||
* @param totalAmount total amount to send to the target node.
|
||||
* @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs).
|
||||
* @param maxAttempts maximum number of retries.
|
||||
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
|
||||
* @param routeParams parameters to fine-tune the routing algorithm.
|
||||
* @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node.
|
||||
*/
|
||||
case class SendMultiPartPayment(paymentSecret: ByteVector32,
|
||||
targetNodeId: PublicKey,
|
||||
totalAmount: MilliSatoshi,
|
||||
finalExpiry: CltvExpiry,
|
||||
targetExpiry: CltvExpiry,
|
||||
maxAttempts: Int,
|
||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||
routeParams: Option[RouteParams] = None,
|
||||
|
@ -400,9 +409,8 @@ object MultiPartPaymentLifecycle {
|
|||
|
||||
private def createChildPayment(nodeParams: NodeParams, request: SendMultiPartPayment, childAmount: MilliSatoshi, channel: OutgoingChannel): SendPayment = {
|
||||
SendPayment(
|
||||
request.paymentHash,
|
||||
request.targetNodeId,
|
||||
Onion.createMultiPartPayload(childAmount, request.totalAmount, request.finalExpiry, request.paymentSecret, request.additionalTlvs),
|
||||
Onion.createMultiPartPayload(childAmount, request.totalAmount, request.targetExpiry, request.paymentSecret, request.additionalTlvs),
|
||||
request.maxAttempts,
|
||||
request.assistedRoutes,
|
||||
request.routeParams,
|
||||
|
|
|
@ -24,13 +24,13 @@ import fr.acinq.bitcoin.Crypto.PublicKey
|
|||
import fr.acinq.eclair.channel.{Channel, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
||||
import fr.acinq.eclair.payment.{LocalFailure, OutgoingPacket, PaymentFailed, PaymentRequest}
|
||||
import fr.acinq.eclair.router.{NodeHop, RouteParams}
|
||||
import fr.acinq.eclair.router.{ChannelHop, Hop, NodeHop, RouteParams}
|
||||
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
||||
import fr.acinq.eclair.wire.{Onion, OnionTlv}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32}
|
||||
|
||||
/**
|
||||
* Created by PM on 29/08/2016.
|
||||
|
@ -39,112 +39,284 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
|
|||
|
||||
import PaymentInitiator._
|
||||
|
||||
override def receive: Receive = {
|
||||
override def receive: Receive = main(Map.empty)
|
||||
|
||||
def main(pending: Map[UUID, PendingPayment]): Receive = {
|
||||
case r: SendPaymentRequest =>
|
||||
val paymentId = UUID.randomUUID()
|
||||
sender ! paymentId
|
||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.targetNodeId, Upstream.Local(paymentId), r.paymentRequest, storeInDb = true, publishEvent = true)
|
||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), r.paymentRequest, storeInDb = true, publishEvent = true, Nil)
|
||||
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
|
||||
r.paymentRequest match {
|
||||
case Some(invoice) if !invoice.features.supported =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(new IllegalArgumentException(s"can't send payment: unknown invoice features (${invoice.features})")) :: Nil)
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice(s"unknown invoice features (${invoice.features})")) :: Nil)
|
||||
case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) =>
|
||||
invoice.paymentSecret match {
|
||||
case Some(paymentSecret) => r.predefinedRoute match {
|
||||
case Nil => spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(r.paymentHash, paymentSecret, r.targetNodeId, r.amount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||
case hops => spawnPaymentFsm(paymentCfg) forward SendPaymentToRoute(r.paymentHash, hops, Onion.createMultiPartPayload(r.amount, invoice.amount.getOrElse(r.amount), finalExpiry, paymentSecret))
|
||||
}
|
||||
case None => sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(new IllegalArgumentException("can't send payment: multi-part invoice is missing a payment secret")) :: Nil)
|
||||
case Some(paymentSecret) =>
|
||||
spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||
case None =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(InvalidInvoice("multi-part invoice is missing a payment secret")) :: Nil)
|
||||
}
|
||||
case _ =>
|
||||
val payFsm = spawnPaymentFsm(paymentCfg)
|
||||
// NB: we only generate legacy payment onions for now for maximum compatibility.
|
||||
r.predefinedRoute match {
|
||||
case Nil => payFsm forward SendPayment(r.paymentHash, r.targetNodeId, FinalLegacyPayload(r.amount, finalExpiry), r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||
case hops => payFsm forward SendPaymentToRoute(r.paymentHash, hops, FinalLegacyPayload(r.amount, finalExpiry))
|
||||
}
|
||||
spawnPaymentFsm(paymentCfg) forward SendPayment(r.recipientNodeId, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.maxAttempts, r.assistedRoutes, r.routeParams)
|
||||
}
|
||||
|
||||
case r: SendTrampolinePaymentRequest =>
|
||||
val paymentId = UUID.randomUUID()
|
||||
sender ! paymentId
|
||||
if (!r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty) {
|
||||
// Phoenix special case: we allow paying an amountless payment request over trampoline. It's ok because user has been warned in Phoenix.
|
||||
log.info("trying to pay an amountless invoice over trampoline")
|
||||
r.trampolineAttempts match {
|
||||
case Nil =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineFeesMissing) :: Nil)
|
||||
case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts =>
|
||||
if (!r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty) {
|
||||
// Phoenix special case: we allow paying an amountless payment request over trampoline. It's ok because user has been warned in Phoenix.
|
||||
log.info("trying to pay an amountless invoice over trampoline")
|
||||
}
|
||||
log.info(s"sending trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta")
|
||||
sendTrampolinePayment(paymentId, r, trampolineFees, trampolineExpiryDelta)
|
||||
context become main(pending + (paymentId -> PendingPayment(sender, remainingAttempts, r)))
|
||||
}
|
||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentRequest.paymentHash, r.trampolineNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, Some(r))
|
||||
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
|
||||
Onion.createMultiPartPayload(r.finalAmount, r.finalAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
|
||||
} else {
|
||||
Onion.createSinglePartPayload(r.finalAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret)
|
||||
|
||||
case pf: PaymentFailed => pending.get(pf.id).foreach(pp => {
|
||||
val decryptedFailures = pf.failures.collect { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(_, f)) => f }
|
||||
val canRetry = decryptedFailures.contains(TrampolineFeeInsufficient) || decryptedFailures.contains(TrampolineExpiryTooSoon)
|
||||
pp.remainingAttempts match {
|
||||
case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts if canRetry =>
|
||||
log.info(s"retrying trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta")
|
||||
sendTrampolinePayment(pf.id, pp.r, trampolineFees, trampolineExpiryDelta)
|
||||
context become main(pending + (pf.id -> pp.copy(remainingAttempts = remainingAttempts)))
|
||||
case _ =>
|
||||
pp.sender ! pf
|
||||
context.system.eventStream.publish(pf)
|
||||
context become main(pending - pf.id)
|
||||
}
|
||||
val trampolineRoute = Seq(
|
||||
NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat),
|
||||
NodeHop(r.trampolineNodeId, r.paymentRequest.nodeId, r.trampolineExpiryDelta, r.trampolineFees) // for now we only use a single trampoline hop
|
||||
)
|
||||
// We assume that the trampoline node supports multi-part payments (it should).
|
||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
|
||||
OutgoingPacket.buildPacket(Sphinx.TrampolinePacket)(r.paymentRequest.paymentHash, trampolineRoute, finalPayload)
|
||||
} else {
|
||||
OutgoingPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload)
|
||||
})
|
||||
|
||||
case ps: PaymentSent => pending.get(ps.id).foreach(pp => {
|
||||
pp.sender ! ps
|
||||
context.system.eventStream.publish(ps)
|
||||
context become main(pending - ps.id)
|
||||
})
|
||||
|
||||
case r: SendPaymentToRouteRequest =>
|
||||
val paymentId = UUID.randomUUID()
|
||||
val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID())
|
||||
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
|
||||
val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq
|
||||
val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, additionalHops)
|
||||
val payFsm = spawnPaymentFsm(paymentCfg)
|
||||
r.trampolineNodes match {
|
||||
case trampoline :: recipient :: Nil =>
|
||||
log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}")
|
||||
// We generate a random secret for the payment to the first trampoline node.
|
||||
val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32)
|
||||
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret))
|
||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(SendTrampolinePaymentRequest(r.recipientAmount, r.paymentRequest, trampoline, Seq((r.trampolineFees, r.trampolineExpiryDelta)), r.finalExpiryDelta), r.trampolineFees, r.trampolineExpiryDelta)
|
||||
payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))))
|
||||
case Nil =>
|
||||
sender ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None)
|
||||
r.paymentRequest.paymentSecret match {
|
||||
case Some(paymentSecret) => payFsm forward SendPaymentToRoute(r.route, Onion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, paymentSecret))
|
||||
case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry))
|
||||
}
|
||||
case _ =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(new IllegalArgumentException(s"unsupported number of trampoline nodes: ${r.trampolineNodes}")) :: Nil)
|
||||
}
|
||||
// We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node.
|
||||
val trampolineSecret = randomBytes32
|
||||
spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(r.paymentRequest.paymentHash, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, 1, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion.packet)))
|
||||
}
|
||||
|
||||
def spawnPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(PaymentLifecycle.props(nodeParams, paymentCfg, router, register))
|
||||
|
||||
def spawnMultiPartPaymentFsm(paymentCfg: SendPaymentConfig): ActorRef = context.actorOf(MultiPartPaymentLifecycle.props(nodeParams, paymentCfg, relayer, router, register))
|
||||
|
||||
private def buildTrampolinePayment(r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): (MilliSatoshi, CltvExpiry, OnionRoutingPacket) = {
|
||||
val trampolineRoute = Seq(
|
||||
NodeHop(nodeParams.nodeId, r.trampolineNodeId, nodeParams.expiryDeltaBlocks, 0 msat),
|
||||
NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop
|
||||
)
|
||||
val finalPayload = if (r.paymentRequest.features.allowMultiPart) {
|
||||
Onion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get)
|
||||
} else {
|
||||
Onion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret)
|
||||
}
|
||||
// We assume that the trampoline node supports multi-part payments (it should).
|
||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (r.paymentRequest.features.allowTrampoline) {
|
||||
OutgoingPacket.buildPacket(Sphinx.TrampolinePacket)(r.paymentHash, trampolineRoute, finalPayload)
|
||||
} else {
|
||||
OutgoingPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload)
|
||||
}
|
||||
(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
|
||||
}
|
||||
|
||||
private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePaymentRequest, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Unit = {
|
||||
val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees)))
|
||||
// We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node.
|
||||
val trampolineSecret = randomBytes32
|
||||
val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampolineFees, trampolineExpiryDelta)
|
||||
spawnMultiPartPaymentFsm(paymentCfg) ! SendMultiPartPayment(trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, 1, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PaymentInitiator {
|
||||
|
||||
def props(nodeParams: NodeParams, router: ActorRef, relayer: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], nodeParams, router, relayer, register)
|
||||
|
||||
case class PendingPayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePaymentRequest)
|
||||
|
||||
/**
|
||||
* We temporarily let the caller decide to use Trampoline (instead of a normal payment) and set the fees/cltv.
|
||||
* It's the caller's responsibility to retry with a higher fee/cltv on certain failures.
|
||||
* Once we have trampoline fee estimation built into the router, the decision to use Trampoline or not should be done
|
||||
* automatically by the router instead of the caller.
|
||||
* TODO: @t-bast: remove this message once full Trampoline is implemented.
|
||||
*/
|
||||
case class SendTrampolinePaymentRequest(finalAmount: MilliSatoshi,
|
||||
trampolineFees: MilliSatoshi,
|
||||
* We temporarily let the caller decide to use Trampoline (instead of a normal payment) and set the fees/cltv.
|
||||
* Once we have trampoline fee estimation built into the router, the decision to use Trampoline or not should be done
|
||||
* automatically by the router instead of the caller.
|
||||
*
|
||||
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
|
||||
* @param paymentRequest Bolt 11 invoice.
|
||||
* @param trampolineNodeId id of the trampoline node.
|
||||
* @param trampolineAttempts fees and expiry delta for the trampoline node. If this list contains multiple entries,
|
||||
* the payment will automatically be retried in case of TrampolineFeeInsufficient errors.
|
||||
* For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10
|
||||
* msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs.
|
||||
* @param finalExpiryDelta expiry delta for the final recipient.
|
||||
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
|
||||
*/
|
||||
case class SendTrampolinePaymentRequest(recipientAmount: MilliSatoshi,
|
||||
paymentRequest: PaymentRequest,
|
||||
trampolineNodeId: PublicKey,
|
||||
trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)],
|
||||
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
|
||||
trampolineExpiryDelta: CltvExpiryDelta,
|
||||
routeParams: Option[RouteParams] = None) {
|
||||
val recipientNodeId = paymentRequest.nodeId
|
||||
val paymentHash = paymentRequest.paymentHash
|
||||
|
||||
// We add one block in order to not have our htlcs fail when a new block has just been found.
|
||||
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
|
||||
}
|
||||
|
||||
case class SendPaymentRequest(amount: MilliSatoshi,
|
||||
/**
|
||||
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
|
||||
* @param paymentHash payment hash.
|
||||
* @param recipientNodeId id of the final recipient.
|
||||
* @param maxAttempts maximum number of retries.
|
||||
* @param finalExpiryDelta expiry delta for the final recipient.
|
||||
* @param paymentRequest (optional) Bolt 11 invoice.
|
||||
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
|
||||
* @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice).
|
||||
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
|
||||
*/
|
||||
case class SendPaymentRequest(recipientAmount: MilliSatoshi,
|
||||
paymentHash: ByteVector32,
|
||||
targetNodeId: PublicKey,
|
||||
recipientNodeId: PublicKey,
|
||||
maxAttempts: Int,
|
||||
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
|
||||
paymentRequest: Option[PaymentRequest] = None,
|
||||
externalId: Option[String] = None,
|
||||
predefinedRoute: Seq[PublicKey] = Nil,
|
||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||
routeParams: Option[RouteParams] = None) {
|
||||
// We add one block in order to not have our htlcs fail when a new block has just been found.
|
||||
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* The sender can skip the routing algorithm by specifying the route to use.
|
||||
* When combining with MPP and Trampoline, extra-care must be taken to make sure payments are correctly grouped: only
|
||||
* amount, route and trampolineNodes should be changing.
|
||||
*
|
||||
* Example 1: MPP containing two HTLCs for a 600 msat invoice:
|
||||
* SendPaymentToRouteRequest(200 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||
* SendPaymentToRouteRequest(400 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||
*
|
||||
* Example 2: Trampoline with MPP for a 600 msat invoice and 100 msat trampoline fees:
|
||||
* SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter))
|
||||
* SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter))
|
||||
*
|
||||
* @param amount amount that should be received by the last node in the route (should take trampoline
|
||||
* fees into account).
|
||||
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
|
||||
* This amount may be split between multiple requests if using MPP.
|
||||
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
|
||||
* @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make
|
||||
* sure all partial payments use the same parentId. If not provided, a random parentId will
|
||||
* be generated that can be used for the remaining partial payments.
|
||||
* @param paymentRequest Bolt 11 invoice.
|
||||
* @param finalExpiryDelta expiry delta for the final recipient.
|
||||
* @param route route to use to reach either the final recipient or the first trampoline node.
|
||||
* @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline
|
||||
* node against probing. When manually sending a multi-part payment, you need to make sure
|
||||
* all partial payments use the same trampolineSecret.
|
||||
* @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same
|
||||
* for all partial payments in the set.
|
||||
* @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be
|
||||
* the same for all partial payments in the set.
|
||||
* @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a
|
||||
* single trampoline node).
|
||||
*/
|
||||
case class SendPaymentToRouteRequest(amount: MilliSatoshi,
|
||||
recipientAmount: MilliSatoshi,
|
||||
externalId: Option[String],
|
||||
parentId: Option[UUID],
|
||||
paymentRequest: PaymentRequest,
|
||||
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
|
||||
route: Seq[PublicKey],
|
||||
trampolineSecret: Option[ByteVector32],
|
||||
trampolineFees: MilliSatoshi,
|
||||
trampolineExpiryDelta: CltvExpiryDelta,
|
||||
trampolineNodes: Seq[PublicKey]) {
|
||||
val recipientNodeId = paymentRequest.nodeId
|
||||
val paymentHash = paymentRequest.paymentHash
|
||||
|
||||
// We add one block in order to not have our htlcs fail when a new block has just been found.
|
||||
def finalExpiry(currentBlockHeight: Long) = finalExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param paymentId id of the outgoing payment (mapped to a single outgoing HTLC).
|
||||
* @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make sure
|
||||
* all partial payments use the same parentId.
|
||||
* @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline node
|
||||
* against probing. When manually sending a multi-part payment, you need to make sure all
|
||||
* partial payments use the same trampolineSecret.
|
||||
*/
|
||||
case class SendPaymentToRouteResponse(paymentId: UUID, parentId: UUID, trampolineSecret: Option[ByteVector32])
|
||||
|
||||
/**
|
||||
* Configuration for an instance of a payment state machine.
|
||||
*
|
||||
* @param id id of the outgoing payment (mapped to a single outgoing HTLC).
|
||||
* @param parentId id of the whole payment (if using multi-part, there will be N associated child payments,
|
||||
* each with a different id).
|
||||
* @param externalId externally-controlled identifier (to reconcile between application DB and eclair DB).
|
||||
* @param paymentHash payment hash.
|
||||
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
|
||||
* @param recipientNodeId id of the final recipient.
|
||||
* @param upstream information about the payment origin (to link upstream to downstream when relaying a payment).
|
||||
* @param paymentRequest Bolt 11 invoice.
|
||||
* @param storeInDb whether to store data in the payments DB (e.g. when we're relaying a trampoline payment, we
|
||||
* don't want to store in the DB).
|
||||
* @param publishEvent whether to publish a [[fr.acinq.eclair.payment.PaymentEvent]] on success/failure (e.g. for
|
||||
* multi-part child payments, we don't want to emit events for each child, only for the whole payment).
|
||||
* @param additionalHops additional hops that the payment state machine isn't aware of (e.g. when using trampoline, hops
|
||||
* that occur after the first trampoline node).
|
||||
*/
|
||||
case class SendPaymentConfig(id: UUID,
|
||||
parentId: UUID,
|
||||
externalId: Option[String],
|
||||
paymentHash: ByteVector32,
|
||||
targetNodeId: PublicKey,
|
||||
recipientAmount: MilliSatoshi,
|
||||
recipientNodeId: PublicKey,
|
||||
upstream: Upstream,
|
||||
paymentRequest: Option[PaymentRequest],
|
||||
storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments
|
||||
publishEvent: Boolean,
|
||||
// TODO: @t-bast: this is a very awkward work-around to get accurate data in the DB: fix this once we update the DB schema
|
||||
trampolineData: Option[SendTrampolinePaymentRequest] = None)
|
||||
additionalHops: Seq[NodeHop]) {
|
||||
def fullRoute(hops: Seq[ChannelHop]): Seq[Hop] = hops ++ additionalHops
|
||||
|
||||
def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts)
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
case class InvalidInvoice(message: String) extends IllegalArgumentException(s"can't send payment: $message")
|
||||
object TrampolineFeesMissing extends IllegalArgumentException("trampoline fees and cltv expiry delta are missing")
|
||||
object TrampolineLegacyAmountLessInvoice extends IllegalArgumentException("cannot pay a 0-value invoice via trampoline-to-legacy (trampoline may steal funds)")
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
|
|||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Register}
|
||||
import fr.acinq.eclair.crypto.{Sphinx, TransportHandler}
|
||||
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
|
||||
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
|
||||
import fr.acinq.eclair.payment._
|
||||
|
@ -45,6 +45,7 @@ import scala.util.{Failure, Success}
|
|||
class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) extends FSMDiagnosticActorLogging[PaymentLifecycle.State, PaymentLifecycle.Data] {
|
||||
|
||||
val id = cfg.id
|
||||
val paymentHash = cfg.paymentHash
|
||||
val paymentsDb = nodeParams.db.payments
|
||||
|
||||
private val span = Kamon.runWithContextEntry(MultiPartPaymentLifecycle.parentPaymentIdKey, cfg.parentId) {
|
||||
|
@ -55,8 +56,9 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
}
|
||||
spanBuilder
|
||||
.tag("paymentId", cfg.id.toString)
|
||||
.tag("paymentHash", cfg.paymentHash.toHex)
|
||||
.tag("targetNodeId", cfg.targetNodeId.toString())
|
||||
.tag("paymentHash", paymentHash.toHex)
|
||||
.tag("recipientNodeId", cfg.recipientNodeId.toString())
|
||||
.tag("recipientAmount", cfg.recipientAmount.toLong)
|
||||
.start()
|
||||
}
|
||||
|
||||
|
@ -64,20 +66,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
|
||||
when(WAITING_FOR_REQUEST) {
|
||||
case Event(c: SendPaymentToRoute, WaitingForRequest) =>
|
||||
span.tag("targetNodeId", c.targetNodeId.toString())
|
||||
span.tag("amount", c.finalPayload.amount.toLong)
|
||||
span.tag("totalAmount", c.finalPayload.totalAmount.toLong)
|
||||
span.tag("expiry", c.finalPayload.expiry.toLong)
|
||||
log.debug("sending {} to route {}", c.finalPayload.amount, c.hops.mkString("->"))
|
||||
val send = SendPayment(c.paymentHash, c.hops.last, c.finalPayload, maxAttempts = 1)
|
||||
val send = SendPayment(c.hops.last, c.finalPayload, maxAttempts = 1)
|
||||
router ! FinalizeRoute(c.hops)
|
||||
if (cfg.storeInDb) {
|
||||
val targetNodeId = cfg.trampolineData.map(_.paymentRequest.nodeId).getOrElse(cfg.targetNodeId)
|
||||
val finalAmount = c.finalPayload.amount - cfg.trampolineData.map(_.trampolineFees).getOrElse(0 msat)
|
||||
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, cfg.paymentHash, finalAmount, targetNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
|
||||
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
|
||||
}
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, failures = Nil)
|
||||
|
||||
case Event(c: SendPayment, WaitingForRequest) =>
|
||||
span.tag("targetNodeId", c.targetNodeId.toString())
|
||||
span.tag("amount", c.finalPayload.amount.toLong)
|
||||
span.tag("totalAmount", c.finalPayload.totalAmount.toLong)
|
||||
span.tag("expiry", c.finalPayload.expiry.toLong)
|
||||
|
@ -91,9 +93,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, routeParams = c.routeParams, ignoreNodes = ignoredNodes)
|
||||
}
|
||||
if (cfg.storeInDb) {
|
||||
val targetNodeId = cfg.trampolineData.map(_.paymentRequest.nodeId).getOrElse(cfg.targetNodeId)
|
||||
val finalAmount = c.finalPayload.amount - cfg.trampolineData.map(_.trampolineFees).getOrElse(0 msat)
|
||||
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, cfg.paymentHash, finalAmount, targetNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
|
||||
paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, Platform.currentTime, cfg.paymentRequest, OutgoingPaymentStatus.Pending))
|
||||
}
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil)
|
||||
}
|
||||
|
@ -103,12 +103,12 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
val hops = c.routePrefix ++ routeHops
|
||||
log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId).mkString("->")}")
|
||||
val firstHop = hops.head
|
||||
val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(cfg.upstream, c.paymentHash, hops, c.finalPayload)
|
||||
val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(cfg.upstream, paymentHash, hops, c.finalPayload)
|
||||
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)) =>
|
||||
onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t)))
|
||||
case Event(Status.Failure(t), WaitingForRoute(s, _, failures)) =>
|
||||
onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(t)))
|
||||
myStop()
|
||||
}
|
||||
|
||||
|
@ -116,9 +116,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
case Event("ok", _) => stay
|
||||
|
||||
case Event(fulfill: UpdateFulfillHtlc, WaitingForComplete(s, c, cmd, _, _, _, _, route)) =>
|
||||
val trampolineFees = cfg.trampolineData.map(_.trampolineFees).getOrElse(0 msat)
|
||||
val p = PartialPayment(id, c.finalPayload.amount - trampolineFees, cmd.amount - c.finalPayload.amount + trampolineFees, fulfill.channelId, Some(route))
|
||||
onSuccess(s, PaymentSent(id, c.paymentHash, fulfill.paymentPreimage, p :: Nil))
|
||||
val p = PartialPayment(id, c.finalPayload.amount, cmd.amount - c.finalPayload.amount, fulfill.channelId, Some(cfg.fullRoute(route)))
|
||||
onSuccess(s, cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil))
|
||||
myStop()
|
||||
|
||||
case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) =>
|
||||
|
@ -126,20 +125,20 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
case Success(e@Sphinx.DecryptedFailurePacket(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)")
|
||||
onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ RemoteFailure(hops, e)))
|
||||
onFailure(s, PaymentFailed(id, paymentHash, failures :+ RemoteFailure(cfg.fullRoute(hops), e)))
|
||||
myStop()
|
||||
case res if failures.size + 1 >= c.maxAttempts =>
|
||||
// otherwise we never try more than maxAttempts, no matter the kind of error returned
|
||||
val failure = res match {
|
||||
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
|
||||
log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)")
|
||||
RemoteFailure(hops, e)
|
||||
RemoteFailure(cfg.fullRoute(hops), e)
|
||||
case Failure(t) =>
|
||||
log.warning(s"cannot parse returned error: ${t.getMessage}")
|
||||
UnreadableRemoteFailure(hops)
|
||||
UnreadableRemoteFailure(cfg.fullRoute(hops))
|
||||
}
|
||||
log.warning(s"too many failed attempts, failing the payment")
|
||||
onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ failure))
|
||||
onFailure(s, PaymentFailed(id, paymentHash, failures :+ failure))
|
||||
myStop()
|
||||
case Failure(t) =>
|
||||
log.warning(s"cannot parse returned error: ${t.getMessage}")
|
||||
|
@ -147,12 +146,12 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1)
|
||||
log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}")
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops))
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(cfg.fullRoute(hops)))
|
||||
case Success(e@Sphinx.DecryptedFailurePacket(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(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(hops), e))
|
||||
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) =>
|
||||
log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)")
|
||||
if (Announcements.checkSig(failureMessage.update, nodeId)) {
|
||||
|
@ -194,13 +193,13 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}")
|
||||
router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams)
|
||||
}
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(hops), e))
|
||||
case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) =>
|
||||
log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)")
|
||||
// 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(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams)
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e))
|
||||
goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(hops), e))
|
||||
}
|
||||
|
||||
case Event(fail: UpdateFailMalformedHtlc, _) =>
|
||||
|
@ -215,7 +214,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
// If the first hop was selected by the sender (in routePrefix) and it failed, it doesn't make sense to retry (we
|
||||
// will end up retrying over that same faulty channel).
|
||||
if (failures.size + 1 >= c.maxAttempts || c.routePrefix.nonEmpty) {
|
||||
onFailure(s, PaymentFailed(id, c.paymentHash, failures :+ LocalFailure(t)))
|
||||
onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(t)))
|
||||
myStop()
|
||||
} else {
|
||||
log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})")
|
||||
|
@ -236,7 +235,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
// this means that previous state was WAITING_FOR_COMPLETE
|
||||
d.failures.lastOption.foreach(failure => stateSpan.foreach(span => KamonExt.failSpan(span, failure)))
|
||||
case d: WaitingForComplete =>
|
||||
stateSpanBuilder.tag("route", s"${d.hops.map(_.nextNodeId).mkString("->")}")
|
||||
stateSpanBuilder.tag("route", s"${cfg.fullRoute(d.hops).map(_.nextNodeId).mkString("->")}")
|
||||
case _ => ()
|
||||
}
|
||||
stateSpan.foreach(_.finish())
|
||||
|
@ -267,7 +266,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A
|
|||
}
|
||||
|
||||
override def mdc(currentMessage: Any): MDC = {
|
||||
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(cfg.paymentHash))
|
||||
Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(cfg.parentId), paymentId_opt = Some(id), paymentHash_opt = Some(paymentHash))
|
||||
}
|
||||
|
||||
initialize()
|
||||
|
@ -280,26 +279,27 @@ object PaymentLifecycle {
|
|||
/**
|
||||
* Send a payment to a pre-defined route without running the path-finding algorithm.
|
||||
*
|
||||
* @param paymentHash payment hash.
|
||||
* @param hops payment route to use.
|
||||
* @param finalPayload payload for the target node.
|
||||
* @param finalPayload onion payload for the target node.
|
||||
*/
|
||||
case class SendPaymentToRoute(paymentHash: ByteVector32, hops: Seq[PublicKey], finalPayload: FinalPayload)
|
||||
case class SendPaymentToRoute(hops: Seq[PublicKey], finalPayload: FinalPayload) {
|
||||
require(hops.nonEmpty, s"payment route must not be empty")
|
||||
val targetNodeId = hops.last
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a payment to a given node. A path-finding algorithm will run to find a suitable payment route.
|
||||
*
|
||||
* @param paymentHash payment hash.
|
||||
* @param targetNodeId target node (payment recipient).
|
||||
* @param finalPayload payload for the target node.
|
||||
* @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline
|
||||
* node when using trampoline).
|
||||
* @param finalPayload onion payload for the target node.
|
||||
* @param maxAttempts maximum number of retries.
|
||||
* @param assistedRoutes routing hints for the last part of the route (provided in the Bolt 11 invoice).
|
||||
* @param routeParams parameters to tweak the path-finding algorithm.
|
||||
* @param assistedRoutes routing hints (usually from a Bolt 11 invoice).
|
||||
* @param routeParams parameters to fine-tune the routing algorithm.
|
||||
* @param routePrefix when provided, the payment route will start with these hops. Path-finding will run only to
|
||||
* find how to route from the last node of the route prefix to the target node.
|
||||
*/
|
||||
case class SendPayment(paymentHash: ByteVector32,
|
||||
targetNodeId: PublicKey,
|
||||
case class SendPayment(targetNodeId: PublicKey,
|
||||
finalPayload: FinalPayload,
|
||||
maxAttempts: Int,
|
||||
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
|
||||
|
|
|
@ -128,7 +128,6 @@ case class ChannelHop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: Chan
|
|||
* A directed hop between two trampoline nodes.
|
||||
* These nodes need not be connected and we don't need to know a route between them.
|
||||
* The start node will compute the route to the end node itself when it receives our payment.
|
||||
* TODO: @t-bast: once the NodeUpdate message is implemented, we should use that instead of inline cltv and fee.
|
||||
*
|
||||
* @param nodeId id of the start node.
|
||||
* @param nextNodeId id of the end node.
|
||||
|
@ -171,9 +170,16 @@ case object GetRoutingState
|
|||
|
||||
case class RoutingState(channels: Iterable[PublicChannel], nodes: Iterable[NodeAnnouncement])
|
||||
|
||||
case class Stash(updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]])
|
||||
// @formatter:off
|
||||
sealed trait GossipOrigin
|
||||
/** Gossip that we received from a remote peer. */
|
||||
case class RemoteGossip(peer: ActorRef) extends GossipOrigin
|
||||
/** Gossip that was generated by our node. */
|
||||
case object LocalGossip extends GossipOrigin
|
||||
|
||||
case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[ActorRef]], updates: Map[ChannelUpdate, Set[ActorRef]], nodes: Map[NodeAnnouncement, Set[ActorRef]])
|
||||
case class Stash(updates: Map[ChannelUpdate, Set[GossipOrigin]], nodes: Map[NodeAnnouncement, Set[GossipOrigin]])
|
||||
case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[GossipOrigin]], updates: Map[ChannelUpdate, Set[GossipOrigin]], nodes: Map[NodeAnnouncement, Set[GossipOrigin]])
|
||||
// @formatter:on
|
||||
|
||||
case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Long)
|
||||
|
||||
|
@ -183,7 +189,7 @@ case class Data(nodes: Map[PublicKey, NodeAnnouncement],
|
|||
channels: SortedMap[ShortChannelId, PublicChannel],
|
||||
stats: Option[NetworkStats],
|
||||
stash: Stash,
|
||||
awaiting: Map[ChannelAnnouncement, Seq[ActorRef]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done
|
||||
awaiting: Map[ChannelAnnouncement, Seq[RemoteGossip]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done
|
||||
privateChannels: Map[ShortChannelId, PrivateChannel], // short_channel_id -> node_id
|
||||
excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure
|
||||
graph: DirectedGraph,
|
||||
|
@ -244,26 +250,26 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
d.channels.get(shortChannelId) match {
|
||||
case Some(_) =>
|
||||
// channel has already been announced and router knows about it, we can process the channel_update
|
||||
stay using handle(u, self, d)
|
||||
stay using handle(u, LocalGossip, d)
|
||||
case None =>
|
||||
channelAnnouncement_opt match {
|
||||
case Some(c) if d.awaiting.contains(c) =>
|
||||
// channel is currently being verified, we can process the channel_update right away (it will be stashed)
|
||||
stay using handle(u, self, d)
|
||||
stay using handle(u, LocalGossip, d)
|
||||
case Some(c) =>
|
||||
// channel wasn't announced but here is the announcement, we will process it *before* the channel_update
|
||||
watcher ! ValidateRequest(c)
|
||||
val d1 = d.copy(awaiting = d.awaiting + (c -> Nil)) // no origin
|
||||
// On android we don't track pruned channels in our db
|
||||
stay using handle(u, self, d1)
|
||||
stay using handle(u, LocalGossip, d1)
|
||||
case None if d.privateChannels.contains(shortChannelId) =>
|
||||
// channel isn't announced but we already know about it, we can process the channel_update
|
||||
stay using handle(u, self, d)
|
||||
stay using handle(u, LocalGossip, d)
|
||||
case None =>
|
||||
// channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations)
|
||||
// let's create a corresponding private channel and process the channel_update
|
||||
log.info("adding unannounced local channel to remote={} shortChannelId={}", remoteNodeId, shortChannelId)
|
||||
stay using handle(u, self, d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(nodeParams.nodeId, remoteNodeId, None, None))))
|
||||
stay using handle(u, LocalGossip, d.copy(privateChannels = d.privateChannels + (shortChannelId -> PrivateChannel(nodeParams.nodeId, remoteNodeId, None, None))))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -440,14 +446,14 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
stay
|
||||
|
||||
case Event(u: ChannelUpdate, d: Data) =>
|
||||
// it was sent by us (e.g. the payment lifecycle); routing messages that are sent by our peers are now wrapped in a PeerRoutingMessage
|
||||
// it was sent by us (e.g. the payment lifecycle); routing messages that are sent by our peers are wrapped in a PeerRoutingMessage
|
||||
log.debug("received channel update from {}", sender)
|
||||
stay using handle(u, sender, d)
|
||||
stay using handle(u, LocalGossip, d)
|
||||
|
||||
case Event(PeerRoutingMessage(transport, remoteNodeId, u: ChannelUpdate), d) =>
|
||||
sender ! TransportHandler.ReadAck(u)
|
||||
log.debug("received channel update for shortChannelId={}", u.shortChannelId)
|
||||
stay using handle(u, sender, d, remoteNodeId_opt = Some(remoteNodeId), transport_opt = Some(transport))
|
||||
stay using handle(u, RemoteGossip(sender), d, remoteNodeId_opt = Some(remoteNodeId), transport_opt = Some(transport))
|
||||
|
||||
case Event(PeerRoutingMessage(_, _, c: ChannelAnnouncement), d) =>
|
||||
log.debug("received channel announcement for shortChannelId={} nodeId1={} nodeId2={}", c.shortChannelId, c.nodeId1, c.nodeId2)
|
||||
|
@ -459,7 +465,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
sender ! TransportHandler.ReadAck(c)
|
||||
log.debug("ignoring {} (being verified)", c)
|
||||
// adding the sender to the list of origins so that we don't send back the same announcement to this peer later
|
||||
val origins = d.awaiting(c) :+ sender
|
||||
val origins = d.awaiting(c) :+ RemoteGossip(sender)
|
||||
stay using d.copy(awaiting = d.awaiting + (c -> origins))
|
||||
} else if (!Announcements.checkSigs(c)) {
|
||||
// On Android we don't track pruned channels in our db
|
||||
|
@ -488,7 +494,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
}
|
||||
|
||||
case Event(n: NodeAnnouncement, d: Data) =>
|
||||
// it was sent by us, routing messages that are sent by our peers are now wrapped in a PeerRoutingMessage
|
||||
// it was sent by us, routing messages that are sent by our peers are now wrapped in a PeerRoutingMessage
|
||||
stay // we just ignore node_announcements on Android
|
||||
|
||||
case Event(PeerRoutingMessage(_, _, n: NodeAnnouncement), d: Data) =>
|
||||
|
@ -532,17 +538,25 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
(c1, u1)
|
||||
}
|
||||
log.info(s"received reply_channel_range with {} channels, we're missing {} channel announcements and {} updates, format={}", shortChannelIds.array.size, channelCount, updatesCount, shortChannelIds.encoding)
|
||||
|
||||
def buildQuery(chunk: List[ShortChannelIdAndFlag]): QueryShortChannelIds = {
|
||||
// always encode empty lists as UNCOMPRESSED
|
||||
val encoding = if (chunk.isEmpty) EncodingType.UNCOMPRESSED else shortChannelIds.encoding
|
||||
QueryShortChannelIds(chainHash,
|
||||
shortChannelIds = EncodedShortChannelIds(encoding, chunk.map(_.shortChannelId)),
|
||||
if (routingMessage.timestamps_opt.isDefined || routingMessage.checksums_opt.isDefined)
|
||||
TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(encoding, chunk.map(_.flag)))
|
||||
else
|
||||
TlvStream.empty
|
||||
)
|
||||
}
|
||||
|
||||
// we update our sync data to this node (there may be multiple channel range responses and we can only query one set of ids at a time)
|
||||
val replies = shortChannelIdAndFlags
|
||||
.grouped(nodeParams.routerConf.channelQueryChunkSize)
|
||||
.map(chunk => QueryShortChannelIds(chainHash,
|
||||
shortChannelIds = EncodedShortChannelIds(shortChannelIds.encoding, chunk.map(_.shortChannelId)),
|
||||
if (routingMessage.timestamps_opt.isDefined || routingMessage.checksums_opt.isDefined)
|
||||
TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(shortChannelIds.encoding, chunk.map(_.flag)))
|
||||
else
|
||||
TlvStream.empty
|
||||
))
|
||||
.map(buildQuery)
|
||||
.toList
|
||||
|
||||
val (sync1, replynow_opt) = addToSync(d.sync, remoteNodeId, replies)
|
||||
// we only send a reply right away if there were no pending requests
|
||||
replynow_opt.foreach(transport ! _)
|
||||
|
@ -585,7 +599,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
|
||||
initialize()
|
||||
|
||||
def handle(n: NodeAnnouncement, origin: ActorRef, d: Data): Data =
|
||||
def handle(n: NodeAnnouncement, origin: GossipOrigin, d: Data): Data =
|
||||
if (d.stash.nodes.contains(n)) {
|
||||
log.debug("ignoring {} (already stashed)", n)
|
||||
val origins = d.stash.nodes(n) + origin
|
||||
|
@ -595,7 +609,10 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
d
|
||||
} else if (!Announcements.checkSig(n)) {
|
||||
log.warning("bad signature for {}", n)
|
||||
origin ! InvalidSignature(n)
|
||||
origin match {
|
||||
case RemoteGossip(peer) => peer ! InvalidSignature(n)
|
||||
case LocalGossip =>
|
||||
}
|
||||
d
|
||||
} else if (d.nodes.contains(n.nodeId)) {
|
||||
log.debug("updated node nodeId={}", n.nodeId)
|
||||
|
@ -617,7 +634,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
d
|
||||
}
|
||||
|
||||
def handle(uOriginal: ChannelUpdate, origin: ActorRef, d: Data, remoteNodeId_opt: Option[PublicKey] = None, transport_opt: Option[ActorRef] = None): Data = {
|
||||
def handle(uOriginal: ChannelUpdate, origin: GossipOrigin, d: Data, remoteNodeId_opt: Option[PublicKey] = None, transport_opt: Option[ActorRef] = None): Data = {
|
||||
// On Android, after checking the sig we remove as much data as possible to reduce RAM consumption
|
||||
require(uOriginal.chainHash == nodeParams.chainHash, s"invalid chainhash for $uOriginal, we're on ${nodeParams.chainHash}")
|
||||
// instead of keeping a copy of chainhash in each channel_update we now have a reference to then same object
|
||||
|
@ -635,7 +652,10 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
d
|
||||
} else if (!Announcements.checkSig(uOriginal, pc.getNodeIdSameSideAs(u))) {
|
||||
log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u)
|
||||
origin ! InvalidSignature(u)
|
||||
origin match {
|
||||
case RemoteGossip(peer) => peer ! InvalidSignature(u)
|
||||
case LocalGossip =>
|
||||
}
|
||||
d
|
||||
} else if (pc.getChannelUpdateSameSideAs(u).isDefined) {
|
||||
log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u)
|
||||
|
@ -677,7 +697,10 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[
|
|||
d
|
||||
} else if (!Announcements.checkSig(u, desc.a)) {
|
||||
log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u)
|
||||
origin ! InvalidSignature(u)
|
||||
origin match {
|
||||
case RemoteGossip(peer) => peer ! InvalidSignature(u)
|
||||
case LocalGossip =>
|
||||
}
|
||||
d
|
||||
} else if (pc.getChannelUpdateSameSideAs(u).isDefined) {
|
||||
log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u)
|
||||
|
@ -1000,9 +1023,9 @@ object Router {
|
|||
* there could be several reply_channel_range messages for a single query, but we make sure that the returned
|
||||
* chunks fully covers the [firstBlockNum, numberOfBlocks] range that was requested
|
||||
*
|
||||
* @param shortChannelIds list of short channel ids to split
|
||||
* @param firstBlockNum first block height requested by our peers
|
||||
* @param numberOfBlocks number of blocks requested by our peer
|
||||
* @param shortChannelIds list of short channel ids to split
|
||||
* @param firstBlockNum first block height requested by our peers
|
||||
* @param numberOfBlocks number of blocks requested by our peer
|
||||
* @param channelRangeChunkSize target chunk size. All ids that have the same block height will be grouped together, so
|
||||
* returned chunks may still contain more than `channelRangeChunkSize` elements
|
||||
* @return a list of short channel id chunks
|
||||
|
@ -1028,14 +1051,15 @@ object Router {
|
|||
else {
|
||||
// we always prepend because it's more efficient so we have to reverse the current chunk
|
||||
// for the first chunk, we make sure that we start at the request first block
|
||||
val first = if (acc.isEmpty) firstBlockNum else currentChunk.last.blockHeight
|
||||
// for the next chunks we start at the end of the range covered by the last chunk
|
||||
val first = if (acc.isEmpty) firstBlockNum else acc.head.firstBlock + acc.head.numBlocks
|
||||
val count = currentChunk.head.blockHeight - first + 1
|
||||
loop(id :: Nil, ShortChannelIdsChunk(first, count, currentChunk.reverse) :: acc)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// for the last chunk, we make sure that we cover the request block range
|
||||
val first = if (acc.isEmpty) firstBlockNum else currentChunk.last.blockHeight
|
||||
// for the last chunk, we make sure that we cover the requested block range
|
||||
val first = if (acc.isEmpty) firstBlockNum else acc.head.firstBlock + acc.head.numBlocks
|
||||
val count = numberOfBlocks - first + firstBlockNum
|
||||
(ShortChannelIdsChunk(first, count, currentChunk.reverse) :: acc).reverse
|
||||
}
|
||||
|
@ -1051,10 +1075,38 @@ object Router {
|
|||
|
||||
/**
|
||||
* Enforce max-size constraints for each chunk
|
||||
*
|
||||
* @param chunks list of short channel id chunks
|
||||
* @return a processed list of chunks
|
||||
*/
|
||||
def enforceMaximumSize(chunks: List[ShortChannelIdsChunk]) : List[ShortChannelIdsChunk] = chunks.map(_.enforceMaximumSize(MAXIMUM_CHUNK_SIZE))
|
||||
def enforceMaximumSize(chunks: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = chunks.map(_.enforceMaximumSize(MAXIMUM_CHUNK_SIZE))
|
||||
|
||||
/**
|
||||
* Build a `reply_channel_range` message
|
||||
* @param chunk chunk of scids
|
||||
* @param chainHash chain hash
|
||||
* @param defaultEncoding default encoding
|
||||
* @param queryFlags_opt query flag set by the requester
|
||||
* @param channels channels map
|
||||
* @return a ReplyChannelRange object
|
||||
*/
|
||||
def buildReplyChannelRange(chunk: ShortChannelIdsChunk, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[ShortChannelId, PublicChannel]): ReplyChannelRange = {
|
||||
val encoding = if (chunk.shortChannelIds.isEmpty) EncodingType.UNCOMPRESSED else defaultEncoding
|
||||
val (timestamps, checksums) = queryFlags_opt match {
|
||||
case Some(extension) if extension.wantChecksums | extension.wantTimestamps =>
|
||||
// we always compute timestamps and checksums even if we don't need both, overhead is negligible
|
||||
val (timestamps, checksums) = chunk.shortChannelIds.map(getChannelDigestInfo(channels)).unzip
|
||||
val encodedTimestamps = if (extension.wantTimestamps) Some(ReplyChannelRangeTlv.EncodedTimestamps(encoding, timestamps)) else None
|
||||
val encodedChecksums = if (extension.wantChecksums) Some(ReplyChannelRangeTlv.EncodedChecksums(checksums)) else None
|
||||
(encodedTimestamps, encodedChecksums)
|
||||
case _ => (None, None)
|
||||
}
|
||||
ReplyChannelRange(chainHash, chunk.firstBlock, chunk.numBlocks,
|
||||
complete = 1,
|
||||
shortChannelIds = EncodedShortChannelIds(encoding, chunk.shortChannelIds),
|
||||
timestamps = timestamps,
|
||||
checksums = checksums)
|
||||
}
|
||||
|
||||
def addToSync(syncMap: Map[PublicKey, Sync], remoteNodeId: PublicKey, pending: List[RoutingMessage]): (Map[PublicKey, Sync], Option[RoutingMessage]) = {
|
||||
pending match {
|
||||
|
|
|
@ -109,15 +109,16 @@ object Transactions {
|
|||
val mainPenaltyWeight = 484
|
||||
val htlcPenaltyWeight = 578 // based on spending an HTLC-Success output (would be 571 with HTLC-Timeout)
|
||||
|
||||
def weight2fee(feeratePerKw: Long, weight: Int) = Satoshi((feeratePerKw * weight) / 1000)
|
||||
def weight2feeMsat(feeratePerKw: Long, weight: Int) = MilliSatoshi(feeratePerKw * weight)
|
||||
|
||||
def weight2fee(feeratePerKw: Long, weight: Int): Satoshi = weight2feeMsat(feeratePerKw, weight).truncateToSatoshi
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fee tx fee
|
||||
* @param weight tx weight
|
||||
* @return the fee rate (in Satoshi/Kw) for this tx
|
||||
*/
|
||||
def fee2rate(fee: Satoshi, weight: Int) = (fee.toLong * 1000L) / weight
|
||||
def fee2rate(fee: Satoshi, weight: Int): Long = (fee.toLong * 1000L) / weight
|
||||
|
||||
/** Offered HTLCs below this amount will be trimmed. */
|
||||
def offeredHtlcTrimThreshold(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = dustLimit + weight2fee(spec.feeratePerKw, htlcTimeoutWeight)
|
||||
|
@ -142,15 +143,23 @@ object Transactions {
|
|||
}
|
||||
|
||||
/** Fee for an un-trimmed HTLC. */
|
||||
def htlcOutputFee(feeratePerKw: Long): Satoshi = weight2fee(feeratePerKw, htlcOutputWeight)
|
||||
def htlcOutputFee(feeratePerKw: Long): MilliSatoshi = weight2feeMsat(feeratePerKw, htlcOutputWeight)
|
||||
|
||||
def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = {
|
||||
/**
|
||||
* While fees are generally computed in Satoshis (since this is the smallest on-chain unit), it may be useful in some
|
||||
* cases to calculate it in MilliSatoshi to avoid rounding issues.
|
||||
* If you are adding multiple fees together for example, you should always add them in MilliSatoshi and then round
|
||||
* down to Satoshi.
|
||||
*/
|
||||
def commitTxFeeMsat(dustLimit: Satoshi, spec: CommitmentSpec): MilliSatoshi = {
|
||||
val trimmedOfferedHtlcs = trimOfferedHtlcs(dustLimit, spec)
|
||||
val trimmedReceivedHtlcs = trimReceivedHtlcs(dustLimit, spec)
|
||||
val weight = commitWeight + htlcOutputWeight * (trimmedOfferedHtlcs.size + trimmedReceivedHtlcs.size)
|
||||
weight2fee(spec.feeratePerKw, weight)
|
||||
weight2feeMsat(spec.feeratePerKw, weight)
|
||||
}
|
||||
|
||||
def commitTxFee(dustLimit: Satoshi, spec: CommitmentSpec): Satoshi = commitTxFeeMsat(dustLimit, spec).truncateToSatoshi
|
||||
|
||||
/**
|
||||
*
|
||||
* @param commitTxNumber commit tx number
|
||||
|
|
|
@ -55,10 +55,12 @@ case object RequiredChannelFeatureMissing extends Perm { def message = "channel
|
|||
case object UnknownNextPeer extends Perm { def message = "processing node does not know the next peer in the route" }
|
||||
case class AmountBelowMinimum(amount: MilliSatoshi, update: ChannelUpdate) extends Update { def message = s"payment amount was below the minimum required by the channel" }
|
||||
case class FeeInsufficient(amount: MilliSatoshi, update: ChannelUpdate) extends Update { def message = s"payment fee was below the minimum required by the channel" }
|
||||
case object TrampolineFeeInsufficient extends Node { def message = "payment fee was below the minimum required by the trampoline node" }
|
||||
case class ChannelDisabled(messageFlags: Byte, channelFlags: Byte, update: ChannelUpdate) extends Update { def message = "channel is currently disabled" }
|
||||
case class IncorrectCltvExpiry(expiry: CltvExpiry, update: ChannelUpdate) extends Update { def message = "payment expiry doesn't match the value in the onion" }
|
||||
case class IncorrectOrUnknownPaymentDetails(amount: MilliSatoshi, height: Long) extends Perm { def message = "incorrect payment details or unknown payment hash" }
|
||||
case class ExpiryTooSoon(update: ChannelUpdate) extends Update { def message = "payment expiry is too close to the current block height for safe handling by the relaying node" }
|
||||
case object TrampolineExpiryTooSoon extends Node { def message = "payment expiry is too close to the current block height for safe handling by the relaying node" }
|
||||
case class FinalIncorrectCltvExpiry(expiry: CltvExpiry) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" }
|
||||
case class FinalIncorrectHtlcAmount(amount: MilliSatoshi) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" }
|
||||
case object ExpiryTooFar extends FailureMessage { def message = "payment expiry is too far in the future" }
|
||||
|
@ -117,7 +119,11 @@ object FailureMessageCodecs {
|
|||
.typecase(19, ("amountMsat" | millisatoshi).as[FinalIncorrectHtlcAmount])
|
||||
.typecase(21, provide(ExpiryTooFar))
|
||||
.typecase(PERM | 22, (("tag" | varint) :: ("offset" | uint16)).as[InvalidOnionPayload])
|
||||
.typecase(23, provide(PaymentTimeout)),
|
||||
.typecase(23, provide(PaymentTimeout))
|
||||
// TODO: @t-bast: once fully spec-ed, these should probably include a NodeUpdate and use a different ID.
|
||||
// We should update Phoenix and our nodes at the same time, or first update Phoenix to understand both new and old errors.
|
||||
.typecase(NODE | 51, provide(TrampolineFeeInsufficient))
|
||||
.typecase(NODE | 52, provide(TrampolineExpiryTooSoon)),
|
||||
uint16.xmap(code => {
|
||||
val failureMessage = code match {
|
||||
// @formatter:off
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2019 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.wire
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.eclair.UInt64
|
||||
import fr.acinq.eclair.wire.CommonCodecs._
|
||||
import scodec.Codec
|
||||
import scodec.codecs.{discriminated, list, variableSizeBytesLong}
|
||||
|
||||
/**
|
||||
* Created by t-bast on 13/12/2019.
|
||||
*/
|
||||
|
||||
/** Tlv types used inside Init messages. */
|
||||
sealed trait InitTlv extends Tlv
|
||||
|
||||
object InitTlv {
|
||||
|
||||
/** The chains the node is interested in. */
|
||||
case class Networks(chainHashes: List[ByteVector32]) extends InitTlv
|
||||
|
||||
}
|
||||
|
||||
object InitTlvCodecs {
|
||||
|
||||
import InitTlv._
|
||||
|
||||
private val networks: Codec[Networks] = variableSizeBytesLong(varintoverflow, list(bytes32)).as[Networks]
|
||||
|
||||
val initTlvCodec = TlvCodecs.tlvStream(discriminated[InitTlv].by(varint)
|
||||
.typecase(UInt64(1), networks)
|
||||
)
|
||||
|
||||
}
|
|
@ -20,9 +20,9 @@ import fr.acinq.eclair.wire.CommonCodecs._
|
|||
import fr.acinq.eclair.{KamonExt, wire}
|
||||
import kamon.Kamon
|
||||
import kamon.tag.TagSet
|
||||
import scodec.bits.{BitVector, ByteVector}
|
||||
import scodec.bits.{BitVector, ByteVector, HexStringSyntax}
|
||||
import scodec.codecs._
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.{Attempt, Codec, DecodeResult}
|
||||
|
||||
import scodec.bits._
|
||||
|
||||
|
@ -41,7 +41,7 @@ object LightningMessageCodecs {
|
|||
},
|
||||
{ features => (ByteVector.empty, features) })
|
||||
|
||||
val initCodec: Codec[Init] = combinedFeaturesCodec.as[Init]
|
||||
val initCodec: Codec[Init] = (("features" | combinedFeaturesCodec) :: ("tlvStream" | InitTlvCodecs.initTlvCodec)).as[Init]
|
||||
|
||||
val errorCodec: Codec[Error] = (
|
||||
("channelId" | bytes32) ::
|
||||
|
@ -69,7 +69,30 @@ object LightningMessageCodecs {
|
|||
("myCurrentPerCommitmentPoint" | optional(bitsRemaining, publicKey)) ::
|
||||
("channelData" | channeldataoptional)).as[ChannelReestablish]
|
||||
|
||||
val openChannelCodec: Codec[OpenChannel] = (
|
||||
// Legacy nodes may encode an empty upfront_shutdown_script (0x0000) even if we didn't advertise support for option_upfront_shutdown_script.
|
||||
// To allow extending all messages with TLV streams, the upfront_shutdown_script field was made mandatory in https://github.com/lightningnetwork/lightning-rfc/pull/714.
|
||||
// This codec decodes both legacy and new versions, while always encoding with an upfront_shutdown_script (of length 0 if none actually provided).
|
||||
private val shutdownScriptGuard = Codec[Boolean](
|
||||
// Similar to bitsRemaining but encodes 0x0000 for an empty upfront_shutdown_script.
|
||||
(included: Boolean) => if (included) Attempt.Successful(BitVector.empty) else Attempt.Successful(hex"0000".bits),
|
||||
// Bolt 2 specifies that upfront_shutdown_scripts must be P2PKH/P2SH or segwit-v0 P2WPK/P2WSH.
|
||||
// The length of such scripts will always start with 0x00.
|
||||
// On top of that, since TLV records start with a varint, a TLV stream will never start with 0x00 unless the spec
|
||||
// assigns TLV type 0 to a new record. If that happens, that record should be the upfront_shutdown_script to allow
|
||||
// easy backwards-compatibility (as proposed here: https://github.com/lightningnetwork/lightning-rfc/pull/714).
|
||||
// That means we can discriminate on byte 0x00 to know whether we're decoding an upfront_shutdown_script or a TLV
|
||||
// stream.
|
||||
(b: BitVector) => Attempt.successful(DecodeResult(b.startsWith(hex"00".bits), b))
|
||||
)
|
||||
|
||||
private def emptyToNone(script: Option[ByteVector]): Option[ByteVector] = script match {
|
||||
case Some(s) if s.nonEmpty => script
|
||||
case _ => None
|
||||
}
|
||||
|
||||
private val upfrontShutdownScript = optional(shutdownScriptGuard, variableSizeBytes(uint16, bytes)).xmap(emptyToNone, emptyToNone)
|
||||
|
||||
private def openChannelCodec_internal(upfrontShutdownScriptCodec: Codec[Option[ByteVector]]): Codec[OpenChannel] = (
|
||||
("chainHash" | bytes32) ::
|
||||
("temporaryChannelId" | bytes32) ::
|
||||
("fundingSatoshis" | satoshi) ::
|
||||
|
@ -88,8 +111,20 @@ object LightningMessageCodecs {
|
|||
("htlcBasepoint" | publicKey) ::
|
||||
("firstPerCommitmentPoint" | publicKey) ::
|
||||
("channelFlags" | byte) ::
|
||||
("upfront_shutdown_script" | upfrontShutdownScriptCodec) ::
|
||||
("tlvStream_opt" | optional(bitsRemaining, OpenTlv.openTlvCodec))).as[OpenChannel]
|
||||
|
||||
val openChannelCodec = Codec[OpenChannel](
|
||||
(open: OpenChannel) => {
|
||||
// Phoenix versions <= 1.1.0 don't support the upfront_shutdown_script field (they interpret it as a tlv stream
|
||||
// with an unknown tlv record). For these channels we use an encoding that omits the upfront_shutdown_script for
|
||||
// backwards-compatibility (once enough Phoenix users have upgraded, we can remove work-around).
|
||||
val upfrontShutdownScriptCodec = if (open.tlvStream_opt.isDefined) provide(Option.empty[ByteVector]) else upfrontShutdownScript
|
||||
openChannelCodec_internal(upfrontShutdownScriptCodec).encode(open)
|
||||
},
|
||||
(bits: BitVector) => openChannelCodec_internal(upfrontShutdownScript).decode(bits)
|
||||
)
|
||||
|
||||
val acceptChannelCodec: Codec[AcceptChannel] = (
|
||||
("temporaryChannelId" | bytes32) ::
|
||||
("dustLimitSatoshis" | satoshi) ::
|
||||
|
@ -104,7 +139,8 @@ object LightningMessageCodecs {
|
|||
("paymentBasepoint" | publicKey) ::
|
||||
("delayedPaymentBasepoint" | publicKey) ::
|
||||
("htlcBasepoint" | publicKey) ::
|
||||
("firstPerCommitmentPoint" | publicKey)).as[AcceptChannel]
|
||||
("firstPerCommitmentPoint" | publicKey) ::
|
||||
("upfront_shutdown_script" | upfrontShutdownScript)).as[AcceptChannel]
|
||||
|
||||
val fundingCreatedCodec: Codec[FundingCreated] = (
|
||||
("temporaryChannelId" | bytes32) ::
|
||||
|
|
|
@ -45,7 +45,9 @@ sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32
|
|||
sealed trait UpdateMessage extends HtlcMessage // <- not in the spec
|
||||
// @formatter:on
|
||||
|
||||
case class Init(features: ByteVector) extends SetupMessage
|
||||
case class Init(features: ByteVector, tlvs: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
|
||||
val networks = tlvs.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil)
|
||||
}
|
||||
|
||||
case class Error(channelId: ByteVector32, data: ByteVector) extends SetupMessage with HasChannelId {
|
||||
def toAscii: String = if (fr.acinq.eclair.isAsciiPrintable(data)) new String(data.toArray, StandardCharsets.US_ASCII) else "n/a"
|
||||
|
@ -84,6 +86,7 @@ case class OpenChannel(chainHash: ByteVector32,
|
|||
htlcBasepoint: PublicKey,
|
||||
firstPerCommitmentPoint: PublicKey,
|
||||
channelFlags: Byte,
|
||||
upfrontShutdownScript: Option[ByteVector] = None,
|
||||
tlvStream_opt: Option[TlvStream[OpenTlv]] = None) extends ChannelMessage with HasTemporaryChannelId with HasChainHash
|
||||
|
||||
case class AcceptChannel(temporaryChannelId: ByteVector32,
|
||||
|
@ -99,7 +102,8 @@ case class AcceptChannel(temporaryChannelId: ByteVector32,
|
|||
paymentBasepoint: PublicKey,
|
||||
delayedPaymentBasepoint: PublicKey,
|
||||
htlcBasepoint: PublicKey,
|
||||
firstPerCommitmentPoint: PublicKey) extends ChannelMessage with HasTemporaryChannelId
|
||||
firstPerCommitmentPoint: PublicKey,
|
||||
upfrontShutdownScript: Option[ByteVector] = None) extends ChannelMessage with HasTemporaryChannelId
|
||||
|
||||
case class FundingCreated(temporaryChannelId: ByteVector32,
|
||||
fundingTxid: ByteVector32,
|
||||
|
|
|
@ -22,41 +22,40 @@ import scodec.bits.ByteVector
|
|||
import scala.reflect.ClassTag
|
||||
|
||||
/**
|
||||
* Created by t-bast on 20/06/2019.
|
||||
*/
|
||||
* Created by t-bast on 20/06/2019.
|
||||
*/
|
||||
|
||||
trait Tlv
|
||||
|
||||
/**
|
||||
* Generic tlv type we fallback to if we don't understand the incoming tlv.
|
||||
*
|
||||
* @param tag tlv tag.
|
||||
* @param value tlv value (length is implicit, and encoded as a varint).
|
||||
*/
|
||||
* Generic tlv type we fallback to if we don't understand the incoming tlv.
|
||||
*
|
||||
* @param tag tlv tag.
|
||||
* @param value tlv value (length is implicit, and encoded as a varint).
|
||||
*/
|
||||
case class GenericTlv(tag: UInt64, value: ByteVector) extends Tlv
|
||||
|
||||
/**
|
||||
* A tlv stream is a collection of tlv records.
|
||||
* A tlv stream is constrained to a specific tlv namespace that dictates how to parse the tlv records.
|
||||
* That namespace is provided by a trait extending the top-level tlv trait.
|
||||
*
|
||||
* @param records known tlv records.
|
||||
* @param unknown unknown tlv records.
|
||||
* @tparam T the stream namespace is a trait extending the top-level tlv trait.
|
||||
*/
|
||||
* A tlv stream is a collection of tlv records.
|
||||
* A tlv stream is constrained to a specific tlv namespace that dictates how to parse the tlv records.
|
||||
* That namespace is provided by a trait extending the top-level tlv trait.
|
||||
*
|
||||
* @param records known tlv records.
|
||||
* @param unknown unknown tlv records.
|
||||
* @tparam T the stream namespace is a trait extending the top-level tlv trait.
|
||||
*/
|
||||
case class TlvStream[T <: Tlv](records: Traversable[T], unknown: Traversable[GenericTlv] = Nil) {
|
||||
/**
|
||||
*
|
||||
* @tparam R input type parameter, must be a subtype of the main TLV type
|
||||
* @return the TLV record of type that matches the input type parameter if any (there can be at most one, since BOLTs specify
|
||||
* that TLV records are supposed to be unique)
|
||||
*/
|
||||
*
|
||||
* @tparam R input type parameter, must be a subtype of the main TLV type
|
||||
* @return the TLV record of type that matches the input type parameter if any (there can be at most one, since BOLTs specify
|
||||
* that TLV records are supposed to be unique)
|
||||
*/
|
||||
def get[R <: T : ClassTag]: Option[R] = records.collectFirst { case r: R => r }
|
||||
}
|
||||
|
||||
object TlvStream {
|
||||
def empty[T <: Tlv] = TlvStream[T](Nil, Nil)
|
||||
def empty[T <: Tlv]: TlvStream[T] = TlvStream[T](Nil, Nil)
|
||||
|
||||
def apply[T <: Tlv](records: T*): TlvStream[T] = TlvStream(records, Nil)
|
||||
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package fr.acinq.eclair
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import akka.util.Timeout
|
||||
|
@ -30,7 +32,7 @@ import fr.acinq.eclair.payment.PaymentRequest
|
|||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
|
||||
import fr.acinq.eclair.payment.receive.PaymentHandler
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentRequest
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest}
|
||||
import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdate
|
||||
import fr.acinq.eclair.router.{Announcements, PublicChannel, Router, GetNetworkStats, NetworkStats, Stats}
|
||||
import org.mockito.Mockito
|
||||
|
@ -99,8 +101,8 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
|
|||
eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = None)
|
||||
val send = paymentInitiator.expectMsgType[SendPaymentRequest]
|
||||
assert(send.externalId === None)
|
||||
assert(send.targetNodeId === nodeId)
|
||||
assert(send.amount === 123.msat)
|
||||
assert(send.recipientNodeId === nodeId)
|
||||
assert(send.recipientAmount === 123.msat)
|
||||
assert(send.paymentHash === ByteVector32.Zeroes)
|
||||
assert(send.paymentRequest === None)
|
||||
assert(send.assistedRoutes === Seq.empty)
|
||||
|
@ -112,8 +114,8 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
|
|||
eclair.send(Some(externalId1), nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(invoice1))
|
||||
val send1 = paymentInitiator.expectMsgType[SendPaymentRequest]
|
||||
assert(send1.externalId === Some(externalId1))
|
||||
assert(send1.targetNodeId === nodeId)
|
||||
assert(send1.amount === 123.msat)
|
||||
assert(send1.recipientNodeId === nodeId)
|
||||
assert(send1.recipientAmount === 123.msat)
|
||||
assert(send1.paymentHash === ByteVector32.Zeroes)
|
||||
assert(send1.paymentRequest === Some(invoice1))
|
||||
assert(send1.assistedRoutes === hints)
|
||||
|
@ -124,8 +126,8 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
|
|||
eclair.send(Some(externalId2), nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = Some(invoice2))
|
||||
val send2 = paymentInitiator.expectMsgType[SendPaymentRequest]
|
||||
assert(send2.externalId === Some(externalId2))
|
||||
assert(send2.targetNodeId === nodeId)
|
||||
assert(send2.amount === 123.msat)
|
||||
assert(send2.recipientNodeId === nodeId)
|
||||
assert(send2.recipientAmount === 123.msat)
|
||||
assert(send2.paymentHash === ByteVector32.Zeroes)
|
||||
assert(send2.paymentRequest === Some(invoice2))
|
||||
assert(send2.finalExpiryDelta === CltvExpiryDelta(96))
|
||||
|
@ -134,8 +136,8 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
|
|||
eclair.send(None, nodeId, 123 msat, ByteVector32.Zeroes, invoice_opt = None, feeThreshold_opt = Some(123 sat), maxFeePct_opt = Some(4.20))
|
||||
val send3 = paymentInitiator.expectMsgType[SendPaymentRequest]
|
||||
assert(send3.externalId === None)
|
||||
assert(send3.targetNodeId === nodeId)
|
||||
assert(send3.amount === 123.msat)
|
||||
assert(send3.recipientNodeId === nodeId)
|
||||
assert(send3.recipientAmount === 123.msat)
|
||||
assert(send3.paymentHash === ByteVector32.Zeroes)
|
||||
assert(send3.routeParams.get.maxFeeBase === 123000.msat) // conversion sat -> msat
|
||||
assert(send3.routeParams.get.maxFeePct === 4.20)
|
||||
|
@ -311,18 +313,15 @@ class EclairImplSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteL
|
|||
test("sendtoroute should pass the parameters correctly") { f =>
|
||||
import f._
|
||||
|
||||
val route = Seq(PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87"))
|
||||
val eclair = new EclairImpl(kit)
|
||||
val route = Seq(randomKey.publicKey)
|
||||
val trampolines = Seq(randomKey.publicKey, randomKey.publicKey)
|
||||
val parentId = UUID.randomUUID()
|
||||
val secret = randomBytes32
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey, "Some invoice")
|
||||
eclair.sendToRoute(Some("42"), route, 1234 msat, ByteVector32.One, CltvExpiryDelta(123), Some(pr))
|
||||
eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines)
|
||||
|
||||
val send = paymentInitiator.expectMsgType[SendPaymentRequest]
|
||||
assert(send.externalId === Some("42"))
|
||||
assert(send.predefinedRoute === route)
|
||||
assert(send.amount === 1234.msat)
|
||||
assert(send.finalExpiryDelta === CltvExpiryDelta(123))
|
||||
assert(send.paymentHash === ByteVector32.One)
|
||||
assert(send.paymentRequest === Some(pr))
|
||||
paymentInitiator.expectMsg(SendPaymentToRouteRequest(1000 msat, 1200 msat, Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,15 +61,15 @@ class FeaturesSpec extends FunSuite {
|
|||
bin"000000000000010000000000" -> false,
|
||||
bin"000000000000100010000000" -> true,
|
||||
bin"000000000000100001000000" -> true,
|
||||
// payment_secret depends on var_onion_optin
|
||||
bin"000000001000000000000000" -> false,
|
||||
bin"000000000100000000000000" -> false,
|
||||
// payment_secret depends on var_onion_optin, but we allow not setting it to be compatible with Phoenix
|
||||
bin"000000001000000000000000" -> true,
|
||||
bin"000000000100000000000000" -> true,
|
||||
bin"000000000100001000000000" -> true,
|
||||
// basic_mpp depends on payment_secret
|
||||
bin"000000100000000000000000" -> false,
|
||||
bin"000000010000000000000000" -> false,
|
||||
bin"000000101000000000000000" -> false,
|
||||
bin"000000011000000000000000" -> false,
|
||||
bin"000000101000000000000000" -> true, // we allow not setting var_onion_optin
|
||||
bin"000000011000000000000000" -> true, // we allow not setting var_onion_optin
|
||||
bin"000000011000001000000000" -> true,
|
||||
bin"000000100100000100000000" -> true
|
||||
)
|
||||
|
|
|
@ -75,8 +75,10 @@ class StartupSpec extends FunSuite {
|
|||
|
||||
test("NodeParams should fail if features are inconsistent") {
|
||||
val legalFeaturesConf = ConfigFactory.parseString("features = \"028a8a\"")
|
||||
val illegalFeaturesConf = ConfigFactory.parseString("features = \"028000\"") // basic_mpp without var_onion_optin
|
||||
val illegalButAllowedFeaturesConf = ConfigFactory.parseString("features = \"028000\"") // basic_mpp without var_onion_optin
|
||||
val illegalFeaturesConf = ConfigFactory.parseString("features = \"020000\"") // basic_mpp without payment_secret
|
||||
assert(Try(makeNodeParamsWithDefaults(legalFeaturesConf.withFallback(defaultConf))).isSuccess)
|
||||
assert(Try(makeNodeParamsWithDefaults(illegalButAllowedFeaturesConf.withFallback(defaultConf))).isSuccess)
|
||||
assert(Try(makeNodeParamsWithDefaults(illegalFeaturesConf.withFallback(defaultConf))).isFailure)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,14 +18,21 @@ package fr.acinq.eclair.channel
|
|||
|
||||
import java.util.UUID
|
||||
|
||||
import fr.acinq.bitcoin.{DeterministicWallet, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.channel.Commitments._
|
||||
import fr.acinq.eclair.channel.Helpers.Funding
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
import fr.acinq.eclair.payment.relay.Origin.Local
|
||||
import fr.acinq.eclair.wire.IncorrectOrUnknownPaymentDetails
|
||||
import fr.acinq.eclair.transactions.CommitmentSpec
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.{TestkitBaseClass, _}
|
||||
import org.scalatest.Outcome
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Random, Success, Try}
|
||||
|
||||
class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
|
||||
|
@ -379,4 +386,105 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
assert(ac16.availableBalanceForReceive == b + p1 - p3)
|
||||
}
|
||||
|
||||
test("can send availableForSend") { f =>
|
||||
for (isFunder <- Seq(true, false)) {
|
||||
val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, 2679, 546 sat, isFunder)
|
||||
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight)
|
||||
val result = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight)
|
||||
assert(result.isRight, result)
|
||||
}
|
||||
}
|
||||
|
||||
test("can receive availableForReceive") { f =>
|
||||
for (isFunder <- Seq(true, false)) {
|
||||
val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, 2679, 546 sat, isFunder)
|
||||
val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket)
|
||||
receiveAdd(c, add)
|
||||
}
|
||||
}
|
||||
|
||||
test("should always be able to send availableForSend", Tag("fuzzy")) { f =>
|
||||
val maxPendingHtlcAmount = 1000000.msat
|
||||
case class FuzzTest(isFunder: Boolean, pendingHtlcs: Int, feeRatePerKw: Long, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi)
|
||||
for (_ <- 1 to 100) {
|
||||
val t = FuzzTest(
|
||||
isFunder = Random.nextInt(2) == 0,
|
||||
pendingHtlcs = Random.nextInt(10),
|
||||
feeRatePerKw = Random.nextInt(10000),
|
||||
dustLimit = Random.nextInt(1000).sat,
|
||||
// We make sure both sides have enough to send/receive at least the initial pending HTLCs.
|
||||
toLocal = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat,
|
||||
toRemote = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat)
|
||||
var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isFunder)
|
||||
// Add some initial HTLCs to the pending list (bigger commit tx).
|
||||
for (_ <- 0 to t.pendingHtlcs) {
|
||||
val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat
|
||||
val (_, cmdAdd) = makeCmdAdd(amount, randomKey.publicKey, f.currentBlockHeight)
|
||||
sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight) match {
|
||||
case Right((cc, _)) => c = cc
|
||||
case Left(e) => fail(s"$t -> could not setup initial htlcs: $e")
|
||||
}
|
||||
}
|
||||
val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey.publicKey, f.currentBlockHeight)
|
||||
val result = sendAdd(c, cmdAdd, Local(UUID.randomUUID, None), f.currentBlockHeight)
|
||||
assert(result.isRight, s"$t -> $result")
|
||||
}
|
||||
}
|
||||
|
||||
test("should always be able to receive availableForReceive", Tag("fuzzy")) { f =>
|
||||
val maxPendingHtlcAmount = 1000000.msat
|
||||
case class FuzzTest(isFunder: Boolean, pendingHtlcs: Int, feeRatePerKw: Long, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi)
|
||||
for (_ <- 1 to 100) {
|
||||
val t = FuzzTest(
|
||||
isFunder = Random.nextInt(2) == 0,
|
||||
pendingHtlcs = Random.nextInt(10),
|
||||
feeRatePerKw = Random.nextInt(10000),
|
||||
dustLimit = Random.nextInt(1000).sat,
|
||||
// We make sure both sides have enough to send/receive at least the initial pending HTLCs.
|
||||
toLocal = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat,
|
||||
toRemote = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat)
|
||||
var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isFunder)
|
||||
// Add some initial HTLCs to the pending list (bigger commit tx).
|
||||
for (_ <- 0 to t.pendingHtlcs) {
|
||||
val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat
|
||||
val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, amount, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket)
|
||||
Try(receiveAdd(c, add)) match {
|
||||
case Success(cc) => c = cc
|
||||
case Failure(e) => fail(s"$t -> could not setup initial htlcs: $e")
|
||||
}
|
||||
}
|
||||
val add = UpdateAddHtlc(randomBytes32, c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32, CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket)
|
||||
Try(receiveAdd(c, add)) match {
|
||||
case Success(_) => ()
|
||||
case Failure(e) => fail(s"$t -> $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object CommitmentsSpec {
|
||||
|
||||
def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: Long = 0, dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = {
|
||||
val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, ByteVector.empty)
|
||||
val remoteParams = RemoteParams(randomKey.publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty)
|
||||
val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey)
|
||||
Commitments(
|
||||
ChannelVersion.STANDARD,
|
||||
localParams,
|
||||
remoteParams,
|
||||
channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty,
|
||||
LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)),
|
||||
RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomBytes32, randomKey.publicKey),
|
||||
LocalChanges(Nil, Nil, Nil),
|
||||
RemoteChanges(Nil, Nil, Nil),
|
||||
localNextHtlcId = 1,
|
||||
remoteNextHtlcId = 1,
|
||||
originChannels = Map.empty,
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey),
|
||||
commitInput = commitmentInput,
|
||||
remotePerCommitmentSecrets = ShaChain.init,
|
||||
channelId = randomBytes32)
|
||||
}
|
||||
|
||||
}
|
|
@ -70,10 +70,14 @@ NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
import f._
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val sender = TestProbe()
|
||||
val listener = TestProbe()
|
||||
system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged])
|
||||
val h = randomBytes32
|
||||
val add = CMD_ADD_HTLC(50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))
|
||||
sender.send(alice, add)
|
||||
sender.expectMsg("ok")
|
||||
val e = listener.expectMsgType[AvailableBalanceChanged]
|
||||
assert(e.commitments.availableBalanceForSend < initialState.commitments.availableBalanceForSend)
|
||||
val htlc = alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
assert(htlc.id == 0 && htlc.paymentHash == h)
|
||||
awaitCond(alice.stateData == initialState.copy(
|
||||
|
@ -726,6 +730,19 @@ NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate === bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate)
|
||||
}
|
||||
|
||||
|
||||
test("recv CMD_SIGN (after CMD_UPDATE_FEE)") { f =>
|
||||
import f._
|
||||
val sender = TestProbe()
|
||||
val listener = TestProbe()
|
||||
system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged])
|
||||
sender.send(alice, CMD_UPDATE_FEE(654564))
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[UpdateFee]
|
||||
sender.send(alice, CMD_SIGN)
|
||||
listener.expectMsgType[AvailableBalanceChanged]
|
||||
}
|
||||
|
||||
test("recv CommitSig (one htlc received)") { f =>
|
||||
import f._
|
||||
val sender = TestProbe()
|
||||
|
|
|
@ -366,7 +366,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
alice ! Error(ByteVector32.Zeroes, "oops")
|
||||
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx))
|
||||
alice2blockchain.expectMsgType[PublishAsap]
|
||||
alice2blockchain.expectMsgType[WatchConfirmed].txId == aliceCommitTx.txid
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === aliceCommitTx.txid)
|
||||
awaitCond(alice.stateName == CLOSING)
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_CLOSING]
|
||||
assert(initialState.localCommitPublished.isDefined)
|
||||
|
@ -517,7 +517,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
|
||||
|
||||
alice2blockchain.expectMsgType[PublishAsap]
|
||||
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState)
|
||||
|
@ -532,7 +532,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
assert(bobCommitTx.txOut.size == 2) // two main outputs
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined)
|
||||
assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState)
|
||||
|
||||
|
@ -542,6 +542,103 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_TX_CONFIRMED (remote commit) followed by CMD_FULFILL_HTLC") { f =>
|
||||
import f._
|
||||
// An HTLC Bob -> Alice is cross-signed that will be fulfilled later.
|
||||
val (r1, htlc1) = addHtlc(110000000 msat, bob, alice, bob2alice, alice2bob)
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
relayerA.expectMsgType[ForwardAdd]
|
||||
|
||||
// An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx.
|
||||
addHtlc(95000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
alice ! CMD_SIGN
|
||||
alice2bob.expectMsgType[CommitSig] // We stop here: Alice sent her CommitSig, but doesn't hear back from Bob.
|
||||
|
||||
// Now Bob publishes the first commit tx (force-close).
|
||||
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
assert(bobCommitTx.txOut.length === 3) // two main outputs + 1 HTLC
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
|
||||
|
||||
// Alice can claim her main output.
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid)
|
||||
alice2blockchain.expectNoMsg(100 millis)
|
||||
|
||||
// Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output.
|
||||
alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true)
|
||||
assert(alice2blockchain.expectMsgType[PublishAsap].tx.txid === claimMainTx.txid)
|
||||
val claimHtlcSuccessTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].txId === bobCommitTx.txid)
|
||||
alice2blockchain.expectNoMsg(100 millis)
|
||||
|
||||
val claimedOutputs = (claimMainTx.txIn ++ claimHtlcSuccessTx.txIn).filter(_.outPoint.txid == bobCommitTx.txid).map(_.outPoint.index)
|
||||
assert(claimedOutputs.length === 2)
|
||||
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0, claimMainTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcSuccessTx), 0, 0, claimHtlcSuccessTx)
|
||||
// TODO: can we also verify that we correctly sweep the HTLC success after the delay?
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_TX_CONFIRMED (next remote commit) followed by CMD_FULFILL_HTLC") { f =>
|
||||
import f._
|
||||
// An HTLC Bob -> Alice is cross-signed that will be fulfilled later.
|
||||
val (r1, htlc1) = addHtlc(110000000 msat, bob, alice, bob2alice, alice2bob)
|
||||
crossSign(bob, alice, bob2alice, alice2bob)
|
||||
relayerA.expectMsgType[ForwardAdd]
|
||||
|
||||
// An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx.
|
||||
addHtlc(95000000 msat, alice, bob, alice2bob, bob2alice)
|
||||
alice ! CMD_SIGN
|
||||
alice2bob.expectMsgType[CommitSig]
|
||||
alice2bob.forward(bob)
|
||||
bob2alice.expectMsgType[RevokeAndAck] // not forwarded to Alice (malicious Bob)
|
||||
bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob)
|
||||
|
||||
// Now Bob publishes the next commit tx (force-close).
|
||||
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
|
||||
assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs
|
||||
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx)
|
||||
|
||||
// Alice can claim her main output.
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
val claimHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
Transaction.correctlySpends(claimHtlcTimeoutTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].txId === bobCommitTx.txid)
|
||||
alice2blockchain.expectNoMsg(100 millis)
|
||||
|
||||
// Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output.
|
||||
alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true)
|
||||
assert(alice2blockchain.expectMsgType[PublishAsap].tx.txid === claimMainTx.txid)
|
||||
val claimHtlcSuccessTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
assert(alice2blockchain.expectMsgType[PublishAsap].tx.txid === claimHtlcTimeoutTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === claimMainTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].txId === bobCommitTx.txid)
|
||||
assert(alice2blockchain.expectMsgType[WatchSpent].txId === bobCommitTx.txid)
|
||||
alice2blockchain.expectNoMsg(100 millis)
|
||||
|
||||
val claimedOutputs = (claimMainTx.txIn ++ claimHtlcSuccessTx.txIn ++ claimHtlcTimeoutTx.txIn).filter(_.outPoint.txid == bobCommitTx.txid).map(_.outPoint.index)
|
||||
assert(claimedOutputs.length === 3)
|
||||
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainTx), 0, 0, claimMainTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcSuccessTx), 0, 0, claimHtlcSuccessTx)
|
||||
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimHtlcTimeoutTx), 0, 0, claimHtlcTimeoutTx)
|
||||
// TODO: can we also verify that we correctly sweep the HTLC success and timeout after the delay?
|
||||
awaitCond(alice.stateName == CLOSED)
|
||||
}
|
||||
|
||||
test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f =>
|
||||
import f._
|
||||
val sender = TestProbe()
|
||||
|
@ -578,7 +675,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
// alice is able to claim its main output
|
||||
val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx
|
||||
Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
alice2blockchain.expectMsgType[WatchConfirmed].txId == bobCommitTx.txid
|
||||
assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobCommitTx.txid)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined)
|
||||
|
||||
// actual test starts here
|
||||
|
|
|
@ -18,7 +18,8 @@ package fr.acinq.eclair.db
|
|||
|
||||
import java.util.UUID
|
||||
|
||||
import fr.acinq.bitcoin.Transaction
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{ByteVector32, Transaction}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.channel.Channel.{LocalError, RemoteError}
|
||||
import fr.acinq.eclair.channel.{AvailableBalanceChanged, ChannelErrorOccurred, NetworkFeePaid}
|
||||
|
@ -26,10 +27,11 @@ import fr.acinq.eclair.db.sqlite.SqliteAuditDb
|
|||
import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, ChannelCodecsSpec}
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.{FunSuite, Tag}
|
||||
|
||||
import scala.compat.Platform
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
|
||||
class SqliteAuditDbSpec extends FunSuite {
|
||||
|
@ -44,7 +46,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val sqlite = TestConstants.sqliteInMemory()
|
||||
val db = new SqliteAuditDb(sqlite)
|
||||
|
||||
val e1 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, PaymentSent.PartialPayment(ChannelCodecs.UNKNOWN_UUID, 42000 msat, 1000 msat, randomBytes32, None) :: Nil)
|
||||
val e1 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, 40000 msat, randomKey.publicKey, PaymentSent.PartialPayment(ChannelCodecs.UNKNOWN_UUID, 42000 msat, 1000 msat, randomBytes32, None) :: Nil)
|
||||
val pp2a = PaymentReceived.PartialPayment(42000 msat, randomBytes32)
|
||||
val pp2b = PaymentReceived.PartialPayment(42100 msat, randomBytes32)
|
||||
val e2 = PaymentReceived(randomBytes32, pp2a :: pp2b :: Nil)
|
||||
|
@ -52,16 +54,16 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val e4 = NetworkFeePaid(null, randomKey.publicKey, randomBytes32, Transaction(0, Seq.empty, Seq.empty, 0), 42 sat, "mutual")
|
||||
val pp5a = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None, timestamp = 0)
|
||||
val pp5b = PaymentSent.PartialPayment(UUID.randomUUID(), 42100 msat, 900 msat, randomBytes32, None, timestamp = 1)
|
||||
val e5 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, pp5a :: pp5b :: Nil)
|
||||
val e5 = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, 84100 msat, randomKey.publicKey, pp5a :: pp5b :: Nil)
|
||||
val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None, timestamp = (Platform.currentTime.milliseconds + 10.minutes).toMillis)
|
||||
val e6 = PaymentSent(ChannelCodecs.UNKNOWN_UUID, randomBytes32, randomBytes32, pp6 :: Nil)
|
||||
val e7 = AvailableBalanceChanged(null, randomBytes32, ShortChannelId(500000, 42, 1), 456123000 msat, ChannelCodecsSpec.normal.commitments)
|
||||
val e8 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual")
|
||||
val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
|
||||
val e10 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
|
||||
val e11 = TrampolinePaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomKey.publicKey, Seq(randomBytes32), Seq(randomBytes32))
|
||||
// TrampolinePaymentRelayed events are converted to ChannelPaymentRelayed events for now. We need to udpate the DB schema to fix this.
|
||||
val e11bis = ChannelPaymentRelayed(42000 msat, 40000 msat, e11.paymentHash, e11.fromChannelIds.head, e11.toChannelIds.head, e11.timestamp)
|
||||
val e6 = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, 42000 msat, randomKey.publicKey, pp6 :: Nil)
|
||||
val e7 = ChannelLifecycleEvent(randomBytes32, randomKey.publicKey, 456123000 sat, isFunder = true, isPrivate = false, "mutual")
|
||||
val e8 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
|
||||
val e9 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
|
||||
val e10 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(20000 msat, randomBytes32), PaymentRelayed.Part(22000 msat, randomBytes32)), Seq(PaymentRelayed.Part(10000 msat, randomBytes32), PaymentRelayed.Part(12000 msat, randomBytes32), PaymentRelayed.Part(15000 msat, randomBytes32)))
|
||||
val multiPartPaymentHash = randomBytes32
|
||||
val e11 = ChannelPaymentRelayed(13000 msat, 11000 msat, multiPartPaymentHash, randomBytes32, randomBytes32)
|
||||
val e12 = ChannelPaymentRelayed(15000 msat, 12500 msat, multiPartPaymentHash, randomBytes32, randomBytes32)
|
||||
|
||||
db.add(e1)
|
||||
db.add(e2)
|
||||
|
@ -74,11 +76,12 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
db.add(e9)
|
||||
db.add(e10)
|
||||
db.add(e11)
|
||||
db.add(e12)
|
||||
|
||||
assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5.copy(id = pp5a.id, parts = pp5a :: Nil), e5.copy(id = pp5b.id, parts = pp5b :: Nil), e6.copy(id = pp6.id)))
|
||||
assert(db.listSent(from = 0L, to = (Platform.currentTime.milliseconds + 15.minute).toMillis).toSet === Set(e1, e5, e6))
|
||||
assert(db.listSent(from = 100000L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e1))
|
||||
assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2.copy(parts = pp2a :: Nil), e2.copy(parts = pp2b :: Nil)))
|
||||
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e11bis))
|
||||
assert(db.listReceived(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e2))
|
||||
assert(db.listRelayed(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).toList === List(e3, e10, e11, e12))
|
||||
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).size === 1)
|
||||
assert(db.listNetworkFees(from = 0L, to = (Platform.currentTime.milliseconds + 1.minute).toMillis).head.txType === "mutual")
|
||||
}
|
||||
|
@ -90,30 +93,68 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val n1 = randomKey.publicKey
|
||||
val n2 = randomKey.publicKey
|
||||
val n3 = randomKey.publicKey
|
||||
val n4 = randomKey.publicKey
|
||||
|
||||
val c1 = randomBytes32
|
||||
val c2 = randomBytes32
|
||||
val c3 = randomBytes32
|
||||
val c4 = randomBytes32
|
||||
|
||||
db.add(ChannelPaymentRelayed(46000 msat, 44000 msat, randomBytes32, randomBytes32, c1))
|
||||
db.add(ChannelPaymentRelayed(41000 msat, 40000 msat, randomBytes32, randomBytes32, c1))
|
||||
db.add(ChannelPaymentRelayed(43000 msat, 42000 msat, randomBytes32, randomBytes32, c1))
|
||||
db.add(ChannelPaymentRelayed(42000 msat, 40000 msat, randomBytes32, randomBytes32, c2))
|
||||
db.add(TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(25000 msat, randomBytes32)), Seq(PaymentRelayed.Part(20000 msat, c4))))
|
||||
db.add(TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(46000 msat, randomBytes32)), Seq(PaymentRelayed.Part(16000 msat, c2), PaymentRelayed.Part(10000 msat, c4), PaymentRelayed.Part(14000 msat, c4))))
|
||||
|
||||
db.add(NetworkFeePaid(null, n1, c1, Transaction(0, Seq.empty, Seq.empty, 0), 100 sat, "funding"))
|
||||
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 200 sat, "funding"))
|
||||
db.add(NetworkFeePaid(null, n2, c2, Transaction(0, Seq.empty, Seq.empty, 0), 300 sat, "mutual"))
|
||||
db.add(NetworkFeePaid(null, n3, c3, Transaction(0, Seq.empty, Seq.empty, 0), 400 sat, "funding"))
|
||||
db.add(NetworkFeePaid(null, n4, c4, Transaction(0, Seq.empty, Seq.empty, 0), 500 sat, "funding"))
|
||||
|
||||
assert(db.stats.toSet === Set(
|
||||
Stats(channelId = c1, avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 100 sat),
|
||||
Stats(channelId = c2, avgPaymentAmount = 40 sat, paymentCount = 1, relayFee = 2 sat, networkFee = 500 sat),
|
||||
Stats(channelId = c3, avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat)
|
||||
Stats(channelId = c2, avgPaymentAmount = 40 sat, paymentCount = 2, relayFee = 4 sat, networkFee = 500 sat),
|
||||
Stats(channelId = c3, avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 400 sat),
|
||||
Stats(channelId = c4, avgPaymentAmount = 30 sat, paymentCount = 2, relayFee = 9 sat, networkFee = 500 sat)
|
||||
))
|
||||
}
|
||||
|
||||
test("handle migration version 1 -> 3") {
|
||||
ignore("relay stats performance", Tag("perf")) {
|
||||
val sqlite = TestConstants.sqliteInMemory()
|
||||
val db = new SqliteAuditDb(sqlite)
|
||||
val nodeCount = 100
|
||||
val channelCount = 1000
|
||||
val eventCount = 100000
|
||||
val nodeIds = (1 to nodeCount).map(_ => randomKey.publicKey)
|
||||
val channelIds = (1 to channelCount).map(_ => randomBytes32)
|
||||
// Fund channels.
|
||||
channelIds.foreach(channelId => {
|
||||
val nodeId = nodeIds(Random.nextInt(nodeCount))
|
||||
db.add(NetworkFeePaid(null, nodeId, channelId, Transaction(0, Seq.empty, Seq.empty, 0), 100 sat, "funding"))
|
||||
})
|
||||
// Add relay events.
|
||||
(1 to eventCount).foreach(_ => {
|
||||
// 25% trampoline relays.
|
||||
if (Random.nextInt(4) == 0) {
|
||||
val outgoingCount = 1 + Random.nextInt(4)
|
||||
val incoming = Seq(PaymentRelayed.Part(10000 msat, randomBytes32))
|
||||
val outgoing = (1 to outgoingCount).map(_ => PaymentRelayed.Part(Random.nextInt(2000).msat, channelIds(Random.nextInt(channelCount))))
|
||||
db.add(TrampolinePaymentRelayed(randomBytes32, incoming, outgoing))
|
||||
} else {
|
||||
val toChannelId = channelIds(Random.nextInt(channelCount))
|
||||
db.add(ChannelPaymentRelayed(10000 msat, Random.nextInt(10000).msat, randomBytes32, randomBytes32, toChannelId))
|
||||
}
|
||||
})
|
||||
// Test starts here.
|
||||
val start = Platform.currentTime
|
||||
assert(db.stats.nonEmpty)
|
||||
val end = Platform.currentTime
|
||||
fail(s"took ${end - start}ms")
|
||||
}
|
||||
|
||||
test("handle migration version 1 -> 4") {
|
||||
val connection = TestConstants.sqliteInMemory()
|
||||
|
||||
// simulate existing previous version db
|
||||
|
@ -135,19 +176,19 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
}
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 3) == 1) // we expect version 1
|
||||
assert(getVersion(statement, "audit", 4) == 1) // we expect version 1
|
||||
}
|
||||
|
||||
val ps = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None) :: Nil)
|
||||
val ps = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, 42000 msat, PrivateKey(ByteVector32.One).publicKey, PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32, None) :: Nil)
|
||||
val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 42001 msat, 1001 msat, randomBytes32, None)
|
||||
val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 42002 msat, 1002 msat, randomBytes32, None)
|
||||
val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, pp1 :: pp2 :: Nil)
|
||||
val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, 84003 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil)
|
||||
val e1 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
|
||||
val e2 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, RemoteError(wire.Error(randomBytes32, "remote oops")), isFatal = true)
|
||||
|
||||
// add a row (no ID on sent)
|
||||
using(connection.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setLong(1, ps.amount.toLong)
|
||||
statement.setLong(1, ps.recipientAmount.toLong)
|
||||
statement.setLong(2, ps.feesPaid.toLong)
|
||||
statement.setBytes(3, ps.paymentHash.toArray)
|
||||
statement.setBytes(4, ps.paymentPreimage.toArray)
|
||||
|
@ -159,7 +200,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val migratedDb = new SqliteAuditDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 3) == 3) // version changed from 1 -> 3
|
||||
assert(getVersion(statement, "audit", 4) == 4) // version changed from 1 -> 4
|
||||
}
|
||||
|
||||
// existing rows in the 'sent' table will use id=00000000-0000-0000-0000-000000000000 as default
|
||||
|
@ -168,7 +209,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val postMigrationDb = new SqliteAuditDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 3) == 3) // version 3
|
||||
assert(getVersion(statement, "audit", 4) == 4) // version 4
|
||||
}
|
||||
|
||||
postMigrationDb.add(ps1)
|
||||
|
@ -176,14 +217,11 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
postMigrationDb.add(e2)
|
||||
|
||||
// the old record will have the UNKNOWN_UUID but the new ones will have their actual id
|
||||
assert(postMigrationDb.listSent(0, (Platform.currentTime.milliseconds + 1.minute).toMillis) === Seq(
|
||||
ps.copy(id = ChannelCodecs.UNKNOWN_UUID, parts = Seq(ps.parts.head.copy(id = ChannelCodecs.UNKNOWN_UUID))),
|
||||
ps1.copy(id = pp1.id, parts = pp1 :: Nil),
|
||||
ps1.copy(id = pp2.id, parts = pp2 :: Nil)))
|
||||
val expected = Seq(ps.copy(id = ChannelCodecs.UNKNOWN_UUID, parts = Seq(ps.parts.head.copy(id = ChannelCodecs.UNKNOWN_UUID))), ps1)
|
||||
assert(postMigrationDb.listSent(0, (Platform.currentTime.milliseconds + 1.minute).toMillis) === expected)
|
||||
}
|
||||
|
||||
test("handle migration version 2 -> 3") {
|
||||
|
||||
test("handle migration version 2 -> 4") {
|
||||
val connection = TestConstants.sqliteInMemory()
|
||||
|
||||
// simulate existing previous version db
|
||||
|
@ -205,7 +243,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
}
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 3) == 2) // version 2 is deployed now
|
||||
assert(getVersion(statement, "audit", 4) == 2) // version 2 is deployed now
|
||||
}
|
||||
|
||||
val e1 = ChannelErrorOccurred(null, randomBytes32, randomKey.publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true)
|
||||
|
@ -214,7 +252,7 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val migratedDb = new SqliteAuditDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 3) == 3) // version changed from 2 -> 3
|
||||
assert(getVersion(statement, "audit", 4) == 4) // version changed from 2 -> 4
|
||||
}
|
||||
|
||||
migratedDb.add(e1)
|
||||
|
@ -222,10 +260,138 @@ class SqliteAuditDbSpec extends FunSuite {
|
|||
val postMigrationDb = new SqliteAuditDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 3) == 3) // version 3
|
||||
assert(getVersion(statement, "audit", 4) == 4) // version 4
|
||||
}
|
||||
|
||||
postMigrationDb.add(e2)
|
||||
}
|
||||
|
||||
test("handle migration version 3 -> 4") {
|
||||
val connection = TestConstants.sqliteInMemory()
|
||||
|
||||
// simulate existing previous version db
|
||||
using(connection.createStatement()) { statement =>
|
||||
getVersion(statement, "audit", 3)
|
||||
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, 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)")
|
||||
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 TEXT NOT NULL, timestamp INTEGER NOT NULL)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS channel_errors (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal INTEGER 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)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_errors_timestamp_idx ON channel_errors(timestamp)")
|
||||
}
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 4) == 3) // version 3 is deployed now
|
||||
}
|
||||
|
||||
val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32, None, 100)
|
||||
val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32, None, 110)
|
||||
val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, 1100 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil)
|
||||
|
||||
for (pp <- Seq(pp1, pp2)) {
|
||||
using(connection.prepareStatement("INSERT INTO sent (amount_msat, fees_msat, payment_hash, payment_preimage, to_channel_id, timestamp, id) VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setLong(1, pp.amount.toLong)
|
||||
statement.setLong(2, pp.feesPaid.toLong)
|
||||
statement.setBytes(3, ps1.paymentHash.toArray)
|
||||
statement.setBytes(4, ps1.paymentPreimage.toArray)
|
||||
statement.setBytes(5, pp.toChannelId.toArray)
|
||||
statement.setLong(6, pp.timestamp)
|
||||
statement.setBytes(7, pp.id.toString.getBytes)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32, randomBytes32, randomBytes32, 105)
|
||||
val relayed2 = ChannelPaymentRelayed(650 msat, 500 msat, randomBytes32, randomBytes32, randomBytes32, 115)
|
||||
|
||||
for (relayed <- Seq(relayed1, relayed2)) {
|
||||
using(connection.prepareStatement("INSERT INTO relayed (amount_in_msat, amount_out_msat, payment_hash, from_channel_id, to_channel_id, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setLong(1, relayed.amountIn.toLong)
|
||||
statement.setLong(2, relayed.amountOut.toLong)
|
||||
statement.setBytes(3, relayed.paymentHash.toArray)
|
||||
statement.setBytes(4, relayed.fromChannelId.toArray)
|
||||
statement.setBytes(5, relayed.toChannelId.toArray)
|
||||
statement.setLong(6, relayed.timestamp)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
val migratedDb = new SqliteAuditDb(connection)
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 4) == 4) // version changed from 3 -> 4
|
||||
}
|
||||
|
||||
assert(migratedDb.listSent(50, 150).toSet === Set(
|
||||
ps1.copy(id = pp1.id, recipientAmount = pp1.amount, parts = pp1 :: Nil),
|
||||
ps1.copy(id = pp2.id, recipientAmount = pp2.amount, parts = pp2 :: Nil)
|
||||
))
|
||||
assert(migratedDb.listRelayed(100, 120) === Seq(relayed1, relayed2))
|
||||
|
||||
val postMigrationDb = new SqliteAuditDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "audit", 4) == 4) // version 4
|
||||
}
|
||||
|
||||
val ps2 = PaymentSent(UUID.randomUUID(), randomBytes32, randomBytes32, 1100 msat, randomKey.publicKey, Seq(
|
||||
PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32, None, 160),
|
||||
PaymentSent.PartialPayment(UUID.randomUUID(), 600 msat, 5 msat, randomBytes32, None, 165)
|
||||
))
|
||||
val relayed3 = TrampolinePaymentRelayed(randomBytes32, Seq(PaymentRelayed.Part(450 msat, randomBytes32), PaymentRelayed.Part(500 msat, randomBytes32)), Seq(PaymentRelayed.Part(800 msat, randomBytes32)), 150)
|
||||
|
||||
postMigrationDb.add(ps2)
|
||||
assert(postMigrationDb.listSent(155, 200) === Seq(ps2))
|
||||
postMigrationDb.add(relayed3)
|
||||
assert(postMigrationDb.listRelayed(100, 160) === Seq(relayed1, relayed2, relayed3))
|
||||
}
|
||||
|
||||
test("ignore invalid values in the DB") {
|
||||
val sqlite = TestConstants.sqliteInMemory()
|
||||
val db = new SqliteAuditDb(sqlite)
|
||||
|
||||
using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, randomBytes32.toArray)
|
||||
statement.setLong(2, 42)
|
||||
statement.setBytes(3, randomBytes32.toArray)
|
||||
statement.setString(4, "IN")
|
||||
statement.setString(5, "unknown") // invalid relay type
|
||||
statement.setLong(6, 10)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, randomBytes32.toArray)
|
||||
statement.setLong(2, 51)
|
||||
statement.setBytes(3, randomBytes32.toArray)
|
||||
statement.setString(4, "UP") // invalid direction
|
||||
statement.setString(5, "channel")
|
||||
statement.setLong(6, 20)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
val paymentHash = randomBytes32
|
||||
val channelId = randomBytes32
|
||||
|
||||
using(sqlite.prepareStatement("INSERT INTO relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setBytes(1, paymentHash.toArray)
|
||||
statement.setLong(2, 65)
|
||||
statement.setBytes(3, channelId.toArray)
|
||||
statement.setString(4, "IN") // missing a corresponding OUT
|
||||
statement.setString(5, "channel")
|
||||
statement.setLong(6, 30)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
assert(db.listRelayed(0, 40) === Nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ class SqliteChannelsDbSpec extends FunSuite {
|
|||
val paymentHash2 = ByteVector32(ByteVector.fill(32)(1))
|
||||
val cltvExpiry2 = CltvExpiry(656)
|
||||
|
||||
intercept[SQLiteException](db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel
|
||||
intercept[SQLiteException](db.addHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel
|
||||
|
||||
assert(db.listLocalChannels().toSet === Set.empty)
|
||||
db.addOrUpdateChannel(channel)
|
||||
|
@ -55,8 +55,8 @@ class SqliteChannelsDbSpec extends FunSuite {
|
|||
assert(db.listLocalChannels() === List(channel))
|
||||
|
||||
assert(db.listHtlcInfos(channel.channelId, commitNumber).toList == Nil)
|
||||
db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)
|
||||
db.addOrUpdateHtlcInfo(channel.channelId, commitNumber, paymentHash2, cltvExpiry2)
|
||||
db.addHtlcInfo(channel.channelId, commitNumber, paymentHash1, cltvExpiry1)
|
||||
db.addHtlcInfo(channel.channelId, commitNumber, paymentHash2, cltvExpiry2)
|
||||
assert(db.listHtlcInfos(channel.channelId, commitNumber).toList == List((paymentHash1, cltvExpiry1), (paymentHash2, cltvExpiry2)))
|
||||
assert(db.listHtlcInfos(channel.channelId, 43).toList == Nil)
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ import fr.acinq.eclair.crypto.Sphinx
|
|||
import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb
|
||||
import fr.acinq.eclair.db.sqlite.SqliteUtils._
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.ChannelHop
|
||||
import fr.acinq.eclair.router.{ChannelHop, NodeHop}
|
||||
import fr.acinq.eclair.wire.{ChannelUpdate, UnknownNextPeer}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, db, randomBytes32, randomBytes64, randomKey}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomBytes64, randomKey}
|
||||
import org.scalatest.FunSuite
|
||||
|
||||
import scala.compat.Platform
|
||||
|
@ -42,7 +42,7 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val db2 = new SqlitePaymentsDb(sqlite)
|
||||
}
|
||||
|
||||
test("handle version migration 1->3") {
|
||||
test("handle version migration 1->4") {
|
||||
val connection = TestConstants.sqliteInMemory()
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
|
@ -67,16 +67,16 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val preMigrationDb = new SqlitePaymentsDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 1) == 3) // version changed from 1 -> 3
|
||||
assert(getVersion(statement, "payments", 1) == 4) // version changed from 1 -> 4
|
||||
}
|
||||
|
||||
// the existing received payment can NOT be queried anymore
|
||||
assert(preMigrationDb.getIncomingPayment(paymentHash1).isEmpty)
|
||||
|
||||
// add a few rows
|
||||
val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, 12345 msat, alice, 1000, None, OutgoingPaymentStatus.Pending)
|
||||
val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, PaymentType.Standard, 12345 msat, 12345 msat, alice, 1000, None, OutgoingPaymentStatus.Pending)
|
||||
val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, "Some invoice", expirySeconds = None, timestamp = 1)
|
||||
val pr1 = IncomingPayment(i1, preimage1, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(550 msat, 1100))
|
||||
val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(550 msat, 1100))
|
||||
|
||||
preMigrationDb.addOutgoingPayment(ps1)
|
||||
preMigrationDb.addIncomingPayment(i1, preimage1)
|
||||
|
@ -88,14 +88,14 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val postMigrationDb = new SqlitePaymentsDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 3) == 3) // version still to 3
|
||||
assert(getVersion(statement, "payments", 4) == 4) // version still to 4
|
||||
}
|
||||
|
||||
assert(postMigrationDb.listIncomingPayments(1, 1500) === Seq(pr1))
|
||||
assert(postMigrationDb.listOutgoingPayments(1, 1500) === Seq(ps1))
|
||||
}
|
||||
|
||||
test("handle version migration 2->3") {
|
||||
test("handle version migration 2->4") {
|
||||
val connection = TestConstants.sqliteInMemory()
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
|
@ -113,13 +113,13 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val id1 = UUID.randomUUID()
|
||||
val id2 = UUID.randomUUID()
|
||||
val id3 = UUID.randomUUID()
|
||||
val ps1 = OutgoingPayment(id1, id1, None, randomBytes32, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000, None, OutgoingPaymentStatus.Pending)
|
||||
val ps2 = OutgoingPayment(id2, id2, None, randomBytes32, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010, None, OutgoingPaymentStatus.Failed(Nil, 1050))
|
||||
val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060))
|
||||
val ps1 = OutgoingPayment(id1, id1, None, randomBytes32, PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000, None, OutgoingPaymentStatus.Pending)
|
||||
val ps2 = OutgoingPayment(id2, id2, None, randomBytes32, PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010, None, OutgoingPaymentStatus.Failed(Nil, 1050))
|
||||
val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060))
|
||||
val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, "Some invoice", expirySeconds = None, timestamp = 1)
|
||||
val pr1 = IncomingPayment(i1, preimage1, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(12345678 msat, 1090))
|
||||
val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(12345678 msat, 1090))
|
||||
val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, "Another invoice", expirySeconds = Some(30), timestamp = 1)
|
||||
val pr2 = IncomingPayment(i2, preimage2, i2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired)
|
||||
val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired)
|
||||
|
||||
// Changes between version 2 and 3 to sent_payments:
|
||||
// - removed the status column
|
||||
|
@ -185,7 +185,7 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val preMigrationDb = new SqlitePaymentsDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 2) == 3) // version changed from 2 -> 3
|
||||
assert(getVersion(statement, "payments", 2) == 4) // version changed from 2 -> 4
|
||||
}
|
||||
|
||||
assert(preMigrationDb.getIncomingPayment(i1.paymentHash) === Some(pr1))
|
||||
|
@ -195,19 +195,19 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val postMigrationDb = new SqlitePaymentsDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 3) == 3) // version still to 3
|
||||
assert(getVersion(statement, "payments", 4) == 4) // version still to 4
|
||||
}
|
||||
|
||||
val i3 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, "invoice #3", expirySeconds = Some(30))
|
||||
val pr3 = IncomingPayment(i3, preimage3, i3.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending)
|
||||
val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending)
|
||||
postMigrationDb.addIncomingPayment(i3, pr3.paymentPreimage)
|
||||
|
||||
val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32, 123 msat, alice, 1100, Some(i3), OutgoingPaymentStatus.Pending)
|
||||
val ps5 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("2"), randomBytes32, 456 msat, bob, 1150, Some(i2), OutgoingPaymentStatus.Succeeded(preimage1, 42 msat, Nil, 1180))
|
||||
val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32, 789 msat, bob, 1250, None, OutgoingPaymentStatus.Failed(Nil, 1300))
|
||||
val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32, PaymentType.Standard, 123 msat, 123 msat, alice, 1100, Some(i3), OutgoingPaymentStatus.Pending)
|
||||
val ps5 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("2"), randomBytes32, PaymentType.Standard, 456 msat, 456 msat, bob, 1150, Some(i2), OutgoingPaymentStatus.Succeeded(preimage1, 42 msat, Nil, 1180))
|
||||
val ps6 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("3"), randomBytes32, PaymentType.Standard, 789 msat, 789 msat, bob, 1250, None, OutgoingPaymentStatus.Failed(Nil, 1300))
|
||||
postMigrationDb.addOutgoingPayment(ps4)
|
||||
postMigrationDb.addOutgoingPayment(ps5.copy(status = OutgoingPaymentStatus.Pending))
|
||||
postMigrationDb.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32, None, 1180))))
|
||||
postMigrationDb.updateOutgoingPayment(PaymentSent(ps5.parentId, ps5.paymentHash, preimage1, ps5.amount, ps5.recipientNodeId, Seq(PaymentSent.PartialPayment(ps5.id, ps5.amount, 42 msat, randomBytes32, None, 1180))))
|
||||
postMigrationDb.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending))
|
||||
postMigrationDb.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300))
|
||||
|
||||
|
@ -216,6 +216,99 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
assert(postMigrationDb.listExpiredIncomingPayments(1, 2000) === Seq(pr2))
|
||||
}
|
||||
|
||||
test("handle version migration 3->4") {
|
||||
val connection = TestConstants.sqliteInMemory()
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
getVersion(statement, "payments", 3)
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, payment_preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, received_at INTEGER)")
|
||||
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, parent_id TEXT NOT NULL, external_id TEXT, payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, target_node_id BLOB NOT NULL, created_at INTEGER NOT NULL, payment_request TEXT, completed_at INTEGER, payment_preimage BLOB, fees_msat INTEGER, payment_route BLOB, failures BLOB)")
|
||||
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_parent_id_idx ON sent_payments(parent_id)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_payment_hash_idx ON sent_payments(payment_hash)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS sent_created_idx ON sent_payments(created_at)")
|
||||
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_created_idx ON received_payments(created_at)")
|
||||
}
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 3) == 3) // version 3 is deployed now
|
||||
}
|
||||
|
||||
// Insert a bunch of old version 3 rows.
|
||||
val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
|
||||
val parentId = UUID.randomUUID()
|
||||
val invoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, "invoice #1", expirySeconds = Some(30))
|
||||
val ps1 = OutgoingPayment(id1, id1, Some("42"), randomBytes32, PaymentType.Standard, 561 msat, 561 msat, alice, 1000, None, OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.REMOTE, "no candy for you", List(HopSummary(hop_ab), HopSummary(hop_bc)))), 1020))
|
||||
val ps2 = OutgoingPayment(id2, parentId, Some("42"), paymentHash1, PaymentType.Standard, 1105 msat, 1105 msat, bob, 1010, Some(invoice1), OutgoingPaymentStatus.Pending)
|
||||
val ps3 = OutgoingPayment(id3, parentId, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, bob, 1040, None, OutgoingPaymentStatus.Succeeded(preimage1, 10 msat, Seq(HopSummary(hop_ab), HopSummary(hop_bc)), 1060))
|
||||
|
||||
using(connection.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, amount_msat, target_node_id, created_at, completed_at, failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setString(1, ps1.id.toString)
|
||||
statement.setString(2, ps1.parentId.toString)
|
||||
statement.setString(3, ps1.externalId.get.toString)
|
||||
statement.setBytes(4, ps1.paymentHash.toArray)
|
||||
statement.setLong(5, ps1.amount.toLong)
|
||||
statement.setBytes(6, ps1.recipientNodeId.value.toArray)
|
||||
statement.setLong(7, ps1.createdAt)
|
||||
statement.setLong(8, ps1.status.asInstanceOf[OutgoingPaymentStatus.Failed].completedAt)
|
||||
statement.setBytes(9, SqlitePaymentsDb.paymentFailuresCodec.encode(ps1.status.asInstanceOf[OutgoingPaymentStatus.Failed].failures.toList).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(connection.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, amount_msat, target_node_id, created_at, payment_request) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setString(1, ps2.id.toString)
|
||||
statement.setString(2, ps2.parentId.toString)
|
||||
statement.setString(3, ps2.externalId.get.toString)
|
||||
statement.setBytes(4, ps2.paymentHash.toArray)
|
||||
statement.setLong(5, ps2.amount.toLong)
|
||||
statement.setBytes(6, ps2.recipientNodeId.value.toArray)
|
||||
statement.setLong(7, ps2.createdAt)
|
||||
statement.setString(8, PaymentRequest.write(invoice1))
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
using(connection.prepareStatement("INSERT INTO sent_payments (id, parent_id, payment_hash, amount_msat, target_node_id, created_at, completed_at, payment_preimage, fees_msat, payment_route) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
|
||||
statement.setString(1, ps3.id.toString)
|
||||
statement.setString(2, ps3.parentId.toString)
|
||||
statement.setBytes(3, ps3.paymentHash.toArray)
|
||||
statement.setLong(4, ps3.amount.toLong)
|
||||
statement.setBytes(5, ps3.recipientNodeId.value.toArray)
|
||||
statement.setLong(6, ps3.createdAt)
|
||||
statement.setLong(7, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].completedAt)
|
||||
statement.setBytes(8, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].paymentPreimage.toArray)
|
||||
statement.setLong(9, ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid.toLong)
|
||||
statement.setBytes(10, SqlitePaymentsDb.paymentRouteCodec.encode(ps3.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].route.toList).require.toByteArray)
|
||||
statement.executeUpdate()
|
||||
}
|
||||
|
||||
// Changes between version 3 and 4 to sent_payments:
|
||||
// - added final amount column
|
||||
// - added payment type column, with a default to "Standard"
|
||||
// - renamed target_node_id -> recipient_node_id
|
||||
// - re-ordered columns
|
||||
|
||||
val preMigrationDb = new SqlitePaymentsDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 3) == 4) // version changed from 3 -> 4
|
||||
}
|
||||
|
||||
assert(preMigrationDb.getOutgoingPayment(id1) === Some(ps1))
|
||||
assert(preMigrationDb.listOutgoingPayments(parentId) === Seq(ps2, ps3))
|
||||
|
||||
val postMigrationDb = new SqlitePaymentsDb(connection)
|
||||
|
||||
using(connection.createStatement()) { statement =>
|
||||
assert(getVersion(statement, "payments", 4) == 4) // version still to 4
|
||||
}
|
||||
|
||||
val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, randomBytes32, PaymentType.SwapOut, 50 msat, 100 msat, carol, 1100, Some(invoice1), OutgoingPaymentStatus.Pending)
|
||||
postMigrationDb.addOutgoingPayment(ps4)
|
||||
postMigrationDb.updateOutgoingPayment(PaymentSent(parentId, paymentHash1, preimage1, ps2.recipientAmount, ps2.recipientNodeId, Seq(PaymentSent.PartialPayment(id2, ps2.amount, 15 msat, randomBytes32, Some(Seq(hop_ab)), 1105))))
|
||||
|
||||
assert(postMigrationDb.listOutgoingPayments(1, 2000) === Seq(ps1, ps2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Seq(HopSummary(hop_ab)), 1105)), ps3, ps4))
|
||||
}
|
||||
|
||||
test("add/retrieve/update incoming payments") {
|
||||
val sqlite = TestConstants.sqliteInMemory()
|
||||
val db = new SqlitePaymentsDb(sqlite)
|
||||
|
@ -225,23 +318,23 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
|
||||
val expiredInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32, alicePriv, "invoice #1", timestamp = 1)
|
||||
val expiredInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32, bobPriv, "invoice #2", timestamp = 2, expirySeconds = Some(30))
|
||||
val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32, expiredInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired)
|
||||
val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32, expiredInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired)
|
||||
val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32, PaymentType.Standard, expiredInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired)
|
||||
val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32, PaymentType.Standard, expiredInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Expired)
|
||||
|
||||
val pendingInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32, alicePriv, "invoice #3")
|
||||
val pendingInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32, bobPriv, "invoice #4", expirySeconds = Some(30))
|
||||
val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32, pendingInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending)
|
||||
val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32, pendingInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending)
|
||||
val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32, PaymentType.Standard, pendingInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending)
|
||||
val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32, PaymentType.SwapIn, pendingInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Pending)
|
||||
|
||||
val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32, alicePriv, "invoice #5")
|
||||
val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32, bobPriv, "invoice #6", expirySeconds = Some(60))
|
||||
val receivedAt1 = Platform.currentTime + 1
|
||||
val receivedAt2 = Platform.currentTime + 2
|
||||
val payment1 = IncomingPayment(paidInvoice1, randomBytes32, paidInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(561 msat, receivedAt2))
|
||||
val payment2 = IncomingPayment(paidInvoice2, randomBytes32, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(1111 msat, receivedAt2))
|
||||
val payment1 = IncomingPayment(paidInvoice1, randomBytes32, PaymentType.Standard, paidInvoice1.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(561 msat, receivedAt2))
|
||||
val payment2 = IncomingPayment(paidInvoice2, randomBytes32, PaymentType.Standard, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(1111 msat, receivedAt2))
|
||||
|
||||
db.addIncomingPayment(pendingInvoice1, pendingPayment1.paymentPreimage)
|
||||
db.addIncomingPayment(pendingInvoice2, pendingPayment2.paymentPreimage)
|
||||
db.addIncomingPayment(pendingInvoice2, pendingPayment2.paymentPreimage, PaymentType.SwapIn)
|
||||
db.addIncomingPayment(expiredInvoice1, expiredPayment1.paymentPreimage)
|
||||
db.addIncomingPayment(expiredInvoice2, expiredPayment2.paymentPreimage)
|
||||
db.addIncomingPayment(paidInvoice1, payment1.paymentPreimage)
|
||||
|
@ -273,8 +366,8 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
|
||||
val parentId = UUID.randomUUID()
|
||||
val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, "Some invoice", expirySeconds = None, timestamp = 0)
|
||||
val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, 123 msat, alice, 100, Some(i1), OutgoingPaymentStatus.Pending)
|
||||
val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, 456 msat, bob, 200, None, OutgoingPaymentStatus.Pending)
|
||||
val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, PaymentType.Standard, 123 msat, 600 msat, dave, 100, Some(i1), OutgoingPaymentStatus.Pending)
|
||||
val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, PaymentType.SwapOut, 456 msat, 600 msat, dave, 200, None, OutgoingPaymentStatus.Pending)
|
||||
|
||||
assert(db.listOutgoingPayments(0, Platform.currentTime).isEmpty)
|
||||
db.addOutgoingPayment(s1)
|
||||
|
@ -294,7 +387,7 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
assert(db.listOutgoingPayments(ByteVector32.Zeroes) === Nil)
|
||||
|
||||
val s3 = s2.copy(id = UUID.randomUUID(), amount = 789 msat, createdAt = 300)
|
||||
val s4 = s2.copy(id = UUID.randomUUID(), createdAt = 300)
|
||||
val s4 = s2.copy(id = UUID.randomUUID(), paymentType = PaymentType.Standard, createdAt = 300)
|
||||
db.addOutgoingPayment(s3)
|
||||
db.addOutgoingPayment(s4)
|
||||
|
||||
|
@ -302,18 +395,18 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310))
|
||||
assert(db.getOutgoingPayment(s3.id) === Some(ss3))
|
||||
db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(new RuntimeException("woops")), RemoteFailure(Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320))
|
||||
val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", Nil), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, Some(ShortChannelId(43)))))), 320))
|
||||
val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", Nil), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)))), 320))
|
||||
assert(db.getOutgoingPayment(s4.id) === Some(ss4))
|
||||
|
||||
// can't update again once it's in a final state
|
||||
assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32, None)))))
|
||||
assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32, None)))))
|
||||
|
||||
val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, Seq(
|
||||
val paymentSent = PaymentSent(parentId, paymentHash1, preimage1, 600 msat, carol, Seq(
|
||||
PaymentSent.PartialPayment(s1.id, s1.amount, 15 msat, randomBytes32, None, 400),
|
||||
PaymentSent.PartialPayment(s2.id, s2.amount, 20 msat, randomBytes32, Some(Seq(hop_ab, hop_bc)), 410)
|
||||
))
|
||||
val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400))
|
||||
val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, Some(ShortChannelId(43)))), 410))
|
||||
val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410))
|
||||
db.updateOutgoingPayment(paymentSent)
|
||||
assert(db.getOutgoingPayment(s1.id) === Some(ss1))
|
||||
assert(db.getOutgoingPayment(s2.id) === Some(ss2))
|
||||
|
@ -328,15 +421,15 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
|
||||
// -- feed db with incoming payments
|
||||
val expiredInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32, alicePriv, "incoming #1", timestamp = 1)
|
||||
val expiredPayment = IncomingPayment(expiredInvoice, randomBytes32, 100, IncomingPaymentStatus.Expired)
|
||||
val expiredPayment = IncomingPayment(expiredInvoice, randomBytes32, PaymentType.Standard, 100, IncomingPaymentStatus.Expired)
|
||||
val pendingInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(456 msat), randomBytes32, alicePriv, "incoming #2")
|
||||
val pendingPayment = IncomingPayment(pendingInvoice, randomBytes32, 120, IncomingPaymentStatus.Pending)
|
||||
val pendingPayment = IncomingPayment(pendingInvoice, randomBytes32, PaymentType.Standard, 120, IncomingPaymentStatus.Pending)
|
||||
val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(789 msat), randomBytes32, alicePriv, "incoming #3")
|
||||
val receivedAt1 = 150
|
||||
val receivedPayment1 = IncomingPayment(paidInvoice1, randomBytes32, 130, IncomingPaymentStatus.Received(561 msat, receivedAt1))
|
||||
val receivedPayment1 = IncomingPayment(paidInvoice1, randomBytes32, PaymentType.Standard, 130, IncomingPaymentStatus.Received(561 msat, receivedAt1))
|
||||
val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(888 msat), randomBytes32, alicePriv, "incoming #4")
|
||||
val receivedAt2 = 160
|
||||
val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(889 msat, receivedAt2))
|
||||
val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32, PaymentType.Standard, paidInvoice2.timestamp.seconds.toMillis, IncomingPaymentStatus.Received(889 msat, receivedAt2))
|
||||
db.addIncomingPayment(pendingInvoice, pendingPayment.paymentPreimage)
|
||||
db.addIncomingPayment(expiredInvoice, expiredPayment.paymentPreimage)
|
||||
db.addIncomingPayment(paidInvoice1, receivedPayment1.paymentPreimage)
|
||||
|
@ -350,11 +443,11 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, "outgoing #1", expirySeconds = None, timestamp = 0)
|
||||
|
||||
// 1st attempt, pending -> failed
|
||||
val outgoing1 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, 123 msat, alice, 200, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
val outgoing1 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 200, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
db.addOutgoingPayment(outgoing1)
|
||||
db.updateOutgoingPayment(PaymentFailed(outgoing1.id, outgoing1.paymentHash, Nil, 210))
|
||||
// 2nd attempt: pending
|
||||
val outgoing2 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, 123 msat, alice, 211, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
val outgoing2 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 211, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
db.addOutgoingPayment(outgoing2)
|
||||
|
||||
// -- 1st check: result contains 2 incoming PAID, 1 outgoing PENDING. Outgoing1 must not be overridden by Outgoing2
|
||||
|
@ -366,12 +459,12 @@ class SqlitePaymentsDbSpec extends FunSuite {
|
|||
|
||||
// failed #2 and add a successful payment (made of 2 partial payments)
|
||||
db.updateOutgoingPayment(PaymentFailed(outgoing2.id, outgoing2.paymentHash, Nil, 250))
|
||||
val outgoing3 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, 200 msat, bob, 300, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
val outgoing4 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, 300 msat, bob, 310, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
val outgoing3 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, PaymentType.Standard, 200 msat, 500 msat, bob, 300, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
val outgoing4 = OutgoingPayment(UUID.randomUUID(), parentId2, None, paymentHash1, PaymentType.Standard, 300 msat, 500 msat, bob, 310, Some(invoice), OutgoingPaymentStatus.Pending)
|
||||
db.addOutgoingPayment(outgoing3)
|
||||
db.addOutgoingPayment(outgoing4)
|
||||
// complete #2 and #3 partial payments
|
||||
val sent = PaymentSent(parentId2, paymentHash1, preimage1, Seq(
|
||||
val sent = PaymentSent(parentId2, paymentHash1, preimage1, outgoing3.recipientAmount, outgoing3.recipientNodeId, Seq(
|
||||
PaymentSent.PartialPayment(outgoing3.id, outgoing3.amount, 15 msat, randomBytes32, None, 400),
|
||||
PaymentSent.PartialPayment(outgoing4.id, outgoing4.amount, 20 msat, randomBytes32, None, 410)
|
||||
))
|
||||
|
@ -403,7 +496,7 @@ object SqlitePaymentsDbSpec {
|
|||
val (alicePriv, bobPriv, carolPriv, davePriv) = (randomKey, randomKey, randomKey, randomKey)
|
||||
val (alice, bob, carol, dave) = (alicePriv.publicKey, bobPriv.publicKey, carolPriv.publicKey, davePriv.publicKey)
|
||||
val hop_ab = ChannelHop(alice, bob, ChannelUpdate(randomBytes64, randomBytes32, ShortChannelId(42), 1, 0, 0, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None))
|
||||
val hop_bc = ChannelHop(bob, carol, ChannelUpdate(randomBytes64, randomBytes32, ShortChannelId(43), 1, 0, 0, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None))
|
||||
val hop_bc = NodeHop(bob, carol, CltvExpiryDelta(14), 1 msat)
|
||||
val (preimage1, preimage2, preimage3, preimage4) = (randomBytes32, randomBytes32, randomBytes32, randomBytes32)
|
||||
val (paymentHash1, paymentHash2, paymentHash3, paymentHash4) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3), Crypto.sha256(preimage4))
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh}
|
|||
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
|
||||
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, OutgoingPaymentStatus}
|
||||
import fr.acinq.eclair.db._
|
||||
import fr.acinq.eclair.io.Peer
|
||||
import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
|
@ -252,7 +252,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
}
|
||||
|
||||
test("wait for network announcements") {
|
||||
val sender = TestProbe()
|
||||
// generating more blocks so that all funding txes are buried under at least 6 blocks
|
||||
generateBlocks(bitcoincli, 4)
|
||||
// A requires private channels, as a consequence:
|
||||
|
@ -436,13 +435,14 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
awaitCond({
|
||||
sender.expectMsgType[PaymentEvent](10 seconds) match {
|
||||
case PaymentFailed(_, _, failures, _) => failures == Seq.empty // if something went wrong fail with a hint
|
||||
case PaymentSent(_, _, _, part :: Nil) => part.route.get.exists(_.nodeId == nodes("G").nodeParams.nodeId)
|
||||
case PaymentSent(_, _, _, _, _, part :: Nil) => part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId)
|
||||
case _ => false
|
||||
}
|
||||
}, max = 30 seconds, interval = 10 seconds)
|
||||
}
|
||||
|
||||
test("send a multi-part payment B->D") {
|
||||
val start = Platform.currentTime
|
||||
val sender = TestProbe()
|
||||
val amount = 1000000000L.msat
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), "split the restaurant bill"))
|
||||
|
@ -452,23 +452,29 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 5, paymentRequest = Some(pr)))
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||
assert(paymentSent.id === paymentId)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
||||
assert(paymentSent.parts.length > 1)
|
||||
assert(paymentSent.amount === amount)
|
||||
assert(paymentSent.feesPaid > 0.msat)
|
||||
assert(paymentSent.parts.forall(p => p.id != paymentSent.id))
|
||||
assert(paymentSent.parts.forall(p => p.route.isDefined))
|
||||
assert(paymentSent.id === paymentId, paymentSent)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||
assert(paymentSent.parts.length > 1, paymentSent)
|
||||
assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId, paymentSent)
|
||||
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||
assert(paymentSent.feesPaid > 0.msat, paymentSent)
|
||||
assert(paymentSent.parts.forall(p => p.id != paymentSent.id), paymentSent)
|
||||
assert(paymentSent.parts.forall(p => p.route.isDefined), paymentSent)
|
||||
|
||||
val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(paymentParts.length == paymentSent.parts.length)
|
||||
assert(paymentParts.map(_.amount).sum === amount)
|
||||
assert(paymentParts.forall(p => p.parentId == paymentId))
|
||||
assert(paymentParts.forall(p => p.parentId != p.id))
|
||||
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat))
|
||||
assert(paymentParts.length == paymentSent.parts.length, paymentParts)
|
||||
assert(paymentParts.map(_.amount).sum === amount, paymentParts)
|
||||
assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts)
|
||||
assert(paymentParts.forall(p => p.parentId != p.id), paymentParts)
|
||||
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat), paymentParts)
|
||||
|
||||
awaitCond(nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
|
||||
val sent = nodes("B").nodeParams.db.audit.listSent(start, Platform.currentTime)
|
||||
assert(sent.length === 1, sent)
|
||||
assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent)
|
||||
|
||||
awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
}
|
||||
|
||||
|
@ -489,9 +495,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
sender.send(nodes("B").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("D").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentFailed = sender.expectMsgType[PaymentFailed](45 seconds)
|
||||
assert(paymentFailed.id === paymentId)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
||||
assert(paymentFailed.failures.length > 1)
|
||||
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||
assert(paymentFailed.failures.length > 1, paymentFailed)
|
||||
|
||||
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||
|
||||
|
@ -512,20 +518,20 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 3, paymentRequest = Some(pr)))
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||
assert(paymentSent.id === paymentId)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
||||
assert(paymentSent.parts.length > 1)
|
||||
assert(paymentSent.amount === amount)
|
||||
assert(paymentSent.feesPaid === 0.msat) // no fees when using direct channels
|
||||
assert(paymentSent.id === paymentId, paymentSent)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||
assert(paymentSent.parts.length > 1, paymentSent)
|
||||
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||
assert(paymentSent.feesPaid === 0.msat, paymentSent) // no fees when using direct channels
|
||||
|
||||
val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(paymentParts.map(_.amount).sum === amount)
|
||||
assert(paymentParts.forall(p => p.parentId == paymentId))
|
||||
assert(paymentParts.forall(p => p.parentId != p.id))
|
||||
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat))
|
||||
assert(paymentParts.map(_.amount).sum === amount, paymentParts)
|
||||
assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts)
|
||||
assert(paymentParts.forall(p => p.parentId != p.id), paymentParts)
|
||||
assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat), paymentParts)
|
||||
|
||||
awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
}
|
||||
|
||||
|
@ -544,10 +550,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
sender.send(nodes("D").paymentInitiator, SendPaymentRequest(amount, pr.paymentHash, nodes("C").nodeParams.nodeId, 1, paymentRequest = Some(pr)))
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
||||
assert(paymentFailed.id === paymentId)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
||||
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||
|
||||
assert(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||
val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(incoming.get.status === IncomingPaymentStatus.Pending, incoming)
|
||||
|
||||
sender.send(nodes("D").relayer, GetOutgoingChannels())
|
||||
val canSend2 = sender.expectMsgType[OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum
|
||||
|
@ -555,7 +562,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
assert(math.abs((canSend - canSend2).toLong) < 50000000)
|
||||
}
|
||||
|
||||
test("send a trampoline payment B->F3 (via trampoline G)") {
|
||||
test("send a trampoline payment B->F3 with retry (via trampoline G)") {
|
||||
val start = Platform.currentTime
|
||||
val sender = TestProbe()
|
||||
val amount = 4000000000L.msat
|
||||
|
@ -564,28 +571,34 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
assert(pr.features.allowMultiPart)
|
||||
assert(pr.features.allowTrampoline)
|
||||
|
||||
val payment = SendTrampolinePaymentRequest(amount, 1000000 msat, pr, nodes("G").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(288))
|
||||
// The first attempt should fail, but the second one should succeed.
|
||||
val attempts = (1000 msat, CltvExpiryDelta(42)) :: (1000000 msat, CltvExpiryDelta(288)) :: Nil
|
||||
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("G").nodeParams.nodeId, attempts)
|
||||
sender.send(nodes("B").paymentInitiator, payment)
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||
assert(paymentSent.id === paymentId)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
||||
assert(paymentSent.amount === amount)
|
||||
assert(paymentSent.feesPaid === payment.trampolineFees)
|
||||
assert(paymentSent.id === paymentId, paymentSent)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||
assert(paymentSent.recipientNodeId === nodes("F3").nodeParams.nodeId, paymentSent)
|
||||
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||
assert(paymentSent.feesPaid === 1000000.msat, paymentSent)
|
||||
assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent)
|
||||
|
||||
awaitCond(nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F3").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
|
||||
awaitCond(nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
||||
val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
||||
assert(relayed.amountIn - relayed.amountOut === payment.trampolineFees)
|
||||
assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
|
||||
assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed)
|
||||
|
||||
// TODO: @t-bast: validate trampoline route data once implemented
|
||||
val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(outgoingSuccess.forall(p => p.targetNodeId == nodes("F3").nodeParams.nodeId))
|
||||
assert(outgoingSuccess.map(_.amount).sum === amount)
|
||||
assert(outgoingSuccess.map(_.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid).sum === payment.trampolineFees)
|
||||
outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
||||
assert(recipientNodeId === nodes("F3").nodeParams.nodeId, p)
|
||||
assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F3").nodeParams.nodeId)), p)
|
||||
}
|
||||
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess)
|
||||
}
|
||||
|
||||
test("send a trampoline payment D->B (via trampoline C)") {
|
||||
|
@ -597,28 +610,36 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
assert(pr.features.allowMultiPart)
|
||||
assert(pr.features.allowTrampoline)
|
||||
|
||||
val payment = SendTrampolinePaymentRequest(amount, 300000 msat, pr, nodes("C").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(144))
|
||||
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("C").nodeParams.nodeId, Seq((300000 msat, CltvExpiryDelta(144))))
|
||||
sender.send(nodes("D").paymentInitiator, payment)
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||
assert(paymentSent.id === paymentId)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
||||
assert(paymentSent.amount === amount)
|
||||
assert(paymentSent.feesPaid === payment.trampolineFees)
|
||||
assert(paymentSent.id === paymentId, paymentSent)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||
assert(paymentSent.feesPaid === 300000.msat, paymentSent)
|
||||
assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent)
|
||||
|
||||
awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
|
||||
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
||||
assert(relayed.amountIn - relayed.amountOut === payment.trampolineFees)
|
||||
assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
|
||||
assert(relayed.amountIn - relayed.amountOut < 300000.msat, relayed)
|
||||
|
||||
// TODO: @t-bast: validate trampoline route data once implemented
|
||||
val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(outgoingSuccess.forall(p => p.targetNodeId == nodes("B").nodeParams.nodeId))
|
||||
assert(outgoingSuccess.map(_.amount).sum === amount)
|
||||
assert(outgoingSuccess.map(_.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid).sum === payment.trampolineFees)
|
||||
outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
||||
assert(recipientNodeId === nodes("B").nodeParams.nodeId, p)
|
||||
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p)
|
||||
}
|
||||
assert(outgoingSuccess.map(_.amount).sum === amount + 300000.msat, outgoingSuccess)
|
||||
|
||||
awaitCond(nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime).nonEmpty)
|
||||
val sent = nodes("D").nodeParams.db.audit.listSent(start, Platform.currentTime)
|
||||
assert(sent.length === 1, sent)
|
||||
assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent)
|
||||
}
|
||||
|
||||
test("send a trampoline payment F3->A (via trampoline C, non-trampoline recipient)") {
|
||||
|
@ -635,28 +656,30 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
assert(pr.features.allowMultiPart)
|
||||
assert(!pr.features.allowTrampoline)
|
||||
|
||||
val payment = SendTrampolinePaymentRequest(amount, 1000000 msat, pr, nodes("C").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(432))
|
||||
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))))
|
||||
sender.send(nodes("F3").paymentInitiator, payment)
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentSent = sender.expectMsgType[PaymentSent](30 seconds)
|
||||
assert(paymentSent.id === paymentId)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash)
|
||||
assert(paymentSent.amount === amount)
|
||||
assert(paymentSent.feesPaid === payment.trampolineFees)
|
||||
assert(paymentSent.id === paymentId, paymentSent)
|
||||
assert(paymentSent.paymentHash === pr.paymentHash, paymentSent)
|
||||
assert(paymentSent.recipientAmount === amount, paymentSent)
|
||||
assert(paymentSent.trampolineFees === 1000000.msat, paymentSent)
|
||||
|
||||
awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received]))
|
||||
val Some(IncomingPayment(_, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
|
||||
assert(receivedAmount === amount)
|
||||
|
||||
awaitCond(nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).exists(_.paymentHash == pr.paymentHash))
|
||||
val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, Platform.currentTime).filter(_.paymentHash == pr.paymentHash).head
|
||||
assert(relayed.amountIn - relayed.amountOut === payment.trampolineFees)
|
||||
assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed)
|
||||
assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed)
|
||||
|
||||
// TODO: @t-bast: validate trampoline route data once implemented
|
||||
val outgoingSuccess = nodes("F3").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(outgoingSuccess.forall(p => p.targetNodeId == nodes("A").nodeParams.nodeId))
|
||||
assert(outgoingSuccess.map(_.amount).sum === amount)
|
||||
assert(outgoingSuccess.map(_.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid).sum === payment.trampolineFees)
|
||||
outgoingSuccess.foreach { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) =>
|
||||
assert(recipientNodeId === nodes("A").nodeParams.nodeId, p)
|
||||
assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p)
|
||||
}
|
||||
assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess)
|
||||
}
|
||||
|
||||
test("send a trampoline payment B->D (temporary local failure at trampoline)") {
|
||||
|
@ -676,17 +699,17 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
assert(pr.features.allowMultiPart)
|
||||
assert(pr.features.allowTrampoline)
|
||||
|
||||
val payment = SendTrampolinePaymentRequest(amount, 250000 msat, pr, nodes("C").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(144))
|
||||
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(144))))
|
||||
sender.send(nodes("B").paymentInitiator, payment)
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
||||
assert(paymentFailed.id === paymentId)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
||||
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||
|
||||
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||
val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId)
|
||||
assert(outgoingPayments.nonEmpty)
|
||||
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
|
||||
assert(outgoingPayments.nonEmpty, outgoingPayments)
|
||||
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments)
|
||||
}
|
||||
|
||||
test("send a trampoline payment A->D (temporary remote failure at trampoline)") {
|
||||
|
@ -697,17 +720,17 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
|
|||
assert(pr.features.allowMultiPart)
|
||||
assert(pr.features.allowTrampoline)
|
||||
|
||||
val payment = SendTrampolinePaymentRequest(amount, 450000 msat, pr, nodes("B").nodeParams.nodeId, trampolineExpiryDelta = CltvExpiryDelta(288))
|
||||
val payment = SendTrampolinePaymentRequest(amount, pr, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))))
|
||||
sender.send(nodes("A").paymentInitiator, payment)
|
||||
val paymentId = sender.expectMsgType[UUID](30 seconds)
|
||||
val paymentFailed = sender.expectMsgType[PaymentFailed](30 seconds)
|
||||
assert(paymentFailed.id === paymentId)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash)
|
||||
assert(paymentFailed.id === paymentId, paymentFailed)
|
||||
assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed)
|
||||
|
||||
assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
|
||||
val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId)
|
||||
assert(outgoingPayments.nonEmpty)
|
||||
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]))
|
||||
assert(outgoingPayments.nonEmpty, outgoingPayments)
|
||||
assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.net.{Inet4Address, InetAddress, InetSocketAddress, ServerSocket}
|
|||
import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition}
|
||||
import akka.actor.{ActorRef, PoisonPill}
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Block
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.TestConstants._
|
||||
import fr.acinq.eclair._
|
||||
|
@ -30,8 +31,8 @@ import fr.acinq.eclair.channel.{ChannelCreated, HasCommitments}
|
|||
import fr.acinq.eclair.crypto.TransportHandler
|
||||
import fr.acinq.eclair.io.Peer._
|
||||
import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo
|
||||
import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec, SendChannelQuery}
|
||||
import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, LightningMessageCodecs, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, TlvStream}
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, InitTlv, LightningMessageCodecs, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, TlvStream}
|
||||
import org.scalatest.{Outcome, Tag}
|
||||
import scodec.bits.{ByteVector, _}
|
||||
|
||||
|
@ -81,7 +82,8 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
probe.send(peer, Peer.Init(None, channels))
|
||||
authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, None))
|
||||
transport.expectMsgType[TransportHandler.Listener]
|
||||
transport.expectMsgType[wire.Init]
|
||||
val localInit = transport.expectMsgType[wire.Init]
|
||||
assert(localInit.networks === List(Block.RegtestGenesisBlock.hash))
|
||||
transport.send(peer, remoteInit)
|
||||
transport.expectMsgType[TransportHandler.ReadAck]
|
||||
if (expectSync) {
|
||||
|
@ -258,6 +260,19 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
}
|
||||
}
|
||||
|
||||
test("disconnect if incompatible networks") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
probe.watch(transport.ref)
|
||||
probe.send(peer, Peer.Init(None, Set.empty))
|
||||
authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, new InetSocketAddress("1.2.3.4", 42000), outgoing = true, None))
|
||||
transport.expectMsgType[TransportHandler.Listener]
|
||||
transport.expectMsgType[wire.Init]
|
||||
transport.send(peer, wire.Init(Bob.nodeParams.features, TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil))))
|
||||
transport.expectMsgType[TransportHandler.ReadAck]
|
||||
probe.expectTerminated(transport.ref)
|
||||
}
|
||||
|
||||
test("handle disconnect in status INITIALIZING") { f =>
|
||||
import f._
|
||||
|
||||
|
@ -357,21 +372,23 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
test("filter gossip message (no filtering)") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
|
||||
connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer)
|
||||
val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap)
|
||||
val rebroadcast = Rebroadcast(channels.map(_ -> gossipOrigin).toMap, updates.map(_ -> gossipOrigin).toMap, nodes.map(_ -> gossipOrigin).toMap)
|
||||
probe.send(peer, rebroadcast)
|
||||
transport.expectNoMsg(2 seconds)
|
||||
transport.expectNoMsg(10 seconds)
|
||||
}
|
||||
|
||||
test("filter gossip message (filtered by origin)") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer)
|
||||
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
|
||||
val peerActor: ActorRef = peer
|
||||
val rebroadcast = Rebroadcast(
|
||||
channels.map(_ -> Set.empty[ActorRef]).toMap + (channels(5) -> Set(peerActor)),
|
||||
updates.map(_ -> Set.empty[ActorRef]).toMap + (updates(6) -> Set(peerActor)) + (updates(10) -> Set(peerActor)),
|
||||
nodes.map(_ -> Set.empty[ActorRef]).toMap + (nodes(4) -> Set(peerActor)))
|
||||
channels.map(_ -> gossipOrigin).toMap + (channels(5) -> Set(RemoteGossip(peerActor))),
|
||||
updates.map(_ -> gossipOrigin).toMap + (updates(6) -> (gossipOrigin + RemoteGossip(peerActor))) + (updates(10) -> Set(RemoteGossip(peerActor))),
|
||||
nodes.map(_ -> gossipOrigin).toMap + (nodes(4) -> Set(RemoteGossip(peerActor))))
|
||||
val filter = wire.GossipTimestampFilter(Alice.nodeParams.chainHash, 0, Long.MaxValue) // no filtering on timestamps
|
||||
probe.send(peer, filter)
|
||||
probe.send(peer, rebroadcast)
|
||||
|
@ -385,7 +402,8 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
import f._
|
||||
val probe = TestProbe()
|
||||
connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer)
|
||||
val rebroadcast = Rebroadcast(channels.map(_ -> Set.empty[ActorRef]).toMap, updates.map(_ -> Set.empty[ActorRef]).toMap, nodes.map(_ -> Set.empty[ActorRef]).toMap)
|
||||
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
|
||||
val rebroadcast = Rebroadcast(channels.map(_ -> gossipOrigin).toMap, updates.map(_ -> gossipOrigin).toMap, nodes.map(_ -> gossipOrigin).toMap)
|
||||
val timestamps = updates.map(_.timestamp).sorted.slice(10, 30)
|
||||
val filter = wire.GossipTimestampFilter(Alice.nodeParams.chainHash, timestamps.head, timestamps.last - timestamps.head)
|
||||
probe.send(peer, filter)
|
||||
|
@ -397,6 +415,24 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
nodes.filter(u => timestamps.contains(u.timestamp)).foreach(transport.expectMsg(_))
|
||||
}
|
||||
|
||||
test("does not filter our own gossip message") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer)
|
||||
val gossipOrigin = Set[GossipOrigin](RemoteGossip(TestProbe().ref))
|
||||
val rebroadcast = Rebroadcast(
|
||||
channels.map(_ -> gossipOrigin).toMap + (channels(5) -> Set(LocalGossip)),
|
||||
updates.map(_ -> gossipOrigin).toMap + (updates(6) -> (gossipOrigin + LocalGossip)) + (updates(10) -> Set(LocalGossip)),
|
||||
nodes.map(_ -> gossipOrigin).toMap + (nodes(4) -> Set(LocalGossip)))
|
||||
// No timestamp filter set -> the only gossip we should broadcast is our own.
|
||||
probe.send(peer, rebroadcast)
|
||||
transport.expectMsg(channels(5))
|
||||
transport.expectMsg(updates(6))
|
||||
transport.expectMsg(updates(10))
|
||||
transport.expectMsg(nodes(4))
|
||||
transport.expectNoMsg(10 seconds)
|
||||
}
|
||||
|
||||
test("react to peer's bad behavior") { f =>
|
||||
import f._
|
||||
val probe = TestProbe()
|
||||
|
@ -445,7 +481,6 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
|||
assert(error1.channelId === CHANNELID_ZERO)
|
||||
assert(new String(error1.data.toArray).startsWith("couldn't verify channel! shortChannelId="))
|
||||
|
||||
|
||||
// let's assume that one of the sigs were invalid
|
||||
router.send(peer, Peer.InvalidSignature(channels(0)))
|
||||
// peer will return a connection-wide error, including the hex-encoded representation of the bad message
|
||||
|
|
|
@ -20,13 +20,11 @@ import java.util.UUID
|
|||
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Block, Crypto, DeterministicWallet, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.TestConstants.TestFeeEstimator
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
|
||||
import fr.acinq.eclair.channel.Helpers.Funding
|
||||
import fr.acinq.eclair.channel.{ChannelFlags, Commitments, Upstream}
|
||||
import fr.acinq.eclair.channel.{ChannelFlags, Commitments, CommitmentsSpec, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
|
||||
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannel, OutgoingChannels}
|
||||
|
@ -35,8 +33,6 @@ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._
|
|||
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
|
||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
|
||||
import fr.acinq.eclair.router._
|
||||
import fr.acinq.eclair.transactions.CommitmentSpec
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.wire._
|
||||
import org.scalatest.{Outcome, Tag, fixture}
|
||||
import scodec.bits.ByteVector
|
||||
|
@ -63,12 +59,12 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
override def withFixture(test: OneArgTest): Outcome = {
|
||||
val id = UUID.randomUUID()
|
||||
val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, b, Upstream.Local(id), None, storeInDb = true, publishEvent = true)
|
||||
val cfg = SendPaymentConfig(id, id, Some("42"), paymentHash, finalAmount, finalRecipient, Upstream.Local(id), None, storeInDb = true, publishEvent = true, Nil)
|
||||
val nodeParams = TestConstants.Alice.nodeParams
|
||||
nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator].setFeerate(FeeratesPerKw.single(500))
|
||||
val (childPayFsm, router, relayer, sender, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe())
|
||||
class TestMultiPartPaymentLifecycle extends MultiPartPaymentLifecycle(nodeParams, cfg, relayer.ref, router.ref, TestProbe().ref) {
|
||||
override def spawnChildPaymentFsm(childId: UUID, includeTrampolineFees: Boolean): ActorRef = childPayFsm.ref
|
||||
override def spawnChildPaymentFsm(childId: UUID): ActorRef = childPayFsm.ref
|
||||
}
|
||||
val paymentHandler = TestFSMRef(new TestMultiPartPaymentLifecycle().asInstanceOf[MultiPartPaymentLifecycle])
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
|
@ -94,7 +90,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
import f._
|
||||
|
||||
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, b, 1500 * 1000 msat, expiry, 1)
|
||||
val payment = SendMultiPartPayment(randomBytes32, b, 1500 * 1000 msat, expiry, 1)
|
||||
sender.send(payFsm, payment)
|
||||
router.expectMsg(GetNetworkStats)
|
||||
assert(payFsm.stateName === WAIT_FOR_NETWORK_STATS)
|
||||
|
@ -108,7 +104,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
import f._
|
||||
|
||||
assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST)
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, b, 2500 * 1000 msat, expiry, 1)
|
||||
val payment = SendMultiPartPayment(randomBytes32, b, 2500 * 1000 msat, expiry, 1)
|
||||
sender.send(payFsm, payment)
|
||||
router.expectMsg(GetNetworkStats)
|
||||
assert(payFsm.stateName === WAIT_FOR_NETWORK_STATS)
|
||||
|
@ -129,7 +125,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("send to peer node via multiple channels") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, b, 2000 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, b, 2000 * 1000 msat, expiry, 3)
|
||||
// When sending to a peer node, we should not filter out unannounced channels.
|
||||
val channels = OutgoingChannels(Seq(
|
||||
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1000 * 1000 msat, 0)),
|
||||
|
@ -143,8 +139,8 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
// The payment should be split in two, using direct channels with b.
|
||||
// MaxAttempts should be set to 1 when using direct channels to the destination.
|
||||
childPayFsm.expectMsgAllOf(
|
||||
SendPayment(paymentHash, b, Onion.createMultiPartPayload(1000 * 1000 msat, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_1.copy(channelFlags = ChannelFlags.Empty)))),
|
||||
SendPayment(paymentHash, b, Onion.createMultiPartPayload(1000 * 1000 msat, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_2.copy(channelFlags = ChannelFlags.Empty))))
|
||||
SendPayment(b, Onion.createMultiPartPayload(1000 * 1000 msat, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_1.copy(channelFlags = ChannelFlags.Empty)))),
|
||||
SendPayment(b, Onion.createMultiPartPayload(1000 * 1000 msat, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_2.copy(channelFlags = ChannelFlags.Empty))))
|
||||
)
|
||||
childPayFsm.expectNoMsg(50 millis)
|
||||
val childIds = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.toSeq
|
||||
|
@ -152,26 +148,32 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
val pp1 = PartialPayment(childIds.head, 1000 * 1000 msat, 0 msat, randomBytes32, None)
|
||||
val pp2 = PartialPayment(childIds(1), 1000 * 1000 msat, 0 msat, randomBytes32, None)
|
||||
childPayFsm.send(payFsm, PaymentSent(childIds.head, paymentHash, paymentPreimage, Seq(pp1)))
|
||||
childPayFsm.send(payFsm, PaymentSent(childIds(1), paymentHash, paymentPreimage, Seq(pp2)))
|
||||
val expectedMsg = PaymentSent(paymentId, paymentHash, paymentPreimage, Seq(pp1, pp2))
|
||||
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, b, Seq(pp1)))
|
||||
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, b, Seq(pp2)))
|
||||
val expectedMsg = PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, finalRecipient, Seq(pp1, pp2))
|
||||
sender.expectMsg(expectedMsg)
|
||||
eventListener.expectMsg(expectedMsg)
|
||||
|
||||
assert(expectedMsg.recipientAmount === finalAmount)
|
||||
assert(expectedMsg.amountWithFees === (2000 * 1000).msat)
|
||||
assert(expectedMsg.trampolineFees === (1000 * 1000).msat)
|
||||
assert(expectedMsg.nonTrampolineFees === 0.msat)
|
||||
assert(expectedMsg.feesPaid === expectedMsg.trampolineFees)
|
||||
}
|
||||
|
||||
test("send to peer node via single big channel") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, b, 1000 * 1000 msat, expiry, 1)
|
||||
val payment = SendMultiPartPayment(randomBytes32, b, 1000 * 1000 msat, expiry, 1)
|
||||
// Network statistics should be ignored when sending to peer (otherwise we should have split into multiple payments).
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(100), d => Satoshi(d.toLong))), localChannels(0))
|
||||
childPayFsm.expectMsg(SendPayment(paymentHash, b, Onion.createMultiPartPayload(payment.totalAmount, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_1))))
|
||||
childPayFsm.expectMsg(SendPayment(b, Onion.createMultiPartPayload(payment.totalAmount, payment.totalAmount, expiry, payment.paymentSecret), 1, routePrefix = Seq(ChannelHop(nodeParams.nodeId, b, channelUpdate_ab_1))))
|
||||
childPayFsm.expectNoMsg(50 millis)
|
||||
}
|
||||
|
||||
test("send to peer node via remote channels") { f =>
|
||||
import f._
|
||||
// d only has a single channel with capacity 1000 sat, we try to send more.
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, d, 2000 * 1000 msat, expiry, 1)
|
||||
val payment = SendMultiPartPayment(randomBytes32, d, 2000 * 1000 msat, expiry, 1)
|
||||
val testChannels = localChannels()
|
||||
val balanceToTarget = testChannels.channels.filter(_.nextNodeId == d).map(_.commitments.availableBalanceForSend).sum
|
||||
assert(balanceToTarget < (1000 * 1000).msat) // the commit tx fee prevents us from completely emptying our channel
|
||||
|
@ -186,28 +188,27 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("send to remote node without splitting") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 300 * 1000 msat, expiry, 1)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 300 * 1000 msat, expiry, 1)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1500), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
payFsm.stateData.asInstanceOf[PaymentProgress].pending.foreach {
|
||||
case (id, payment) => childPayFsm.send(payFsm, PaymentSent(id, paymentHash, paymentPreimage, Seq(PartialPayment(id, payment.finalPayload.amount, 5 msat, randomBytes32, None))))
|
||||
case (id, payment) => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, e, Seq(PartialPayment(id, payment.finalPayload.amount, 5 msat, randomBytes32, None))))
|
||||
}
|
||||
|
||||
val result = sender.expectMsgType[PaymentSent]
|
||||
assert(result.id === paymentId)
|
||||
assert(result.amount === payment.totalAmount)
|
||||
assert(result.amountWithFees === payment.totalAmount + result.nonTrampolineFees)
|
||||
assert(result.parts.length === 1)
|
||||
}
|
||||
|
||||
test("send to remote node via multiple channels") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3200 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3200 * 1000 msat, expiry, 3)
|
||||
// A network capacity of 1000 sat should split the payment in at least 3 parts.
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), localChannels())
|
||||
|
||||
val payments = Iterator.iterate(0 msat)(sent => {
|
||||
val child = childPayFsm.expectMsgType[SendPayment]
|
||||
assert(child.paymentHash === paymentHash)
|
||||
assert(child.targetNodeId === e)
|
||||
assert(child.maxAttempts === 3)
|
||||
assert(child.finalPayload.expiry === expiry)
|
||||
|
@ -225,19 +226,21 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
val partialPayments = pending.map {
|
||||
case (id, payment) => PartialPayment(id, payment.finalPayload.amount, 1 msat, randomBytes32, Some(hop_ac_1 :: hop_ab_2 :: Nil))
|
||||
}
|
||||
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(pp.id, paymentHash, paymentPreimage, Seq(pp))))
|
||||
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, finalAmount, e, Seq(pp))))
|
||||
val result = sender.expectMsgType[PaymentSent]
|
||||
assert(result.id === paymentId)
|
||||
assert(result.paymentHash === paymentHash)
|
||||
assert(result.paymentPreimage === paymentPreimage)
|
||||
assert(result.parts === partialPayments)
|
||||
assert(result.amount === (3200 * 1000).msat)
|
||||
assert(result.feesPaid === partialPayments.map(_.feesPaid).sum)
|
||||
assert(result.recipientAmount === finalAmount)
|
||||
assert(result.amountWithFees > (3200 * 1000).msat)
|
||||
assert(result.trampolineFees === (2200 * 1000).msat)
|
||||
assert(result.nonTrampolineFees === partialPayments.map(_.feesPaid).sum)
|
||||
}
|
||||
|
||||
test("send to remote node via single big channel") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3500 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3500 * 1000 msat, expiry, 3)
|
||||
// When splitting inside a channel, we need to take the fees of the commit tx into account (multiple outgoing HTLCs
|
||||
// will increase the size of the commit tx and thus its fee.
|
||||
val feeRatePerKw = 100
|
||||
|
@ -252,20 +255,21 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
val partialPayments = pending.map {
|
||||
case (id, payment) => PartialPayment(id, payment.finalPayload.amount, 1 msat, randomBytes32, None)
|
||||
}
|
||||
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(pp.id, paymentHash, paymentPreimage, Seq(pp))))
|
||||
partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(pp))))
|
||||
val result = sender.expectMsgType[PaymentSent]
|
||||
assert(result.id === paymentId)
|
||||
assert(result.paymentHash === paymentHash)
|
||||
assert(result.paymentPreimage === paymentPreimage)
|
||||
assert(result.parts === partialPayments)
|
||||
assert(result.amount === (3500 * 1000).msat)
|
||||
assert(result.feesPaid === partialPayments.map(_.feesPaid).sum)
|
||||
assert(result.amountWithFees - result.nonTrampolineFees === (3500 * 1000).msat)
|
||||
assert(result.recipientNodeId === finalRecipient) // the recipient is obtained from the config, not from the request (which may be to the first trampoline node)
|
||||
assert(result.nonTrampolineFees === partialPayments.map(_.feesPaid).sum)
|
||||
}
|
||||
|
||||
test("send to remote trampoline node") { f =>
|
||||
import f._
|
||||
val trampolineTlv = OnionTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32))
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 3, additionalTlvs = Seq(trampolineTlv))
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3, additionalTlvs = Seq(trampolineTlv))
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
|
||||
|
@ -278,7 +282,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
test("split fees between child payments") { f =>
|
||||
import f._
|
||||
val routeParams = RouteParams(randomize = false, 100 msat, 0.05, 20, CltvExpiryDelta(144), None)
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 3, routeParams = Some(routeParams))
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3, routeParams = Some(routeParams))
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, 3000 * 1000 msat)
|
||||
|
||||
|
@ -293,7 +297,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("skip empty channels") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3)
|
||||
val testChannels = localChannels()
|
||||
val testChannels1 = testChannels.copy(channels = testChannels.channels ++ Seq(
|
||||
OutgoingChannel(b, channelUpdate_ab_1.copy(shortChannelId = ShortChannelId(42)), makeCommitments(0 msat, 10)),
|
||||
|
@ -302,17 +306,17 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), testChannels1)
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
payFsm.stateData.asInstanceOf[PaymentProgress].pending.foreach {
|
||||
case (id, payment) => childPayFsm.send(payFsm, PaymentSent(id, paymentHash, paymentPreimage, Seq(PartialPayment(id, payment.finalPayload.amount, 5 msat, randomBytes32, None))))
|
||||
case (id, p) => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id, p.finalPayload.amount, 5 msat, randomBytes32, None))))
|
||||
}
|
||||
|
||||
val result = sender.expectMsgType[PaymentSent]
|
||||
assert(result.id === paymentId)
|
||||
assert(result.amount === payment.totalAmount)
|
||||
assert(result.amountWithFees > payment.totalAmount)
|
||||
}
|
||||
|
||||
test("retry after error") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3)
|
||||
val testChannels = localChannels()
|
||||
// A network capacity of 1000 sat should split the payment in at least 3 parts.
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), testChannels)
|
||||
|
@ -345,7 +349,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("cannot send (not enough capacity on local channels)") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 3)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), OutgoingChannels(Seq(
|
||||
OutgoingChannel(b, channelUpdate_ab_1, makeCommitments(1000 * 1000 msat, 10)),
|
||||
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1000 * 1000 msat, 10)),
|
||||
|
@ -360,7 +364,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("cannot send (fee rate too high)") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 2500 * 1000 msat, expiry, 3)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 2500 * 1000 msat, expiry, 3)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), OutgoingChannels(Seq(
|
||||
OutgoingChannel(b, channelUpdate_ab_1, makeCommitments(1500 * 1000 msat, 1000)),
|
||||
OutgoingChannel(c, channelUpdate_ac_2, makeCommitments(1500 * 1000 msat, 1000)),
|
||||
|
@ -375,7 +379,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("payment timeout") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 5)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 5)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
val (childId1, _) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
|
||||
|
@ -388,7 +392,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("failure received from final recipient") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 5)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 5)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
val (childId1, _) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
|
||||
|
@ -401,7 +405,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("fail after too many attempts") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 3000 * 1000 msat, expiry, 2)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 3000 * 1000 msat, expiry, 2)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1000), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
val (childId1, childPayment1) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head
|
||||
|
@ -431,14 +435,14 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
|
||||
test("receive partial failure after success (recipient spec violation)") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 4000 * 1000 msat, expiry, 2)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 4000 * 1000 msat, expiry, 2)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(1500), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
|
||||
|
||||
// If one of the payments succeeds, the recipient MUST succeed them all: we can consider the whole payment succeeded.
|
||||
val (id1, payment1) = pending.head
|
||||
childPayFsm.send(payFsm, PaymentSent(id1, paymentHash, paymentPreimage, Seq(PartialPayment(id1, payment1.finalPayload.amount, 10 msat, randomBytes32, None))))
|
||||
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id1, payment1.finalPayload.amount, 0 msat, randomBytes32, None))))
|
||||
awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED)
|
||||
|
||||
// A partial failure should simply be ignored.
|
||||
|
@ -446,16 +450,16 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
childPayFsm.send(payFsm, PaymentFailed(id2, paymentHash, Nil))
|
||||
|
||||
pending.tail.tail.foreach {
|
||||
case (id, payment) => childPayFsm.send(payFsm, PaymentSent(id, paymentHash, paymentPreimage, Seq(PartialPayment(id, payment.finalPayload.amount, 10 msat, randomBytes32, None))))
|
||||
case (id, p) => childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id, p.finalPayload.amount, 0 msat, randomBytes32, None))))
|
||||
}
|
||||
val result = sender.expectMsgType[PaymentSent]
|
||||
assert(result.id === paymentId)
|
||||
assert(result.amount === payment.totalAmount - payment2.finalPayload.amount)
|
||||
assert(result.amountWithFees === payment.totalAmount - payment2.finalPayload.amount)
|
||||
}
|
||||
|
||||
test("receive partial success after abort (recipient spec violation)") { f =>
|
||||
import f._
|
||||
val payment = SendMultiPartPayment(paymentHash, randomBytes32, e, 5000 * 1000 msat, expiry, 1)
|
||||
val payment = SendMultiPartPayment(randomBytes32, e, 5000 * 1000 msat, expiry, 1)
|
||||
initPayment(f, payment, emptyStats.copy(capacity = Stats(Seq(2000), d => Satoshi(d.toLong))), localChannels())
|
||||
waitUntilAmountSent(f, payment.totalAmount)
|
||||
val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending
|
||||
|
@ -468,7 +472,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
// The in-flight HTLC set doesn't pay the full amount, so the recipient MUST not fulfill any of those.
|
||||
// But if he does, it's too bad for him as we have obtained a cheaper proof of payment.
|
||||
val (id2, payment2) = pending.tail.head
|
||||
childPayFsm.send(payFsm, PaymentSent(id2, paymentHash, paymentPreimage, Seq(PartialPayment(id2, payment2.finalPayload.amount, 5 msat, randomBytes32, None))))
|
||||
childPayFsm.send(payFsm, PaymentSent(paymentId, paymentHash, paymentPreimage, payment.totalAmount, e, Seq(PartialPayment(id2, payment2.finalPayload.amount, 5 msat, randomBytes32, None))))
|
||||
awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED)
|
||||
|
||||
// Even if all other child payments fail, we obtained the preimage so the payment is a success from our point of view.
|
||||
|
@ -477,8 +481,8 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
}
|
||||
val result = sender.expectMsgType[PaymentSent]
|
||||
assert(result.id === paymentId)
|
||||
assert(result.amount === payment2.finalPayload.amount)
|
||||
assert(result.feesPaid === 5.msat)
|
||||
assert(result.amountWithFees === payment2.finalPayload.amount + 5.msat)
|
||||
assert(result.nonTrampolineFees === 5.msat)
|
||||
}
|
||||
|
||||
test("split payment", Tag("fuzzy")) { f =>
|
||||
|
@ -489,7 +493,7 @@ class MultiPartPaymentLifecycleSpec extends TestKit(ActorSystem("test")) with fi
|
|||
val toSend = ((1 + Random.nextInt(3500)) * 1000).msat
|
||||
val networkStats = emptyStats.copy(capacity = Stats(Seq(400 + Random.nextInt(1600)), d => Satoshi(d.toLong)))
|
||||
val routeParams = RouteParams(randomize = true, Random.nextInt(1000).msat, Random.nextInt(10).toDouble / 100, 20, CltvExpiryDelta(144), None)
|
||||
val request = SendMultiPartPayment(paymentHash, randomBytes32, e, toSend, CltvExpiry(561), 1, Nil, Some(routeParams))
|
||||
val request = SendMultiPartPayment(randomBytes32, e, toSend, CltvExpiry(561), 1, Nil, Some(routeParams))
|
||||
val fuzzParams = s"(sending $toSend with network capacity ${networkStats.capacity.percentile75.toMilliSatoshi}, fee base ${routeParams.maxFeeBase} and fee percentage ${routeParams.maxFeePct})"
|
||||
val (remaining, payments) = splitPayment(f.nodeParams, toSend, testChannels.channels, Some(networkStats), request, randomize = true)
|
||||
assert(remaining === 0.msat, fuzzParams)
|
||||
|
@ -505,6 +509,8 @@ object MultiPartPaymentLifecycleSpec {
|
|||
val paymentPreimage = randomBytes32
|
||||
val paymentHash = Crypto.sha256(paymentPreimage)
|
||||
val expiry = CltvExpiry(1105)
|
||||
val finalAmount = 1000000 msat
|
||||
val finalRecipient = randomKey.publicKey
|
||||
|
||||
/**
|
||||
* We simulate a multi-part-friendly network:
|
||||
|
@ -516,7 +522,7 @@ object MultiPartPaymentLifecycleSpec {
|
|||
* where a has multiple channels with each of his peers.
|
||||
*/
|
||||
|
||||
val a :: b :: c :: d :: e :: Nil = Seq.fill(5)(PrivateKey(randomBytes32).publicKey)
|
||||
val a :: b :: c :: d :: e :: Nil = Seq.fill(5)(randomKey.publicKey)
|
||||
val channelId_ab_1 = ShortChannelId(1)
|
||||
val channelId_ab_2 = ShortChannelId(2)
|
||||
val channelId_ac_1 = ShortChannelId(11)
|
||||
|
@ -546,29 +552,8 @@ object MultiPartPaymentLifecycleSpec {
|
|||
|
||||
val emptyStats = NetworkStats(0, 0, Stats(Seq(0), d => Satoshi(d.toLong)), Stats(Seq(0), d => CltvExpiryDelta(d.toInt)), Stats(Seq(0), d => MilliSatoshi(d.toLong)), Stats(Seq(0), d => d.toLong))
|
||||
|
||||
def makeCommitments(canSend: MilliSatoshi, feeRatePerKw: Long, announceChannel: Boolean = true): Commitments = {
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.ShaChain
|
||||
// We are only interested in availableBalanceForSend so we can put dummy values in most places.
|
||||
val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64(50000000), 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, ByteVector.empty)
|
||||
val remoteParams = RemoteParams(randomKey.publicKey, 0 sat, UInt64(5000000), 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty)
|
||||
val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, canSend.truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey)
|
||||
Commitments(
|
||||
ChannelVersion.STANDARD,
|
||||
localParams,
|
||||
remoteParams,
|
||||
channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty,
|
||||
LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, canSend, 0 msat), PublishableTxs(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), Nil)),
|
||||
RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, 0 msat, canSend), randomBytes32, randomKey.publicKey),
|
||||
LocalChanges(Nil, Nil, Nil),
|
||||
RemoteChanges(Nil, Nil, Nil),
|
||||
localNextHtlcId = 1,
|
||||
remoteNextHtlcId = 1,
|
||||
originChannels = Map.empty,
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey),
|
||||
commitInput = commitmentInput,
|
||||
remotePerCommitmentSecrets = ShaChain.init,
|
||||
channelId = randomBytes32)
|
||||
}
|
||||
// We are only interested in availableBalanceForSend so we can put dummy values for the rest.
|
||||
def makeCommitments(canSend: MilliSatoshi, feeRatePerKw: Long, announceChannel: Boolean = true): Commitments =
|
||||
CommitmentsSpec.makeCommitments(canSend, 0 msat, feeRatePerKw, 0 sat, announceChannel = announceChannel)
|
||||
|
||||
}
|
|
@ -27,9 +27,10 @@ import fr.acinq.eclair.crypto.Sphinx
|
|||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
|
||||
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
|
||||
import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer}
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{BalanceTooLow, SendMultiPartPayment}
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
|
||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
|
||||
import fr.acinq.eclair.router.RouteNotFound
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, TestConstants, TestkitBaseClass, nodeFee, randomBytes, randomBytes32, randomKey}
|
||||
import org.scalatest.Outcome
|
||||
|
@ -130,9 +131,7 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut)
|
||||
relayer.send(nodeRelayer, p)
|
||||
|
||||
// TODO: @t-bast: should be an Expiry failure
|
||||
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
|
||||
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
|
||||
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
outgoingPayFSM.expectNoMsg(100 millis)
|
||||
}
|
||||
|
@ -144,14 +143,12 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
val expiryIn2 = CltvExpiry(500000) // not ok (delta = 100)
|
||||
val expiryOut = CltvExpiry(499900)
|
||||
val p = Seq(
|
||||
createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2500000 msat, expiryOut),
|
||||
createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2500000 msat, expiryOut)
|
||||
createValidIncomingPacket(2000000 msat, 3000000 msat, expiryIn1, 2100000 msat, expiryOut),
|
||||
createValidIncomingPacket(1000000 msat, 3000000 msat, expiryIn2, 2100000 msat, expiryOut)
|
||||
)
|
||||
p.foreach(p => relayer.send(nodeRelayer, p))
|
||||
|
||||
// TODO: @t-bast: should be an Expiry failure
|
||||
val failure = IncorrectOrUnknownPaymentDetails(3000000 msat, nodeParams.currentBlockHeight)
|
||||
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true))))
|
||||
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true))))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
outgoingPayFSM.expectNoMsg(100 millis)
|
||||
}
|
||||
|
@ -162,9 +159,7 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000))
|
||||
relayer.send(nodeRelayer, p)
|
||||
|
||||
// TODO: @t-bast: should be a Fee failure
|
||||
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
|
||||
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
|
||||
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
outgoingPayFSM.expectNoMsg(100 millis)
|
||||
}
|
||||
|
@ -178,13 +173,41 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
)
|
||||
p.foreach(p => relayer.send(nodeRelayer, p))
|
||||
|
||||
// TODO: @t-bast: should be a Fee failure
|
||||
val failure = IncorrectOrUnknownPaymentDetails(3000000 msat, nodeParams.currentBlockHeight)
|
||||
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true))))
|
||||
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true))))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
outgoingPayFSM.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
test("fail to relay because outgoing balance isn't sufficient") { f =>
|
||||
import f._
|
||||
|
||||
// Receive an upstream multi-part payment.
|
||||
incomingMultiPart.foreach(p => relayer.send(nodeRelayer, p))
|
||||
val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id
|
||||
outgoingPayFSM.expectMsgType[SendMultiPartPayment]
|
||||
|
||||
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, LocalFailure(BalanceTooLow) :: Nil))
|
||||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true))))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
eventListener.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
test("fail to relay because incoming fee isn't enough to find routes downstream") { f =>
|
||||
import f._
|
||||
|
||||
// Receive an upstream multi-part payment.
|
||||
incomingMultiPart.foreach(p => relayer.send(nodeRelayer, p))
|
||||
val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id
|
||||
outgoingPayFSM.expectMsgType[SendMultiPartPayment]
|
||||
|
||||
// If we're having a hard time finding routes, raising the fee/cltv will likely help.
|
||||
val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure)) :: LocalFailure(RouteNotFound) :: Nil
|
||||
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, failures))
|
||||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true))))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
eventListener.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
test("fail to relay because of downstream failures") { f =>
|
||||
import f._
|
||||
|
||||
|
@ -193,8 +216,9 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id
|
||||
outgoingPayFSM.expectMsgType[SendMultiPartPayment]
|
||||
|
||||
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, Nil))
|
||||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(IncorrectOrUnknownPaymentDetails(incomingAmount, nodeParams.currentBlockHeight)), commit = true))))
|
||||
val failures = RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(Nil) :: LocalFailure(RouteNotFound) :: Nil
|
||||
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, failures))
|
||||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true))))
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
eventListener.expectNoMsg(100 millis)
|
||||
}
|
||||
|
@ -228,8 +252,8 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
|
||||
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
|
||||
validateRelayEvent(relayEvent)
|
||||
assert(relayEvent.fromChannelIds.toSet === incomingMultiPart.map(_.add.channelId).toSet)
|
||||
assert(relayEvent.toChannelIds.nonEmpty)
|
||||
assert(relayEvent.incoming.toSet === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)).toSet)
|
||||
assert(relayEvent.outgoing.nonEmpty)
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
|
@ -249,8 +273,8 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
commandBuffer.expectMsg(CommandBuffer.CommandSend(incomingAdd.channelId, CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)))
|
||||
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
|
||||
validateRelayEvent(relayEvent)
|
||||
assert(relayEvent.fromChannelIds === Seq(incomingSinglePart.add.channelId))
|
||||
assert(relayEvent.toChannelIds.nonEmpty)
|
||||
assert(relayEvent.incoming === Seq(PaymentRelayed.Part(incomingSinglePart.add.amountMsat, incomingSinglePart.add.channelId)))
|
||||
assert(relayEvent.outgoing.nonEmpty)
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
|
@ -269,10 +293,9 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
|
||||
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add)))
|
||||
val outgoingPayment = outgoingPayFSM.expectMsgType[SendMultiPartPayment]
|
||||
assert(outgoingPayment.paymentHash === paymentHash)
|
||||
assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret
|
||||
assert(outgoingPayment.totalAmount === outgoingAmount)
|
||||
assert(outgoingPayment.finalExpiry === outgoingExpiry)
|
||||
assert(outgoingPayment.targetExpiry === outgoingExpiry)
|
||||
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
||||
assert(outgoingPayment.additionalTlvs === Nil)
|
||||
assert(outgoingPayment.routeParams.isDefined)
|
||||
|
@ -282,8 +305,8 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
|
||||
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
|
||||
validateRelayEvent(relayEvent)
|
||||
assert(relayEvent.fromChannelIds === incomingMultiPart.map(_.add.channelId))
|
||||
assert(relayEvent.toChannelIds.nonEmpty)
|
||||
assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)))
|
||||
assert(relayEvent.outgoing.nonEmpty)
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
|
@ -300,7 +323,6 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
val outgoingCfg = outgoingPayFSM.expectMsgType[SendPaymentConfig]
|
||||
validateOutgoingCfg(outgoingCfg, Upstream.TrampolineRelayed(incomingMultiPart.map(_.add)))
|
||||
val outgoingPayment = outgoingPayFSM.expectMsgType[SendPayment]
|
||||
assert(outgoingPayment.paymentHash === paymentHash)
|
||||
assert(outgoingPayment.routePrefix === Nil)
|
||||
assert(outgoingPayment.finalPayload.amount === outgoingAmount)
|
||||
assert(outgoingPayment.finalPayload.expiry === outgoingExpiry)
|
||||
|
@ -312,8 +334,8 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
|
||||
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
|
||||
validateRelayEvent(relayEvent)
|
||||
assert(relayEvent.fromChannelIds === incomingMultiPart.map(_.add.channelId))
|
||||
assert(relayEvent.toChannelIds.length === 1)
|
||||
assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)))
|
||||
assert(relayEvent.outgoing.length === 1)
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
|
@ -322,15 +344,15 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
assert(!outgoingCfg.storeInDb)
|
||||
assert(outgoingCfg.paymentHash === paymentHash)
|
||||
assert(outgoingCfg.paymentRequest === None)
|
||||
assert(outgoingCfg.targetNodeId === outgoingNodeId)
|
||||
assert(outgoingCfg.recipientAmount === outgoingAmount)
|
||||
assert(outgoingCfg.recipientNodeId === outgoingNodeId)
|
||||
assert(outgoingCfg.upstream === upstream)
|
||||
}
|
||||
|
||||
def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = {
|
||||
assert(outgoingPayment.paymentHash === paymentHash)
|
||||
assert(outgoingPayment.paymentSecret !== incomingSecret) // we should generate a new outgoing secret
|
||||
assert(outgoingPayment.totalAmount === outgoingAmount)
|
||||
assert(outgoingPayment.finalExpiry === outgoingExpiry)
|
||||
assert(outgoingPayment.targetExpiry === outgoingExpiry)
|
||||
assert(outgoingPayment.targetNodeId === outgoingNodeId)
|
||||
assert(outgoingPayment.additionalTlvs === Seq(OnionTlv.TrampolineOnion(nextTrampolinePacket)))
|
||||
assert(outgoingPayment.routeParams.isDefined)
|
||||
|
@ -339,9 +361,8 @@ class NodeRelayerSpec extends TestkitBaseClass {
|
|||
|
||||
def validateRelayEvent(e: TrampolinePaymentRelayed): Unit = {
|
||||
assert(e.amountIn === incomingAmount)
|
||||
assert(e.amountOut === outgoingAmount)
|
||||
assert(e.amountOut >= outgoingAmount) // outgoingAmount + routing fees
|
||||
assert(e.paymentHash === paymentHash)
|
||||
assert(e.toNodeId === outgoingNodeId)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -370,7 +391,7 @@ object NodeRelayerSpec {
|
|||
createValidIncomingPacket(incomingAmount, incomingAmount, CltvExpiry(500000), outgoingAmount, outgoingExpiry)
|
||||
|
||||
def createSuccessEvent(id: UUID): PaymentSent =
|
||||
PaymentSent(id, paymentHash, paymentPreimage, Seq(PaymentSent.PartialPayment(id, outgoingAmount, 10 msat, randomBytes32, None)))
|
||||
PaymentSent(id, paymentHash, paymentPreimage, outgoingAmount, outgoingNodeId, Seq(PaymentSent.PartialPayment(id, outgoingAmount, 10 msat, randomBytes32, None)))
|
||||
|
||||
def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPacket.NodeRelayPacket = {
|
||||
val outerPayload = if (amountIn == totalAmountIn) {
|
||||
|
|
|
@ -28,12 +28,12 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._
|
|||
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features}
|
||||
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPaymentRequest, SendTrampolinePaymentRequest}
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator._
|
||||
import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
|
||||
import fr.acinq.eclair.router.RouteParams
|
||||
import fr.acinq.eclair.router.{NodeHop, RouteParams}
|
||||
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
|
||||
import fr.acinq.eclair.wire.{OnionCodecs, OnionTlv}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomKey}
|
||||
import fr.acinq.eclair.wire.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient}
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, randomBytes32, randomKey}
|
||||
import org.scalatest.{Outcome, Tag, fixture}
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
|
@ -45,12 +45,14 @@ import scala.concurrent.duration._
|
|||
|
||||
class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.FunSuiteLike {
|
||||
|
||||
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe)
|
||||
case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe)
|
||||
|
||||
override def withFixture(test: OneArgTest): Outcome = {
|
||||
val features = if (test.tags.contains("mpp_disabled")) hex"0a8a" else hex"028a8a"
|
||||
val nodeParams = TestConstants.Alice.nodeParams.copy(features = features)
|
||||
val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe())
|
||||
val eventListener = TestProbe()
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
class TestPaymentInitiator extends PaymentInitiator(nodeParams, TestProbe().ref, TestProbe().ref, TestProbe().ref) {
|
||||
// @formatter:off
|
||||
override def spawnPaymentFsm(cfg: SendPaymentConfig): ActorRef = {
|
||||
|
@ -64,7 +66,7 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
// @formatter:on
|
||||
}
|
||||
val initiator = TestActorRef(new TestPaymentInitiator().asInstanceOf[PaymentInitiator])
|
||||
withFixture(test.toNoArgTest(FixtureParam(nodeParams, initiator, payFsm, multiPartPayFsm, sender)))
|
||||
withFixture(test.toNoArgTest(FixtureParam(nodeParams, initiator, payFsm, multiPartPayFsm, sender, eventListener)))
|
||||
}
|
||||
|
||||
test("reject payment with unknown mandatory feature") { f =>
|
||||
|
@ -81,10 +83,11 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
|
||||
test("forward payment with pre-defined route") { f =>
|
||||
import f._
|
||||
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, predefinedRoute = Seq(a, b, c)))
|
||||
val paymentId = sender.expectMsgType[UUID]
|
||||
payFsm.expectMsg(SendPaymentConfig(paymentId, paymentId, None, paymentHash, c, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true))
|
||||
payFsm.expectMsg(SendPaymentToRoute(paymentHash, Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = None)
|
||||
sender.send(initiator, SendPaymentToRouteRequest(finalAmount, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil))
|
||||
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
||||
payFsm.expectMsg(SendPaymentToRoute(Seq(a, b, c), FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1))))
|
||||
}
|
||||
|
||||
test("forward legacy payment") { f =>
|
||||
|
@ -93,13 +96,13 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
val routeParams = RouteParams(randomize = true, 15 msat, 1.5, 5, CltvExpiryDelta(561), None)
|
||||
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, c, 1, CltvExpiryDelta(42), assistedRoutes = hints, routeParams = Some(routeParams)))
|
||||
val id1 = sender.expectMsgType[UUID]
|
||||
payFsm.expectMsg(SendPaymentConfig(id1, id1, None, paymentHash, c, Upstream.Local(id1), None, storeInDb = true, publishEvent = true))
|
||||
payFsm.expectMsg(SendPayment(paymentHash, c, FinalLegacyPayload(finalAmount, CltvExpiryDelta(42).toCltvExpiry(nodeParams.currentBlockHeight + 1)), 1, hints, Some(routeParams)))
|
||||
payFsm.expectMsg(SendPaymentConfig(id1, id1, None, paymentHash, finalAmount, c, Upstream.Local(id1), None, storeInDb = true, publishEvent = true, Nil))
|
||||
payFsm.expectMsg(SendPayment(c, FinalLegacyPayload(finalAmount, CltvExpiryDelta(42).toCltvExpiry(nodeParams.currentBlockHeight + 1)), 1, hints, Some(routeParams)))
|
||||
|
||||
sender.send(initiator, SendPaymentRequest(finalAmount, paymentHash, e, 3))
|
||||
val id2 = sender.expectMsgType[UUID]
|
||||
payFsm.expectMsg(SendPaymentConfig(id2, id2, None, paymentHash, e, Upstream.Local(id2), None, storeInDb = true, publishEvent = true))
|
||||
payFsm.expectMsg(SendPayment(paymentHash, e, FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)), 3))
|
||||
payFsm.expectMsg(SendPaymentConfig(id2, id2, None, paymentHash, finalAmount, e, Upstream.Local(id2), None, storeInDb = true, publishEvent = true, Nil))
|
||||
payFsm.expectMsg(SendPayment(e, FinalLegacyPayload(finalAmount, Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)), 3))
|
||||
}
|
||||
|
||||
test("forward legacy payment when multi-part deactivated", Tag("mpp_disabled")) { f =>
|
||||
|
@ -108,8 +111,8 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
val req = SendPaymentRequest(finalAmount, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true))
|
||||
payFsm.expectMsg(SendPayment(paymentHash, c, FinalLegacyPayload(finalAmount, req.finalExpiry(nodeParams.currentBlockHeight)), 1))
|
||||
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
||||
payFsm.expectMsg(SendPayment(c, FinalLegacyPayload(finalAmount, req.finalExpiry(nodeParams.currentBlockHeight)), 1))
|
||||
}
|
||||
|
||||
test("forward multi-part payment") { f =>
|
||||
|
@ -118,19 +121,18 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr))
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true))
|
||||
multiPartPayFsm.expectMsg(SendMultiPartPayment(paymentHash, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1))
|
||||
multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
||||
multiPartPayFsm.expectMsg(SendMultiPartPayment(pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1))
|
||||
}
|
||||
|
||||
test("forward multi-part payment with pre-defined route") { f =>
|
||||
import f._
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val req = SendPaymentRequest(finalAmount / 2, paymentHash, c, 1, paymentRequest = Some(pr), predefinedRoute = Seq(a, b, c))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)))
|
||||
val req = SendPaymentToRouteRequest(finalAmount / 2, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil)
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true))
|
||||
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Nil))
|
||||
val msg = payFsm.expectMsgType[SendPaymentToRoute]
|
||||
assert(msg.paymentHash === paymentHash)
|
||||
assert(msg.hops === Seq(a, b, c))
|
||||
assert(msg.finalPayload.amount === finalAmount / 2)
|
||||
assert(msg.finalPayload.paymentSecret === pr.paymentSecret)
|
||||
|
@ -143,16 +145,15 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))))
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features), extraHops = ignoredRoutingHints)
|
||||
val trampolineFees = 21000 msat
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, trampolineFees, pr, b, CltvExpiryDelta(9), CltvExpiryDelta(12))
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9))
|
||||
sender.send(initiator, req)
|
||||
sender.expectMsgType[UUID]
|
||||
multiPartPayFsm.expectMsgType[SendPaymentConfig]
|
||||
|
||||
val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
|
||||
assert(msg.paymentHash === pr.paymentHash)
|
||||
assert(msg.paymentSecret !== pr.paymentSecret.get) // we should not leak the invoice secret to the trampoline node
|
||||
assert(msg.targetNodeId === b)
|
||||
assert(msg.finalExpiry.toLong === currentBlockCount + 9 + 12 + 1)
|
||||
assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1)
|
||||
assert(msg.totalAmount === finalAmount + trampolineFees)
|
||||
assert(msg.additionalTlvs.head.isInstanceOf[OnionTlv.TrampolineOnion])
|
||||
|
||||
|
@ -183,16 +184,15 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
import f._
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some eclair-mobile invoice")
|
||||
val trampolineFees = 21000 msat
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, trampolineFees, pr, b, CltvExpiryDelta(9), CltvExpiryDelta(12))
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9))
|
||||
sender.send(initiator, req)
|
||||
sender.expectMsgType[UUID]
|
||||
multiPartPayFsm.expectMsgType[SendPaymentConfig]
|
||||
|
||||
val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
|
||||
assert(msg.paymentHash === pr.paymentHash)
|
||||
assert(msg.paymentSecret !== pr.paymentSecret.get) // we should not leak the invoice secret to the trampoline node
|
||||
assert(msg.targetNodeId === b)
|
||||
assert(msg.finalExpiry.toLong === currentBlockCount + 9 + 12 + 1)
|
||||
assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1)
|
||||
assert(msg.totalAmount === finalAmount + trampolineFees)
|
||||
assert(msg.additionalTlvs.head.isInstanceOf[OnionTlv.TrampolineOnion])
|
||||
|
||||
|
@ -216,15 +216,103 @@ class PaymentInitiatorSpec extends TestKit(ActorSystem("test")) with fixture.Fun
|
|||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional)
|
||||
val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, "#abittooreckless", None, None, routingHints, features = Some(features))
|
||||
val trampolineFees = 21000 msat
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, trampolineFees, pr, b, CltvExpiryDelta(9), CltvExpiryDelta(12))
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9))
|
||||
sender.send(initiator, req)
|
||||
val id = sender.expectMsgType[UUID]
|
||||
val fail = sender.expectMsgType[PaymentFailed]
|
||||
assert(fail.id === id)
|
||||
assert(fail.failures.head.isInstanceOf[LocalFailure])
|
||||
assert(fail.failures === LocalFailure(TrampolineLegacyAmountLessInvoice) :: Nil)
|
||||
|
||||
multiPartPayFsm.expectNoMsg(50 millis)
|
||||
payFsm.expectNoMsg(50 millis)
|
||||
}
|
||||
|
||||
test("retry trampoline payment") { f =>
|
||||
import f._
|
||||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features))
|
||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
|
||||
sender.send(initiator, req)
|
||||
sender.expectMsgType[UUID]
|
||||
val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig]
|
||||
assert(cfg.storeInDb)
|
||||
assert(!cfg.publishEvent)
|
||||
|
||||
val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
|
||||
assert(msg1.totalAmount === finalAmount + 21000.msat)
|
||||
|
||||
// Simulate a failure which should trigger a retry.
|
||||
multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))))
|
||||
multiPartPayFsm.expectMsgType[SendPaymentConfig]
|
||||
val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
|
||||
assert(msg2.totalAmount === finalAmount + 25000.msat)
|
||||
|
||||
// Simulate success which should publish the event and respond to the original sender.
|
||||
val success = PaymentSent(cfg.parentId, pr.paymentHash, randomBytes32, finalAmount, c, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), 1000 msat, 500 msat, randomBytes32, None)))
|
||||
multiPartPayFsm.send(initiator, success)
|
||||
sender.expectMsg(success)
|
||||
eventListener.expectMsg(success)
|
||||
sender.expectNoMsg(100 millis)
|
||||
eventListener.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
test("retry trampoline payment and fail") { f =>
|
||||
import f._
|
||||
val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional)
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features))
|
||||
val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil
|
||||
val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9))
|
||||
sender.send(initiator, req)
|
||||
sender.expectMsgType[UUID]
|
||||
val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig]
|
||||
assert(cfg.storeInDb)
|
||||
assert(!cfg.publishEvent)
|
||||
|
||||
val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
|
||||
assert(msg1.totalAmount === finalAmount + 21000.msat)
|
||||
|
||||
// Simulate a failure which should trigger a retry.
|
||||
val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))
|
||||
multiPartPayFsm.send(initiator, failed)
|
||||
multiPartPayFsm.expectMsgType[SendPaymentConfig]
|
||||
val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment]
|
||||
assert(msg2.totalAmount === finalAmount + 25000.msat)
|
||||
|
||||
// Simulate a failure that exhausts payment attempts.
|
||||
multiPartPayFsm.send(initiator, failed)
|
||||
sender.expectMsg(failed)
|
||||
eventListener.expectMsg(failed)
|
||||
sender.expectNoMsg(100 millis)
|
||||
eventListener.expectNoMsg(100 millis)
|
||||
}
|
||||
|
||||
test("forward trampoline payment with pre-defined route") { f =>
|
||||
import f._
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice")
|
||||
val trampolineFees = 100 msat
|
||||
val req = SendPaymentToRouteRequest(finalAmount + trampolineFees, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b), None, trampolineFees, CltvExpiryDelta(144), Seq(b, c))
|
||||
sender.send(initiator, req)
|
||||
val payment = sender.expectMsgType[SendPaymentToRouteResponse]
|
||||
assert(payment.trampolineSecret.nonEmpty)
|
||||
payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat))))
|
||||
val msg = payFsm.expectMsgType[SendPaymentToRoute]
|
||||
assert(msg.hops === Seq(a, b))
|
||||
assert(msg.finalPayload.amount === finalAmount + trampolineFees)
|
||||
assert(msg.finalPayload.paymentSecret === payment.trampolineSecret)
|
||||
assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees)
|
||||
assert(msg.finalPayload.isInstanceOf[Onion.FinalTlvPayload])
|
||||
val trampolineOnion = msg.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionTlv.TrampolineOnion]
|
||||
assert(trampolineOnion.nonEmpty)
|
||||
|
||||
// Verify that the trampoline node can correctly peel the trampoline onion.
|
||||
val Right(decrypted) = Sphinx.TrampolinePacket.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion.get.packet)
|
||||
assert(!decrypted.isLastPacket)
|
||||
val trampolinePayload = OnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value
|
||||
assert(trampolinePayload.amountToForward === finalAmount)
|
||||
assert(trampolinePayload.totalAmount === finalAmount)
|
||||
assert(trampolinePayload.outgoingNodeId === c)
|
||||
assert(trampolinePayload.paymentSecret === pr.paymentSecret)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult,
|
|||
import fr.acinq.eclair.channel.Register.ForwardShortId
|
||||
import fr.acinq.eclair.channel.{AddHtlcFailed, Channel, ChannelUnavailable, Upstream}
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
|
||||
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
|
||||
import fr.acinq.eclair.io.Peer.PeerRoutingMessage
|
||||
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
|
||||
import fr.acinq.eclair.payment.PaymentSent.PartialPayment
|
||||
|
@ -59,6 +59,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val defaultPaymentRequest = SendPaymentRequest(defaultAmountMsat, defaultPaymentHash, d, 1, externalId = Some(defaultExternalId))
|
||||
|
||||
case class PaymentFixture(id: UUID,
|
||||
parentId: UUID,
|
||||
nodeParams: NodeParams,
|
||||
paymentFSM: TestFSMRef[PaymentLifecycle.State, PaymentLifecycle.Data, PaymentLifecycle],
|
||||
routerForwarder: TestProbe,
|
||||
|
@ -68,15 +69,15 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
eventListener: TestProbe)
|
||||
|
||||
def createPaymentLifecycle(storeInDb: Boolean = true, publishEvent: Boolean = true): PaymentFixture = {
|
||||
val id = UUID.randomUUID()
|
||||
val (id, parentId) = (UUID.randomUUID(), UUID.randomUUID())
|
||||
val nodeParams = TestConstants.Alice.nodeParams.copy(keyManager = testKeyManager)
|
||||
val cfg = SendPaymentConfig(id, id, Some(defaultExternalId), defaultPaymentHash, d, Upstream.Local(id), defaultPaymentRequest.paymentRequest, storeInDb, publishEvent)
|
||||
val cfg = SendPaymentConfig(id, parentId, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, d, Upstream.Local(id), defaultPaymentRequest.paymentRequest, storeInDb, publishEvent, Nil)
|
||||
val (routerForwarder, register, sender, monitor, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe(), TestProbe())
|
||||
val paymentFSM = TestFSMRef(new PaymentLifecycle(nodeParams, cfg, routerForwarder.ref, register.ref))
|
||||
paymentFSM ! SubscribeTransitionCallBack(monitor.ref)
|
||||
val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]])
|
||||
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
|
||||
PaymentFixture(id, nodeParams, paymentFSM, routerForwarder, register, sender, monitor, eventListener)
|
||||
PaymentFixture(id, parentId, nodeParams, paymentFSM, routerForwarder, register, sender, monitor, eventListener)
|
||||
}
|
||||
|
||||
test("send to route") { routerFixture =>
|
||||
|
@ -84,7 +85,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
|
||||
// pre-computed route going from A to D
|
||||
val request = SendPaymentToRoute(defaultPaymentHash, Seq(a, b, c, d), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
|
||||
val request = SendPaymentToRoute(Seq(a, b, c, d), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
|
||||
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(FinalizeRoute(Seq(a, b, c, d)))
|
||||
|
@ -94,10 +95,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending))
|
||||
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
|
||||
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, id, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
|
||||
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
|
||||
sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash))
|
||||
|
||||
sender.expectMsgType[PaymentSent]
|
||||
val ps = sender.expectMsgType[PaymentSent]
|
||||
assert(ps.id === parentId)
|
||||
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]))
|
||||
}
|
||||
|
||||
|
@ -105,7 +107,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val brokenRoute = SendPaymentToRoute(randomBytes32, Seq(randomKey.publicKey, randomKey.publicKey, randomKey.publicKey), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
|
||||
val brokenRoute = SendPaymentToRoute(Seq(randomKey.publicKey, randomKey.publicKey, randomKey.publicKey), FinalLegacyPayload(defaultAmountMsat, defaultExpiry))
|
||||
sender.send(paymentFSM, brokenRoute)
|
||||
routerForwarder.expectMsgType[FinalizeRoute]
|
||||
routerForwarder.forward(routerFixture.router)
|
||||
|
@ -118,7 +120,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b)))
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -132,7 +134,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, c, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
|
||||
val request = SendPayment(c, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectNoMsg(50 millis) // we don't need the router when we already have the whole route
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -144,7 +146,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3, routePrefix = Seq(ChannelHop(a, b, channelUpdate_ab), ChannelHop(b, c, channelUpdate_bc)))
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(RouteRequest(c, d, defaultAmountMsat, ignoreNodes = Set(a, b)))
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -163,7 +165,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, f, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
val request = SendPayment(f, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
sender.send(paymentFSM, request)
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
||||
|
@ -178,7 +180,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, routeParams = Some(RouteParams(randomize = false, maxFeeBase = 100 msat, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = CltvExpiryDelta(2016), ratios = None)))
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, routeParams = Some(RouteParams(randomize = false, maxFeeBase = 100 msat, maxFeePct = 0.0, routeMaxLength = 20, routeMaxCltv = CltvExpiryDelta(2016), ratios = None)))
|
||||
sender.send(paymentFSM, request)
|
||||
val routeRequest = routerForwarder.expectMsgType[RouteRequest]
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -192,7 +194,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty))
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
@ -225,7 +227,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -236,7 +238,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val WaitingForComplete(_, _, cmd1, Nil, _, _, _, _) = paymentFSM.stateData
|
||||
|
||||
register.expectMsg(ForwardShortId(channelId_ab, cmd1))
|
||||
sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, request.paymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None)))
|
||||
sender.send(paymentFSM, Status.Failure(AddHtlcFailed(ByteVector32.Zeroes, defaultPaymentHash, ChannelUnavailable(ByteVector32.Zeroes), Local(id, Some(paymentFSM.underlying.self)), None, None)))
|
||||
|
||||
// then the payment lifecycle will ask for a new route excluding the channel
|
||||
routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b))))
|
||||
|
@ -247,7 +249,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -269,7 +271,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE)
|
||||
val WaitingForRoute(_, _, Nil) = paymentFSM.stateData
|
||||
|
@ -298,7 +300,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -356,7 +358,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
ExtraHop(c, channelId_cd, channelUpdate_cd.feeBaseMsat, channelUpdate_cd.feeProportionalMillionths, channelUpdate_cd.cltvExpiryDelta)
|
||||
))
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, assistedRoutes = assistedRoutes)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5, assistedRoutes = assistedRoutes)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -394,7 +396,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2)
|
||||
sender.send(paymentFSM, request)
|
||||
awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
|
||||
|
@ -431,7 +433,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle()
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
@ -439,14 +441,16 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending))
|
||||
val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id)
|
||||
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, id, Some(defaultExternalId), defaultPaymentHash, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
|
||||
assert(outgoing.copy(createdAt = 0) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0, None, OutgoingPaymentStatus.Pending))
|
||||
sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage))
|
||||
|
||||
val ps = eventListener.expectMsgType[PaymentSent]
|
||||
assert(ps.id === parentId)
|
||||
assert(ps.feesPaid > 0.msat)
|
||||
assert(ps.amount === defaultAmountMsat)
|
||||
assert(ps.recipientAmount === defaultAmountMsat)
|
||||
assert(ps.paymentHash === defaultPaymentHash)
|
||||
assert(ps.paymentPreimage === defaultPaymentPreimage)
|
||||
assert(ps.parts.head.id === id)
|
||||
awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]))
|
||||
}
|
||||
|
||||
|
@ -478,7 +482,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
import payFixture._
|
||||
|
||||
// we send a payment to G
|
||||
val request = SendPayment(defaultPaymentHash, g, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
val request = SendPayment(g, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 5)
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
|
||||
|
@ -489,13 +493,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
|
||||
sender.send(paymentFSM, UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash))
|
||||
val paymentOK = sender.expectMsgType[PaymentSent]
|
||||
val PaymentSent(_, request.paymentHash, paymentOK.paymentPreimage, PartialPayment(_, request.finalPayload.amount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent]
|
||||
val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, request.finalPayload.amount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent]
|
||||
assert(finalAmount === defaultAmountMsat)
|
||||
|
||||
// during the route computation the fees were treated as if they were 1msat but when sending the onion we actually put zero
|
||||
// NB: A -> B doesn't pay fees because it's our direct neighbor
|
||||
// NB: B -> G doesn't asks for fees at all
|
||||
assert(fee === 0.msat)
|
||||
assert(paymentOK.amount === request.finalPayload.amount)
|
||||
assert(paymentOK.recipientAmount === request.finalPayload.amount)
|
||||
}
|
||||
|
||||
test("filter errors properly") { _ =>
|
||||
|
@ -508,7 +513,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec {
|
|||
val payFixture = createPaymentLifecycle(storeInDb = false, publishEvent = false)
|
||||
import payFixture._
|
||||
|
||||
val request = SendPayment(defaultPaymentHash, d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3)
|
||||
val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 3)
|
||||
sender.send(paymentFSM, request)
|
||||
routerForwarder.expectMsgType[RouteRequest]
|
||||
val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]])
|
||||
|
|
|
@ -22,7 +22,7 @@ import akka.actor.ActorRef
|
|||
import akka.testkit.TestProbe
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus}
|
||||
import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType}
|
||||
import fr.acinq.eclair.payment.OutgoingPacket.buildCommand
|
||||
import fr.acinq.eclair.payment.PaymentPacketSpec._
|
||||
import fr.acinq.eclair.payment.relay.Relayer.{ForwardFail, ForwardFulfill}
|
||||
|
@ -257,7 +257,8 @@ class PostRestartHtlcCleanerSpec extends TestkitBaseClass {
|
|||
assert(e1.paymentPreimage === preimage2)
|
||||
assert(e1.paymentHash === paymentHash2)
|
||||
assert(e1.parts.length === 2)
|
||||
assert(e1.amount === 2834.msat)
|
||||
assert(e1.amountWithFees === 2834.msat)
|
||||
assert(e1.recipientAmount === 2500.msat)
|
||||
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending)
|
||||
|
@ -268,7 +269,7 @@ class PostRestartHtlcCleanerSpec extends TestkitBaseClass {
|
|||
assert(e2.paymentPreimage === preimage1)
|
||||
assert(e2.paymentHash === paymentHash1)
|
||||
assert(e2.parts.length === 1)
|
||||
assert(e2.amount === 561.msat)
|
||||
assert(e2.recipientAmount === 561.msat)
|
||||
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
|
||||
|
||||
commandBuffer.expectNoMsg(100 millis)
|
||||
|
@ -402,9 +403,9 @@ object PostRestartHtlcCleanerSpec {
|
|||
val origin3 = Origin.Local(id3, None)
|
||||
|
||||
// Prepare channels and payment state before restart.
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, id1, None, paymentHash1, add1.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash2, add2.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, parentId, None, paymentHash2, add3.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id1, id1, None, paymentHash1, PaymentType.Standard, add1.amountMsat, add1.amountMsat, c, 0, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id2, parentId, None, paymentHash2, PaymentType.Standard, add2.amountMsat, 2500 msat, c, 0, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.payments.addOutgoingPayment(OutgoingPayment(id3, parentId, None, paymentHash2, PaymentType.Standard, add3.amountMsat, 2500 msat, c, 0, None, OutgoingPaymentStatus.Pending))
|
||||
nodeParams.db.channels.addOrUpdateChannel(ChannelCodecsSpec.makeChannelDataNormal(
|
||||
Seq(add1, add2, add3).map(add => DirectedHtlc(OUT, add)),
|
||||
Map(add1.id -> origin1, add2.id -> origin2, add3.id -> origin3))
|
||||
|
|
|
@ -554,12 +554,12 @@ class RelayerSpec extends TestkitBaseClass {
|
|||
assert(channels1.last.channelUpdate === channelUpdate_bc)
|
||||
assert(channels1.last.toUsableBalance === UsableBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false))
|
||||
|
||||
relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, 0 msat, makeCommitments(channelId_bc, 200000 msat, 500000 msat))
|
||||
relayer ! AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat))
|
||||
sender.send(relayer, GetOutgoingChannels())
|
||||
val OutgoingChannels(channels2) = sender.expectMsgType[OutgoingChannels]
|
||||
assert(channels2.last.commitments.availableBalanceForReceive === 500000.msat && channels2.last.commitments.availableBalanceForSend === 200000.msat)
|
||||
|
||||
relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, 0 msat, makeCommitments(channelId_ab, 100000 msat, 200000 msat))
|
||||
relayer ! AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, makeCommitments(channelId_ab, 100000 msat, 200000 msat))
|
||||
relayer ! LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c)
|
||||
sender.send(relayer, GetOutgoingChannels())
|
||||
val OutgoingChannels(channels3) = sender.expectMsgType[OutgoingChannels]
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
|
||||
package fr.acinq.eclair.router
|
||||
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||
import fr.acinq.eclair.router.Router.ShortChannelIdsChunk
|
||||
import fr.acinq.eclair.wire.QueryChannelRangeTlv.QueryFlags
|
||||
import fr.acinq.eclair.wire.{EncodedShortChannelIds, EncodingType, QueryChannelRange, QueryChannelRangeTlv, ReplyChannelRange}
|
||||
import fr.acinq.eclair.wire.ReplyChannelRangeTlv._
|
||||
import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId, randomKey}
|
||||
import org.scalatest.FunSuite
|
||||
|
@ -152,13 +154,14 @@ class ChannelRangeQueriesSpec extends FunSuite {
|
|||
require(chunk.shortChannelIds.forall(Router.keep(chunk.firstBlock, chunk.numBlocks, _)))
|
||||
}
|
||||
|
||||
// check that chunks do not overlap and contain exactly the ids they were built from
|
||||
// check that chunks contain exactly the ids they were built from are are consistent i.e each chunk covers a range that immediately follows
|
||||
// the previous one even if there are gaps in block heights
|
||||
def validate(ids: SortedSet[ShortChannelId], firstBlockNum: Long, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = {
|
||||
|
||||
@tailrec
|
||||
def noOverlap(chunks: List[ShortChannelIdsChunk]): Boolean = chunks match {
|
||||
case Nil => true
|
||||
case a :: b :: _ if b.firstBlock < a.firstBlock + a.numBlocks => false
|
||||
case a :: b :: _ if b.firstBlock != a.firstBlock + a.numBlocks => false
|
||||
case _ => noOverlap(chunks.tail)
|
||||
}
|
||||
|
||||
|
@ -240,27 +243,27 @@ class ChannelRangeQueriesSpec extends FunSuite {
|
|||
|
||||
// all ids in different blocks, chunk size == 2
|
||||
{
|
||||
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
|
||||
val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050))
|
||||
val firstBlockNum = 900
|
||||
val numberOfBlocks = 200
|
||||
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
|
||||
assert(chunks == List(
|
||||
ShortChannelIdsChunk(firstBlockNum, 100 + 2, List(ids(0), ids(1))),
|
||||
ShortChannelIdsChunk(1002, 2, List(ids(2), ids(3))),
|
||||
ShortChannelIdsChunk(1004, numberOfBlocks - 1004 + firstBlockNum, List(ids(4), ids(5)))
|
||||
ShortChannelIdsChunk(firstBlockNum, 100 + 6, List(ids(0), ids(1))),
|
||||
ShortChannelIdsChunk(1006, 8, List(ids(2), ids(3))),
|
||||
ShortChannelIdsChunk(1014, numberOfBlocks - 1014 + firstBlockNum, List(ids(4), ids(5)))
|
||||
))
|
||||
}
|
||||
|
||||
// all ids in different blocks, chunk size == 2, first id outside of range
|
||||
{
|
||||
val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005))
|
||||
val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050))
|
||||
val firstBlockNum = 1001
|
||||
val numberOfBlocks = 200
|
||||
val chunks = Router.split(SortedSet.empty[ShortChannelId] ++ ids, firstBlockNum, numberOfBlocks, 2)
|
||||
assert(chunks == List(
|
||||
ShortChannelIdsChunk(firstBlockNum, 2, List(ids(1), ids(2))),
|
||||
ShortChannelIdsChunk(1003, 2, List(ids(3), ids(4))),
|
||||
ShortChannelIdsChunk(1005, numberOfBlocks - 1005 + firstBlockNum, List(ids(5)))
|
||||
ShortChannelIdsChunk(firstBlockNum, 12, List(ids(1), ids(2))),
|
||||
ShortChannelIdsChunk(1013, 1040 - 1013 + 1, List(ids(3), ids(4))),
|
||||
ShortChannelIdsChunk(1041, numberOfBlocks - 1041 + firstBlockNum, List(ids(5)))
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -312,7 +315,7 @@ class ChannelRangeQueriesSpec extends FunSuite {
|
|||
}
|
||||
|
||||
test("split short channel ids correctly (comprehensive tests)") {
|
||||
val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(42, 100) ++ makeShortChannelIds(43, 70) ++ makeShortChannelIds(44, 50) ++ makeShortChannelIds(45, 30) ++ makeShortChannelIds(50, 120)
|
||||
val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(42, 100) ++ makeShortChannelIds(43, 70) ++ makeShortChannelIds(45, 50) ++ makeShortChannelIds(47, 30) ++ makeShortChannelIds(50, 120)
|
||||
for (firstBlockNum <- 0 to 60) {
|
||||
for (numberOfBlocks <- 1 to 60) {
|
||||
for (chunkSize <- 1 :: 2 :: 20 :: 50 :: 100 :: 1000 :: Nil) {
|
||||
|
@ -356,4 +359,23 @@ class ChannelRangeQueriesSpec extends FunSuite {
|
|||
validateChunks(chunks.toList, pruned)
|
||||
}
|
||||
}
|
||||
|
||||
test("do not encode empty lists as COMPRESSED_ZLIB") {
|
||||
{
|
||||
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_ALL)), SortedMap())
|
||||
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, Nil)), Some(EncodedChecksums(Nil))))
|
||||
}
|
||||
{
|
||||
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_TIMESTAMPS)), SortedMap())
|
||||
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, Nil)), None))
|
||||
}
|
||||
{
|
||||
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_CHECKSUMS)), SortedMap())
|
||||
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), None, Some(EncodedChecksums(Nil))))
|
||||
}
|
||||
{
|
||||
val reply = Router.buildReplyChannelRange(ShortChannelIdsChunk(0, 42, Nil), Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, None, SortedMap())
|
||||
assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, 0L, 42L, 1.toByte, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), None, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ class FailureMessageCodecsSpec extends FunSuite {
|
|||
InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) ::
|
||||
TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer ::
|
||||
AmountBelowMinimum(123456 msat, channelUpdate) :: FeeInsufficient(546463 msat, channelUpdate) :: IncorrectCltvExpiry(CltvExpiry(1211), channelUpdate) :: ExpiryTooSoon(channelUpdate) ::
|
||||
IncorrectOrUnknownPaymentDetails(123456 msat, 1105) :: FinalIncorrectCltvExpiry(CltvExpiry(1234)) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: InvalidOnionPayload(UInt64(561), 1105) :: PaymentTimeout :: Nil
|
||||
IncorrectOrUnknownPaymentDetails(123456 msat, 1105) :: FinalIncorrectCltvExpiry(CltvExpiry(1234)) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: InvalidOnionPayload(UInt64(561), 1105) :: PaymentTimeout ::
|
||||
TrampolineFeeInsufficient :: TrampolineExpiryTooSoon :: Nil
|
||||
|
||||
msgs.foreach {
|
||||
msg => {
|
||||
|
|
|
@ -130,21 +130,37 @@ class LightningMessageCodecsSpec extends FunSuite {
|
|||
}
|
||||
|
||||
test("encode/decode init message") {
|
||||
case class TestCase(encoded: ByteVector, features: ByteVector, networks: List[ByteVector32], valid: Boolean, reEncoded: Option[ByteVector] = None)
|
||||
val chainHash1 = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101")
|
||||
val chainHash2 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202")
|
||||
val testCases = Seq(
|
||||
(hex"0000 0000", hex"", hex"0000 0000"), // no features
|
||||
(hex"0000 0002088a", hex"088a", hex"0000 0002088a"), // no global features
|
||||
(hex"00020200 0000", hex"0200", hex"0000 00020200"), // no local features
|
||||
(hex"00020200 0002088a", hex"0a8a", hex"0000 00020a8a"), // local and global - no conflict - same size
|
||||
(hex"00020200 0003020002", hex"020202", hex"0000 0003020202"), // local and global - no conflict - different sizes
|
||||
(hex"00020a02 0002088a", hex"0a8a", hex"0000 00020a8a"), // local and global - conflict - same size
|
||||
(hex"00022200 000302aaa2", hex"02aaa2", hex"0000 000302aaa2") // local and global - conflict - different sizes
|
||||
TestCase(hex"0000 0000", hex"", Nil, valid = true), // no features
|
||||
TestCase(hex"0000 0002088a", hex"088a", Nil, valid = true), // no global features
|
||||
TestCase(hex"00020200 0000", hex"0200", Nil, valid = true, Some(hex"0000 00020200")), // no local features
|
||||
TestCase(hex"00020200 0002088a", hex"0a8a", Nil, valid = true, Some(hex"0000 00020a8a")), // local and global - no conflict - same size
|
||||
TestCase(hex"00020200 0003020002", hex"020202", Nil, valid = true, Some(hex"0000 0003020202")), // local and global - no conflict - different sizes
|
||||
TestCase(hex"00020a02 0002088a", hex"0a8a", Nil, valid = true, Some(hex"0000 00020a8a")), // local and global - conflict - same size
|
||||
TestCase(hex"00022200 000302aaa2", hex"02aaa2", Nil, valid = true, Some(hex"0000 000302aaa2")), // local and global - conflict - different sizes
|
||||
TestCase(hex"0000 0002088a 03012a05022aa2", hex"088a", Nil, valid = true), // unknown odd records
|
||||
TestCase(hex"0000 0002088a 03012a04022aa2", hex"088a", Nil, valid = false), // unknown even records
|
||||
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101", hex"088a", Nil, valid = false), // invalid tlv stream
|
||||
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101", hex"088a", List(chainHash1), valid = true), // single network
|
||||
TestCase(hex"0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202", hex"088a", List(chainHash1, chainHash2), valid = true), // multiple networks
|
||||
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101010103012a", hex"088a", List(chainHash1), valid = true), // network and unknown odd records
|
||||
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101010102012a", hex"088a", Nil, valid = false) // network and unknown even records
|
||||
)
|
||||
|
||||
for ((bin, features, encoded) <- testCases) {
|
||||
val init = initCodec.decode(bin.bits).require.value
|
||||
assert(init.features === features)
|
||||
assert(initCodec.encode(init).require.bytes === encoded)
|
||||
assert(initCodec.decode(encoded.bits).require.value === init)
|
||||
for (testCase <- testCases) {
|
||||
if (testCase.valid) {
|
||||
val init = initCodec.decode(testCase.encoded.bits).require.value
|
||||
assert(init.features === testCase.features)
|
||||
assert(init.networks === testCase.networks)
|
||||
val encoded = initCodec.encode(init).require
|
||||
assert(encoded.bytes === testCase.reEncoded.getOrElse(testCase.encoded))
|
||||
assert(initCodec.decode(encoded).require.value === init)
|
||||
} else {
|
||||
assert(initCodec.decode(testCase.encoded.bits).isFailure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,6 +173,75 @@ class LightningMessageCodecsSpec extends FunSuite {
|
|||
assert(bin === bin2)
|
||||
}
|
||||
|
||||
test("encode/decode open_channel") {
|
||||
val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte)
|
||||
// Default encoding that completely omits the upfront_shutdown_script and trailing tlv stream.
|
||||
// To allow extending all messages with TLV streams, the upfront_shutdown_script was made mandatory in https://github.com/lightningnetwork/lightning-rfc/pull/714
|
||||
val defaultEncoded = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000100000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00"
|
||||
case class TestCase(encoded: ByteVector, decoded: OpenChannel, reEncoded: Option[ByteVector] = None)
|
||||
val testCases = Seq(
|
||||
// legacy encoding without upfront_shutdown_script
|
||||
TestCase(defaultEncoded, defaultOpen, Some(defaultEncoded ++ hex"0000")),
|
||||
// empty upfront_shutdown_script
|
||||
TestCase(defaultEncoded ++ hex"0000", defaultOpen),
|
||||
// non-empty upfront_shutdown_script
|
||||
TestCase(defaultEncoded ++ hex"0004 01abcdef", defaultOpen.copy(upfrontShutdownScript = Some(hex"01abcdef"))),
|
||||
// missing upfront_shutdown_script + unknown odd tlv records
|
||||
TestCase(defaultEncoded ++ hex"0302002a 050102", defaultOpen.copy(tlvStream_opt = Some(TlvStream(Nil, Seq(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02")))))),
|
||||
// empty upfront_shutdown_script + unknown odd tlv records: we don't encode the upfront_shutdown_script when a tlv stream is provided
|
||||
TestCase(defaultEncoded ++ hex"0000 0302002a 050102", defaultOpen.copy(tlvStream_opt = Some(TlvStream(Nil, Seq(GenericTlv(UInt64(3), hex"002a"), GenericTlv(UInt64(5), hex"02"))))), Some(defaultEncoded ++ hex"0302002a 050102")),
|
||||
// non-empty upfront_shutdown_script + unknown odd tlv records: we don't encode the upfront_shutdown_script when a tlv stream is provided
|
||||
TestCase(defaultEncoded ++ hex"0002 1234 0303010203", defaultOpen.copy(upfrontShutdownScript = Some(hex"1234"), tlvStream_opt = Some(TlvStream(Nil, Seq(GenericTlv(UInt64(3), hex"010203"))))), Some(defaultEncoded ++ hex"0303010203"))
|
||||
)
|
||||
|
||||
for (testCase <- testCases) {
|
||||
val decoded = openChannelCodec.decode(testCase.encoded.bits).require.value
|
||||
assert(decoded === testCase.decoded)
|
||||
val reEncoded = openChannelCodec.encode(decoded).require.bytes
|
||||
assert(reEncoded === testCase.reEncoded.getOrElse(testCase.encoded))
|
||||
}
|
||||
}
|
||||
|
||||
test("decode invalid open_channel") {
|
||||
val defaultEncoded = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000100000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00"
|
||||
val testCases = Seq(
|
||||
defaultEncoded ++ hex"00", // truncated length
|
||||
defaultEncoded ++ hex"01", // truncated length
|
||||
defaultEncoded ++ hex"0004 123456", // truncated script
|
||||
defaultEncoded ++ hex"0000 02012a", // invalid tlv stream (unknown even record)
|
||||
defaultEncoded ++ hex"0000 01012a 030201", // invalid tlv stream (truncated)
|
||||
defaultEncoded ++ hex"02012a", // invalid tlv stream (unknown even record)
|
||||
defaultEncoded ++ hex"01012a 030201" // invalid tlv stream (truncated)
|
||||
)
|
||||
|
||||
for (testCase <- testCases) {
|
||||
assert(openChannelCodec.decode(testCase.bits).isFailure, testCase.toHex)
|
||||
}
|
||||
}
|
||||
|
||||
test("encode/decode accept_channel") {
|
||||
val defaultAccept = AcceptChannel(ByteVector32.Zeroes, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6))
|
||||
// Default encoding that completely omits the upfront_shutdown_script (nodes were supposed to encode it only if both
|
||||
// sides advertised support for option_upfront_shutdown_script).
|
||||
// To allow extending all messages with TLV streams, the upfront_shutdown_script was made mandatory in https://github.com/lightningnetwork/lightning-rfc/pull/714
|
||||
val defaultEncoded = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000001000000000000000100000000000000010000000100010001031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a"
|
||||
case class TestCase(encoded: ByteVector, decoded: AcceptChannel, reEncoded: Option[ByteVector] = None)
|
||||
val testCases = Seq(
|
||||
TestCase(defaultEncoded, defaultAccept, Some(defaultEncoded ++ hex"0000")), // legacy encoding without upfront_shutdown_script
|
||||
TestCase(defaultEncoded ++ hex"0000", defaultAccept), // empty upfront_shutdown_script
|
||||
TestCase(defaultEncoded ++ hex"0004 01abcdef", defaultAccept.copy(upfrontShutdownScript = Some(hex"01abcdef"))), // non-empty upfront_shutdown_script
|
||||
TestCase(defaultEncoded ++ hex"0000 010202a 030102", defaultAccept, Some(defaultEncoded ++ hex"0000")), // empty upfront_shutdown_script + unknown odd tlv records
|
||||
TestCase(defaultEncoded ++ hex"0002 1234 0303010203", defaultAccept.copy(upfrontShutdownScript = Some(hex"1234")), Some(defaultEncoded ++ hex"0002 1234")) // non-empty upfront_shutdown_script + unknown odd tlv records
|
||||
)
|
||||
|
||||
for (testCase <- testCases) {
|
||||
val decoded = acceptChannelCodec.decode(testCase.encoded.bits).require.value
|
||||
assert(decoded === testCase.decoded)
|
||||
val reEncoded = acceptChannelCodec.encode(decoded).require.bytes
|
||||
assert(reEncoded === testCase.reEncoded.getOrElse(testCase.encoded))
|
||||
}
|
||||
}
|
||||
|
||||
test("encode/decode all channel messages") {
|
||||
val open = OpenChannel(randomBytes32, randomBytes32, 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, 9, CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte)
|
||||
val accept = AcceptChannel(randomBytes32, 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6))
|
||||
|
|
|
@ -165,13 +165,6 @@ trait Service extends ExtraDirectives with Logging {
|
|||
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
|
||||
}
|
||||
} ~
|
||||
// TODO: @t-bast: remove this API once stabilized: should re-work the payment APIs to integrate Trampoline nicely
|
||||
path("sendtotrampoline") {
|
||||
formFields(invoiceFormParam_opt, "trampolineId".as[Option[PublicKey]](publicKeyUnmarshaller), "trampolineFeesMsat".as[Option[MilliSatoshi]](millisatoshiUnmarshaller), "trampolineExpiryDelta".as[Int]) {
|
||||
(invoice, trampolineId, trampolineFees, trampolineExpiryDelta) =>
|
||||
complete(eclairApi.sendToTrampoline(invoice.get, trampolineId.get, trampolineFees.get, CltvExpiryDelta(trampolineExpiryDelta)))
|
||||
}
|
||||
} ~
|
||||
path("sendtonode") {
|
||||
formFields(amountMsatFormParam_opt, paymentHashFormParam_opt, nodeIdFormParam_opt, "maxAttempts".as[Int].?, "feeThresholdSat".as[Option[Satoshi]](satoshiUnmarshaller), "maxFeePct".as[Double].?, "externalId".?) {
|
||||
(amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt, externalId_opt) =>
|
||||
|
@ -179,9 +172,9 @@ trait Service extends ExtraDirectives with Logging {
|
|||
}
|
||||
} ~
|
||||
path("sendtoroute") {
|
||||
formFields(amountMsatFormParam_opt, paymentHashFormParam_opt, "finalCltvExpiry".as[Int], "route".as[Option[List[PublicKey]]](pubkeyListUnmarshaller), "externalId".?) {
|
||||
(amountMsat, paymentHash, finalCltvExpiry, route, externalId_opt) =>
|
||||
complete(eclairApi.sendToRoute(externalId_opt, route.get, amountMsat.get, paymentHash.get, CltvExpiryDelta(finalCltvExpiry)))
|
||||
formFields(amountMsatFormParam_opt, "recipientAmountMsat".as[Option[MilliSatoshi]](millisatoshiUnmarshaller), invoiceFormParam_opt, "finalCltvExpiry".as[Int], "route".as[Option[List[PublicKey]]](pubkeyListUnmarshaller), "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[Option[ByteVector32]](sha256HashUnmarshaller), "trampolineFeesMsat".as[Option[MilliSatoshi]](millisatoshiUnmarshaller), "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[Option[List[PublicKey]]](pubkeyListUnmarshaller)) {
|
||||
(amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) =>
|
||||
complete(eclairApi.sendToRoute(amountMsat.get, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice.get, CltvExpiryDelta(finalCltvExpiry), route.get, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil)))
|
||||
}
|
||||
} ~
|
||||
path("getsentinfo") {
|
||||
|
@ -192,9 +185,8 @@ trait Service extends ExtraDirectives with Logging {
|
|||
}
|
||||
} ~
|
||||
path("createinvoice") {
|
||||
formFields("description".as[String], amountMsatFormParam_opt, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[Option[ByteVector32]](sha256HashUnmarshaller)) {
|
||||
(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) =>
|
||||
complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt))
|
||||
formFields("description".as[String], amountMsatFormParam_opt, "expireIn".as[Long].?, "fallbackAddress".as[String].?, "paymentPreimage".as[Option[ByteVector32]](sha256HashUnmarshaller)) { (desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt) =>
|
||||
complete(eclairApi.receive(desc, amountMsat, expire, fallBackAddress, paymentPreimage_opt))
|
||||
}
|
||||
} ~
|
||||
path("getinvoice") {
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"expired"}}
|
||||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}}
|
|
@ -1 +1 @@
|
|||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"pending"}}
|
||||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}}
|
|
@ -1 +1 @@
|
|||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}}
|
||||
{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}}
|
|
@ -1 +1 @@
|
|||
[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"failed","failures":[],"completedAt":2}}]
|
||||
[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"failed","failures":[],"completedAt":2}}]
|
|
@ -1 +1 @@
|
|||
[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"pending"}}]
|
||||
[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"pending"}}]
|
|
@ -1 +1 @@
|
|||
[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","amount":42,"targetNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":3}}]
|
||||
[{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":1,"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":3}}]
|
|
@ -19,13 +19,15 @@ package fr.acinq.eclair.api
|
|||
import java.util.UUID
|
||||
|
||||
import akka.util.Timeout
|
||||
import fr.acinq.bitcoin.ByteVector32
|
||||
import fr.acinq.bitcoin.{Block, ByteVector32}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi}
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.db._
|
||||
import fr.acinq.eclair.io.NodeURI
|
||||
import fr.acinq.eclair.io.Peer.PeerInfo
|
||||
import fr.acinq.eclair.payment.relay.Relayer.UsableBalance
|
||||
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse
|
||||
import fr.acinq.eclair.payment.{PaymentFailed, _}
|
||||
import fr.acinq.eclair.wire.NodeAddress
|
||||
import org.mockito.scalatest.IdiomaticMockito
|
||||
|
@ -313,38 +315,38 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest with RouteTest wit
|
|||
}
|
||||
|
||||
test("'sendtoroute' method should accept a both a json-encoded AND comma separaterd list of pubkeys") {
|
||||
val rawUUID = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f"
|
||||
val paymentUUID = UUID.fromString(rawUUID)
|
||||
val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None)
|
||||
val externalId = UUID.randomUUID().toString
|
||||
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey, "Some invoice")
|
||||
val expectedRoute = List(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))
|
||||
val csvNodes = "0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9, 0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3, 026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"
|
||||
val jsonNodes = serialization.write(expectedRoute)
|
||||
val mockEclair = mock[Eclair]
|
||||
val service = new MockService(mockEclair)
|
||||
|
||||
mockEclair.sendToRoute(any[Option[String]], any[List[PublicKey]], any[MilliSatoshi], any[ByteVector32], any[CltvExpiryDelta], any[Option[PaymentRequest]])(any[Timeout]) returns Future.successful(paymentUUID)
|
||||
mockEclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[List[PublicKey]], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment)
|
||||
|
||||
Post("/sendtoroute", FormData(Map("route" -> jsonNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.Zeroes.toHex, "finalCltvExpiry" -> "190", "externalId" -> externalId.toString))) ~>
|
||||
Post("/sendtoroute", FormData(Map("route" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId.toString, "invoice" -> PaymentRequest.write(pr)))) ~>
|
||||
addCredentials(BasicHttpCredentials("", mockPassword)) ~>
|
||||
addHeader("Content-Type", "application/json") ~>
|
||||
HttpService.sealRoute(service.route) ~>
|
||||
check {
|
||||
assert(handled)
|
||||
assert(status == OK)
|
||||
assert(responseAs[String] == "\"" + rawUUID + "\"")
|
||||
mockEclair.sendToRoute(Some(externalId), expectedRoute, 1234 msat, ByteVector32.Zeroes, CltvExpiryDelta(190), any[Option[PaymentRequest]])(any[Timeout]).wasCalled(once)
|
||||
assert(responseAs[String] == "\"" + payment.paymentId + "\"")
|
||||
mockEclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once)
|
||||
}
|
||||
|
||||
// this test uses CSV encoded route
|
||||
Post("/sendtoroute", FormData(Map("route" -> csvNodes, "amountMsat" -> "1234", "paymentHash" -> ByteVector32.One.toHex, "finalCltvExpiry" -> "190"))) ~>
|
||||
Post("/sendtoroute", FormData(Map("route" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> PaymentRequest.write(pr)))) ~>
|
||||
addCredentials(BasicHttpCredentials("", mockPassword)) ~>
|
||||
addHeader("Content-Type", "application/json") ~>
|
||||
HttpService.sealRoute(service.route) ~>
|
||||
check {
|
||||
assert(handled)
|
||||
assert(status == OK)
|
||||
assert(responseAs[String] == "\"" + rawUUID + "\"")
|
||||
mockEclair.sendToRoute(None, expectedRoute, 1234 msat, ByteVector32.One, CltvExpiryDelta(190), any[Option[PaymentRequest]])(any[Timeout]).wasCalled(once)
|
||||
assert(responseAs[String] == "\"" + payment.paymentId + "\"")
|
||||
mockEclair.sendToRoute(1234 msat, None, None, None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -59,6 +59,7 @@
|
|||
</developers>
|
||||
|
||||
<properties>
|
||||
<project.build.outputTimestamp>2020-01-01T00:00:00Z</project.build.outputTimestamp>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.7</maven.compiler.source>
|
||||
<maven.compiler.target>1.7</maven.compiler.target>
|
||||
|
@ -83,7 +84,7 @@
|
|||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>3.2.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.chrisdchristo</groupId>
|
||||
|
@ -161,7 +162,7 @@
|
|||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.0.1</version>
|
||||
<version>3.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
|
|
Loading…
Add table
Reference in a new issue