1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 19:37:35 +01:00

Merge commit 'ea57bb266' into android

This commit is contained in:
pm47 2020-10-09 12:57:53 +02:00
commit d051fa76c7
No known key found for this signature in database
GPG key ID: E434ED292E85643A
47 changed files with 749 additions and 648 deletions

33
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Build & Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Cache Maven dependencies
uses: actions/cache@v2
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Configure OS settings
run: echo "fs.file-max = 1024000" | sudo tee -a /etc/sysctl.conf
- name: Build with Maven
run: mvn clean test

View file

@ -1,26 +0,0 @@
sudo: required
services:
-docker
dist: trusty
language: scala
scala:
- 2.11.12
env:
- export LD_LIBRARY_PATH=/usr/local/lib
before_install:
- wget https://apache.osuosl.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.zip
- unzip -qq apache-maven-3.6.3-bin.zip
- export M2_HOME=$PWD/apache-maven-3.6.3
- export PATH=$M2_HOME/bin:$PATH
script:
- echo "fs.file-max = 1024000" | sudo tee -a /etc/sysctl.conf
- mvn scoverage:report
cache:
directories:
- .autoconf
- $HOME/.m2
jdk:
- openjdk11
notifications:
email:
- ops@acinq.fr

View file

@ -1,6 +1,6 @@
![Eclair Logo](.readme/logo.png)
[![Build Status](https://travis-ci.org/ACINQ/eclair.svg?branch=master)](https://travis-ci.org/ACINQ/eclair)
[![Build Status](https://github.com/ACINQ/eclair/workflows/Build%20&%20Test/badge.svg)](https://github.com/ACINQ/eclair/actions?query=workflow%3A%22Build+%26+Test%22)
[![codecov](https://codecov.io/gh/acinq/eclair/branch/master/graph/badge.svg)](https://codecov.io/gh/acinq/eclair)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Gitter chat](https://img.shields.io/badge/chat-on%20gitter-red.svg)](https://gitter.im/ACINQ/eclair)

View file

@ -21,7 +21,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.4.0-android-SNAPSHOT</version>
<version>0.4.2-android-SNAPSHOT</version>
</parent>
<artifactId>eclair-core_2.11</artifactId>

View file

@ -209,9 +209,8 @@ eclair {
// do not edit or move this section
eclair {
backup-mailbox {
mailbox-type = "akka.dispatch.BoundedMailbox"
mailbox-type = "akka.dispatch.NonBlockingBoundedMailbox"
mailbox-capacity = 1
mailbox-push-timeout-time = 0
}
backup-dispatcher {
executor = "thread-pool-executor"

View file

@ -25,7 +25,6 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.TimestampQueryFilters._
import fr.acinq.eclair.blockchain.OnChainBalance
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.WalletTransaction
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
import fr.acinq.eclair.channel._
@ -224,21 +223,18 @@ class EclairImpl(appKit: Kit) extends Eclair {
override def onChainBalance(): Future[OnChainBalance] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.getBalance
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}
override def onChainTransactions(count: Int, skip: Int): Future[Iterable[WalletTransaction]] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.listTransactions(count, skip)
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}
override def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] = {
appKit.wallet match {
case w: BitcoinCoreWallet => w.sendToAddress(address, amount, confirmationTarget)
case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend"))
}
}

View file

@ -43,7 +43,7 @@ import fr.acinq.eclair.db.{Databases, FileBackupHandler}
import fr.acinq.eclair.io.{ClientSpawner, Switchboard}
import fr.acinq.eclair.payment.Auditor
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.send.PaymentInitiator
import fr.acinq.eclair.router._
import grizzled.slf4j.Logging
@ -246,9 +246,8 @@ class Setup(datadir: File,
}
audit = system.actorOf(SimpleSupervisor.props(Auditor.props(nodeParams), "auditor", SupervisorStrategy.Resume))
register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
commandBuffer = system.actorOf(SimpleSupervisor.props(Props(new CommandBuffer(nodeParams, register)), "command-buffer", SupervisorStrategy.Resume))
paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, commandBuffer), "payment-handler", SupervisorStrategy.Resume))
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, commandBuffer, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume))
paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register), "payment-handler", SupervisorStrategy.Resume))
relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume))
// Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system,
// we want to make sure the handler for post-restart broken HTLCs has finished initializing.
_ <- postRestartCleanUpInitialized.future
@ -262,7 +261,6 @@ class Setup(datadir: File,
watcher = watcher,
paymentHandler = paymentHandler,
register = register,
commandBuffer = commandBuffer,
relayer = relayer,
router = router,
switchboard = switchboard,
@ -282,7 +280,6 @@ case class Kit(nodeParams: NodeParams,
watcher: ActorRef,
paymentHandler: ActorRef,
register: ActorRef,
commandBuffer: ActorRef,
relayer: ActorRef,
router: ActorRef,
switchboard: ActorRef,

View file

@ -27,17 +27,20 @@ import org.json4s.JsonAST._
import scodec.bits.ByteVector
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
/**
* Created by PM on 06/07/2017.
*/
* Created by PM on 06/07/2017.
*/
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionContext) extends EclairWallet with Logging {
import BitcoinCoreWallet._
val bitcoinClient = new ExtendedBitcoinClient(rpcClient)
def fundTransaction(hex: String, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = {
def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw)
private def fundTransaction(hex: String, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = {
val feeRatePerKB = BigDecimal(feerateKw2KB(feeRatePerKw))
rpcClient.invoke("fundrawtransaction", hex, Options(lockUnspents, feeRatePerKB.bigDecimal.scaleByPowerOfTen(-8))).map(json => {
val JString(hex) = json \ "hex"
@ -47,9 +50,9 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
})
}
def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw)
def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toHex)
def signTransaction(hex: String): Future[SignTransactionResponse] =
private def signTransaction(hex: String): Future[SignTransactionResponse] =
rpcClient.invoke("signrawtransactionwithwallet", hex).map(json => {
val JString(hex) = json \ "hex"
val JBool(complete) = json \ "complete"
@ -60,9 +63,19 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
SignTransactionResponse(Transaction.read(hex), complete)
})
def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toHex)
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = bitcoinClient.publishTransaction(tx)
private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = {
val f = signTransaction(tx)
// if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos
f.recoverWith { case _ =>
unlockOutpoints(tx.txIn.map(_.outPoint))
.recover { case t: Throwable => // no-op, just add a log in case of failure
logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t)
t
}
.flatMap(_ => f) // return signTransaction error
.recoverWith { case _ => f } // return signTransaction error
}
}
def listTransactions(count: Int, skip: Int): Future[List[WalletTransaction]] = rpcClient.invoke("listtransactions", "*", count, skip).map {
case JArray(txs) => txs.map(tx => {
@ -100,12 +113,64 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
}
}
override def getBalance: Future[OnChainBalance] = rpcClient.invoke("getbalances").map(json => {
val JDecimal(confirmed) = json \ "mine" \ "trusted"
val JDecimal(unconfirmed) = json \ "mine" \ "untrusted_pending"
OnChainBalance(toSatoshi(confirmed), toSatoshi(unconfirmed))
})
override def getReceiveAddress: Future[String] = for {
JString(address) <- rpcClient.invoke("getnewaddress")
} yield address
override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = for {
address <- receiveAddress.map(Future.successful).getOrElse(getReceiveAddress)
JString(rawKey) <- rpcClient.invoke("getaddressinfo", address).map(_ \ "pubkey")
} yield PublicKey(ByteVector.fromValidHex(rawKey))
override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
val partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
for {
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw)
// now let's sign the funding tx
SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx)
// there will probably be a change output, so we need to find which output is ours
outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, amount_opt = None) match {
case Right(outputIndex) => Future.successful(outputIndex)
case Left(skipped) => Future.failed(new RuntimeException(skipped.toString))
}
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
} yield MakeFundingTxResponse(fundingTx, outputIndex, fee)
}
override def commit(tx: Transaction): Future[Boolean] = bitcoinClient.publishTransaction(tx)
.map(_ => true) // if bitcoind says OK, then we consider the tx successfully published
.recoverWith {
case e =>
logger.warn(s"txid=${tx.txid} error=$e")
bitcoinClient.getTransaction(tx.txid)
.map(_ => true) // tx is in the mempool, we consider that it was published
.recoverWith {
case _ =>
rollback(tx).map { _ => false }.recover { case _ => false } // we use transform here because we want to return false in all cases even if rollback fails
}
}
.recover { case _ => true } // in all other cases we consider that the tx has been published
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoints(tx.txIn.map(_.outPoint)) // we unlock all utxos used by the tx
override def doubleSpent(tx: Transaction): Future[Boolean] = bitcoinClient.doubleSpent(tx)
/**
*
* @param outPoints outpoints to unlock
* @return true if all outpoints were successfully unlocked, false otherwise
*/
def unlockOutpoints(outPoints: Seq[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = {
private def unlockOutpoints(outPoints: Seq[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = {
// we unlock utxos one by one and not as a list as it would fail at the first utxo that is not actually lock and the rest would not be processed
val futures = outPoints
.map(outPoint => Utxo(outPoint.txid, outPoint.index))
@ -127,91 +192,6 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
future.map(_.forall(b => b))
}
override def getBalance: Future[OnChainBalance] = rpcClient.invoke("getbalances").map(json => {
val JDecimal(confirmed) = json \ "mine" \ "trusted"
val JDecimal(unconfirmed) = json \ "mine" \ "untrusted_pending"
OnChainBalance(toSatoshi(confirmed), toSatoshi(unconfirmed))
})
override def getReceiveAddress: Future[String] = for {
JString(address) <- rpcClient.invoke("getnewaddress")
} yield address
override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = for {
address <- receiveAddress.map(Future.successful).getOrElse(getReceiveAddress)
JString(rawKey) <- rpcClient.invoke("getaddressinfo", address).map(_ \ "pubkey")
} yield PublicKey(ByteVector.fromValidHex(rawKey))
private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = {
val f = signTransaction(tx)
// if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos
f.recoverWith { case _ =>
unlockOutpoints(tx.txIn.map(_.outPoint))
.recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure
.flatMap { case _ => f } // return signTransaction error
.recoverWith { case _ => f } // return signTransaction error
}
}
override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
// partial funding tx
val partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
for {
// we ask bitcoin core to add inputs to the funding tx, and use the specified change address
FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw)
// now let's sign the funding tx
SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx)
// there will probably be a change output, so we need to find which output is ours
outputIndex <- Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, amount_opt = None) match {
case Right(outputIndex) => Future.successful(outputIndex)
case Left(skipped) => Future.failed(new RuntimeException(skipped.toString))
}
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
} yield MakeFundingTxResponse(fundingTx, outputIndex, fee)
}
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)
.map(_ => true) // if bitcoind says OK, then we consider the tx successfully published
.recoverWith {
case e =>
logger.warn(s"txid=${tx.txid} error=$e")
bitcoinClient.getTransaction(tx.txid)
.map(_ => true) // tx is in the mempool, we consider that it was published
.recoverWith {
case _ =>
rollback(tx).map { _ => false }.recover { case _ => false } // we use transform here because we want to return false in all cases even if rollback fails
}
}
.recover { case _ => true } // in all other cases we consider that the tx has been published
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoints(tx.txIn.map(_.outPoint)) // we unlock all utxos used by the tx
override def doubleSpent(tx: Transaction): Future[Boolean] =
for {
exists <- bitcoinClient.getTransaction(tx.txid)
.map(_ => true) // we have found the transaction
.recover {
case JsonRPCError(Error(_, message)) if message.contains("index") =>
sys.error("Fatal error: bitcoind is indexing!!")
System.exit(1) // bitcoind is indexing, that's a fatal error!!
false // won't be reached
case _ => false
}
doublespent <- if (exists) {
// if the tx is in the blockchain, it can't have been double-spent
Future.successful(false)
} else {
// if the tx wasn't in the blockchain and one of it's input has been spent, it is double-spent
// NB: we don't look in the mempool, so it means that we will only consider that the tx has been double-spent if
// the overriding transaction has been confirmed at least once
Future.sequence(tx.txIn.map(txIn => bitcoinClient.isTransactionOutputSpendable(txIn.outPoint.txid, txIn.outPoint.index.toInt, includeMempool = false))).map(_.exists(_ == false))
}
} yield doublespent
}
object BitcoinCoreWallet {

View file

@ -22,37 +22,131 @@ import fr.acinq.eclair.TxCoordinates
import fr.acinq.eclair.blockchain.{GetTxWithMetaResponse, UtxoStatus, ValidateResult}
import fr.acinq.eclair.wire.ChannelAnnouncement
import kamon.Kamon
import org.json4s.Formats
import org.json4s.JsonAST._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
/**
* Created by PM on 26/04/2016.
*/
* Created by PM on 26/04/2016.
*/
/**
* The ExtendedBitcoinClient adds some high-level utility methods to interact with Bitcoin Core.
* Note that all wallet utilities (signing transactions, setting fees, locking outputs, etc) can be found in
* [[fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet]].
*/
class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
implicit val formats = org.json4s.DefaultFormats
implicit val formats: Formats = org.json4s.DefaultFormats
def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] =
getRawTransaction(txid).map(raw => Transaction.read(raw))
private def getRawTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("getrawtransaction", txid).collect {
case JString(raw) => raw
}
def getTransactionMeta(txid: ByteVector32)(implicit ec: ExecutionContext): Future[GetTxWithMetaResponse] =
for {
tx_opt <- getTransaction(txid).map(Some(_)).recover { case _ => None }
blockchaininfo <- rpcClient.invoke("getblockchaininfo")
JInt(timestamp) = blockchaininfo \ "mediantime"
} yield GetTxWithMetaResponse(txid, tx_opt, timestamp.toLong)
/** Get the number of confirmations of a given transaction. */
def getTxConfirmations(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[Int]] =
rpcClient.invoke("getrawtransaction", txid, 1) // we choose verbose output to get the number of confirmations
rpcClient.invoke("getrawtransaction", txid, 1 /* verbose output is needed to get the number of confirmations */)
.map(json => Some((json \ "confirmations").extractOrElse[Int](0)))
.recover {
case t: JsonRPCError if t.error.code == -5 => None
case t: JsonRPCError if t.error.code == -5 => None // Invalid or non-wallet transaction id (code: -5)
}
def getTxBlockHash(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[ByteVector32]] =
rpcClient.invoke("getrawtransaction", txid, 1) // we choose verbose output to get the number of confirmations
/** Get the hash of the block containing a given transaction. */
private def getTxBlockHash(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Option[ByteVector32]] =
rpcClient.invoke("getrawtransaction", txid, 1 /* verbose output is needed to get the block hash */)
.map(json => (json \ "blockhash").extractOpt[String].map(ByteVector32.fromValidHex))
.recover {
case t: JsonRPCError if t.error.code == -5 => None
case t: JsonRPCError if t.error.code == -5 => None // Invalid or non-wallet transaction id (code: -5)
}
/**
* @return a Future[height, index] where height is the height of the block where this transaction was published, and
* index is the index of the transaction in that block.
*/
def getTransactionShortId(txid: ByteVector32)(implicit ec: ExecutionContext): Future[(Int, Int)] =
for {
Some(blockHash) <- getTxBlockHash(txid)
json <- rpcClient.invoke("getblock", blockHash)
JInt(height) = json \ "height"
JArray(txs) = json \ "tx"
index = txs.indexOf(JString(txid.toHex))
} yield (height.toInt, index)
/**
* Publish a transaction on the bitcoin network.
*
* Note that this method is idempotent, meaning that if the tx was already published a long time ago, then this is
* considered a success even if bitcoin core rejects this new attempt.
*
* @return the transaction id (txid)
*/
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ByteVector32] =
rpcClient.invoke("sendrawtransaction", tx.toString()).collect {
case JString(txid) => ByteVector32.fromValidHex(txid)
}.recoverWith {
case JsonRPCError(Error(-27, _)) =>
// "transaction already in block chain (code: -27)"
Future.successful(tx.txid)
case e@JsonRPCError(Error(-25, _)) =>
// "missing inputs (code: -25)": it may be that the tx has already been published and its output spent.
getRawTransaction(tx.txid).map { _ => tx.txid }.recoverWith { case _ => Future.failed(e) }
}
def isTransactionOutputSpendable(txid: ByteVector32, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
for {
json <- rpcClient.invoke("gettxout", txid, outputIndex, includeMempool)
} yield json != JNull
def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] =
for {
exists <- getTransaction(tx.txid)
.map(_ => true) // we have found the transaction
.recover {
case JsonRPCError(Error(_, message)) if message.contains("index") =>
sys.error("Fatal error: bitcoind is indexing!!")
sys.exit(1) // bitcoind is indexing, that's a fatal error!!
false // won't be reached
case _ => false
}
doublespent <- if (exists) {
// if the tx is in the blockchain, it can't have been double-spent
Future.successful(false)
} else {
// if the tx wasn't in the blockchain and one of its inputs has been spent, it is double-spent
// NB: we don't look in the mempool, so it means that we will only consider that the tx has been double-spent if
// the overriding transaction has been confirmed
Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpendable(txIn.outPoint.txid, txIn.outPoint.index.toInt, includeMempool = false))).map(_.exists(_ == false))
}
} yield doublespent
/**
* Iterate over blocks to find the transaction that has spent a given output.
* NB: only call this method when you're sure the output has been spent, otherwise this will iterate over the whole
* blockchain history.
*
* @param blockhash_opt hash of a block *after* the output has been spent. If not provided, we will use the blockchain tip.
* @param txid id of the transaction output that has been spent.
* @param outputIndex index of the transaction output that has been spent.
* @return the transaction spending the given output.
*/
def lookForSpendingTx(blockhash_opt: Option[ByteVector32], txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] =
for {
blockhash <- blockhash_opt match {
case Some(b) => Future.successful(b)
case None => rpcClient.invoke("getbestblockhash") collect { case JString(b) => ByteVector32.fromValidHex(b) }
case None => rpcClient.invoke("getbestblockhash").collect { case JString(b) => ByteVector32.fromValidHex(b) }
}
// with a verbosity of 0, getblock returns the raw serialized block
block <- rpcClient.invoke("getblock", blockhash, 0).collect { case JString(b) => Block.read(b) }
@ -69,118 +163,41 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
txs <- Future.sequence(txids.map(getTransaction(_)))
} yield txs
/**
* @param txid
* @param ec
* @return
*/
def getRawTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("getrawtransaction", txid) collect {
case JString(raw) => raw
}
def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] =
getRawTransaction(txid).map(raw => Transaction.read(raw))
def getTransactionMeta(txid: ByteVector32)(implicit ec: ExecutionContext): Future[GetTxWithMetaResponse] =
for {
tx_opt <- getTransaction(txid) map(Some(_)) recover { case _ => None }
blockchaininfo <- rpcClient.invoke("getblockchaininfo")
JInt(timestamp) = blockchaininfo \ "mediantime"
} yield GetTxWithMetaResponse(txid = txid, tx_opt, timestamp.toLong)
def isTransactionOutputSpendable(txid: ByteVector32, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
for {
json <- rpcClient.invoke("gettxout", txid, outputIndex, includeMempool)
} yield json != JNull
/**
*
* @param txid transaction id
* @param ec
* @return a Future[height, index] where height is the height of the block where this transaction was published, and index is
* the index of the transaction in that block
*/
def getTransactionShortId(txid: ByteVector32)(implicit ec: ExecutionContext): Future[(Int, Int)] = {
val future = for {
Some(blockHash) <- getTxBlockHash(txid)
json <- rpcClient.invoke("getblock", blockHash)
JInt(height) = json \ "height"
JString(hash) = json \ "hash"
JArray(txs) = json \ "tx"
index = txs.indexOf(JString(txid.toHex))
} yield (height.toInt, index)
future
}
/**
* Publish a transaction on the bitcoin network.
*
* Note that this method is idempotent, meaning that if the tx was already published a long time ago, then this is
* considered a success even if bitcoin core rejects this new attempt.
*
* @param tx
* @param ec
* @return
*/
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("sendrawtransaction", tx.toString()) collect {
case JString(txid) => txid
} recoverWith {
case JsonRPCError(Error(-27, _)) =>
// "transaction already in block chain (code: -27)" ignore error
Future.successful(tx.txid.toHex)
case e@JsonRPCError(Error(-25, _)) =>
// "missing inputs (code: -25)" it may be that the tx has already been published and its output spent
getRawTransaction(tx.txid).map { _ => tx.txid.toHex }.recoverWith { case _ => Future.failed[String](e) }
}
/**
* We need this to compute absolute timeouts expressed in number of blocks (where getBlockCount would be equivalent
* to time.now())
*
* @param ec
* @return the current number of blocks in the active chain
*/
def getBlockCount(implicit ec: ExecutionContext): Future[Long] =
rpcClient.invoke("getblockcount") collect {
rpcClient.invoke("getblockcount").collect {
case JInt(count) => count.toLong
}
def validate(c: ChannelAnnouncement)(implicit ec: ExecutionContext): Future[ValidateResult] = {
val TxCoordinates(blockHeight, txIndex, outputIndex) = coordinates(c.shortChannelId)
val span = Kamon.spanBuilder("validate-bitcoin-client").start()
for {
_ <- Future.successful(0)
span0 = Kamon.spanBuilder("getblockhash").start()
blockHash <- rpcClient.invoke("getblockhash", blockHeight).map(_.extractOpt[String].map(ByteVector32.fromValidHex).getOrElse(ByteVector32.Zeroes))
_ = span0.finish()
span1 = Kamon.spanBuilder("getblock").start()
txid: ByteVector32 <- rpcClient.invoke("getblock", blockHash).map {
case json => Try {
val JArray(txs) = json \ "tx"
ByteVector32.fromValidHex(txs(txIndex).extract[String])
} getOrElse ByteVector32.Zeroes
}
_ = span1.finish()
span2 = Kamon.spanBuilder("getrawtx").start()
tx <- getRawTransaction(txid)
_ = span2.finish()
span3 = Kamon.spanBuilder("utxospendable-mempool").start()
unspent <- isTransactionOutputSpendable(txid, outputIndex, includeMempool = true)
_ = span3.finish()
fundingTxStatus <- if (unspent) {
Future.successful(UtxoStatus.Unspent)
} else {
// if this returns true, it means that the spending tx is *not* in the blockchain
isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map {
case res => UtxoStatus.Spent(spendingTxConfirmed = !res)
}
}
_ = span.finish()
} yield ValidateResult(c, Right((Transaction.read(tx), fundingTxStatus)))
} recover { case t: Throwable => ValidateResult(c, Left(t)) }
val span = Kamon.spanBuilder("validate-bitcoin-client").start()
for {
_ <- Future.successful(0)
span0 = Kamon.spanBuilder("getblockhash").start()
blockHash <- rpcClient.invoke("getblockhash", blockHeight).map(_.extractOpt[String].map(ByteVector32.fromValidHex).getOrElse(ByteVector32.Zeroes))
_ = span0.finish()
span1 = Kamon.spanBuilder("getblock").start()
txid: ByteVector32 <- rpcClient.invoke("getblock", blockHash).map(json => Try {
val JArray(txs) = json \ "tx"
ByteVector32.fromValidHex(txs(txIndex).extract[String])
}.getOrElse(ByteVector32.Zeroes))
_ = span1.finish()
span2 = Kamon.spanBuilder("getrawtx").start()
tx <- getRawTransaction(txid)
_ = span2.finish()
span3 = Kamon.spanBuilder("utxospendable-mempool").start()
unspent <- isTransactionOutputSpendable(txid, outputIndex, includeMempool = true)
_ = span3.finish()
fundingTxStatus <- if (unspent) {
Future.successful(UtxoStatus.Unspent)
} else {
// if this returns true, it means that the spending tx is *not* in the blockchain
isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map(res => UtxoStatus.Spent(spendingTxConfirmed = !res))
}
_ = span.finish()
} yield ValidateResult(c, Right((Transaction.read(tx), fundingTxStatus)))
} recover {
case t: Throwable => ValidateResult(c, Left(t))
}
}

