1
0
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:
Dominique 2018-02-20 14:26:38 +01:00 committed by Pierre-Marie Padiou
parent 1b247ae613
commit 8684fb238b
14 changed files with 17 additions and 1355 deletions

View File

@ -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>

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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()}")

View File

@ -51,7 +51,7 @@
<scala.version.short>2.11</scala.version.short>
<akka.version>2.4.18</akka.version>
<bitcoinlib.version>0.9.14</bitcoinlib.version>
<bitcoinj.version>0.15-rc4</bitcoinj.version>
<guava.version>24.0-android</guava.version>
</properties>
<build>