mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-14 03:48:13 +01:00
Merge branch 'master' into android
This commit is contained in:
commit
74c4706a08
64 changed files with 2386 additions and 983 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, Shutdown, 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.Placeholder] = 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,7 +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 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
|
||||
|
|
|
@ -138,12 +138,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 =>
|
||||
|
|
|
@ -22,7 +22,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Stash, Terminated}
|
|||
import fr.acinq.bitcoin.{BlockHeader, ByteVector32, Script, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.computeScriptHash
|
||||
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_PARENT_TX_CONFIRMED}
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.{LongToBtcAmount, ShortChannelId, TxCoordinates}
|
||||
|
||||
|
@ -97,7 +97,7 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi
|
|||
case watch@WatchConfirmed(_, txid, publicKeyScript, _, _) =>
|
||||
val scriptHash = computeScriptHash(publicKeyScript)
|
||||
log.info(s"added watch-confirmed on txid=$txid scriptHash=$scriptHash")
|
||||
client ! ElectrumClient.GetScriptHashHistory(scriptHash)
|
||||
client ! ElectrumClient.ScriptHashSubscription(scriptHash, self)
|
||||
context.watch(watch.channel)
|
||||
context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent)
|
||||
|
||||
|
@ -117,7 +117,8 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi
|
|||
|
||||
case ElectrumClient.GetScriptHashHistoryResponse(_, history) =>
|
||||
// we retrieve the transaction before checking watches
|
||||
history.filter(_.height >= 0).foreach { item => client ! ElectrumClient.GetTransaction(item.tx_hash, Some(item)) }
|
||||
// NB: height=-1 means that the tx is unconfirmed and at least one of its inputs is also unconfirmed. we need to take them into consideration if we want to handle unconfirmed txes (which is the case for turbo channels)
|
||||
history.filter(_.height >= -1).foreach { item => client ! ElectrumClient.GetTransaction(item.tx_hash, Some(item)) }
|
||||
|
||||
case ElectrumClient.GetTransactionResponse(tx, Some(item: ElectrumClient.TransactionHistoryItem)) =>
|
||||
// this is for WatchSpent/WatchSpendBasic
|
||||
|
@ -133,21 +134,27 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi
|
|||
channel ! WatchEventSpentBasic(event)
|
||||
Some(w)
|
||||
}).flatten
|
||||
|
||||
// this is for WatchConfirmed
|
||||
// don't ask for merkle proof for unconfirmed transactions
|
||||
if (item.height > 0) {
|
||||
watches.collect {
|
||||
case WatchConfirmed(_, txid, _, minDepth, _) if txid == tx.txid =>
|
||||
val txheight = item.height
|
||||
val confirmations = height - txheight + 1
|
||||
log.info(s"txid=$txid was confirmed at height=$txheight and now has confirmations=$confirmations (currentHeight=$height)")
|
||||
if (confirmations >= minDepth) {
|
||||
// we need to get the tx position in the block
|
||||
client ! ElectrumClient.GetMerkle(txid, txheight, Some(tx))
|
||||
}
|
||||
}
|
||||
}
|
||||
context become running(height, tip, watches -- watchSpentTriggered, scriptHashStatus, block2tx, sent)
|
||||
val watchConfirmedTriggered = watches.collect {
|
||||
case w@WatchConfirmed(channel, txid, _, minDepth, BITCOIN_FUNDING_DEPTHOK) if txid == tx.txid && minDepth == 0 =>
|
||||
// special case for mempool watches (min depth = 0)
|
||||
val (dummyHeight, dummyTxIndex) = ElectrumWatcher.makeDummyShortChannelId(txid)
|
||||
channel ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, dummyHeight, dummyTxIndex, tx)
|
||||
Some(w)
|
||||
case WatchConfirmed(_, txid, _, minDepth, _) if txid == tx.txid && minDepth > 0 =>
|
||||
// min depth > 0 here
|
||||
val txheight = item.height
|
||||
val confirmations = height - txheight + 1
|
||||
log.info(s"txid=$txid was confirmed at height=$txheight and now has confirmations=$confirmations (currentHeight=$height)")
|
||||
if (confirmations >= minDepth) {
|
||||
// we need to get the tx position in the block
|
||||
client ! ElectrumClient.GetMerkle(txid, txheight, Some(tx))
|
||||
}
|
||||
None
|
||||
}.flatten
|
||||
|
||||
context become running(height, tip, watches -- watchSpentTriggered -- watchConfirmedTriggered, scriptHashStatus, block2tx, sent)
|
||||
|
||||
case ElectrumClient.GetMerkleResponse(tx_hash, _, txheight, pos, Some(tx: Transaction)) =>
|
||||
val confirmations = height - txheight + 1
|
||||
|
@ -215,3 +222,24 @@ class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor wi
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
object ElectrumWatcher {
|
||||
/**
|
||||
*
|
||||
* @param txid funding transaction id
|
||||
* @return a (blockHeight, txIndex) tuple that is extracted from the input source
|
||||
* This is used to create unique "dummy" short channel ids for zero-conf channels
|
||||
*/
|
||||
def makeDummyShortChannelId(txid: ByteVector32): (Int, Int) = {
|
||||
// we use a height of 0
|
||||
// - to make sure that the tx will be marked as "confirmed"
|
||||
// - to easily identify scids linked to 0-conf channels
|
||||
//
|
||||
// this gives us a probability of collisions of 0.1% for 5 0-conf channels and 1% for 20
|
||||
// collisions mean that users may temporarily see incorrect numbers for their 0-conf channels (until they've been confirmed)
|
||||
// if this ever becomes a problem we could just extract some bits for our dummy height instead of just returning 0
|
||||
val height = 0
|
||||
val txIndex = txid.bits.sliceToInt(0, 16, false)
|
||||
(height, txIndex)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -618,6 +618,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)
|
||||
|
@ -633,6 +634,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
|
||||
|
@ -654,6 +656,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
|
||||
|
@ -665,6 +668,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
|
||||
|
@ -692,6 +696,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)
|
||||
}
|
||||
|
@ -720,7 +725,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
|
||||
|
@ -728,10 +733,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
|
||||
|
@ -751,6 +752,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))
|
||||
|
@ -863,8 +868,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)
|
||||
|
@ -1134,8 +1138,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)
|
||||
|
||||
|
@ -1205,7 +1208,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)
|
||||
|
@ -2305,4 +2312,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._
|
||||
|
@ -92,7 +91,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
|
||||
|
@ -115,7 +114,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) {
|
||||
|
@ -148,9 +149,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)
|
||||
|
@ -181,11 +192,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), _) =>
|
||||
|
@ -372,11 +378,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, _)) =>
|
||||
|
@ -587,6 +593,31 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A
|
|||
nodeParams.db.network.getNode(remoteNodeId).flatMap(_.addresses.headOption.map(_.socketAddress))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
|
||||
|
@ -697,23 +728,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,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
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
|
|||
log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId")
|
||||
context become main(channelUpdates - shortChannelId, 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,57 +39,87 @@ 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) {
|
||||
sender ! PaymentFailed(paymentId, r.paymentRequest.paymentHash, LocalFailure(new IllegalArgumentException("cannot pay a 0-value invoice via trampoline-to-legacy (trampoline may steal funds)")) :: Nil)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
// 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)))
|
||||
r.trampolineAttempts match {
|
||||
case Nil =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineFeesMissing) :: Nil)
|
||||
case _ if !r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty =>
|
||||
sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineLegacyAmountLessInvoice) :: Nil)
|
||||
case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts =>
|
||||
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)))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,54 +127,194 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR
|
|||
|
||||
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.
|
||||
*
|
||||
* @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(finalAmount: MilliSatoshi,
|
||||
trampolineFees: MilliSatoshi,
|
||||
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}
|
||||
|
||||
/**
|
||||
* Created by PM on 15/11/2016.
|
||||
|
@ -39,7 +39,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) ::
|
||||
|
@ -59,7 +59,30 @@ object LightningMessageCodecs {
|
|||
("yourLastPerCommitmentSecret" | optional(bitsRemaining, privateKey)) ::
|
||||
("myCurrentPerCommitmentPoint" | optional(bitsRemaining, publicKey))).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) ::
|
||||
|
@ -77,7 +100,20 @@ object LightningMessageCodecs {
|
|||
("delayedPaymentBasepoint" | publicKey) ::
|
||||
("htlcBasepoint" | publicKey) ::
|
||||
("firstPerCommitmentPoint" | publicKey) ::
|
||||
("channelFlags" | byte)).as[OpenChannel]
|
||||
("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) ::
|
||||
|
@ -93,7 +129,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"
|
||||
|
@ -82,7 +84,9 @@ case class OpenChannel(chainHash: ByteVector32,
|
|||
delayedPaymentBasepoint: PublicKey,
|
||||
htlcBasepoint: PublicKey,
|
||||
firstPerCommitmentPoint: PublicKey,
|
||||
channelFlags: Byte) extends ChannelMessage with HasTemporaryChannelId with HasChainHash
|
||||
channelFlags: Byte,
|
||||
upfrontShutdownScript: Option[ByteVector] = None,
|
||||
tlvStream_opt: Option[TlvStream[OpenTlv]] = None) extends ChannelMessage with HasTemporaryChannelId with HasChainHash
|
||||
|
||||
case class AcceptChannel(temporaryChannelId: ByteVector32,
|
||||
dustLimitSatoshis: Satoshi,
|
||||
|
@ -97,7 +101,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,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.eclair.UInt64
|
||||
import fr.acinq.eclair.wire.CommonCodecs._
|
||||
import fr.acinq.eclair.wire.TlvCodecs.tlvStream
|
||||
import scodec.Codec
|
||||
import scodec.bits.ByteVector
|
||||
import scodec.codecs._
|
||||
|
||||
sealed trait OpenTlv extends Tlv
|
||||
|
||||
object OpenTlv {
|
||||
|
||||
case class Placeholder(b: ByteVector) extends OpenTlv
|
||||
|
||||
val openTlvCodec: Codec[TlvStream[OpenTlv]] = tlvStream(discriminated.by(varint)
|
||||
.typecase(UInt64(65717), variableSizeBytesLong(varintoverflow, bytes).as[Placeholder])
|
||||
)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,16 +22,15 @@ import java.util.concurrent.atomic.AtomicLong
|
|||
import akka.actor.{ActorSystem, Props}
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.bitcoin.{Base58, ByteVector32, OutPoint, SIGHASH_ALL, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.LongToBtcAmount
|
||||
import fr.acinq.bitcoin.{Base58, Bech32, ByteVector32, OutPoint, SIGHASH_ALL, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
|
||||
import fr.acinq.eclair.{LongToBtcAmount, randomBytes32}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.json4s
|
||||
import org.json4s.JsonAST.{JArray, JString, JValue}
|
||||
import org.json4s.JsonAST.{JString, JValue}
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
import scodec.bits._
|
||||
|
||||
|
@ -123,6 +122,124 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike
|
|||
system.stop(watcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a chain of unspent txs
|
||||
* @param tx tx that sends funds to a p2wpkh of priv
|
||||
* @param priv private key that tx sends funds to
|
||||
* @return a (tx1, tx2) tuple where tx2 spends tx1 which spends tx
|
||||
*/
|
||||
def createUnspentTxChain(tx: Transaction, priv: PrivateKey) : (Transaction, Transaction) = {
|
||||
// tx sends funds to our key
|
||||
val pub = priv.publicKey
|
||||
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(pub)))
|
||||
|
||||
val fee = 10000 sat
|
||||
val tx1 = {
|
||||
val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx, outputIndex), Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(tx.txOut(outputIndex).amount - fee, Script.pay2wpkh(pub)) :: Nil, lockTime = 0)
|
||||
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(pub), SIGHASH_ALL, tx.txOut(outputIndex).amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
|
||||
val tmp1 = tmp.updateWitness(0, ScriptWitness(sig :: pub.value :: Nil))
|
||||
Transaction.correctlySpends(tmp1, tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
tmp1
|
||||
}
|
||||
// tx1 spends tx
|
||||
|
||||
val tx2 = {
|
||||
val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx1, 0), Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(tx1.txOut(0).amount - fee, Script.pay2wpkh(pub)) :: Nil, lockTime = 0)
|
||||
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(pub), SIGHASH_ALL, tx1.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
|
||||
val tmp1 = tmp.updateWitness(0, ScriptWitness(sig :: pub.value :: Nil))
|
||||
Transaction.correctlySpends(tmp1, tx1 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
|
||||
tmp1
|
||||
}
|
||||
// and tx2 spends tx1
|
||||
(tx1, tx2)
|
||||
}
|
||||
|
||||
test("watch for mempool transactions (txs in mempool before we set the watch)") {
|
||||
val probe = TestProbe()
|
||||
val blockCount = new AtomicLong()
|
||||
val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress))))
|
||||
probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref))
|
||||
probe.expectMsgType[ElectrumClient.ElectrumReady]
|
||||
|
||||
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
|
||||
|
||||
val priv = PrivateKey(ByteVector32.fromValidHex("01" * 32))
|
||||
val pub = priv.publicKey
|
||||
val address = Bech32.encodeWitnessAddress("bcrt", 0, pub.hash160)
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
|
||||
val JString(hex) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction.read(hex)
|
||||
|
||||
val (tx1, tx2) = createUnspentTxChain(tx, priv)
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString()))
|
||||
probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString()))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
|
||||
// wait until tx1 and tx2 are in the mempool (as seen by our ElectrumX server)
|
||||
awaitCond({
|
||||
probe.send(electrumClient, ElectrumClient.GetScriptHashHistory(ElectrumClient.computeScriptHash(tx2.txOut(0).publicKeyScript)))
|
||||
val ElectrumClient.GetScriptHashHistoryResponse(_, history) = probe.expectMsgType[ElectrumClient.GetScriptHashHistoryResponse]
|
||||
history.map(_.tx_hash).toSet == Set(tx.txid, tx1.txid, tx2.txid)
|
||||
}, max = 30 seconds, interval = 5 seconds)
|
||||
|
||||
// then set a watch
|
||||
val listener = TestProbe()
|
||||
probe.send(watcher, WatchConfirmed(listener.ref, tx2.txid, tx2.txOut(0).publicKeyScript, 0, BITCOIN_FUNDING_DEPTHOK))
|
||||
val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds)
|
||||
assert(confirmed.tx.txid === tx2.txid)
|
||||
system.stop(watcher)
|
||||
}
|
||||
|
||||
test("watch for mempool transactions (txs not yet in the mempool when we set the watch)") {
|
||||
val probe = TestProbe()
|
||||
val blockCount = new AtomicLong()
|
||||
val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress))))
|
||||
probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref))
|
||||
probe.expectMsgType[ElectrumClient.ElectrumReady]
|
||||
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
|
||||
|
||||
val priv = PrivateKey(ByteVector32.fromValidHex("01" * 32))
|
||||
val pub = priv.publicKey
|
||||
val address = Bech32.encodeWitnessAddress("bcrt", 0, pub.hash160)
|
||||
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
|
||||
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
|
||||
val JString(hex) = probe.expectMsgType[JValue]
|
||||
val tx = Transaction.read(hex)
|
||||
|
||||
val (tx1, tx2) = createUnspentTxChain(tx, priv)
|
||||
|
||||
// here we set the watch * before * we publish our transactions
|
||||
val listener = TestProbe()
|
||||
probe.send(watcher, WatchConfirmed(listener.ref, tx2.txid, tx2.txOut(0).publicKeyScript, 0, BITCOIN_FUNDING_DEPTHOK))
|
||||
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString()))
|
||||
probe.expectMsgType[JValue]
|
||||
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString()))
|
||||
probe.expectMsgType[JValue]
|
||||
|
||||
val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds)
|
||||
assert(confirmed.tx.txid === tx2.txid)
|
||||
system.stop(watcher)
|
||||
}
|
||||
|
||||
test("generate unique dummy scids") {
|
||||
// generate 1000 dummy ids
|
||||
val dummies = (0 until 20).map { _ =>
|
||||
ElectrumWatcher.makeDummyShortChannelId(randomBytes32)
|
||||
} toSet
|
||||
|
||||
// make sure that they are unique (we allow for 1 collision here, actual probability of a collision with the current impl. is 1%
|
||||
// but that could change and we don't want to make this test impl. dependent)
|
||||
// if this test fails it's very likely that the code that generates dummy scids is broken
|
||||
assert(dummies.size >= 19)
|
||||
}
|
||||
|
||||
test("get transaction") {
|
||||
val blockCount = new AtomicLong()
|
||||
val mainnetAddress = ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -69,10 +69,14 @@ class 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(
|
||||
|
@ -664,6 +668,19 @@ class 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
|
||||
|
@ -251,7 +251,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:
|
||||
|
@ -435,13 +434,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"))
|
||||
|
@ -451,23 +451,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)
|
||||
}
|
||||
|
||||
|
@ -488,9 +494,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)
|
||||
|
||||
|
@ -511,20 +517,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)
|
||||
}
|
||||
|
||||
|
@ -543,10 +549,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
|
||||
|
@ -554,7 +561,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
|
||||
|
@ -563,28 +570,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)") {
|
||||
|
@ -596,28 +609,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)") {
|
||||
|
@ -634,28 +655,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)") {
|
||||
|
@ -675,17 +698,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)") {
|
||||
|
@ -696,17 +719,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 => {
|
||||
|
|
|
@ -44,21 +44,37 @@ class LightningMessageCodecsSpec extends FunSuite {
|
|||
def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,6 +87,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))
|
||||
|
|
|
@ -172,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") {
|
||||
|
@ -185,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