View file

@ -28,9 +28,10 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.crypto.ShaChain
import fr.acinq.eclair.db.PendingRelayDb
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin, Relayer}
import fr.acinq.eclair.payment.relay.{Origin, Relayer}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
@ -88,12 +89,6 @@ object Channel {
// we will receive this message when we waited too long for a revocation for that commit number (NB: we explicitly specify the peer to allow for testing)
case class RevocationTimeout(remoteCommitNumber: Long, peer: ActorRef)
def ackPendingFailsAndFulfills(updates: List[UpdateMessage], relayer: ActorRef): Unit = updates.collect {
case u: UpdateFailMalformedHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id)
case u: UpdateFulfillHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id)
case u: UpdateFailHtlc => relayer ! CommandBuffer.CommandAck(u.channelId, u.id)
}
/**
* Outgoing messages go through the [[Peer]] for logging purposes.
*
@ -660,8 +655,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
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
relayer ! CommandBuffer.CommandAck(d.channelId, c.id)
// we acknowledge the command right away in case of failure
PendingRelayDb.ackCommand(nodeParams.db.pendingRelay, d.channelId, c)
handleCommandError(cause, c)
}
@ -681,8 +676,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
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
relayer ! CommandBuffer.CommandAck(d.channelId, c.id)
// we acknowledge the command right away in case of failure
PendingRelayDb.ackCommand(nodeParams.db.pendingRelay, d.channelId, c)
handleCommandError(cause, c)
}
@ -693,8 +688,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
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
relayer ! CommandBuffer.CommandAck(d.channelId, c.id)
// we acknowledge the command right away in case of failure
PendingRelayDb.ackCommand(nodeParams.db.pendingRelay, d.channelId, c)
handleCommandError(cause, c)
}
@ -734,7 +729,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
Commitments.sendCommit(d.commitments, keyManager) match {
case Success((commitments1, commit)) =>
log.debug("sending a new sig, spec:\n{}", Commitments.specs2String(commitments1))
ackPendingFailsAndFulfills(commitments1.localChanges.signed, relayer)
PendingRelayDb.ackPendingFailsAndFulfills(nodeParams.db.pendingRelay, commitments1.localChanges.signed)
val nextRemoteCommit = commitments1.remoteNextCommitInfo.left.get.nextRemoteCommit
val nextCommitNumber = nextRemoteCommit.index
// we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our
@ -1007,8 +1002,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
if (c.commit) self ! CMD_SIGN
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fulfill
case Failure(cause) =>
// we can clean up the command right away in case of failure
relayer ! CommandBuffer.CommandAck(d.channelId, c.id)
// we acknowledge the command right away in case of failure
PendingRelayDb.ackCommand(nodeParams.db.pendingRelay, d.channelId, c)
handleCommandError(cause, c)
}
@ -1027,8 +1022,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
if (c.commit) self ! CMD_SIGN
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fail
case Failure(cause) =>
// we can clean up the command right away in case of failure
relayer ! CommandBuffer.CommandAck(d.channelId, c.id)
// we acknowledge the command right away in case of failure
PendingRelayDb.ackCommand(nodeParams.db.pendingRelay, d.channelId, c)
handleCommandError(cause, c)
}
@ -1038,8 +1033,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
if (c.commit) self ! CMD_SIGN
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending fail
case Failure(cause) =>
// we can clean up the command right away in case of failure
relayer ! CommandBuffer.CommandAck(d.channelId, c.id)
// we acknowledge the command right away in case of failure
PendingRelayDb.ackCommand(nodeParams.db.pendingRelay, d.channelId, c)
handleCommandError(cause, c)
}
@ -1079,7 +1074,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
Commitments.sendCommit(d.commitments, keyManager) match {
case Success((commitments1, commit)) =>
log.debug("sending a new sig, spec:\n{}", Commitments.specs2String(commitments1))
ackPendingFailsAndFulfills(commitments1.localChanges.signed, relayer)
PendingRelayDb.ackPendingFailsAndFulfills(nodeParams.db.pendingRelay, commitments1.localChanges.signed)
context.system.eventStream.publish(ChannelSignatureSent(self, commitments1))
// we expect a quick response from our peer
setTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), timeout = nodeParams.revocationTimeout, repeat = false)
@ -1768,6 +1763,26 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
if (nextState != WAIT_FOR_INIT_INTERNAL) Metrics.ChannelsCount.withTag(Tags.State, nextState.toString).increment()
}
onTransition {
case _ -> CLOSING =>
PendingRelayDb.getPendingFailsAndFulfills(nodeParams.db.pendingRelay, nextStateData.asInstanceOf[HasCommitments].channelId) match {
case Nil =>
log.debug("nothing to replay")
case cmds =>
log.info("replaying {} unacked fulfills/fails", cmds.size)
cmds.foreach(self ! _) // they all have commit = false
}
case SYNCING -> (NORMAL | SHUTDOWN) =>
PendingRelayDb.getPendingFailsAndFulfills(nodeParams.db.pendingRelay, nextStateData.asInstanceOf[HasCommitments].channelId) match {
case Nil =>
log.debug("nothing to replay")
case cmds =>
log.info("replaying {} unacked fulfills/fails", cmds.size)
cmds.foreach(self ! _) // they all have commit = false
self ! CMD_SIGN // so we can sign all of them at once
}
}
/*
888 888 d8888 888b 888 8888888b. 888 8888888888 8888888b. .d8888b.
888 888 d88888 8888b 888 888 "Y88b 888 888 888 Y88b d88P Y88b

View file

@ -16,6 +16,7 @@
package fr.acinq.eclair.channel
import akka.actor.{ActorContext, ActorRef}
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256}
import fr.acinq.bitcoin.Script._
@ -24,7 +25,7 @@ import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeerateTolerance}
import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL
import fr.acinq.eclair.crypto.{Generators, KeyManager}
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.db.{ChannelsDb, PendingRelayDb}
import fr.acinq.eclair.transactions.DirectedHtlc._
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._

View file

@ -184,7 +184,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
connection ! Tcp.ResumeReading
stay using d.copy(decryptor = dec1)
} else {
log.debug(s"read {} messages, waiting for readacks", plaintextMessages.size)
log.debug("read {} messages, waiting for readacks", plaintextMessages.size)
val unackedReceived = sendToListener(d.listener, plaintextMessages)
stay using NormalData(d.encryptor, dec1, d.listener, d.sendBuffer, unackedReceived, d.unackedSent)
}
@ -192,6 +192,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
case Event(ReadAck(msg: T), d: NormalData[T]) =>
// how many occurences of this message are still unacked?
val remaining = d.unackedReceived.getOrElse(msg, 0) - 1
log.debug("acking message {}", msg)
// if all occurences have been acked then we remove the entry from the map
val unackedReceived1 = if (remaining > 0) d.unackedReceived + (msg -> remaining) else d.unackedReceived - msg
if (unackedReceived1.isEmpty) {
@ -199,6 +200,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
connection ! Tcp.ResumeReading
stay using d.copy(unackedReceived = unackedReceived1)
} else {
log.debug("still waiting for readacks, unacked={}", unackedReceived1)
stay using d.copy(unackedReceived = unackedReceived1)
}

View file

@ -20,53 +20,54 @@ import java.io.File
import akka.actor.{Actor, ActorLogging, Props}
import akka.dispatch.{BoundedMessageQueueSemantics, RequiresMessageQueue}
import fr.acinq.eclair.KamonExt
import fr.acinq.eclair.channel.ChannelPersisted
import fr.acinq.eclair.db.Databases.FileBackup
import fr.acinq.eclair.db.Monitoring.Metrics
import scala.sys.process.Process
import scala.util.{Failure, Success, Try}
/**
* This actor will synchronously make a backup of the database it was initialized with whenever it receives
* a ChannelPersisted event.
* To avoid piling up messages and entering an endless backup loop, it is supposed to be used with a bounded mailbox
* with a single item:
*
* backup-mailbox {
* mailbox-type = "akka.dispatch.BoundedMailbox"
* mailbox-capacity = 1
* mailbox-push-timeout-time = 0
* }
*
* Messages that cannot be processed will be sent to dead letters
*
* @param databases database to backup
* @param backupFile backup file
*
* Constructor is private so users will have to use BackupHandler.props() which always specific a custom mailbox
*/
* This actor will synchronously make a backup of the database it was initialized with whenever it receives
* a ChannelPersisted event.
* To avoid piling up messages and entering an endless backup loop, it is supposed to be used with a bounded mailbox
* with a single item:
*
* backup-mailbox {
* mailbox-type = "akka.dispatch.NonBlockingBoundedMailbox"
* mailbox-capacity = 1
* }
*
* Messages that cannot be processed will be sent to dead letters
*
* NB: Constructor is private so users will have to use BackupHandler.props() which always specific a custom mailbox.
*
* @param databases database to backup
* @param backupFile backup file
* @param backupScript_opt (optional) script to execute after the backup completes
*/
class FileBackupHandler private(databases: FileBackup, backupFile: File, backupScript_opt: Option[String]) extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] with ActorLogging {
// we listen to ChannelPersisted events, which will trigger a backup
context.system.eventStream.subscribe(self, classOf[ChannelPersisted])
def receive = {
def receive: Receive = {
case persisted: ChannelPersisted =>
val start = System.currentTimeMillis()
val tmpFile = new File(backupFile.getAbsolutePath.concat(".tmp"))
databases.backup(tmpFile)
KamonExt.time(Metrics.FileBackupDuration.withoutTags()) {
val tmpFile = new File(backupFile.getAbsolutePath.concat(".tmp"))
databases.backup(tmpFile)
// this will throw an exception if it fails, which is possible if the backup file is not on the same filesystem
// as the temporary file
// README: On Android we simply use renameTo because most Path methods are not available at our API level
tmpFile.renameTo(backupFile)
val end = System.currentTimeMillis()
// publish a notification that we have updated our backup
context.system.eventStream.publish(BackupCompleted)
log.debug(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms")
// publish a notification that we have updated our backup
context.system.eventStream.publish(BackupCompleted)
Metrics.FileBackupCompleted.withoutTags().increment()
}
backupScript_opt.foreach(backupScript => {
Try {
@ -88,5 +89,8 @@ case object BackupCompleted extends BackupEvent
object FileBackupHandler {
// using this method is the only way to create a BackupHandler actor
// we make sure that it uses a custom bounded mailbox, and a custom pinned dispatcher (i.e our actor will have its own thread pool with 1 single thread)
def props(databases: FileBackup, backupFile: File, backupScript_opt: Option[String]) = Props(new FileBackupHandler(databases, backupFile, backupScript_opt)).withMailbox("eclair.backup-mailbox").withDispatcher("eclair.backup-dispatcher")
def props(databases: FileBackup, backupFile: File, backupScript_opt: Option[String]) =
Props(new FileBackupHandler(databases, backupFile, backupScript_opt))
.withMailbox("eclair.backup-mailbox")
.withDispatcher("eclair.backup-dispatcher")
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2020 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.acinq.eclair.db
import fr.acinq.eclair.KamonExt
import kamon.Kamon
object Monitoring {
object Metrics {
val FileBackupCompleted = Kamon.counter("db.file-backup.completed")
val FileBackupDuration = Kamon.timer("db.file-backup.duration")
val DbOperation = Kamon.counter("db.operation.execute")
val DbOperationDuration = Kamon.timer("db.operation.duration")
def withMetrics[T](name: String)(operation: => T): T = KamonExt.time(DbOperationDuration.withTag(Tags.DbOperation, name)) {
DbOperation.withTag(Tags.DbOperation, name).increment()
operation
}
}
object Tags {
val DbOperation = "operation"
}
}

View file

@ -18,8 +18,11 @@ package fr.acinq.eclair.db
import java.io.Closeable
import akka.actor.{ActorContext, ActorRef}
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel.{Command, HasHtlcId}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, Command, HasHtlcId, Register}
import fr.acinq.eclair.wire.{UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc, UpdateMessage}
/**
* This database stores CMD_FULFILL_HTLC and CMD_FAIL_HTLC that we have received from downstream
@ -43,4 +46,37 @@ trait PendingRelayDb extends Closeable {
def listPendingRelay(): Set[(ByteVector32, Long)]
}
object PendingRelayDb {
/**
* We store [[CMD_FULFILL_HTLC]]/[[CMD_FAIL_HTLC]]/[[CMD_FAIL_MALFORMED_HTLC]]
* in a database because we don't want to lose preimages, or to forget to fail
* incoming htlcs, which would lead to unwanted channel closings.
*/
def safeSend(register: ActorRef, db: PendingRelayDb, channelId: ByteVector32, cmd: Command with HasHtlcId)(implicit ctx: ActorContext): Unit = {
register ! Register.Forward(channelId, cmd)
// we store the command in a db (note that this happens *after* forwarding the command to the channel, so we don't add latency)
db.addPendingRelay(channelId, cmd)
}
def ackCommand(db: PendingRelayDb, channelId: ByteVector32, cmd: Command with HasHtlcId): Unit = {
db.removePendingRelay(channelId, cmd.id)
}
def ackPendingFailsAndFulfills(db: PendingRelayDb, updates: List[UpdateMessage])(implicit log: LoggingAdapter): Unit = updates.collect {
case u: UpdateFulfillHtlc =>
log.debug(s"fulfill acked for htlcId=${u.id}")
db.removePendingRelay(u.channelId, u.id)
case u: UpdateFailHtlc =>
log.debug(s"fail acked for htlcId=${u.id}")
db.removePendingRelay(u.channelId, u.id)
case u: UpdateFailMalformedHtlc =>
log.debug(s"fail-malformed acked for htlcId=${u.id}")
db.removePendingRelay(u.channelId, u.id)
}
def getPendingFailsAndFulfills(db: PendingRelayDb, channelId: ByteVector32)(implicit log: LoggingAdapter): Seq[Command with HasHtlcId] = {
db.listPendingRelay(channelId)
}
}

View file

@ -22,6 +22,7 @@ import java.util.UUID
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalError, NetworkFeePaid, RemoteError}
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db._
import fr.acinq.eclair.payment._
import fr.acinq.eclair.{LongToBtcAmount, MilliSatoshi}
@ -107,7 +108,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
}
}
override def add(e: ChannelLifecycleEvent): Unit =
override def add(e: ChannelLifecycleEvent): Unit = withMetrics("audit/add-channel-lifecycle") {
using(sqlite.prepareStatement("INSERT INTO channel_events VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement =>
statement.setBytes(1, e.channelId.toArray)
statement.setBytes(2, e.remoteNodeId.value.toArray)
@ -118,8 +119,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
statement.setLong(7, System.currentTimeMillis)
statement.executeUpdate()
}
}
override def add(e: PaymentSent): Unit =
override def add(e: PaymentSent): Unit = withMetrics("audit/add-payment-sent") {
using(sqlite.prepareStatement("INSERT INTO sent VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
e.parts.foreach(p => {
statement.setLong(1, p.amount.toLong)
@ -136,8 +138,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
})
statement.executeBatch()
}
}
override def add(e: PaymentReceived): Unit =
override def add(e: PaymentReceived): Unit = withMetrics("audit/add-payment-received") {
using(sqlite.prepareStatement("INSERT INTO received VALUES (?, ?, ?, ?)")) { statement =>
e.parts.foreach(p => {
statement.setLong(1, p.amount.toLong)
@ -148,8 +151,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
})
statement.executeBatch()
}
}
override def add(e: PaymentRelayed): Unit = {
override def add(e: PaymentRelayed): Unit = withMetrics("audit/add-payment-relayed") {
val payments = e match {
case ChannelPaymentRelayed(amountIn, amountOut, _, fromChannelId, toChannelId, ts) =>
// non-trampoline relayed payments have one input and one output
@ -171,7 +175,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
}
}
override def add(e: NetworkFeePaid): Unit =
override def add(e: NetworkFeePaid): Unit = withMetrics("audit/add-network-fee") {
using(sqlite.prepareStatement("INSERT INTO network_fees VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
statement.setBytes(1, e.channelId.toArray)
statement.setBytes(2, e.remoteNodeId.value.toArray)
@ -181,8 +185,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
statement.setLong(6, System.currentTimeMillis)
statement.executeUpdate()
}
}
override def add(e: ChannelErrorOccurred): Unit =
override def add(e: ChannelErrorOccurred): Unit = withMetrics("audit/add-channel-error") {
using(sqlite.prepareStatement("INSERT INTO channel_errors VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
val (errorName, errorMessage) = e.error match {
case LocalError(t) => (t.getClass.getSimpleName, t.getMessage)
@ -196,6 +201,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
statement.setLong(6, System.currentTimeMillis)
statement.executeUpdate()
}
}
override def listSent(from: Long, to: Long): Seq[PaymentSent] =
using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ?")) { statement =>

View file

@ -22,6 +22,7 @@ import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.CltvExpiry
import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec
import grizzled.slf4j.Logging
@ -62,7 +63,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
}
override def addOrUpdateChannel(state: HasCommitments): Unit = {
override def addOrUpdateChannel(state: HasCommitments): Unit = withMetrics("channels/add-or-update-channel") {
val data = stateDataCodec.encode(state).require.toByteArray
using(sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update =>
update.setBytes(1, data)
@ -77,7 +78,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
}
}
override def removeChannel(channelId: ByteVector32): Unit = {
override def removeChannel(channelId: ByteVector32): Unit = withMetrics("channels/remove-channel") {
using(sqlite.prepareStatement("DELETE FROM pending_relay WHERE channel_id=?")) { statement =>
statement.setBytes(1, channelId.toArray)
statement.executeUpdate()
@ -94,14 +95,14 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
}
}
override def listLocalChannels(): Seq[HasCommitments] = {
override def listLocalChannels(): Seq[HasCommitments] = withMetrics("channels/list-local-channels") {
using(sqlite.createStatement) { statement =>
val rs = statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0")
codecSequence(rs, stateDataCodec)
}
}
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = {
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = withMetrics("channels/add-htlc-info") {
using(sqlite.prepareStatement("INSERT INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
statement.setBytes(1, channelId.toArray)
statement.setLong(2, commitmentNumber)
@ -111,7 +112,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging {
}
}
override def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] = {
override def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] = withMetrics("channels/list-htlc-infos") {
using(sqlite.prepareStatement("SELECT payment_hash, cltv_expiry FROM htlc_infos WHERE channel_id=? AND commitment_number=?")) { statement =>
statement.setBytes(1, channelId.toArray)
statement.setLong(2, commitmentNumber)

View file

@ -20,6 +20,7 @@ import java.sql.Connection
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi}
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.NetworkDb
import fr.acinq.eclair.router.Router.PublicChannel
import fr.acinq.eclair.wire.LightningMessageCodecs.nodeAnnouncementCodec
@ -96,7 +97,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS pruned (short_channel_id INTEGER NOT NULL PRIMARY KEY)")
}
override def addNode(n: NodeAnnouncement): Unit = {
override def addNode(n: NodeAnnouncement): Unit = withMetrics("network/add-node") {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO nodes VALUES (?, ?)")) { statement =>
statement.setBytes(1, n.nodeId.value.toArray)
statement.setBytes(2, nodeAnnouncementCodec.encode(n).require.toByteArray)
@ -104,7 +105,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def updateNode(n: NodeAnnouncement): Unit = {
override def updateNode(n: NodeAnnouncement): Unit = withMetrics("network/update-node") {
using(sqlite.prepareStatement("UPDATE nodes SET data=? WHERE node_id=?")) { statement =>
statement.setBytes(1, nodeAnnouncementCodec.encode(n).require.toByteArray)
statement.setBytes(2, n.nodeId.value.toArray)
@ -112,7 +113,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def getNode(nodeId: Crypto.PublicKey): Option[NodeAnnouncement] = {
override def getNode(nodeId: Crypto.PublicKey): Option[NodeAnnouncement] = withMetrics("network/get-node") {
using(sqlite.prepareStatement("SELECT data FROM nodes WHERE node_id=?")) { statement =>
statement.setBytes(1, nodeId.value.toArray)
val rs = statement.executeQuery()
@ -120,21 +121,21 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def removeNode(nodeId: Crypto.PublicKey): Unit = {
override def removeNode(nodeId: Crypto.PublicKey): Unit = withMetrics("network/remove-node") {
using(sqlite.prepareStatement("DELETE FROM nodes WHERE node_id=?")) { statement =>
statement.setBytes(1, nodeId.value.toArray)
statement.executeUpdate()
}
}
override def listNodes(): Seq[NodeAnnouncement] = {
override def listNodes(): Seq[NodeAnnouncement] = withMetrics("network/list-nodes") {
using(sqlite.createStatement()) { statement =>
val rs = statement.executeQuery("SELECT data FROM nodes")
codecSequence(rs, nodeAnnouncementCodec)
}
}
override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = {
override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = withMetrics("network/add-channel") {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO channels VALUES (?, ?, ?, ?, NULL, NULL)")) { statement =>
statement.setLong(1, c.shortChannelId.toLong)
statement.setString(2, txid.toHex)
@ -144,7 +145,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def updateChannel(u: ChannelUpdate): Unit = {
override def updateChannel(u: ChannelUpdate): Unit = withMetrics("network/update-channel") {
val column = if (u.isNode1) "channel_update_1" else "channel_update_2"
using(sqlite.prepareStatement(s"UPDATE channels SET $column=? WHERE short_channel_id=?")) { statement =>
statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray)
@ -153,7 +154,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = {
override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels") {
using(sqlite.createStatement()) { statement =>
val rs = statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels")
var m = SortedMap.empty[ShortChannelId, PublicChannel]
@ -169,7 +170,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = {
override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/remove-channels") {
using(sqlite.createStatement) { statement =>
shortChannelIds
.grouped(1000) // remove channels by batch of 1000
@ -180,7 +181,7 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = {
override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned") {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), inTransaction = true) { statement =>
shortChannelIds.foreach(shortChannelId => {
statement.setLong(1, shortChannelId.toLong)
@ -190,13 +191,13 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
}
override def removeFromPruned(shortChannelId: ShortChannelId): Unit = {
override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned") {
using(sqlite.createStatement) { statement =>
statement.executeUpdate(s"DELETE FROM pruned WHERE short_channel_id=${shortChannelId.toLong}")
}
}
override def isPruned(shortChannelId: ShortChannelId): Boolean = {
override def isPruned(shortChannelId: ShortChannelId): Boolean = withMetrics("network/is-pruned") {
using(sqlite.prepareStatement("SELECT short_channel_id from pruned WHERE short_channel_id=?")) { statement =>
statement.setLong(1, shortChannelId.toLong)
val rs = statement.executeQuery()
@ -205,5 +206,5 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging {
}
// used by mobile apps
override def close(): Unit = sqlite.close
override def close(): Unit = sqlite.close()
}

View file

@ -22,6 +22,7 @@ import java.util.UUID
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite.SqliteUtils._
import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent}
@ -32,7 +33,6 @@ import scodec.bits.BitVector
import scodec.codecs._
import scala.collection.immutable.Queue
import scala.compat.Platform
import scala.concurrent.duration._
class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
@ -127,7 +127,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
override def addOutgoingPayment(sent: OutgoingPayment): Unit = {
override def addOutgoingPayment(sent: OutgoingPayment): Unit = withMetrics("payments/add-outgoing") {
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, payment_type, amount_msat, recipient_amount_msat, recipient_node_id, created_at, payment_request) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
statement.setString(1, sent.id.toString)
@ -144,7 +144,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
}
override def updateOutgoingPayment(paymentResult: PaymentSent): Unit =
override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = withMetrics("payments/update-outgoing-sent") {
using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, payment_preimage, fees_msat, payment_route) = (?, ?, ?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
paymentResult.parts.foreach(p => {
statement.setLong(1, p.timestamp)
@ -156,14 +156,16 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
})
if (statement.executeBatch().contains(0)) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as succeeded but already in final status (id=${paymentResult.id})")
}
}
override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit =
override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = withMetrics("payments/update-outgoing-failed") {
using(sqlite.prepareStatement("UPDATE sent_payments SET (completed_at, failures) = (?, ?) WHERE id = ? AND completed_at IS NULL")) { statement =>
statement.setLong(1, paymentResult.timestamp)
statement.setBytes(2, paymentFailuresCodec.encode(paymentResult.failures.map(f => FailureSummary(f)).toList).require.toByteArray)
statement.setString(3, paymentResult.id.toString)
if (statement.executeUpdate() == 0) throw new IllegalArgumentException(s"Tried to mark an outgoing payment as failed but already in final status (id=${paymentResult.id})")
}
}
private def parseOutgoingPayment(rs: ResultSet): OutgoingPayment = {
val status = buildOutgoingPaymentStatus(
@ -213,7 +215,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
}
override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] =
override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = withMetrics("payments/get-outgoing") {
using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE id = ?")) { statement =>
statement.setString(1, id.toString)
val rs = statement.executeQuery()
@ -223,8 +225,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
None
}
}
}
override def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] =
override def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-parent-id") {
using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE parent_id = ? ORDER BY created_at")) { statement =>
statement.setString(1, parentId.toString)
val rs = statement.executeQuery()
@ -234,8 +237,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] =
override def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-payment-hash") {
using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE payment_hash = ? ORDER BY created_at")) { statement =>
statement.setBytes(1, paymentHash.toArray)
val rs = statement.executeQuery()
@ -245,8 +249,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] =
override def listOutgoingPayments(from: Long, to: Long): Seq[OutgoingPayment] = withMetrics("payments/list-outgoing-by-timestamp") {
using(sqlite.prepareStatement("SELECT * FROM sent_payments WHERE created_at >= ? AND created_at < ? ORDER BY created_at")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
@ -257,8 +262,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit =
override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming") {
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)
@ -268,8 +274,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS.toLong)).seconds.toMillis)
statement.executeUpdate()
}
}
override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long): Unit =
override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: Long): Unit = withMetrics("payments/receive-incoming") {
using(sqlite.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (? + COALESCE(received_msat, 0), ?) WHERE payment_hash = ?")) { update =>
update.setLong(1, amount.toLong)
update.setLong(2, receivedAt)
@ -279,6 +286,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
throw new IllegalArgumentException("Inserted a received payment without having an invoice")
}
}
}
private def parseIncomingPayment(rs: ResultSet): IncomingPayment = {
val paymentRequest = rs.getString("payment_request")
@ -298,7 +306,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
}
override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] =
override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = withMetrics("payments/get-incoming") {
using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE payment_hash = ?")) { statement =>
statement.setBytes(1, paymentHash.toArray)
val rs = statement.executeQuery()
@ -308,8 +316,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
None
}
}
}
override def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] =
override def listIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming") {
using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE created_at > ? AND created_at < ? ORDER BY created_at")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
@ -320,8 +329,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] =
override def listReceivedIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-received") {
using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat > 0 AND created_at > ? AND created_at < ? ORDER BY created_at")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
@ -332,8 +342,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] =
override def listPendingIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-pending") {
using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at > ? ORDER BY created_at")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
@ -345,8 +356,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] =
override def listExpiredIncomingPayments(from: Long, to: Long): Seq[IncomingPayment] = withMetrics("payments/list-incoming-expired") {
using(sqlite.prepareStatement("SELECT * FROM received_payments WHERE received_msat IS NULL AND created_at > ? AND created_at < ? AND expire_at < ? ORDER BY created_at")) { statement =>
statement.setLong(1, from)
statement.setLong(2, to)
@ -358,8 +370,9 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}
q
}
}
override def listPaymentsOverview(limit: Int): Seq[PlainPayment] = {
override def listPaymentsOverview(limit: Int): Seq[PlainPayment] = withMetrics("payments/list-overview") {
// This query is an UNION of the ``sent_payments`` and ``received_payments`` table
// - missing fields set to NULL when needed.
// - only retrieve incoming payments that did receive funds.

View file

@ -20,6 +20,7 @@ import java.sql.Connection
import fr.acinq.bitcoin.Crypto
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.PeersDb
import fr.acinq.eclair.db.sqlite.SqliteUtils.{codecSequence, getVersion, using}
import fr.acinq.eclair.wire._
@ -37,7 +38,7 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS peers (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)")
}
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, nodeaddress: NodeAddress): Unit = {
override def addOrUpdatePeer(nodeId: Crypto.PublicKey, nodeaddress: NodeAddress): Unit = withMetrics("peers/add-or-update") {
val data = CommonCodecs.nodeaddress.encode(nodeaddress).require.toByteArray
using(sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")) { update =>
update.setBytes(1, data)
@ -52,14 +53,14 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb {
}
}
override def removePeer(nodeId: Crypto.PublicKey): Unit = {
override def removePeer(nodeId: Crypto.PublicKey): Unit = withMetrics("peers/remove") {
using(sqlite.prepareStatement("DELETE FROM peers WHERE node_id=?")) { statement =>
statement.setBytes(1, nodeId.value.toArray)
statement.executeUpdate()
}
}
override def getPeer(nodeId: PublicKey): Option[NodeAddress] = {
override def getPeer(nodeId: PublicKey): Option[NodeAddress] = withMetrics("peers/get") {
using(sqlite.prepareStatement("SELECT data FROM peers WHERE node_id=?")) { statement =>
statement.setBytes(1, nodeId.value.toArray)
val rs = statement.executeQuery()
@ -67,7 +68,7 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb {
}
}
override def listPeers(): Map[PublicKey, NodeAddress] = {
override def listPeers(): Map[PublicKey, NodeAddress] = withMetrics("peers/list") {
using(sqlite.createStatement()) { statement =>
val rs = statement.executeQuery("SELECT node_id, data FROM peers")
var m: Map[PublicKey, NodeAddress] = Map()

View file

@ -20,6 +20,7 @@ import java.sql.Connection
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel.{Command, HasHtlcId}
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.PendingRelayDb
import fr.acinq.eclair.wire.CommandCodecs.cmdCodec
@ -39,7 +40,7 @@ class SqlitePendingRelayDb(sqlite: Connection) extends PendingRelayDb {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS pending_relay (channel_id BLOB NOT NULL, htlc_id INTEGER NOT NULL, data BLOB NOT NULL, PRIMARY KEY(channel_id, htlc_id))")
}
override def addPendingRelay(channelId: ByteVector32, cmd: Command with HasHtlcId): Unit = {
override def addPendingRelay(channelId: ByteVector32, cmd: Command with HasHtlcId): Unit = withMetrics("pending-relay/add") {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO pending_relay VALUES (?, ?, ?)")) { statement =>
statement.setBytes(1, channelId.toArray)
statement.setLong(2, cmd.id)
@ -48,7 +49,7 @@ class SqlitePendingRelayDb(sqlite: Connection) extends PendingRelayDb {
}
}
override def removePendingRelay(channelId: ByteVector32, htlcId: Long): Unit = {
override def removePendingRelay(channelId: ByteVector32, htlcId: Long): Unit = withMetrics("pending-relay/remove") {
using(sqlite.prepareStatement("DELETE FROM pending_relay WHERE channel_id=? AND htlc_id=?")) { statement =>
statement.setBytes(1, channelId.toArray)
statement.setLong(2, htlcId)
@ -56,7 +57,7 @@ class SqlitePendingRelayDb(sqlite: Connection) extends PendingRelayDb {
}
}
override def listPendingRelay(channelId: ByteVector32): Seq[Command with HasHtlcId] = {
override def listPendingRelay(channelId: ByteVector32): Seq[Command with HasHtlcId] = withMetrics("pending-relay/list-channel") {
using(sqlite.prepareStatement("SELECT data FROM pending_relay WHERE channel_id=?")) { statement =>
statement.setBytes(1, channelId.toArray)
val rs = statement.executeQuery()
@ -64,7 +65,7 @@ class SqlitePendingRelayDb(sqlite: Connection) extends PendingRelayDb {
}
}
override def listPendingRelay(): Set[(ByteVector32, Long)] = {
override def listPendingRelay(): Set[(ByteVector32, Long)] = withMetrics("pending-relay/list") {
using(sqlite.prepareStatement("SELECT channel_id, htlc_id FROM pending_relay")) { statement =>
val rs = statement.executeQuery()
var q: Queue[(ByteVector32, Long)] = Queue()

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Status, SupervisorStrategy, Terminated}
import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated}
import akka.event.Logging.MDC
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.Logs.LogCategory
@ -425,7 +425,8 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto
}
def scheduleNextPing(): Unit = {
setTimer(SEND_PING_TIMER, SendPing, 30 seconds)
log.debug("next ping scheduled in {}", nodeParams.pingInterval)
setTimer(SEND_PING_TIMER, SendPing, nodeParams.pingInterval)
}
initialize()

View file

@ -451,13 +451,16 @@ object PaymentRequest {
}
def decode(input: String): Option[MilliSatoshi] =
input match {
(input match {
case "" => None
case a if a.last == 'p' => Some(MilliSatoshi(a.dropRight(1).toLong / 10L)) // 1 pico-bitcoin == 10 milli-satoshis
case a if a.last == 'n' => Some(MilliSatoshi(a.dropRight(1).toLong * 100L))
case a if a.last == 'u' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000L))
case a if a.last == 'm' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L))
case a => Some(MilliSatoshi(a.toLong * 100000000000L))
}).flatMap {
case MilliSatoshi(0) => None
case amount => Some(amount)
}
def encode(amount: Option[MilliSatoshi]): String = {

View file

@ -21,10 +21,9 @@ import akka.actor.{ActorContext, ActorRef, PoisonPill, Status}
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Channel, ChannelCommandResponse}
import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, IncomingPaymentsDb, PaymentType}
import fr.acinq.eclair.db._
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.relay.CommandBuffer
import fr.acinq.eclair.payment.{IncomingPacket, PaymentReceived, PaymentRequest}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, Features, Logs, MilliSatoshi, NodeParams, randomBytes32}
@ -36,7 +35,7 @@ import scala.util.{Failure, Success, Try}
*
* Created by PM on 17/06/2016.
*/
class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBuffer: ActorRef) extends ReceiveHandler {
class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingPaymentsDb) extends ReceiveHandler {
import MultiPartHandler._
@ -82,7 +81,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
case Some(record) => validatePayment(p, record, nodeParams.currentBlockHeight) match {
case Some(cmdFail) =>
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment()
commandBuffer ! CommandBuffer.CommandSend(p.add.channelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.add.channelId, cmdFail)
case None =>
log.info("received payment for amount={} totalAmount={}", p.add.amountMsat, p.payload.totalAmount)
pendingPayments.get(p.add.paymentHash) match {
@ -97,7 +96,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
case None =>
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment()
val cmdFail = CMD_FAIL_HTLC(p.add.id, Right(IncorrectOrUnknownPaymentDetails(p.payload.totalAmount, nodeParams.currentBlockHeight)), commit = true)
commandBuffer ! CommandBuffer.CommandSend(p.add.channelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.add.channelId, cmdFail)
}
}
@ -107,7 +106,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
log.warning("payment with paidAmount={} failed ({})", parts.map(_.amount).sum, failure)
pendingPayments.get(paymentHash).foreach { case (_, handler: ActorRef) => handler ! PoisonPill }
parts.collect {
case p: MultiPartPaymentFSM.HtlcPart => commandBuffer ! CommandBuffer.CommandSend(p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true))
case p: MultiPartPaymentFSM.HtlcPart => PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true))
}
pendingPayments = pendingPayments - paymentHash
}
@ -127,13 +126,13 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
Logs.withMdc(log)(Logs.mdc(paymentHash_opt = Some(paymentHash))) {
failure match {
case Some(failure) => p match {
case p: MultiPartPaymentFSM.HtlcPart => commandBuffer ! CommandBuffer.CommandSend(p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true))
case p: MultiPartPaymentFSM.HtlcPart => PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FAIL_HTLC(p.htlc.id, Right(failure), commit = true))
}
case None => p match {
// NB: this case shouldn't happen unless the sender violated the spec, so it's ok that we take a slightly more
// expensive code path by fetching the preimage from DB.
case p: MultiPartPaymentFSM.HtlcPart => db.getIncomingPayment(paymentHash).foreach(record => {
commandBuffer ! CommandBuffer.CommandSend(p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, commit = true))
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, record.paymentPreimage, commit = true))
val received = PaymentReceived(paymentHash, PaymentReceived.PartialPayment(p.amount, p.htlc.channelId) :: Nil)
db.receiveIncomingPayment(paymentHash, p.amount, received.timestamp)
ctx.system.eventStream.publish(received)
@ -150,7 +149,7 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
})
db.receiveIncomingPayment(paymentHash, received.amount, received.timestamp)
parts.collect {
case p: MultiPartPaymentFSM.HtlcPart => commandBuffer ! CommandBuffer.CommandSend(p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, preimage, commit = true))
case p: MultiPartPaymentFSM.HtlcPart => PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, p.htlc.channelId, CMD_FULFILL_HTLC(p.htlc.id, preimage, commit = true))
}
postFulfill(received)
ctx.system.eventStream.publish(received)
@ -158,8 +157,6 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu
case GetPendingPayments => ctx.sender ! PendingPayments(pendingPayments.keySet)
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
case ChannelCommandResponse.Ok => // ignoring responses from channels
}

View file

@ -29,10 +29,10 @@ trait ReceiveHandler {
/**
* Generic payment handler that delegates handling of incoming messages to a list of handlers.
*/
class PaymentHandler(nodeParams: NodeParams, commandBuffer: ActorRef) extends Actor with DiagnosticActorLogging {
class PaymentHandler(nodeParams: NodeParams, register: ActorRef) extends Actor with DiagnosticActorLogging {
// we do this instead of sending it to ourselves, otherwise there is no guarantee that this would be the first processed message
private val defaultHandler = new MultiPartHandler(nodeParams, nodeParams.db.payments, commandBuffer)
private val defaultHandler = new MultiPartHandler(nodeParams, register, nodeParams.db.payments)
override def receive: Receive = normal(defaultHandler.handle(context, log))
@ -47,5 +47,5 @@ class PaymentHandler(nodeParams: NodeParams, commandBuffer: ActorRef) extends Ac
}
object PaymentHandler {
def props(nodeParams: NodeParams, commandBuffer: ActorRef): Props = Props(new PaymentHandler(nodeParams, commandBuffer))
def props(nodeParams: NodeParams, register: ActorRef): Props = Props(new PaymentHandler(nodeParams, register))
}

View file

@ -21,6 +21,7 @@ import akka.event.Logging.MDC
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.PendingRelayDb
import fr.acinq.eclair.payment.IncomingPacket
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment.relay.Relayer.{ChannelUpdates, NodeChannels, OutgoingChannel}
@ -36,7 +37,7 @@ import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, nodeFee}
* The Channel Relayer is used to relay a single upstream HTLC to a downstream channel.
* It selects the best channel to use to relay and retries using other channels in case a local failure happens.
*/
class ChannelRelayer(nodeParams: NodeParams, relayer: ActorRef, register: ActorRef, commandBuffer: ActorRef) extends Actor with DiagnosticActorLogging {
class ChannelRelayer(nodeParams: NodeParams, relayer: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging {
import ChannelRelayer._
@ -49,7 +50,7 @@ class ChannelRelayer(nodeParams: NodeParams, relayer: ActorRef, register: ActorR
case RelayFailure(cmdFail) =>
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
log.info(s"rejecting htlc #${r.add.id} from channelId=${r.add.channelId} to shortChannelId=${r.payload.outgoingChannelId} reason=${cmdFail.reason}")
commandBuffer ! CommandBuffer.CommandSend(r.add.channelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, r.add.channelId, cmdFail)
case RelaySuccess(selectedShortChannelId, cmdAdd) =>
log.info(s"forwarding htlc #${r.add.id} from channelId=${r.add.channelId} to shortChannelId=$selectedShortChannelId")
register ! Register.ForwardShortId(selectedShortChannelId, cmdAdd)
@ -59,7 +60,7 @@ class ChannelRelayer(nodeParams: NodeParams, relayer: ActorRef, register: ActorR
log.warning(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${add.id}")
val cmdFail = CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
commandBuffer ! CommandBuffer.CommandSend(add.channelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, add.channelId, cmdFail)
case Status.Failure(addFailed: AddHtlcFailed) => addFailed.origin match {
case Origin.Relayed(originChannelId, originHtlcId, _, _) => addFailed.originalCommand match {
@ -71,13 +72,11 @@ class ChannelRelayer(nodeParams: NodeParams, relayer: ActorRef, register: ActorR
val cmdFail = CMD_FAIL_HTLC(originHtlcId, Right(failure), commit = true)
Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel)
log.info(s"rejecting htlc #$originHtlcId from channelId=$originChannelId reason=${cmdFail.reason}")
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, originChannelId, cmdFail)
}
case _ => throw new IllegalArgumentException(s"channel relayer received unexpected failure: $addFailed")
}
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
case ChannelCommandResponse.Ok => // ignoring responses from channels
}
@ -94,7 +93,7 @@ class ChannelRelayer(nodeParams: NodeParams, relayer: ActorRef, register: ActorR
object ChannelRelayer {
def props(nodeParams: NodeParams, relayer: ActorRef, register: ActorRef, commandBuffer: ActorRef) = Props(classOf[ChannelRelayer], nodeParams, relayer, register, commandBuffer)
def props(nodeParams: NodeParams, relayer: ActorRef, register: ActorRef) = Props(new ChannelRelayer(nodeParams, relayer, register))
case class RelayHtlc(r: IncomingPacket.ChannelRelayPacket, previousFailures: Seq[AddHtlcFailed], channelUpdates: ChannelUpdates, node2channels: NodeChannels)

View file

@ -1,69 +0,0 @@
/*
* 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.payment.relay
import akka.actor.{Actor, ActorLogging, ActorRef}
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.channel._
/**
* We store [[CMD_FULFILL_HTLC]]/[[CMD_FAIL_HTLC]]/[[CMD_FAIL_MALFORMED_HTLC]]
* in a database because we don't want to lose preimages, or to forget to fail
* incoming htlcs, which would lead to unwanted channel closings.
*/
class CommandBuffer(nodeParams: NodeParams, register: ActorRef) extends Actor with ActorLogging {
import CommandBuffer._
val db = nodeParams.db.pendingRelay
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
override def receive: Receive = {
case CommandSend(channelId, cmd) =>
register forward Register.Forward(channelId, cmd)
// we store the command in a db (note that this happens *after* forwarding the command to the channel, so we don't add latency)
db.addPendingRelay(channelId, cmd)
case CommandAck(channelId, htlcId) =>
log.debug(s"fulfill/fail acked for channelId=$channelId htlcId=$htlcId")
db.removePendingRelay(channelId, htlcId)
case ChannelStateChanged(channel, _, _, WAIT_FOR_INIT_INTERNAL | OFFLINE | SYNCING, NORMAL | SHUTDOWN | CLOSING, d: HasCommitments) =>
db.listPendingRelay(d.channelId) match {
case Nil => ()
case cmds =>
log.info(s"re-sending ${cmds.size} unacked fulfills/fails to channel ${d.channelId}")
cmds.foreach(channel ! _) // they all have commit = false
channel ! CMD_SIGN // so we can sign all of them at once
}
case _: ChannelStateChanged => () // ignored
}
}
object CommandBuffer {
case class CommandSend[T <: Command with HasHtlcId](channelId: ByteVector32, cmd: T)
case class CommandAck(channelId: ByteVector32, htlcId: Long)
}

View file

@ -23,6 +23,7 @@ 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.db.PendingRelayDb
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
@ -46,7 +47,7 @@ import scala.collection.immutable.Queue
* It aggregates incoming HTLCs (in case multi-part was used upstream) and then forwards the requested amount (using the
* router to find a route to the remote node and potentially splitting the payment using multi-part).
*/
class NodeRelayer(nodeParams: NodeParams, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging {
class NodeRelayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends Actor with DiagnosticActorLogging {
import NodeRelayer._
@ -135,9 +136,6 @@ class NodeRelayer(nodeParams: NodeParams, router: ActorRef, commandBuffer: Actor
rejectPayment(p.upstream, translateError(failures, p.nextPayload.outgoingNodeId))
})
context become main(pendingIncoming, pendingOutgoing - paymentHash)
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
}
def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
@ -179,7 +177,7 @@ class NodeRelayer(nodeParams: NodeParams, router: ActorRef, commandBuffer: Actor
private def rejectHtlc(htlcId: Long, channelId: ByteVector32, amount: MilliSatoshi, failure: Option[FailureMessage] = None): Unit = {
val failureMessage = failure.getOrElse(IncorrectOrUnknownPaymentDetails(amount, nodeParams.currentBlockHeight))
commandBuffer ! CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(failureMessage), commit = true))
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, channelId, CMD_FAIL_HTLC(htlcId, Right(failureMessage), commit = true))
}
private def rejectPayment(upstream: Upstream.TrampolineRelayed, failure: Option[FailureMessage]): Unit = {
@ -189,7 +187,7 @@ class NodeRelayer(nodeParams: NodeParams, router: ActorRef, commandBuffer: Actor
private def fulfillPayment(upstream: Upstream.TrampolineRelayed, paymentPreimage: ByteVector32): Unit = upstream.adds.foreach(add => {
val cmdFulfill = CMD_FULFILL_HTLC(add.id, paymentPreimage, commit = true)
commandBuffer ! CommandBuffer.CommandSend(add.channelId, cmdFulfill)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, add.channelId, cmdFulfill)
})
override def mdc(currentMessage: Any): MDC = {
@ -209,7 +207,7 @@ class NodeRelayer(nodeParams: NodeParams, router: ActorRef, commandBuffer: Actor
object NodeRelayer {
def props(nodeParams: NodeParams, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) = Props(new NodeRelayer(nodeParams, router, commandBuffer, register))
def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef) = Props(new NodeRelayer(nodeParams, router, register))
/**
* We start by aggregating an incoming HTLC set. Once we received the whole set, we will compute a route to the next

View file

@ -30,9 +30,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc.outgoing
import fr.acinq.eclair.transactions.OutgoingHtlc
import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc}
import fr.acinq.eclair.{Features, LongToBtcAmount, NodeParams}
import scodec.bits.ByteVector
import scala.compat.Platform
import scala.concurrent.Promise
import scala.util.Try
@ -51,7 +49,7 @@ import scala.util.Try
* payment (because of multi-part): we have lost the intermediate state necessary to retry that payment, so we need to
* wait for the partial HTLC set sent downstream to either fail or fulfill the payment in our DB.
*/
class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with ActorLogging {
class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with ActorLogging {
import PostRestartHtlcCleaner._
@ -118,8 +116,6 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in
case GetBrokenHtlcs => sender ! brokenHtlcs
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
case ChannelCommandResponse.Ok => // ignoring responses from channels
}
@ -161,7 +157,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in
log.info(s"received preimage for paymentHash=${fulfilledHtlc.paymentHash}: fulfilling ${origins.length} HTLCs upstream")
origins.foreach { case (channelId, htlcId) =>
Metrics.Resolved.withTag(Tags.Success, value = true).withTag(Metrics.Relayed, value = true).increment()
commandBuffer ! CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, commit = true))
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, channelId, CMD_FULFILL_HTLC(htlcId, paymentPreimage, commit = true))
}
}
val relayedOut1 = relayedOut diff Set((fulfilledHtlc.channelId, fulfilledHtlc.id))
@ -210,7 +206,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in
Metrics.Resolved.withTag(Tags.Success, value = false).withTag(Metrics.Relayed, value = true).increment()
// We don't bother decrypting the downstream failure to forward a more meaningful error upstream, it's
// very likely that it won't be actionable anyway because of our node restart.
commandBuffer ! CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
}
case _: Origin.Relayed =>
Metrics.Unhandled.withTag(Metrics.Hint, origin.getClass.getSimpleName).increment()
@ -232,7 +228,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in
object PostRestartHtlcCleaner {
def props(nodeParams: NodeParams, commandBuffer: ActorRef, initialized: Option[Promise[Done]] = None) = Props(classOf[PostRestartHtlcCleaner], nodeParams, commandBuffer, initialized)
def props(nodeParams: NodeParams, register: ActorRef, initialized: Option[Promise[Done]] = None) = Props(new PostRestartHtlcCleaner(nodeParams, register, initialized))
case object GetBrokenHtlcs
@ -375,7 +371,7 @@ object PostRestartHtlcCleaner {
/**
* We store [[CMD_FULFILL_HTLC]]/[[CMD_FAIL_HTLC]]/[[CMD_FAIL_MALFORMED_HTLC]] in a database
* (see [[fr.acinq.eclair.payment.relay.CommandBuffer]]) because we don't want to lose preimages, or to forget to fail
* (see [[fr.acinq.eclair.db.PendingRelayDb]]) because we don't want to lose preimages, or to forget to fail
* incoming htlcs, which would lead to unwanted channel closings.
*
* Because of the way our watcher works, in a scenario where a downstream channel has gone to the blockchain, it may

View file

@ -25,6 +25,7 @@ import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.PendingRelayDb
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Announcements
@ -65,7 +66,7 @@ object Origin {
* It also receives channel HTLC events (fulfill / failed) and relays those to the appropriate handlers.
* It also maintains an up-to-date view of local channel balances.
*/
class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging {
class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) extends Actor with DiagnosticActorLogging {
import Relayer._
@ -77,9 +78,9 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged])
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned])
private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, commandBuffer, initialized), "post-restart-htlc-cleaner")
private val channelRelayer = context.actorOf(ChannelRelayer.props(nodeParams, self, register, commandBuffer), "channel-relayer")
private val nodeRelayer = context.actorOf(NodeRelayer.props(nodeParams, router, commandBuffer, register), "node-relayer")
private val postRestartCleaner = context.actorOf(PostRestartHtlcCleaner.props(nodeParams, register, initialized), "post-restart-htlc-cleaner")
private val channelRelayer = context.actorOf(ChannelRelayer.props(nodeParams, self, register), "channel-relayer")
private val nodeRelayer = context.actorOf(NodeRelayer.props(nodeParams, router, register), "node-relayer")
override def receive: Receive = main(Map.empty, new mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId])
@ -132,7 +133,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
case Right(r: IncomingPacket.NodeRelayPacket) =>
if (!nodeParams.enableTrampolinePayment) {
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} to nodeId=${r.innerPayload.outgoingNodeId} reason=trampoline disabled")
commandBuffer ! CommandBuffer.CommandSend(add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing), commit = true))
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, add.channelId, CMD_FAIL_HTLC(add.id, Right(RequiredNodeFeatureMissing), commit = true))
} else {
nodeRelayer forward r
}
@ -140,11 +141,11 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
log.warning(s"couldn't parse onion: reason=${badOnion.message}")
val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.code, commit = true)
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=malformed onionHash=${cmdFail.onionHash} failureCode=${cmdFail.failureCode}")
commandBuffer ! CommandBuffer.CommandSend(add.channelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, add.channelId, cmdFail)
case Left(failure) =>
log.warning(s"rejecting htlc #${add.id} from channelId=${add.channelId} reason=$failure")
val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), commit = true)
commandBuffer ! CommandBuffer.CommandSend(add.channelId, cmdFail)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, add.channelId, cmdFail)
}
case Status.Failure(addFailed: AddHtlcFailed) => addFailed.origin match {
@ -160,7 +161,7 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
case Origin.Local(_, Some(sender)) => sender ! ff
case Origin.Relayed(originChannelId, originHtlcId, amountIn, amountOut) =>
val cmd = CMD_FULFILL_HTLC(originHtlcId, ff.paymentPreimage, commit = true)
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, originChannelId, cmd)
context.system.eventStream.publish(ChannelPaymentRelayed(amountIn, amountOut, ff.htlc.paymentHash, originChannelId, ff.htlc.channelId))
case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff
case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! ff
@ -176,13 +177,11 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
case ForwardRemoteFailMalformed(fail, _, _) => CMD_FAIL_MALFORMED_HTLC(originHtlcId, fail.onionHash, fail.failureCode, commit = true)
case _: ForwardOnChainFail => CMD_FAIL_HTLC(originHtlcId, Right(PermanentChannelFailure), commit = true)
}
commandBuffer ! CommandBuffer.CommandSend(originChannelId, cmd)
PendingRelayDb.safeSend(register, nodeParams.db.pendingRelay, originChannelId, cmd)
case Origin.TrampolineRelayed(_, None) => postRestartCleaner forward ff
case Origin.TrampolineRelayed(_, Some(paymentSender)) => paymentSender ! ff
}
case ack: CommandBuffer.CommandAck => commandBuffer forward ack
case ChannelCommandResponse.Ok => () // ignoring responses from channels
}
@ -201,8 +200,8 @@ class Relayer(nodeParams: NodeParams, router: ActorRef, register: ActorRef, comm
object Relayer extends Logging {
def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, commandBuffer: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) =
Props(new Relayer(nodeParams, router, register, commandBuffer, paymentHandler, initialized))
def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef, paymentHandler: ActorRef, initialized: Option[Promise[Done]] = None) =
Props(new Relayer(nodeParams, router, register, paymentHandler, initialized))
type ChannelUpdates = Map[ShortChannelId, OutgoingChannel]
type NodeChannels = mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]

