diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..5c38a2314 --- /dev/null +++ b/.github/workflows/main.yml @@ -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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 61a5174e3..000000000 --- a/.travis.yml +++ /dev/null @@ -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 diff --git a/README.md b/README.md index 188e0b95b..0711304e4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 6bd1c1b9e..36be2747d 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.4.0-android-SNAPSHOT + 0.4.2-android-SNAPSHOT eclair-core_2.11 diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 14d3bc68c..be86545ef 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -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" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index f60da5955..b7cf20e1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -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")) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index c3a7062b3..0377b92fb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -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, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index bbee89aa2..be3bc5d19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -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 { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala index 32f73c479..216b33d19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/ExtendedBitcoinClient.scala @@ -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)) + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index b164661f4..13f28fbbb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -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 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 3f3f3f55d..3b48217f8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -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._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index 83c1bb8ca..dbe3358f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -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) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/FileBackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/FileBackupHandler.scala index ff71bb7ad..07ba2a530 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/FileBackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/FileBackupHandler.scala @@ -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") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Monitoring.scala new file mode 100644 index 000000000..cb7dfcf9b --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Monitoring.scala @@ -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" + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingRelayDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingRelayDb.scala index 445eec564..eacd4c5ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingRelayDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingRelayDb.scala @@ -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) + } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 293c20b8e..0ae5b088c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -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 => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index 8133b4994..a49cc6c3a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -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) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index 673c31407..fd5a64dc2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -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() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 6f0b0a95e..b682980d1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -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. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 68fba42ab..aafa5d55b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -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() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala index 1606a4e48..867abdd19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingRelayDb.scala @@ -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() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index 66e30e144..8f1fb6c08 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -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() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index c02fda279..9470e8964 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -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 = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 6a537c4ec..7d54271c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -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 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala index 378bf25ce..a8784d26a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala @@ -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)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 32ac71676..f7eee7bd9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -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) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/CommandBuffer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/CommandBuffer.scala deleted file mode 100644 index 412380600..000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/CommandBuffer.scala +++ /dev/null @@ -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) - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index 52a6e4951..0a3cac148 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -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 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 545b49dbc..00753ee30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -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 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index fca20fd20..3650d3f3d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -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] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index d54bfb5ae..92d7e1cfc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -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, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala index 74dc4def9..cb7a72583 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestkitBaseClass.scala @@ -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) } + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala index 06390d0fe..695988609 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala @@ -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") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 08165dd85..7a96b6191 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -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, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 7ecb7331d..0f96bcd14 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -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 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 0db22f9ea..aa8933bda 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -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) = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index ecbc33102..6f4bb7490 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -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) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 40d891305..8349fb3aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -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 => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 11767f3a0..89e37f28f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -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 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/FileBackupHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/FileBackupHandlerSpec.scala index 537d7c48b..42cac03ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/FileBackupHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/FileBackupHandlerSpec.scala @@ -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()}") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index e92c0ddd8..15dcefd95 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -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)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala index 9555bfee5..a841ffb2c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala @@ -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 = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala index 3907fba34..37ad137b0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala @@ -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 } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index b7e4fe6ab..7964d0e90 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -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) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index 34d7c8b64..c1203fd33 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -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 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 842a777c1..433409ddf 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.11 - 0.4.0-android-SNAPSHOT + 0.4.2-android-SNAPSHOT eclair-node_2.11 diff --git a/pom.xml b/pom.xml index b26d3ea70..2f8111cc9 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.11 - 0.4.0-android-SNAPSHOT + 0.4.2-android-SNAPSHOT pom