mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-13 19:37:35 +01:00
Merge commit 'ea57bb266' into android
This commit is contained in:
commit
d051fa76c7
47 changed files with 749 additions and 648 deletions
33
.github/workflows/main.yml
vendored
Normal file
33
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: Build & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Cache Maven dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2
|
||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-m2
|
||||
|
||||
- name: Configure OS settings
|
||||
run: echo "fs.file-max = 1024000" | sudo tee -a /etc/sysctl.conf
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn clean test
|
26
.travis.yml
26
.travis.yml
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||

|
||||
|
||||
[](https://travis-ci.org/ACINQ/eclair)
|
||||
[](https://github.com/ACINQ/eclair/actions?query=workflow%3A%22Build+%26+Test%22)
|
||||
[](https://codecov.io/gh/acinq/eclair)
|
||||
[](LICENSE)
|
||||
[](https://gitter.im/ACINQ/eclair)
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<parent>
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair_2.11</artifactId>
|
||||
<version>0.4.0-android-SNAPSHOT</version>
|
||||
<version>0.4.2-android-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eclair-core_2.11</artifactId>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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._
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 =>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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") {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()}")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<parent>
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair_2.11</artifactId>
|
||||
<version>0.4.0-android-SNAPSHOT</version>
|
||||
<version>0.4.2-android-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>eclair-node_2.11</artifactId>
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -20,7 +20,7 @@
|
|||
|
||||
<groupId>fr.acinq.eclair</groupId>
|
||||
<artifactId>eclair_2.11</artifactId>
|
||||
<version>0.4.0-android-SNAPSHOT</version>
|
||||
<version>0.4.2-android-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
|
|
Loading…
Add table
Reference in a new issue