View file

@ -66,7 +66,6 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
watcher.ref,
paymentHandler.ref,
register.ref,
commandBuffer.ref,
relayer.ref,
router.ref,
switchboard.ref,

View file

@ -26,7 +26,8 @@ import org.scalatest.{BeforeAndAfterAll, TestSuite}
*/
abstract class TestKitBaseClass extends TestKit(ActorSystem("test")) with TestSuite with BeforeAndAfterAll {
override def afterAll {
override def afterAll(): Unit = {
TestKit.shutdownActorSystem(system)
}
}

View file

@ -26,6 +26,7 @@ import com.whisk.docker.DockerReadyChecker
import fr.acinq.bitcoin.{Block, Btc, ByteVector32, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse}
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse, SSL}
import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress
@ -114,14 +115,14 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
probe.expectMsgType[JValue]
awaitCond({
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
val GetBalanceResponse(_, unconfirmed1) = getBalance(probe)
unconfirmed1 == unconfirmed + 100000000.sat
}, max = 30 seconds, interval = 1 second)
// confirm our tx
generateBlocks(bitcoincli, 1)
awaitCond({
val GetBalanceResponse(confirmed1, unconfirmed1) = getBalance(probe)
val GetBalanceResponse(confirmed1, _) = getBalance(probe)
confirmed1 == confirmed + 100000000.sat
}, max = 30 seconds, interval = 1 second)
@ -156,10 +157,11 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
TxOut(amount, fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash))
), lockTime = 0L)
val btcWallet = new BitcoinCoreWallet(bitcoinrpcclient)
val btcClient = new ExtendedBitcoinClient(bitcoinrpcclient)
val future = for {
FundTransactionResponse(tx1, _, _) <- btcWallet.fundTransaction(tx, false, 10000)
FundTransactionResponse(tx1, _, _) <- btcWallet.fundTransaction(tx, lockUnspents = false, 10000)
SignTransactionResponse(tx2, true) <- btcWallet.signTransaction(tx1)
txid <- btcWallet.publishTransaction(tx2)
txid <- btcClient.publishTransaction(tx2)
} yield txid
Await.result(future, 10 seconds)
@ -193,7 +195,7 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
unconfirmed1 - unconfirmed === 100000000L.sat
}, max = 30 seconds, interval = 1 second)
val TransactionReceived(tx, 0, received, sent, _, _) = listener.receiveOne(5 seconds)
val TransactionReceived(tx, 0, received, _, _, _) = listener.receiveOne(5 seconds)
assert(tx.txid === ByteVector32.fromValidHex(txid))
assert(received === 100000000.sat)
@ -254,7 +256,7 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
val JString(address) = probe.expectMsgType[JValue]
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
probe.send(wallet, CompleteTransaction(tx, 20000))
val CompleteTransactionResponse(tx1, fee1, None) = probe.expectMsgType[CompleteTransactionResponse]
val CompleteTransactionResponse(tx1, _, None) = probe.expectMsgType[CompleteTransactionResponse]
// send it ourselves
logger.info(s"sending 1 btc to $address with tx ${tx1.txid}")
@ -281,7 +283,7 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
val JString(address) = probe.expectMsgType[JValue]
val tmp = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
probe.send(wallet, CompleteTransaction(tmp, 20000))
val CompleteTransactionResponse(tx, fee1, None) = probe.expectMsgType[CompleteTransactionResponse]
val CompleteTransactionResponse(tx, _, None) = probe.expectMsgType[CompleteTransactionResponse]
probe.send(wallet, CancelTransaction(tx))
probe.expectMsg(CancelTransactionResponse(tx))
tx
@ -291,16 +293,16 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
val JString(address) = probe.expectMsgType[JValue]
val tmp = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L)
probe.send(wallet, CompleteTransaction(tmp, 20000))
val CompleteTransactionResponse(tx, fee1, None) = probe.expectMsgType[CompleteTransactionResponse]
val CompleteTransactionResponse(tx, _, None) = probe.expectMsgType[CompleteTransactionResponse]
probe.send(wallet, CancelTransaction(tx))
probe.expectMsg(CancelTransactionResponse(tx))
tx
}
probe.send(wallet, IsDoubleSpent(tx1))
probe.expectMsg(IsDoubleSpentResponse(tx1, false))
probe.expectMsg(IsDoubleSpentResponse(tx1, isDoubleSpent = false))
probe.send(wallet, IsDoubleSpent(tx2))
probe.expectMsg(IsDoubleSpentResponse(tx2, false))
probe.expectMsg(IsDoubleSpentResponse(tx2, isDoubleSpent = false))
// publish tx1
probe.send(wallet, BroadcastTransaction(tx1))
@ -314,9 +316,9 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
// as long as tx1 is unconfirmed tx2 won't be considered double-spent
probe.send(wallet, IsDoubleSpent(tx1))
probe.expectMsg(IsDoubleSpentResponse(tx1, false))
probe.expectMsg(IsDoubleSpentResponse(tx1, isDoubleSpent = false))
probe.send(wallet, IsDoubleSpent(tx2))
probe.expectMsg(IsDoubleSpentResponse(tx2, false))
probe.expectMsg(IsDoubleSpentResponse(tx2, isDoubleSpent = false))
generateBlocks(bitcoincli, 2)
@ -328,9 +330,9 @@ class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitc
// tx2 is double-spent
probe.send(wallet, IsDoubleSpent(tx1))
probe.expectMsg(IsDoubleSpentResponse(tx1, false))
probe.expectMsg(IsDoubleSpentResponse(tx1, isDoubleSpent = false))
probe.send(wallet, IsDoubleSpent(tx2))
probe.expectMsg(IsDoubleSpentResponse(tx2, true))
probe.expectMsg(IsDoubleSpentResponse(tx2, isDoubleSpent = true))
}
test("use all available balance") {

View file

@ -438,7 +438,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val t = FuzzTest(
isFunder = Random.nextInt(2) == 0,
pendingHtlcs = Random.nextInt(10),
feeRatePerKw = Random.nextInt(10000),
feeRatePerKw = Random.nextInt(10000).max(1),
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,
@ -466,7 +466,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
val t = FuzzTest(
isFunder = Random.nextInt(2) == 0,
pendingHtlcs = Random.nextInt(10),
feeRatePerKw = Random.nextInt(10000),
feeRatePerKw = Random.nextInt(10000).max(1),
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,

View file

@ -31,7 +31,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.receive.PaymentHandler
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire._
@ -61,6 +61,8 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT
override def withFixture(test: OneArgTest): Outcome = {
val fuzzy = test.tags.contains("fuzzy")
val pipe = system.actorOf(Props(new FuzzyPipe(fuzzy)))
val aliceParams = Alice.nodeParams
val bobParams = Bob.nodeParams
val alicePeer = TestProbe()
val bobPeer = TestProbe()
TestUtils.forwardOutgoingToPipe(alicePeer, pipe)
@ -69,15 +71,13 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT
val bob2blockchain = TestProbe()
val registerA = system.actorOf(Props(new TestRegister()))
val registerB = system.actorOf(Props(new TestRegister()))
val commandBufferA = system.actorOf(Props(new TestCommandBuffer(Alice.nodeParams, registerA)))
val commandBufferB = system.actorOf(Props(new TestCommandBuffer(Bob.nodeParams, registerB)))
val paymentHandlerA = system.actorOf(Props(new PaymentHandler(Alice.nodeParams, commandBufferA)))
val paymentHandlerB = system.actorOf(Props(new PaymentHandler(Bob.nodeParams, commandBufferB)))
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams, TestProbe().ref, registerA, commandBufferA, paymentHandlerA))
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams, TestProbe().ref, registerB, commandBufferB, paymentHandlerB))
val paymentHandlerA = system.actorOf(Props(new PaymentHandler(aliceParams, registerA)))
val paymentHandlerB = system.actorOf(Props(new PaymentHandler(bobParams, registerB)))
val relayerA = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, registerA, paymentHandlerA))
val relayerB = system.actorOf(Relayer.props(bobParams, TestProbe().ref, registerB, paymentHandlerB))
val wallet = new TestWallet
val alice: TestFSMRef[State, Data, Channel] = new TestFSMRef(system, Channel.props(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA, None), alicePeer.ref, randomName)
val bob: TestFSMRef[State, Data, Channel] = new TestFSMRef(system, Channel.props(Bob.nodeParams, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, relayerB, None), bobPeer.ref, randomName)
val alice: TestFSMRef[State, Data, Channel] = new TestFSMRef(system, Props(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, relayerA)), alicePeer.ref, randomName)
val bob: TestFSMRef[State, Data, Channel] = new TestFSMRef(system, Props(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, relayerB)), bobPeer.ref, randomName)
within(30 seconds) {
val aliceInit = Init(Alice.channelParams.features)
val bobInit = Init(Bob.channelParams.features)
@ -114,16 +114,6 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT
}
}
class TestCommandBuffer(nodeParams: NodeParams, register: ActorRef) extends CommandBuffer(nodeParams, register) {
def filterRemoteEvents: Receive = {
// This is needed because we use the same actor system for A and B's CommandBuffers. If A receives an event from
// B's channel and it has a pending relay with the same htlcId as one of B's htlc, it may mess up B's state.
case ChannelStateChanged(_, _, remoteNodeId, _, _, _) if remoteNodeId == nodeParams.nodeId => ()
}
override def receive: Receive = filterRemoteEvents orElse super.receive
}
class SenderActor(sendChannel: TestFSMRef[State, Data, Channel], paymentHandler: ActorRef, latch: CountDownLatch, count: Int) extends Actor with ActorLogging {
// we don't want to be below htlcMinimumMsat

View file

@ -35,7 +35,7 @@ import fr.acinq.eclair.channel.{ChannelErrorOccurred, _}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin}
import fr.acinq.eclair.payment.relay.Origin
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing}
import fr.acinq.eclair.transactions.Transactions
@ -1238,7 +1238,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
sender.send(bob, CMD_FULFILL_HTLC(42, randomBytes32)) // this will fail
sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42)))
relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42))
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty)
}
private def testUpdateFulfillHtlc(f: FixtureParam) = {
@ -1363,12 +1363,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("recv CMD_FAIL_HTLC (acknowledge in case of failure)") { f =>
import f._
val sender = TestProbe()
val r = randomBytes32
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) // this will fail
sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42)))
relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42))
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty)
}
test("recv CMD_FAIL_MALFORMED_HTLC") { f =>
@ -1413,7 +1412,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, ByteVector32.Zeroes, FailureMessageCodecs.BADONION)) // this will fail
sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42)))
relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42))
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty)
}
private def testUpdateFailHtlc(f: FixtureParam) = {

View file

@ -27,7 +27,6 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.payment.relay.CommandBuffer
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx
import fr.acinq.eclair.wire._
@ -404,11 +403,80 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
assert(!Announcements.isEnabled(update.channelUpdate.channelFlags))
}
test("replay pending commands when going back to NORMAL") { f =>
import f._
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, INPUT_DISCONNECTED)
sender.send(bob, INPUT_DISCONNECTED)
awaitCond(alice.stateName == OFFLINE)
awaitCond(bob.stateName == OFFLINE)
// We simulate a pending fulfill
bob.underlyingActor.nodeParams.db.pendingRelay.addPendingRelay(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true))
// then we reconnect them
sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit))
sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit))
// peers exchange channel_reestablish messages
alice2bob.expectMsgType[ChannelReestablish]
bob2alice.expectMsgType[ChannelReestablish]
alice2bob.forward(bob)
bob2alice.forward(alice)
bob2alice.expectMsgType[UpdateFulfillHtlc]
}
test("replay pending commands when going back to SHUTDOWN") { f =>
import f._
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
// We initiate a mutual close
sender.send(alice, CMD_CLOSE(None))
alice2bob.expectMsgType[Shutdown]
alice2bob.forward(bob)
bob2alice.expectMsgType[Shutdown]
bob2alice.forward(alice)
sender.send(alice, INPUT_DISCONNECTED)
sender.send(bob, INPUT_DISCONNECTED)
awaitCond(alice.stateName == OFFLINE)
awaitCond(bob.stateName == OFFLINE)
// We simulate a pending fulfill
bob.underlyingActor.nodeParams.db.pendingRelay.addPendingRelay(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true))
// then we reconnect them
sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit))
sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit))
// peers exchange channel_reestablish messages
alice2bob.expectMsgType[ChannelReestablish]
bob2alice.expectMsgType[ChannelReestablish]
alice2bob.forward(bob)
bob2alice.forward(alice)
// peers re-exchange shutdown messages
alice2bob.expectMsgType[Shutdown]
bob2alice.expectMsgType[Shutdown]
alice2bob.forward(bob)
bob2alice.forward(alice)
bob2alice.expectMsgType[UpdateFulfillHtlc]
}
test("pending non-relayed fulfill htlcs will timeout upstream") { f =>
import f._
val sender = TestProbe()
val register = TestProbe()
val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref))
val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
@ -426,7 +494,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// We simulate a pending fulfill on that HTLC but not relayed.
// When it is close to expiring upstream, we should close the channel.
sender.send(commandBuffer, CommandBuffer.CommandSend(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)))
bob.underlyingActor.nodeParams.db.pendingRelay.addPendingRelay(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true))
sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong))
val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred]
@ -448,8 +516,6 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
test("pending non-relayed fail htlcs will timeout upstream") { f =>
import f._
val sender = TestProbe()
val register = TestProbe()
val commandBuffer = TestActorRef(new CommandBuffer(bob.underlyingActor.nodeParams, register.ref))
val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
@ -460,7 +526,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
// We simulate a pending failure on that HTLC.
// Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose.
sender.send(commandBuffer, CommandBuffer.CommandSend(htlc.channelId, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, 0)))))
sender.send(bob, CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, 0))))
sender.send(bob, CurrentBlockCount((htlc.cltvExpiry - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeoutBlocks).toLong))
bob2blockchain.expectNoMsg(250 millis)

