mirror of
https://github.com/ACINQ/eclair.git
synced 2025-01-18 21:32:50 +01:00
Removed BitcoinJ watcher (#447)
Added guava dependency which was previously bundled with bitcoinj.
This commit is contained in:
parent
1b247ae613
commit
8684fb238b
@ -143,11 +143,6 @@
|
||||
<artifactId>jeromq</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.acinq</groupId>
|
||||
<artifactId>bitcoinj-core</artifactId>
|
||||
<version>${bitcoinj.version}</version>
|
||||
</dependency>
|
||||
<!-- SERIALIZATION -->
|
||||
<dependency>
|
||||
<groupId>org.scodec</groupId>
|
||||
@ -188,6 +183,11 @@
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<!-- TESTS -->
|
||||
<dependency>
|
||||
<groupId>com.typesafe.akka</groupId>
|
||||
|
@ -15,7 +15,7 @@ eclair {
|
||||
password = "" // password for basic auth, must be non empty if json-rpc api is enabled
|
||||
}
|
||||
|
||||
watcher-type = "bitcoind" // other *experimental* values include "bitcoinj" or "electrum"
|
||||
watcher-type = "bitcoind" // other *experimental* values include "electrum"
|
||||
|
||||
bitcoind {
|
||||
host = "localhost"
|
||||
@ -25,15 +25,6 @@ eclair {
|
||||
zmq = "tcp://127.0.0.1:29000"
|
||||
}
|
||||
|
||||
bitcoinj {
|
||||
static-peers = [
|
||||
#{ // currently used in integration tests to override default port
|
||||
# host = "localhost"
|
||||
# port = 28333
|
||||
#}
|
||||
]
|
||||
}
|
||||
|
||||
default-feerates { // those are in satoshis per byte
|
||||
delay-blocks {
|
||||
1 = 210
|
||||
|
@ -67,8 +67,6 @@ object NodeParams {
|
||||
|
||||
object BITCOIND extends WatcherType
|
||||
|
||||
object BITCOINJ extends WatcherType
|
||||
|
||||
object ELECTRUM extends WatcherType
|
||||
|
||||
/**
|
||||
@ -123,7 +121,6 @@ object NodeParams {
|
||||
require(color.size == 3, "color should be a 3-bytes hex buffer")
|
||||
|
||||
val watcherType = config.getString("watcher-type") match {
|
||||
case "bitcoinj" => BITCOINJ
|
||||
case "electrum" => ELECTRUM
|
||||
case _ => BITCOIND
|
||||
}
|
||||
|
@ -10,12 +10,11 @@ import akka.stream.{ActorMaterializer, BindFailedException}
|
||||
import akka.util.Timeout
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
|
||||
import fr.acinq.eclair.api.{GetInfoResponse, Service}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
|
||||
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
|
||||
import fr.acinq.eclair.blockchain.bitcoinj.{BitcoinjKit, BitcoinjWallet, BitcoinjWatcher}
|
||||
import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumEclairWallet, ElectrumWallet, ElectrumWatcher}
|
||||
import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _}
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, _}
|
||||
@ -25,7 +24,6 @@ import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router._
|
||||
import grizzled.slf4j.Logging
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
|
||||
|
||||
@ -92,14 +90,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
// TODO: add a check on bitcoin version?
|
||||
|
||||
Bitcoind(bitcoinClient)
|
||||
case BITCOINJ =>
|
||||
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
|
||||
val staticPeers = config.getConfigList("bitcoinj.static-peers").map(c => new InetSocketAddress(c.getString("host"), c.getInt("port"))).toList
|
||||
logger.info(s"using staticPeers=$staticPeers")
|
||||
val bitcoinjKit = new BitcoinjKit(chain, datadir, staticPeers)
|
||||
bitcoinjKit.startAsync()
|
||||
Await.ready(bitcoinjKit.initialized, 10 seconds)
|
||||
Bitcoinj(bitcoinjKit)
|
||||
case ELECTRUM =>
|
||||
logger.warn("EXPERIMENTAL ELECTRUM MODE ENABLED!!!")
|
||||
val addressesFile = chain match {
|
||||
@ -137,9 +127,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
case Bitcoind(bitcoinClient) =>
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
|
||||
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(bitcoinClient), "watcher", SupervisorStrategy.Resume))
|
||||
case Bitcoinj(bitcoinj) =>
|
||||
zmqConnected.success(true)
|
||||
system.actorOf(SimpleSupervisor.props(BitcoinjWatcher.props(bitcoinj), "watcher", SupervisorStrategy.Resume))
|
||||
case Electrum(electrumClient) =>
|
||||
zmqConnected.success(true)
|
||||
system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(electrumClient)), "watcher", SupervisorStrategy.Resume))
|
||||
@ -147,7 +134,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
|
||||
val wallet = bitcoin match {
|
||||
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient)
|
||||
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
|
||||
case Electrum(electrumClient) => seed_opt match {
|
||||
case Some(seed) => val electrumWallet = system.actorOf(ElectrumWallet.props(seed, electrumClient, ElectrumWallet.WalletParameters(Block.TestnetGenesisBlock.hash)), "electrum-wallet")
|
||||
new ElectrumEclairWallet(electrumWallet)
|
||||
@ -223,7 +209,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
||||
// @formatter:off
|
||||
sealed trait Bitcoin
|
||||
case class Bitcoind(extendedBitcoinClient: ExtendedBitcoinClient) extends Bitcoin
|
||||
case class Bitcoinj(bitcoinjKit: BitcoinjKit) extends Bitcoin
|
||||
case class Electrum(electrumClient: ActorRef) extends Bitcoin
|
||||
// @formatter:on
|
||||
|
||||
|
@ -18,7 +18,7 @@ sealed trait Watch {
|
||||
def channel: ActorRef
|
||||
def event: BitcoinEvent
|
||||
}
|
||||
// we need a public key script to use bitcoinj or electrum apis
|
||||
// we need a public key script to use electrum apis
|
||||
final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, publicKeyScript: BinaryData, minDepth: Long, event: BitcoinEvent) extends Watch
|
||||
object WatchConfirmed {
|
||||
// if we have the entire transaction, we can get the redeemScript from the witness, and re-compute the publicKeyScript
|
||||
|
@ -1,152 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import com.google.common.util.concurrent.{FutureCallback, Futures}
|
||||
import fr.acinq.bitcoin.Transaction
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.blockchain.CurrentBlockCount
|
||||
import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjKit._
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.TransactionConfidence.ConfidenceType
|
||||
import org.bitcoinj.core.listeners._
|
||||
import org.bitcoinj.core.{Block, Context, FilteredBlock, NetworkParameters, Peer, PeerAddress, StoredBlock, VersionMessage, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.params.{RegTestParams, TestNet3Params}
|
||||
import org.bitcoinj.utils.Threading
|
||||
import org.bitcoinj.wallet.Wallet
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
class BitcoinjKit(chain: String, datadir: File, staticPeers: List[InetSocketAddress] = Nil)(implicit system: ActorSystem) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj", true) with Logging {
|
||||
|
||||
if (staticPeers.size > 0) {
|
||||
logger.info(s"using staticPeers=${staticPeers.mkString(",")}")
|
||||
setPeerNodes(staticPeers.map(addr => new PeerAddress(params, addr)).head)
|
||||
}
|
||||
|
||||
// tells us when the peerGroup/chain/wallet are accessible
|
||||
private val initializedPromise = Promise[Boolean]()
|
||||
val initialized = initializedPromise.future
|
||||
|
||||
// tells us as soon as we know the current block height
|
||||
private val atCurrentHeightPromise = Promise[Boolean]()
|
||||
val atCurrentHeight = atCurrentHeightPromise.future
|
||||
|
||||
// tells us when we are at current block height
|
||||
// private val syncedPromise = Promise[Boolean]()
|
||||
// val synced = syncedPromise.future
|
||||
|
||||
private def updateBlockCount(blockCount: Int) = {
|
||||
// when synchronizing we don't want to advertise previous blocks
|
||||
if (Globals.blockCount.get() < blockCount) {
|
||||
logger.debug(s"current blockchain height=$blockCount")
|
||||
system.eventStream.publish(CurrentBlockCount(blockCount))
|
||||
Globals.blockCount.set(blockCount)
|
||||
}
|
||||
}
|
||||
|
||||
override def onSetupCompleted(): Unit = {
|
||||
|
||||
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
|
||||
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
|
||||
|
||||
peerGroup().setMinRequiredProtocolVersion(70015) // bitcoin core 0.13
|
||||
wallet().watchMode = true
|
||||
|
||||
// setDownloadListener(new DownloadProgressTracker {
|
||||
// override def doneDownload(): Unit = {
|
||||
// super.doneDownload()
|
||||
// // may be called multiple times
|
||||
// syncedPromise.trySuccess(true)
|
||||
// }
|
||||
// })
|
||||
|
||||
// we set the blockcount to the previous stored block height
|
||||
updateBlockCount(chain().getBestChainHeight)
|
||||
|
||||
// as soon as we are connected the peers will tell us their current height and we will advertise it immediately
|
||||
peerGroup().addConnectedEventListener(new PeerConnectedEventListener {
|
||||
override def onPeerConnected(peer: Peer, peerCount: Int): Unit = {
|
||||
if ((peer.getPeerVersionMessage.localServices & VersionMessage.NODE_WITNESS) == 0) {
|
||||
peer.close()
|
||||
} else {
|
||||
Context.propagate(wallet.getContext)
|
||||
// we wait for at least 3 peers before relying on the information they are giving, but we trust localhost
|
||||
if (peer.getAddress.getAddr.isLoopbackAddress || peerCount > 3) {
|
||||
updateBlockCount(peerGroup().getMostCommonChainHeight)
|
||||
// may be called multiple times
|
||||
atCurrentHeightPromise.trySuccess(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener(new BlocksDownloadedEventListener {
|
||||
override def onBlocksDownloaded(peer: Peer, block: Block, filteredBlock: FilteredBlock, blocksLeft: Int): Unit = {
|
||||
Context.propagate(wallet.getContext)
|
||||
logger.debug(s"received block=${block.getHashAsString} (size=${block.bitcoinSerialize().size} txs=${Try(block.getTransactions.size).getOrElse(-1)}) filteredBlock=${Try(filteredBlock.getHash.toString).getOrElse("N/A")} (size=${Try(block.bitcoinSerialize().size).getOrElse(-1)} txs=${Try(filteredBlock.getTransactionCount).getOrElse(-1)})")
|
||||
Try {
|
||||
if (filteredBlock.getAssociatedTransactions.size() > 0) {
|
||||
logger.info(s"retrieving full block ${block.getHashAsString}")
|
||||
Futures.addCallback(peer.getBlock(block.getHash), new FutureCallback[Block] {
|
||||
override def onFailure(throwable: Throwable) = logger.error(s"could not retrieve full block=${block.getHashAsString}")
|
||||
|
||||
override def onSuccess(fullBlock: Block) = {
|
||||
Try {
|
||||
Context.propagate(wallet.getContext)
|
||||
fullBlock.getTransactions.foreach {
|
||||
case tx =>
|
||||
logger.debug(s"received tx=${tx.getHashAsString} witness=${Transaction.read(tx.bitcoinSerialize()).txIn(0).witness.stack.size} from fullBlock=${fullBlock.getHash} confidence=${tx.getConfidence}")
|
||||
val depthInBlocks = tx.getConfidence.getConfidenceType match {
|
||||
case ConfidenceType.DEAD => -1
|
||||
case _ => tx.getConfidence.getDepthInBlocks
|
||||
}
|
||||
system.eventStream.publish(NewConfidenceLevel(Transaction.read(tx.bitcoinSerialize()), 0, depthInBlocks))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, Threading.USER_THREAD)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
chain().addNewBestBlockListener(new NewBestBlockListener {
|
||||
override def notifyNewBestBlock(storedBlock: StoredBlock): Unit =
|
||||
updateBlockCount(storedBlock.getHeight)
|
||||
})
|
||||
|
||||
wallet().addTransactionConfidenceEventListener(new TransactionConfidenceEventListener {
|
||||
override def onTransactionConfidenceChanged(wallet: Wallet, bitcoinjTx: BitcoinjTransaction): Unit = {
|
||||
Context.propagate(wallet.getContext)
|
||||
val tx = Transaction.read(bitcoinjTx.bitcoinSerialize())
|
||||
logger.info(s"tx confidence changed for txid=${tx.txid} confidence=${bitcoinjTx.getConfidence} witness=${bitcoinjTx.getWitness(0)}")
|
||||
val (blockHeight, confirmations) = bitcoinjTx.getConfidence.getConfidenceType match {
|
||||
case ConfidenceType.DEAD => (-1, -1)
|
||||
case ConfidenceType.BUILDING => (bitcoinjTx.getConfidence.getAppearedAtChainHeight, bitcoinjTx.getConfidence.getDepthInBlocks)
|
||||
case _ => (-1, bitcoinjTx.getConfidence.getDepthInBlocks)
|
||||
}
|
||||
system.eventStream.publish(NewConfidenceLevel(tx, blockHeight, confirmations))
|
||||
}
|
||||
})
|
||||
|
||||
initializedPromise.success(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object BitcoinjKit {
|
||||
|
||||
def chain2Params(chain: String): NetworkParameters = chain match {
|
||||
case "regtest" => RegTestParams.get()
|
||||
case "test" => TestNet3Params.get()
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.{Coin, Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.script.Script
|
||||
import org.bitcoinj.wallet.{SendRequest, Wallet}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 08/07/2017.
|
||||
*/
|
||||
class BitcoinjWallet(val fWallet: Future[Wallet])(implicit ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
|
||||
fWallet.map(wallet => wallet.allowSpendingUnconfirmedTransactions())
|
||||
|
||||
override def getBalance: Future[Satoshi] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
Context.propagate(wallet.getContext)
|
||||
Satoshi(wallet.getBalance.longValue())
|
||||
}
|
||||
|
||||
override def getFinalAddress: Future[String] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
Context.propagate(wallet.getContext)
|
||||
wallet.currentReceiveAddress().toBase58
|
||||
}
|
||||
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
logger.info(s"building funding tx")
|
||||
Context.propagate(wallet.getContext)
|
||||
val script = new Script(pubkeyScript)
|
||||
val tx = new BitcoinjTransaction(wallet.getParams)
|
||||
tx.addOutput(Coin.valueOf(amount.amount), script)
|
||||
val req = SendRequest.forTx(tx)
|
||||
wallet.completeTx(req)
|
||||
val txOutputIndex = tx.getOutputs.find(_.getScriptPubKey.equals(script)).get.getIndex
|
||||
MakeFundingTxResponse(Transaction.read(tx.bitcoinSerialize()), txOutputIndex)
|
||||
}
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = {
|
||||
// we make sure that we haven't double spent our own tx (eg by opening 2 channels at the same time)
|
||||
val serializedTx = Transaction.write(tx)
|
||||
logger.info(s"committing tx: txid=${tx.txid} tx=$serializedTx")
|
||||
for {
|
||||
wallet <- fWallet
|
||||
_ = Context.propagate(wallet.getContext)
|
||||
bitcoinjTx = new org.bitcoinj.core.Transaction(wallet.getParams(), serializedTx)
|
||||
canCommit = wallet.maybeCommitTx(bitcoinjTx)
|
||||
_ = logger.info(s"commit txid=${tx.txid} result=$canCommit")
|
||||
} yield canCommit
|
||||
}
|
||||
|
||||
/**
|
||||
* There are no locks on bitcoinj, this is a no-op
|
||||
*
|
||||
* @param tx
|
||||
* @return
|
||||
*/
|
||||
override def rollback(tx: Transaction) = Future.successful(true)
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, Props, Terminated}
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.{FutureCallback, Futures}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.Script.{pay2wsh, write}
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import fr.acinq.eclair.{Globals, fromShortId}
|
||||
import org.bitcoinj.core.{Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.script.Script
|
||||
|
||||
import scala.collection.SortedMap
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
final case class NewConfidenceLevel(tx: Transaction, blockHeight: Int, confirmations: Int) extends BlockchainEvent
|
||||
|
||||
/**
|
||||
* A blockchain watcher that:
|
||||
* - receives bitcoin events (new blocks and new txes) directly from the bitcoin network
|
||||
* - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs)
|
||||
* Created by PM on 21/02/2016.
|
||||
*/
|
||||
class BitcoinjWatcher(val kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[NewConfidenceLevel])
|
||||
|
||||
val broadcaster = context.actorOf(Props(new Broadcaster(kit: WalletAppKit)), name = "broadcaster")
|
||||
|
||||
case class TriggerEvent(w: Watch, e: WatchEvent)
|
||||
|
||||
def receive: Receive = watching(Set(), SortedMap(), Nil, Nil)
|
||||
|
||||
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel], sent: Seq[TriggerEvent]): Receive = {
|
||||
|
||||
case event@NewConfidenceLevel(tx, blockHeight, confirmations) =>
|
||||
log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=$tx")
|
||||
watches.collect {
|
||||
case w@WatchSpentBasic(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpentBasic(event))
|
||||
case w@WatchSpent(_, txid, outputIndex, _, event) if tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex) =>
|
||||
self ! TriggerEvent(w, WatchEventSpent(event, tx))
|
||||
case w@WatchConfirmed(_, txId, _, minDepth, event) if txId == tx.txid && confirmations >= minDepth =>
|
||||
self ! TriggerEvent(w, WatchEventConfirmed(event, blockHeight, 0))
|
||||
}
|
||||
context become watching(watches, block2tx, oldEvents.filterNot(_.tx.txid == tx.txid) :+ event, sent)
|
||||
|
||||
case t@TriggerEvent(w, e) if watches.contains(w) && !sent.contains(t) =>
|
||||
log.info(s"triggering $w")
|
||||
w.channel ! e
|
||||
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
|
||||
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
|
||||
val newWatches = if (!w.isInstanceOf[WatchSpent]) watches - w else watches
|
||||
context.become(watching(newWatches, block2tx, oldEvents, sent :+ t))
|
||||
|
||||
case CurrentBlockCount(count) => {
|
||||
val toPublish = block2tx.filterKeys(_ <= count)
|
||||
toPublish.values.flatten.map(tx => publish(tx))
|
||||
context.become(watching(watches, block2tx -- toPublish.keys, oldEvents, sent))
|
||||
}
|
||||
|
||||
case w: Watch if !watches.contains(w) =>
|
||||
w match {
|
||||
case w: WatchConfirmed => addHint(w.publicKeyScript)
|
||||
case w: WatchSpent => addHint(w.publicKeyScript)
|
||||
case w: WatchSpentBasic => addHint(w.publicKeyScript)
|
||||
case _ => ()
|
||||
}
|
||||
log.debug(s"adding watch $w for $sender")
|
||||
log.info(s"resending ${oldEvents.size} events!")
|
||||
oldEvents.foreach(self ! _)
|
||||
context.watch(w.channel)
|
||||
context.become(watching(watches + w, block2tx, oldEvents, sent))
|
||||
|
||||
case PublishAsap(tx) =>
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val cltvTimeout = Scripts.cltvTimeout(tx)
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
if (csvTimeout > 0) {
|
||||
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
|
||||
val parentTxid = tx.txIn(0).outPoint.txid
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=$tx")
|
||||
val parentPublicKey = fr.acinq.bitcoin.Script.write(fr.acinq.bitcoin.Script.pay2wsh(tx.txIn.head.witness.stack.last))
|
||||
self ! WatchConfirmed(self, parentTxid, parentPublicKey, minDepth = 1, BITCOIN_PARENT_TX_CONFIRMED(tx))
|
||||
} else if (cltvTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} else publish(tx)
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
|
||||
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
val absTimeout = blockHeight + csvTimeout
|
||||
if (absTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(absTimeout, block2tx.getOrElse(absTimeout, Seq.empty[Transaction]) :+ tx)
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} else publish(tx)
|
||||
|
||||
case ValidateRequest(c) =>
|
||||
log.info(s"blindly validating channel=$c")
|
||||
val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
|
||||
val (_, _, outputIndex) = fromShortId(c.shortChannelId)
|
||||
val fakeFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format
|
||||
lockTime = 0)
|
||||
sender ! ValidateResult(c, Some(fakeFundingTx), true, None)
|
||||
|
||||
case Terminated(channel) =>
|
||||
// we remove watches associated to dead actor
|
||||
val deprecatedWatches = watches.filter(_.channel == channel)
|
||||
context.become(watching(watches -- deprecatedWatches, block2tx, oldEvents, sent))
|
||||
|
||||
case 'watches => sender ! watches
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitcoinj needs hints to be able to detect transactions
|
||||
*
|
||||
* @param pubkeyScript
|
||||
* @return
|
||||
*/
|
||||
def addHint(pubkeyScript: BinaryData) = {
|
||||
Context.propagate(kit.wallet.getContext)
|
||||
val script = new Script(pubkeyScript)
|
||||
// set creation time to 2017/09/01, so bitcoinj can still use its checkpoints optimizations
|
||||
script.setCreationTimeSeconds(1501538400L) // 2017-09-01
|
||||
kit.wallet().addWatchedScripts(ImmutableList.of(script))
|
||||
}
|
||||
|
||||
def publish(tx: Transaction): Unit = broadcaster ! tx
|
||||
|
||||
}
|
||||
|
||||
object BitcoinjWatcher {
|
||||
|
||||
def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new BitcoinjWatcher(kit)(ec))
|
||||
|
||||
}
|
||||
|
||||
class Broadcaster(kit: WalletAppKit) extends Actor with ActorLogging {
|
||||
|
||||
override def receive: Receive = {
|
||||
case tx: Transaction =>
|
||||
broadcast(tx)
|
||||
context become waiting(Nil)
|
||||
}
|
||||
|
||||
def waiting(stash: Seq[Transaction]): Receive = {
|
||||
case BroadcastResult(tx, result) =>
|
||||
result match {
|
||||
case Success(_) => log.info(s"broadcast success for txid=${tx.txid}")
|
||||
case Failure(t) => log.error(t, s"broadcast failure for txid=${tx.txid}: ")
|
||||
}
|
||||
stash match {
|
||||
case head :: rest =>
|
||||
broadcast(head)
|
||||
context become waiting(rest)
|
||||
case Nil => context become receive
|
||||
}
|
||||
case tx: Transaction =>
|
||||
log.info(s"stashing txid=${tx.txid} for broadcast")
|
||||
context become waiting(stash :+ tx)
|
||||
}
|
||||
|
||||
case class BroadcastResult(tx: Transaction, result: Try[Boolean])
|
||||
|
||||
def broadcast(tx: Transaction) = {
|
||||
Context.propagate(kit.wallet().getContext)
|
||||
val bitcoinjTx = new org.bitcoinj.core.Transaction(kit.wallet().getParams, Transaction.write(tx))
|
||||
log.info(s"broadcasting txid=${tx.txid}")
|
||||
Futures.addCallback(kit.peerGroup().broadcastTransaction(bitcoinjTx).future(), new FutureCallback[BitcoinjTransaction] {
|
||||
override def onFailure(t: Throwable): Unit = self ! BroadcastResult(tx, Failure(t))
|
||||
|
||||
override def onSuccess(v: BitcoinjTransaction): Unit = self ! BroadcastResult(tx, Success(true))
|
||||
}, context.dispatcher)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -5,9 +5,8 @@ import java.nio.charset.StandardCharsets
|
||||
import akka.actor.{ActorRef, FSM, OneForOneStrategy, Props, Status, SupervisorStrategy}
|
||||
import akka.event.Logging.MDC
|
||||
import akka.pattern.pipe
|
||||
import fr.acinq.bitcoin.Crypto.{PublicKey, ripemd160, sha256}
|
||||
import fr.acinq.bitcoin.Crypto.{PublicKey, sha256}
|
||||
import fr.acinq.bitcoin._
|
||||
import fr.acinq.eclair.NodeParams.BITCOINJ
|
||||
import fr.acinq.eclair._
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
|
||||
@ -16,11 +15,10 @@ import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{ChannelReestablish, _}
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Left, Random, Success, Try}
|
||||
import scala.util.{Failure, Left, Success, Try}
|
||||
|
||||
|
||||
/**
|
||||
@ -437,16 +435,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
|
||||
when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions {
|
||||
case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) =>
|
||||
if (d.commitments.announceChannel && nodeParams.watcherType == BITCOINJ && d.commitments.localParams.isFunder && System.getProperty("spvtest") != null) {
|
||||
// bitcoinj-based watcher currently can't get the tx index in block (which is used to calculate the short id)
|
||||
// instead, we rely on a hack by trusting the index the counterparty sends us
|
||||
// but in testing when connecting to bitcoinj impl together we make the funder choose some random data
|
||||
log.warning("using hardcoded short id for testing with bitcoinj!!!!!")
|
||||
context.system.scheduler.scheduleOnce(5 seconds, self, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, Random.nextInt(100), Random.nextInt(100)))
|
||||
} else {
|
||||
// used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly)
|
||||
blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
|
||||
}
|
||||
// used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly)
|
||||
blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
|
||||
context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId))
|
||||
val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true)
|
||||
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None))
|
||||
@ -759,11 +749,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
// note: no need to persist their message, in case of disconnection they will resend it
|
||||
log.debug(s"received remote announcement signatures, delaying")
|
||||
context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs)
|
||||
if (nodeParams.watcherType == BITCOINJ) {
|
||||
log.warning(s"HACK: since we cannot get the tx index with bitcoinj, we copy the value sent by remote")
|
||||
val (blockHeight, txIndex, _) = fromShortId(remoteAnnSigs.shortChannelId)
|
||||
self ! WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex)
|
||||
}
|
||||
stay
|
||||
}
|
||||
|
||||
@ -1201,14 +1186,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
}
|
||||
|
||||
if (!d.buried) {
|
||||
if (nodeParams.watcherType != BITCOINJ) {
|
||||
// even if we were just disconnected/reconnected, we need to put back the watch because the event may have been
|
||||
// fired while we were in OFFLINE (if not, the operation is idempotent anyway)
|
||||
blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
|
||||
} else {
|
||||
// NB: in BITCOINJ mode we currently can't get the tx index in block (which is used to calculate the short id)
|
||||
// instead, we rely on a hack by trusting the index the counterparty sends us)
|
||||
}
|
||||
// even if we were just disconnected/reconnected, we need to put back the watch because the event may have been
|
||||
// fired while we were in OFFLINE (if not, the operation is idempotent anyway)
|
||||
blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
|
||||
} else {
|
||||
// channel has been buried enough, should we (re)send our announcement sigs?
|
||||
d.channelAnnouncement match {
|
||||
|
@ -14,7 +14,6 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.json4s.{DefaultFormats, JString}
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -1,217 +0,0 @@
|
||||
package fr.acinq.eclair.blockchain.bitcoinj
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{Satoshi, Script}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient}
|
||||
import fr.acinq.eclair.blockchain.{PublishAsap, WatchConfirmed, WatchEventConfirmed, WatchSpent}
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.Ignore
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.sys.process.{Process, _}
|
||||
import scala.util.Random
|
||||
|
||||
@Ignore
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/bitcoinj-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BitcoinjSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](30 seconds)
|
||||
}
|
||||
|
||||
test("bitcoinj wallet commit") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 60 seconds, interval = 1 second)
|
||||
|
||||
logger.info(s"generating blocks")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 10))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
val fundingPubkeyScript1 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result1 = Await.result(wallet.makeFundingTx(fundingPubkeyScript1, Satoshi(10000L), 20000), 10 seconds)
|
||||
val fundingPubkeyScript2 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result2 = Await.result(wallet.makeFundingTx(fundingPubkeyScript2, Satoshi(10000L), 20000), 10 seconds)
|
||||
|
||||
assert(Await.result(wallet.commit(result1.fundingTx), 10 seconds) == true)
|
||||
assert(Await.result(wallet.commit(result2.fundingTx), 10 seconds) == false)
|
||||
|
||||
}
|
||||
|
||||
/*def ticket() = {
|
||||
val wallet: Wallet = ???
|
||||
|
||||
def makeTx(amount: Coin, script: BitcoinjScript): Transaction = {
|
||||
val tx = new Transaction(wallet.getParams)
|
||||
tx.addOutput(amount, script)
|
||||
val req = SendRequest.forTx(tx)
|
||||
wallet.completeTx(req)
|
||||
tx
|
||||
}
|
||||
|
||||
val tx1 = makeTx(amount1, script1)
|
||||
val tx2 = makeTx(amount2, script2)
|
||||
|
||||
// everything is fine until here, and as expected tx1 and tx2 spend the same input
|
||||
|
||||
wallet.maybeCommitTx(tx1) // returns true as expected
|
||||
wallet.maybeCommitTx(tx2) // returns true! how come?
|
||||
}*/
|
||||
|
||||
test("manual publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val watcher = system.actorOf(Props(new BitcoinjWatcher(bitcoinjKit)), name = "bitcoinj-watcher")
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second)
|
||||
|
||||
logger.info(s"generating blocks")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 10))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
val listener = TestProbe()
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds)
|
||||
assert(Await.result(wallet.commit(result.fundingTx), 10 seconds))
|
||||
watcher ! WatchSpent(listener.ref, result.fundingTx, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT)
|
||||
watcher ! WatchConfirmed(listener.ref, result.fundingTx, 3, BITCOIN_FUNDING_DEPTHOK)
|
||||
watcher ! PublishAsap(result.fundingTx)
|
||||
|
||||
logger.info(s"waiting for confirmation of ${result.fundingTx.txid}")
|
||||
val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds)
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
|
||||
test("multiple publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val watcher = system.actorOf(Props(new BitcoinjWatcher(bitcoinjKit)), name = "bitcoinj-watcher")
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second)
|
||||
|
||||
def send() = {
|
||||
val count = Random.nextInt(20)
|
||||
val listeners = (0 to count).map {
|
||||
case i =>
|
||||
val listener = TestProbe()
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds)
|
||||
assert(Await.result(wallet.commit(result.fundingTx), 10 seconds))
|
||||
watcher ! WatchSpent(listener.ref, result.fundingTx, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT)
|
||||
watcher ! WatchConfirmed(listener.ref, result.fundingTx, 3, BITCOIN_FUNDING_DEPTHOK)
|
||||
watcher ! PublishAsap(result.fundingTx)
|
||||
(result.fundingTx.txid, listener)
|
||||
}
|
||||
system.scheduler.scheduleOnce(2 seconds, new Runnable {
|
||||
override def run() = {
|
||||
logger.info(s"generating one block")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 3))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
})
|
||||
for ((txid, listener) <- listeners) {
|
||||
logger.info(s"waiting for confirmation of $txid")
|
||||
val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds)
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
}
|
||||
|
||||
for (i <- 0 to 10) send()
|
||||
|
||||
}
|
||||
}
|
@ -1,661 +0,0 @@
|
||||
package fr.acinq.eclair.integration
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.nio.file.Files
|
||||
import java.util.{Properties, UUID}
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.bitcoinj.BitcoinjWallet
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
||||
import fr.acinq.eclair.io.Peer.Disconnect
|
||||
import fr.acinq.eclair.io.{NodeURI, Peer}
|
||||
import fr.acinq.eclair.payment.{State => _, _}
|
||||
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Bitcoinj, Globals, Kit, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.Transaction
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.Ignore
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
/**
|
||||
* Created by PM on 15/03/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
@Ignore
|
||||
class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
System.setProperty("spvtest", "true")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var nodes: Map[String, Kit] = Map()
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BasicIntegrationSpvSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BasicBitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2, param3) => bitcoinrpcclient.invoke(method, param1, param2, param3) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
setup.system.terminate()
|
||||
}
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
|
||||
def instantiateEclairNode(name: String, config: Config) = {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name")
|
||||
datadir.mkdirs()
|
||||
new PrintWriter(new File(datadir, "eclair.conf")) {
|
||||
write(config.root().render());
|
||||
close
|
||||
}
|
||||
val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name"))
|
||||
val kit = Await.result(setup.bootstrap, 10 seconds)
|
||||
setup.bitcoin.asInstanceOf[Bitcoinj].bitcoinjKit.awaitRunning()
|
||||
nodes = nodes + (name -> kit)
|
||||
}
|
||||
|
||||
def javaProps(props: Seq[(String, String)]) = {
|
||||
val properties = new Properties()
|
||||
props.foreach(p => properties.setProperty(p._1, p._2))
|
||||
properties
|
||||
}
|
||||
|
||||
test("starting eclair nodes") {
|
||||
import collection.JavaConversions._
|
||||
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "eclair.spv" -> true, "eclair.chain" -> "regtest", "eclair.bitcoinj.static-peers.0.host" -> "localhost", "eclair.bitcoinj.static-peers.0.port" -> 28333, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false, "eclair.delay-blocks" -> 6))
|
||||
//instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 29730, "eclair.api.port" -> 28080)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig))
|
||||
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) // NB: eclair.payment-handler = noop allows us to manually fulfill htlcs
|
||||
//instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
}
|
||||
|
||||
def sendFunds(node: Kit) = {
|
||||
val sender = TestProbe()
|
||||
val address = Await.result(node.wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond({
|
||||
node.wallet.getBalance.pipeTo(sender.ref)
|
||||
sender.expectMsgType[Satoshi] > Satoshi(0)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("fund eclair wallets") {
|
||||
//sendFunds(nodes("A"))
|
||||
//sendFunds(nodes("B"))
|
||||
sendFunds(nodes("C"))
|
||||
//sendFunds(nodes("D"))
|
||||
//sendFunds(nodes("E"))
|
||||
}
|
||||
|
||||
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
|
||||
val eventListener1 = TestProbe()
|
||||
val eventListener2 = TestProbe()
|
||||
node1.system.eventStream.subscribe(eventListener1.ref, classOf[ChannelStateChanged])
|
||||
node2.system.eventStream.subscribe(eventListener2.ref, classOf[ChannelStateChanged])
|
||||
val sender = TestProbe()
|
||||
val address = node2.nodeParams.publicAddresses.head
|
||||
sender.send(node1.switchboard, Peer.Connect(NodeURI(
|
||||
nodeId = node2.nodeParams.privateKey.publicKey,
|
||||
address = HostAndPort.fromParts(address.getHostString, address.getPort))))
|
||||
sender.expectMsgAnyOf(10 seconds, "connected", "already connected")
|
||||
sender.send(node1.switchboard, Peer.OpenChannel(
|
||||
remoteNodeId = node2.nodeParams.privateKey.publicKey,
|
||||
fundingSatoshis = Satoshi(fundingSatoshis),
|
||||
pushMsat = MilliSatoshi(pushMsat),
|
||||
channelFlags = None))
|
||||
sender.expectMsgAnyOf(10 seconds, "channel created")
|
||||
|
||||
awaitCond(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds)
|
||||
awaitCond(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds)
|
||||
}
|
||||
|
||||
test("connect nodes") {
|
||||
//
|
||||
// A ---- B ---- C ---- D
|
||||
// | / \
|
||||
// --E--' F{1,2,3,4}
|
||||
//
|
||||
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
|
||||
|
||||
//connect(nodes("A"), nodes("B"), 10000000, 0)
|
||||
//connect(nodes("B"), nodes("C"), 2000000, 0)
|
||||
//connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
//connect(nodes("B"), nodes("E"), 5000000, 0)
|
||||
//connect(nodes("E"), nodes("C"), 5000000, 0)
|
||||
//connect(nodes("C"), nodes("F1"), 5000000, 0)
|
||||
//connect(nodes("C"), nodes("F2"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F3"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F4"), 5000000, 0)
|
||||
|
||||
// a channel has two endpoints
|
||||
val channelEndpointsCount = nodes.values.foldLeft(0) {
|
||||
case (sum, setup) =>
|
||||
sender.send(setup.register, 'channels)
|
||||
val channels = sender.expectMsgType[Map[BinaryData, ActorRef]]
|
||||
sum + channels.size
|
||||
}
|
||||
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
awaitCond({
|
||||
nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
// confirming the funding tx
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
within(60 seconds) {
|
||||
var count = 0
|
||||
while (count < channelEndpointsCount) {
|
||||
if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == NORMAL) count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int) = {
|
||||
val sender = TestProbe()
|
||||
subset.foreach {
|
||||
case (_, setup) =>
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'nodes)
|
||||
sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].size == updates
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
}
|
||||
}
|
||||
|
||||
test("wait for network announcements") {
|
||||
val sender = TestProbe()
|
||||
// generating more blocks so that all funding txes are buried under at least 6 blocks
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 4))
|
||||
sender.expectMsgType[JValue]
|
||||
awaitAnnouncements(nodes, 3, 2, 4)
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D") {
|
||||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey))
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an invalid expiry delta for C") {
|
||||
val sender = TestProbe()
|
||||
// to simulate this, we will update C's relay params
|
||||
// first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel
|
||||
sender.send(nodes("D").register, 'shortIds)
|
||||
val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head
|
||||
val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.blockId, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth)
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will receive an error from C that include the updated channel update, then will retry the payment
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
// in the meantime, the router will have updated its state
|
||||
awaitCond({
|
||||
sender.send(nodes("A").router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD)
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// finally we retry the same payment, this time successfully
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an amount greater than capacity of C-D") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an unknown payment hash") {
|
||||
val sender = TestProbe()
|
||||
val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, pr)
|
||||
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, UnknownPaymentHash))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with a lower amount than requested") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with too much overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with a reasonable overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently use p2pkh script Helpers.getFinalScriptPubKey
|
||||
*
|
||||
* @param scriptPubKey
|
||||
* @return
|
||||
*/
|
||||
def scriptPubKeyToAddress(scriptPubKey: BinaryData) = Script.parse(scriptPubKey) match {
|
||||
case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)
|
||||
case OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUAL :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, pubKeyHash)
|
||||
case _ => ???
|
||||
}
|
||||
|
||||
def incomingTxes(node: Kit) = {
|
||||
val sender = TestProbe()
|
||||
(for {
|
||||
w <- nodes("F1").wallet.asInstanceOf[BitcoinjWallet].fWallet
|
||||
txes = w.getWalletTransactions
|
||||
incomingTxes = txes.toSet.filter(tx => tx.getTransaction.getValueSentToMe(w).longValue() > 0)
|
||||
} yield incomingTxes).pipeTo(sender.ref)
|
||||
sender.expectMsgType[Set[Transaction]]
|
||||
}
|
||||
|
||||
ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
val initialTxesC = incomingTxes(nodes("C"))
|
||||
val initialTxesF1 = incomingTxes(nodes("F1"))
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F1").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F1").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then have C unilateral close the channel (which will make F redeem the htlc onchain)
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then wait for F to detect the unilateral close and go to CLOSING state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == CLOSING
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then fulfill the htlc, which will make F redeem it on-chain
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have received the on-chain tx corresponding to the redeemed htlc
|
||||
awaitCond({
|
||||
incomingTxes(nodes("F1")).size - initialTxesF1.size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we then generate enough blocks so that C gets its main delayed output
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// and C will have its main output
|
||||
awaitCond({
|
||||
incomingTxes(nodes("C")).size - initialTxesC.size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
|
||||
}
|
||||
|
||||
ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F2").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F2").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// then we have F unilateral close the channel
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain)
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc
|
||||
// we then generate enough blocks so that F gets its htlc-success delayed output
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
//ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc and C will have its main output
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 1 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F3").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("C").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F3").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
for (i <- 0 until 11) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// this will fail the htlc
|
||||
//val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
//assert(failed.paymentHash === paymentHash)
|
||||
//assert(failed.failures.size === 1)
|
||||
//assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
//ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F4").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("C").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// then we ask F to unilaterally close the channel
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
for (i <- 0 until 11) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// this will fail the htlc
|
||||
//val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
//assert(failed.paymentHash === paymentHash)
|
||||
//assert(failed.failures.size === 1)
|
||||
//assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
//ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)
|
||||
}
|
||||
|
||||
ignore("generate and validate lots of channels") {
|
||||
implicit val extendedClient = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
// we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random
|
||||
logger.info(s"generating fake channels")
|
||||
val sender = TestProbe()
|
||||
val channels = for (i <- 0 until 242) yield {
|
||||
// let's generate a block every 10 txs so that we can compute short ids
|
||||
if (i % 10 == 0) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
AnnouncementsBatchValidationSpec.simulateChannel
|
||||
}
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
logger.info(s"simulated ${channels.size} channels")
|
||||
// then we make the announcements
|
||||
val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c))
|
||||
announcements.foreach(ann => nodes("A").router ! ann)
|
||||
awaitCond({
|
||||
sender.send(nodes("D").router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared
|
||||
}, max = 120 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -26,7 +26,7 @@ import javafx.util.{Callback, Duration}
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, BITCOINJ, ELECTRUM}
|
||||
import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM}
|
||||
import fr.acinq.eclair.Setup
|
||||
import fr.acinq.eclair.gui.stages._
|
||||
import fr.acinq.eclair.gui.utils.{CoinUtils, ContextMenuUtils, CopyAction}
|
||||
@ -330,7 +330,6 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext
|
||||
val wallet = setup.nodeParams.watcherType match {
|
||||
case BITCOIND => "Bitcoin-core"
|
||||
case ELECTRUM => "Electrum"
|
||||
case BITCOINJ => "BitcoinJ"
|
||||
}
|
||||
bitcoinWallet.setText(wallet)
|
||||
bitcoinChain.setText(s"${setup.chain.toUpperCase()}")
|
||||
|
Loading…
Reference in New Issue
Block a user