View file

@ -28,7 +28,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin}
import fr.acinq.eclair.payment.relay.Origin
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.wire.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
@ -151,7 +151,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
sender.send(bob, CMD_FULFILL_HTLC(42, randomBytes32)) // this will fail
sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42)))
relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42))
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty)
}
test("recv UpdateFulfillHtlc") { f =>
@ -223,7 +223,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
sender.send(bob, CMD_FAIL_HTLC(42, Right(PermanentChannelFailure))) // this will fail
sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42)))
relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42))
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty)
}
test("recv CMD_FAIL_MALFORMED_HTLC") { f =>
@ -262,7 +262,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN]
sender.send(bob, CMD_FAIL_MALFORMED_HTLC(42, randomBytes32, FailureMessageCodecs.BADONION)) // this will fail
sender.expectMsg(Failure(UnknownHtlcId(channelId(bob), 42)))
relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42))
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(initialState.channelId).isEmpty)
}
test("recv UpdateFailHtlc") { f =>

View file

@ -30,7 +30,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Origin}
import fr.acinq.eclair.payment.relay.Origin
import fr.acinq.eclair.transactions.{Scripts, Transactions}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{CltvExpiry, LongToBtcAmount, TestConstants, TestKitBaseClass, randomBytes32}
@ -99,7 +99,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
relayerA.expectMsgType[ForwardFulfill]
crossSign(bob, alice, bob2alice, alice2bob)
// bob confirms that it has forwarded the fulfill to alice
relayerB.expectMsgType[CommandBuffer.CommandAck]
awaitCond(bob.underlyingActor.nodeParams.db.pendingRelay.listPendingRelay(htlc.channelId).isEmpty)
val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs
bobCommitTx1 :: bobCommitTx2 :: Nil
}).flatten

View file

@ -30,7 +30,7 @@ import org.scalatest.funsuite.AnyFunSuiteLike
class FileBackupHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike {
test("process backups") {
ignore("process backups") {
val db = TestConstants.inMemoryDb()
val wip = new File(TestUtils.BUILD_DIRECTORY, s"wip-${UUID.randomUUID()}")
val dest = new File(TestUtils.BUILD_DIRECTORY, s"backup-${UUID.randomUUID()}")

View file

@ -22,14 +22,13 @@ import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.eclair.FeatureSupport.Optional
import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, PaymentSecret, VariableLengthOnion}
import fr.acinq.eclair.TestConstants.Alice
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register}
import fr.acinq.eclair.db.IncomingPaymentStatus
import fr.acinq.eclair.payment.PaymentReceived.PartialPayment
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.payment.receive.MultiPartHandler.{GetPendingPayments, PendingPayments, ReceivePayment}
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart
import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler}
import fr.acinq.eclair.payment.relay.CommandBuffer
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{ActivatedFeature, CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomKey}
import org.scalatest.Outcome
@ -53,18 +52,18 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
ActivatedFeature(BasicMultiPartPayment, Optional)
))
case class FixtureParam(nodeParams: NodeParams, defaultExpiry: CltvExpiry, commandBuffer: TestProbe, eventListener: TestProbe, sender: TestProbe) {
lazy val normalHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, commandBuffer.ref))
lazy val mppHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = featuresWithMpp), commandBuffer.ref))
case class FixtureParam(nodeParams: NodeParams, defaultExpiry: CltvExpiry, register: TestProbe, eventListener: TestProbe, sender: TestProbe) {
lazy val normalHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, register.ref))
lazy val mppHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = featuresWithMpp), register.ref))
}
override def withFixture(test: OneArgTest): Outcome = {
within(30 seconds) {
val nodeParams = Alice.nodeParams
val commandBuffer = TestProbe()
val register = TestProbe()
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
withFixture(test.toNoArgTest(FixtureParam(nodeParams, CltvExpiryDelta(12).toCltvExpiry(nodeParams.currentBlockHeight), commandBuffer, eventListener, TestProbe())))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, CltvExpiryDelta(12).toCltvExpiry(nodeParams.currentBlockHeight), register, eventListener, TestProbe())))
}
}
@ -84,7 +83,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]]
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil))
@ -103,7 +102,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]]
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
val paymentReceived = eventListener.expectMsgType[PaymentReceived]
assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0) :: Nil))
@ -121,7 +120,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket)
sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
@ -231,7 +230,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.FinalLegacyPayload(add.amountMsat, add.cltvExpiry)))
commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]]
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
}
@ -246,7 +245,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired)
@ -261,7 +260,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(normalHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
}
@ -275,7 +274,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, CltvExpiryDelta(1).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
}
@ -289,7 +288,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
}
@ -303,7 +302,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
}
@ -317,7 +316,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
}
@ -332,14 +331,14 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// Invalid payment secret.
val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket)
sender.send(mppHandler, IncomingPacket.FinalPacket(add, Onion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse)))
val cmd = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]].cmd
val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)))
assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending)
}
test("PaymentHandler should handle multi-part payment timeout") { f =>
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis, features = featuresWithMpp)
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref))
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.register.ref))
// Partial payment missing additional parts.
f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 slow coffee"))
@ -356,10 +355,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
f.sender.send(handler, GetPendingPayments)
assert(f.sender.expectMsgType[PendingPayments].paymentHashes.nonEmpty)
val commands = f.commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]] :: f.commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]] :: Nil
val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil
assert(commands.toSet === Set(
CommandBuffer.CommandSend(ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)),
CommandBuffer.CommandSend(ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true))
Register.Forward(ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)),
Register.Forward(ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true))
))
awaitCond({
f.sender.send(handler, GetPendingPayments)
@ -368,7 +367,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// Extraneous HTLCs should be failed.
f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), Some(PaymentTimeout)))
f.commandBuffer.expectMsg(CommandBuffer.CommandSend(ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout), commit = true)))
f.register.expectMsg(Register.Forward(ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout), commit = true)))
// The payment should still be pending in DB.
val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash)
@ -377,7 +376,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("PaymentHandler should handle multi-part payment success") { f =>
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = featuresWithMpp)
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref))
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.register.ref))
f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 fast coffee"))
val pr = f.sender.expectMsgType[PaymentRequest]
@ -390,15 +389,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add3 = add2.copy(id = 43)
f.sender.send(handler, IncomingPacket.FinalPacket(add3, Onion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get)))
f.commandBuffer.expectMsg(CommandBuffer.CommandSend(add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)))
val cmd1 = f.commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]]
assert(cmd1.cmd.id === add1.id)
f.register.expectMsg(Register.Forward(add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)))
val cmd1 = f.register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
assert(cmd1.message.id === add1.id)
assert(cmd1.channelId === add1.channelId)
assert(Crypto.sha256(cmd1.cmd.r) === pr.paymentHash)
f.commandBuffer.expectMsg(CommandBuffer.CommandSend(add3.channelId, CMD_FULFILL_HTLC(add3.id, cmd1.cmd.r, commit = true)))
f.sender.send(handler, CommandBuffer.CommandAck(add1.channelId, add1.id))
f.commandBuffer.expectMsg(CommandBuffer.CommandAck(add1.channelId, add1.id))
assert(Crypto.sha256(cmd1.message.r) === pr.paymentHash)
f.register.expectMsg(Register.Forward(add3.channelId, CMD_FULFILL_HTLC(add3.id, cmd1.message.r, commit = true)))
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(pr.paymentHash, PartialPayment(800 msat, ByteVector32.One, 0) :: PartialPayment(200 msat, ByteVector32.Zeroes, 0) :: Nil))
@ -412,7 +408,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
// Extraneous HTLCs should be fulfilled.
f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, pr.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None))
f.commandBuffer.expectMsg(CommandBuffer.CommandSend(ByteVector32.One, CMD_FULFILL_HTLC(44, cmd1.cmd.r, commit = true)))
f.register.expectMsg(Register.Forward(ByteVector32.One, CMD_FULFILL_HTLC(44, cmd1.message.r, commit = true)))
assert(f.eventListener.expectMsgType[PaymentReceived].amount === 200.msat)
val received2 = nodeParams.db.payments.getIncomingPayment(pr.paymentHash)
assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1200.msat)
@ -423,7 +419,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
test("PaymentHandler should handle multi-part payment timeout then success") { f =>
val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis, features = featuresWithMpp)
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref))
val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.register.ref))
f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 coffee, no sugar"))
val pr = f.sender.expectMsgType[PaymentRequest]
@ -431,7 +427,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPacket.FinalPacket(add1, Onion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get)))
f.commandBuffer.expectMsg(CommandBuffer.CommandSend(ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)))
f.register.expectMsg(Register.Forward(ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)))
awaitCond({
f.sender.send(handler, GetPendingPayments)
f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty
@ -442,11 +438,11 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket)
f.sender.send(handler, IncomingPacket.FinalPacket(add3, Onion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get)))
val cmd1 = f.commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]]
val cmd1 = f.register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]]
assert(cmd1.channelId === add2.channelId)
assert(cmd1.cmd.id === 2)
assert(Crypto.sha256(cmd1.cmd.r) === pr.paymentHash)
f.commandBuffer.expectMsg(CommandBuffer.CommandSend(add3.channelId, CMD_FULFILL_HTLC(5, cmd1.cmd.r, commit = true)))
assert(cmd1.message.id === 2)
assert(Crypto.sha256(cmd1.message.r) === pr.paymentHash)
f.register.expectMsg(Register.Forward(add3.channelId, CMD_FULFILL_HTLC(5, cmd1.message.r, commit = true)))
val paymentReceived = f.eventListener.expectMsgType[PaymentReceived]
assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0))) === PaymentReceived(pr.paymentHash, PartialPayment(300 msat, ByteVector32.One, 0) :: PartialPayment(700 msat, ByteVector32.Zeroes, 0) :: Nil))

View file

@ -22,11 +22,11 @@ import akka.actor.ActorRef
import akka.testkit.{TestActorRef, TestProbe}
import fr.acinq.bitcoin.{Block, Crypto}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream}
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register, Upstream}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM
import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer}
import fr.acinq.eclair.payment.relay.NodeRelayer
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment}
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig
import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment
@ -49,22 +49,22 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
import NodeRelayerSpec._
case class FixtureParam(nodeParams: NodeParams, nodeRelayer: TestActorRef[NodeRelayer], relayer: TestProbe, outgoingPayFSM: TestProbe, commandBuffer: TestProbe, eventListener: TestProbe)
case class FixtureParam(nodeParams: NodeParams, nodeRelayer: TestActorRef[NodeRelayer], relayer: TestProbe, register: TestProbe, outgoingPayFSM: TestProbe, eventListener: TestProbe)
override def withFixture(test: OneArgTest): Outcome = {
within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams
val outgoingPayFSM = TestProbe()
val (router, commandBuffer, register, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe())
val (router, relayer, register, eventListener) = (TestProbe(), TestProbe(), TestProbe(), TestProbe())
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
class TestNodeRelayer extends NodeRelayer(nodeParams, router.ref, commandBuffer.ref, register.ref) {
class TestNodeRelayer extends NodeRelayer(nodeParams, router.ref, register.ref) {
override def spawnOutgoingPayFSM(cfg: SendPaymentConfig, multiPart: Boolean): ActorRef = {
outgoingPayFSM.ref ! cfg
outgoingPayFSM.ref
}
}
val nodeRelayer = TestActorRef(new TestNodeRelayer().asInstanceOf[NodeRelayer])
withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeRelayer, TestProbe(), outgoingPayFSM, commandBuffer, eventListener)))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeRelayer, relayer, register, outgoingPayFSM, eventListener)))
}
}
@ -78,7 +78,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val parts = incomingMultiPart.dropRight(1).map(i => MultiPartPaymentFSM.HtlcPart(incomingAmount, i.add))
sender.send(nodeRelayer, MultiPartPaymentFSM.MultiPartPaymentFailed(paymentHash, PaymentTimeout, Queue(parts: _*)))
incomingMultiPart.dropRight(1).foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(PaymentTimeout), commit = true))))
incomingMultiPart.dropRight(1).foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(PaymentTimeout), commit = true))))
sender.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -90,7 +90,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val partial = MultiPartPaymentFSM.HtlcPart(incomingAmount, UpdateAddHtlc(randomBytes32, 15, 100 msat, paymentHash, CltvExpiry(42000), TestConstants.emptyOnionPacket))
sender.send(nodeRelayer, MultiPartPaymentFSM.ExtraPaymentReceived(paymentHash, partial, Some(InvalidRealm)))
commandBuffer.expectMsg(CommandBuffer.CommandSend(partial.htlc.channelId, CMD_FAIL_HTLC(partial.htlc.id, Right(InvalidRealm), commit = true)))
register.expectMsg(Register.Forward(partial.htlc.channelId, CMD_FAIL_HTLC(partial.htlc.id, Right(InvalidRealm), commit = true)))
sender.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -112,7 +112,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
Onion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId),
nextTrampolinePacket)
relayer.send(nodeRelayer, i1)
commandBuffer.expectMsg(CommandBuffer.CommandSend(i1.add.channelId, CMD_FAIL_HTLC(i1.add.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)))
register.expectMsg(Register.Forward(i1.add.channelId, CMD_FAIL_HTLC(i1.add.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)))
// Receive new HTLC with different details, but for the same payment hash.
val i2 = IncomingPacket.NodeRelayPacket(
@ -121,7 +121,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
Onion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId),
nextTrampolinePacket)
relayer.send(nodeRelayer, i2)
commandBuffer.expectMsg(CommandBuffer.CommandSend(i2.add.channelId, CMD_FAIL_HTLC(i2.add.id, Right(IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight)), commit = true)))
register.expectMsg(Register.Forward(i2.add.channelId, CMD_FAIL_HTLC(i2.add.id, Right(IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight)), commit = true)))
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -135,7 +135,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
relayer.send(nodeRelayer, p)
val failure = IncorrectOrUnknownPaymentDetails(2000000 msat, nodeParams.currentBlockHeight)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(failure), commit = true)))
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -150,8 +150,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
relayer.send(nodeRelayer, p2)
val failure = IncorrectOrUnknownPaymentDetails(1000000 msat, nodeParams.currentBlockHeight)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p2.add.channelId, CMD_FAIL_HTLC(p2.add.id, Right(failure), commit = true)))
commandBuffer.expectNoMsg(100 millis)
register.expectMsg(Register.Forward(p2.add.channelId, CMD_FAIL_HTLC(p2.add.id, Right(failure), commit = true)))
register.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -163,8 +163,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, expiryIn, 1000000 msat, expiryOut)
relayer.send(nodeRelayer, p)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)))
commandBuffer.expectNoMsg(100 millis)
register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)))
register.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -180,8 +180,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
)
p.foreach(p => relayer.send(nodeRelayer, p))
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true))))
commandBuffer.expectNoMsg(100 millis)
p.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true))))
register.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -191,8 +191,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val p = createValidIncomingPacket(2000000 msat, 2000000 msat, CltvExpiry(500000), 1999000 msat, CltvExpiry(490000))
relayer.send(nodeRelayer, p)
commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)))
commandBuffer.expectNoMsg(100 millis)
register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)))
register.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -205,8 +205,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
)
p.foreach(p => relayer.send(nodeRelayer, p))
p.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true))))
commandBuffer.expectNoMsg(100 millis)
p.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true))))
register.expectNoMsg(100 millis)
outgoingPayFSM.expectNoMsg(100 millis)
}
@ -219,8 +219,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
outgoingPayFSM.expectMsgType[SendMultiPartPayment]
outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, LocalFailure(Nil, BalanceTooLow) :: Nil))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true))))
commandBuffer.expectNoMsg(100 millis)
incomingMultiPart.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true))))
register.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
@ -235,8 +235,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
// If we're having a hard time finding routes, raising the fee/cltv will likely help.
val failures = LocalFailure(Nil, RouteNotFound) :: RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure)) :: LocalFailure(Nil, 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)
incomingMultiPart.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true))))
register.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
@ -250,8 +250,8 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
val failures = RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(Nil) :: 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)
incomingMultiPart.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true))))
register.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
@ -282,11 +282,11 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
// A first downstream HTLC is fulfilled: we should immediately forward the fulfill upstream.
outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
incomingMultiPart.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
// If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream.
outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
// Once all the downstream payments have settled, we should emit the relayed event.
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
@ -294,7 +294,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
validateRelayEvent(relayEvent)
assert(relayEvent.incoming.toSet === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)).toSet)
assert(relayEvent.outgoing.nonEmpty)
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
}
test("relay incoming single-part payment") { f =>
@ -310,14 +310,14 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
val incomingAdd = incomingSinglePart.add
commandBuffer.expectMsg(CommandBuffer.CommandSend(incomingAdd.channelId, CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)))
register.expectMsg(Register.Forward(incomingAdd.channelId, CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)))
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.incoming === Seq(PaymentRelayed.Part(incomingSinglePart.add.amountMsat, incomingSinglePart.add.channelId)))
assert(relayEvent.outgoing.nonEmpty)
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
}
test("relay to non-trampoline recipient supporting multi-part") { f =>
@ -343,14 +343,14 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
assert(outgoingPayment.assistedRoutes === hints)
outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
incomingMultiPart.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)))
assert(relayEvent.outgoing.nonEmpty)
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
}
test("relay to non-trampoline recipient without multi-part") { f =>
@ -373,14 +373,14 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
assert(outgoingPayment.assistedRoutes === hints)
outgoingPayFSM.send(nodeRelayer, PreimageReceived(paymentHash, paymentPreimage))
incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
incomingMultiPart.foreach(p => register.expectMsg(Register.Forward(p.add.channelId, CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true))))
outgoingPayFSM.send(nodeRelayer, createSuccessEvent(outgoingCfg.id))
val relayEvent = eventListener.expectMsgType[TrampolinePaymentRelayed]
validateRelayEvent(relayEvent)
assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)))
assert(relayEvent.outgoing.length === 1)
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
}
def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream): Unit = {

View file

@ -53,6 +53,15 @@ class PaymentRequestSpec extends AnyFunSuite {
assert('m' === Amount.unit((1 btc).toMilliSatoshi))
}
test("decode empty amount") {
assert(Amount.decode("") === None)
assert(Amount.decode("0") === None)
assert(Amount.decode("0p") === None)
assert(Amount.decode("0n") === None)
assert(Amount.decode("0u") === None)
assert(Amount.decode("0m") === None)
}
test("check that we can still decode non-minimal amount encoding") {
assert(Amount.decode("1000u") === Some(100000000 msat))
assert(Amount.decode("1000000n") === Some(100000000 msat))
@ -75,7 +84,6 @@ class PaymentRequestSpec extends AnyFunSuite {
test("verify that padding is zero") {
val codec = PaymentRequest.Codecs.alignedBytesCodec(bits)
assert(codec.decode(bin"1010101000").require == DecodeResult(bin"10101010", BitVector.empty))
assert(codec.decode(bin"1010101001").isFailure) // non-zero padding
}

View file

@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods
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.{CommandBuffer, Origin, PostRestartHtlcCleaner, Relayer}
import fr.acinq.eclair.payment.relay.{Origin, PostRestartHtlcCleaner, Relayer}
import fr.acinq.eclair.router.Router.ChannelHop
import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc}
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
@ -49,19 +49,19 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
import PostRestartHtlcCleanerSpec._
case class FixtureParam(nodeParams: NodeParams, commandBuffer: TestProbe, sender: TestProbe, eventListener: TestProbe) {
case class FixtureParam(nodeParams: NodeParams, register: TestProbe, sender: TestProbe, eventListener: TestProbe) {
def createRelayer(): ActorRef = {
system.actorOf(Relayer.props(nodeParams, TestProbe().ref, TestProbe().ref, commandBuffer.ref, TestProbe().ref))
system.actorOf(Relayer.props(nodeParams, TestProbe().ref, register.ref, TestProbe().ref))
}
}
override def withFixture(test: OneArgTest): Outcome = {
within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams
val commandBuffer = TestProbe()
val register = TestProbe()
val eventListener = TestProbe()
system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent])
withFixture(test.toNoArgTest(FixtureParam(nodeParams, commandBuffer, TestProbe(), eventListener)))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, register, TestProbe(), eventListener)))
}
}
@ -110,7 +110,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val channel = TestProbe()
f.createRelayer()
commandBuffer.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
register.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
// channel 1 goes to NORMAL state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
@ -172,7 +172,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val channel = TestProbe()
f.createRelayer()
commandBuffer.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
register.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
// channel 1 goes to NORMAL state:
system.eventStream.publish(ChannelStateChanged(channel.ref, system.deadLetters, a, OFFLINE, NORMAL, channels.head))
@ -219,7 +219,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val testCase = setupLocalPayments(nodeParams)
val relayer = createRelayer()
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
sender.send(relayer, testCase.fails(1))
eventListener.expectNoMsg(100 millis)
@ -240,7 +240,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
assert(e2.paymentHash === paymentHash1)
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Failed])
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
}
test("handle a local payment htlc-fulfill") { f =>
@ -248,7 +248,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val testCase = setupLocalPayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
sender.send(relayer, testCase.fulfills(1))
eventListener.expectNoMsg(100 millis)
@ -276,7 +276,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
assert(e2.recipientAmount === 561.msat)
assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
}
test("ignore htlcs in closing downstream channels that have already been settled upstream") { f =>
@ -284,9 +284,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val testCase = setupTrampolinePayments(nodeParams)
val initialized = Promise[Done]()
val postRestart = system.actorOf(PostRestartHtlcCleaner.props(nodeParams, commandBuffer.ref, Some(initialized)))
val postRestart = system.actorOf(PostRestartHtlcCleaner.props(nodeParams, register.ref, Some(initialized)))
awaitCond(initialized.isCompleted)
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
val probe = TestProbe()
probe.send(postRestart, PostRestartHtlcCleaner.GetBrokenHtlcs)
@ -390,7 +390,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
nodeParams.db.channels.addOrUpdateChannel(data_downstream)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
register.expectNoMsg(100 millis) // nothing should happen while channels are still offline.
val (channel_upstream_1, channel_upstream_2, channel_upstream_3) = (TestProbe(), TestProbe(), TestProbe())
system.eventStream.publish(ChannelStateChanged(channel_upstream_1.ref, system.deadLetters, a, OFFLINE, NORMAL, data_upstream_1))
@ -406,9 +406,9 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
// Payment 2 should fulfill once we receive the preimage.
val origin_2 = Origin.TrampolineRelayed(upstream_2.adds.map(add => (add.channelId, add.id)).toList, None)
sender.send(relayer, Relayer.ForwardOnChainFulfill(preimage2, origin_2, htlc_2_2))
commandBuffer.expectMsgAllOf(
CommandBuffer.CommandSend(channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)),
CommandBuffer.CommandSend(channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true))
register.expectMsgAllOf(
Register.Forward(channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)),
Register.Forward(channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true))
)
// Payment 3 should not be failed: we are still waiting for on-chain confirmation.
@ -420,28 +420,28 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val testCase = setupTrampolinePayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
// This downstream HTLC has two upstream HTLCs.
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1))
val fails = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]] :: commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FAIL_HTLC]] :: Nil
val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil
assert(fails.toSet === testCase.upstream_1.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
case (channelId, htlcId) => Register.Forward(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
}.toSet)
sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.upstream_1))
commandBuffer.expectNoMsg(100 millis) // a duplicate failure should be ignored
register.expectNoMsg(100 millis) // a duplicate failure should be ignored
sender.send(relayer, buildForwardOnChainFail(testCase.downstream_2_1, testCase.upstream_2))
sender.send(relayer, buildForwardFail(testCase.downstream_2_2, testCase.upstream_2))
commandBuffer.expectNoMsg(100 millis) // there is still a third downstream payment pending
register.expectNoMsg(100 millis) // there is still a third downstream payment pending
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
commandBuffer.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
register.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => Register.Forward(channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true))
}.head)
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
eventListener.expectNoMsg(100 millis)
}
@ -450,27 +450,27 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val testCase = setupTrampolinePayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
// This downstream HTLC has two upstream HTLCs.
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
val fails = commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]] :: commandBuffer.expectMsgType[CommandBuffer.CommandSend[CMD_FULFILL_HTLC]] :: Nil
val fails = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil
assert(fails.toSet === testCase.upstream_1.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true))
case (channelId, htlcId) => Register.Forward(channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true))
}.toSet)
sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.upstream_1, preimage1))
commandBuffer.expectNoMsg(100 millis) // a duplicate fulfill should be ignored
register.expectNoMsg(100 millis) // a duplicate fulfill should be ignored
// This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage.
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.upstream_2, preimage2))
commandBuffer.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
register.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => Register.Forward(channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
}.head)
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_3, testCase.upstream_2, preimage2))
commandBuffer.expectNoMsg(100 millis) // the payment has already been fulfilled upstream
register.expectNoMsg(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMsg(100 millis)
}
@ -479,17 +479,17 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit
val testCase = setupTrampolinePayments(nodeParams)
val relayer = f.createRelayer()
commandBuffer.expectNoMsg(100 millis)
register.expectNoMsg(100 millis)
sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.upstream_2))
sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.upstream_2, preimage2))
commandBuffer.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => CommandBuffer.CommandSend(channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
register.expectMsg(testCase.upstream_2.origins.map {
case (channelId, htlcId) => Register.Forward(channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true))
}.head)
sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.upstream_2))
commandBuffer.expectNoMsg(100 millis) // the payment has already been fulfilled upstream
register.expectNoMsg(100 millis) // the payment has already been fulfilled upstream
eventListener.expectNoMsg(100 millis)
}

View file

@ -27,7 +27,7 @@ import fr.acinq.eclair.payment.IncomingPacket.FinalPacket
import fr.acinq.eclair.payment.OutgoingPacket.{buildCommand, buildOnion, buildPacket}
import fr.acinq.eclair.payment.relay.Origin._
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.payment.relay.{CommandBuffer, Relayer}
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.router.Router.{ChannelHop, Ignore, NodeHop}
import fr.acinq.eclair.router.{Announcements, _}
@ -54,10 +54,9 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
within(30 seconds) {
val nodeParams = TestConstants.Bob.nodeParams
val (router, register) = (TestProbe(), TestProbe())
val commandBuffer = system.actorOf(Props(new CommandBuffer(nodeParams, register.ref)))
val paymentHandler = TestProbe()
// we are node B in the route A -> B -> C -> ....
val relayer = system.actorOf(Relayer.props(nodeParams, router.ref, register.ref, commandBuffer, paymentHandler.ref))
val relayer = system.actorOf(Relayer.props(nodeParams, router.ref, register.ref, paymentHandler.ref))
withFixture(test.toNoArgTest(FixtureParam(nodeParams, relayer, router, register, paymentHandler, TestProbe())))
}
}
@ -379,8 +378,7 @@ class RelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
import f._
val nodeParams = TestConstants.Bob.nodeParams.copy(enableTrampolinePayment = false)
val commandBuffer = system.actorOf(Props(new CommandBuffer(nodeParams, register.ref)))
val relayer = system.actorOf(Relayer.props(nodeParams, router.ref, register.ref, commandBuffer, paymentHandler.ref))
val relayer = system.actorOf(Relayer.props(nodeParams, router.ref, register.ref, paymentHandler.ref))
// we use this to build a valid trampoline onion inside a normal onion
val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil

View file

@ -21,7 +21,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.4.0-android-SNAPSHOT</version>
<version>0.4.2-android-SNAPSHOT</version>
</parent>
<artifactId>eclair-node_2.11</artifactId>

View file

@ -20,7 +20,7 @@
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.4.0-android-SNAPSHOT</version>
<version>0.4.2-android-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>