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

added spv support to eclair with bitcoinj

This commit is contained in:
pm47 2017-07-24 13:48:08 +02:00
parent f321d3e4ec
commit 8245f61f1f
51 changed files with 1337 additions and 708 deletions

View file

@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-SNAPSHOT</version>
<version>0.2-spv-SNAPSHOT</version>
</parent>
<artifactId>eclair-core_2.11</artifactId>
@ -140,6 +140,11 @@
<artifactId>jeromq</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>0.15-A5-a5</version>
</dependency>
<!-- SERIALIZATION -->
<dependency>
<groupId>org.scodec</groupId>
@ -182,12 +187,6 @@
<version>${akka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>

View file

@ -1,4 +1,8 @@
eclair {
spv = true
chain = "test"
server {
public-ips = [] // external ips, will be announced on the network
binding-ip = "0.0.0.0"
@ -22,7 +26,7 @@ eclair {
local-features = "08" // initial_routing_sync
channel-flags = 1 // announce channels
dust-limit-satoshis = 542
default-feerate-perkw = 10000 // corresponds to bitcoind's default value of feerate-perkB=20000 for a standard commit tx
default-feerate-perkB = 20000 // default bitcoin core value
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
htlc-minimum-msat = 1000000

View file

@ -35,9 +35,10 @@ object Features {
*/
def areSupported(bitset: BitSet): Boolean = {
// for now there is no mandatory feature bit, so we don't support features with any even bit set
bitset.stream().noneMatch(new IntPredicate {
override def test(value: Int) = value % 2 == 0
})
for(i <- 0 until bitset.length() by 2) {
if (bitset.get(i)) return false
}
return true
}
/**

View file

@ -11,14 +11,14 @@ object Globals {
/**
* This counter holds the current blockchain height.
* It is mainly used to calculate htlc expiries.
* The value is updated by the [[fr.acinq.eclair.blockchain.PeerWatcher]] and read by all actors, hence it needs to be thread-safe.
* The value is updated by the [[fr.acinq.eclair.blockchain.ZmqWatcher]] and read by all actors, hence it needs to be thread-safe.
*/
val blockCount = new AtomicLong(0)
/**
* This counter holds the current feeratePerKw.
* It is used to maintain an up-to-date fee in commitment tx so that they get confirmed fast enough.
* The value is updated by the [[fr.acinq.eclair.blockchain.PeerWatcher]] and read by all actors, hence it needs to be thread-safe.
* The value is updated by the [[fr.acinq.eclair.blockchain.ZmqWatcher]] and read by all actors, hence it needs to be thread-safe.
*/
val feeratePerKw = new AtomicLong(0)
}

View file

@ -39,7 +39,6 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
feeProportionalMillionth: Int,
reserveToFundingRatio: Double,
maxReserveToFundingRatio: Double,
defaultFinalScriptPubKey: BinaryData,
channelsDb: SimpleTypedDb[BinaryData, HasCommitments],
peersDb: SimpleTypedDb[PublicKey, PeerRecord],
announcementsDb: SimpleTypedDb[String, LightningMessage],
@ -50,7 +49,8 @@ case class NodeParams(extendedPrivateKey: ExtendedPrivateKey,
updateFeeMinDiffRatio: Double,
autoReconnect: Boolean,
chainHash: BinaryData,
channelFlags: Byte)
channelFlags: Byte,
spv: Boolean)
object NodeParams {
@ -67,7 +67,7 @@ object NodeParams {
.withFallback(overrideDefaults)
.withFallback(ConfigFactory.load()).getConfig("eclair")
def makeNodeParams(datadir: File, config: Config, chainHash: BinaryData, defaultFinalScriptPubKey: BinaryData): NodeParams = {
def makeNodeParams(datadir: File, config: Config, chainHash: BinaryData): NodeParams = {
datadir.mkdirs()
@ -108,7 +108,6 @@ object NodeParams {
feeProportionalMillionth = config.getInt("fee-proportional-millionth"),
reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"),
maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"),
defaultFinalScriptPubKey = defaultFinalScriptPubKey,
channelsDb = Dbs.makeChannelDb(db),
peersDb = Dbs.makePeerDb(db),
announcementsDb = Dbs.makeAnnouncementDb(db),
@ -119,6 +118,7 @@ object NodeParams {
updateFeeMinDiffRatio = config.getDouble("update-fee_min-diff-ratio"),
autoReconnect = config.getBoolean("auto-reconnect"),
chainHash = chainHash,
channelFlags = config.getInt("channel-flags").toByte)
channelFlags = config.getInt("channel-flags").toByte,
spv = config.getBoolean("spv"))
}
}

View file

@ -5,20 +5,23 @@ import java.net.InetSocketAddress
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
import akka.http.scaladsl.Http
import akka.pattern.after
import akka.stream.ActorMaterializer
import akka.util.Timeout
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.{Base58Check, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Script}
import fr.acinq.bitcoin.{BinaryData, Block}
import fr.acinq.eclair.api.{GetInfoResponse, Service}
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.{BitpayInsightFeeProvider, ConstantFeeProvider}
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.spv.BitcoinjKit
import fr.acinq.eclair.blockchain.wallet.{BitcoinCoreWallet, BitcoinjWallet}
import fr.acinq.eclair.blockchain.zmq.ZMQActor
import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, PeerWatcher}
import fr.acinq.eclair.channel.Register
import fr.acinq.eclair.io.{Server, Switchboard}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router._
import grizzled.slf4j.Logging
import org.json4s.JsonAST.JString
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext, Future, Promise}
@ -33,6 +36,8 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}")
val config = NodeParams.loadConfiguration(datadir, overrideDefaults)
val spv = config.getBoolean("spv")
logger.info(s"initializing secure random generator")
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala)
secureRandom.nextInt()
@ -40,58 +45,74 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
implicit val system = actorSystem
implicit val materializer = ActorMaterializer()
implicit val timeout = Timeout(30 seconds)
val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport")))
implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global
val future = for {
json <- bitcoinClient.client.invoke("getblockchaininfo")
chain = (json \ "chain").extract[String]
blockCount = (json \ "blocks").extract[Long]
progress = (json \ "verificationprogress").extract[Double]
chainHash <- bitcoinClient.client.invoke("getblockhash", 0).map(_.extract[String])
bitcoinVersion <- bitcoinClient.client.invoke("getinfo").map(json => (json \ "version")).map(_.extract[String])
} yield (chain, blockCount, progress, chainHash, bitcoinVersion)
val (chain, blockCount, progress, chainHash, bitcoinVersion) = Try(Await.result(future, 10 seconds)).recover { case _ => throw BitcoinRPCConnectionException }.get
val (chain, chainHash, bitcoin) = if (spv) {
logger.warn("SPV MODE ENABLED")
val chain = config.getString("chain")
val chainHash = chain match {
case "regtest" => Block.RegtestGenesisBlock.blockId
case "test" => Block.TestnetGenesisBlock.blockId
}
val bitcoinjKit = new BitcoinjKit(chain, datadir)
(chain, chainHash, Left(bitcoinjKit))
} else {
val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport")))
val future = for {
json <- bitcoinClient.rpcClient.invoke("getblockchaininfo")
chain = (json \ "chain").extract[String]
progress = (json \ "verificationprogress").extract[Double]
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_))
info <- bitcoinClient.rpcClient.invoke("getinfo")
version = info \ "version"
} yield (chain, progress, chainHash, version)
val (chain, progress, chainHash, version) = Try(Await.result(future, 10 seconds)).recover { case _ => throw BitcoinRPCConnectionException }.get
assert(progress > 0.99, "bitcoind should be synchronized")
(chain, chainHash, Right(bitcoinClient))
}
val nodeParams = NodeParams.makeNodeParams(datadir, config, chainHash)
logger.info(s"using chain=$chain chainHash=$chainHash")
assert(progress > 0.99, "bitcoind should be synchronized")
chain match {
case "test" | "regtest" => ()
case _ => throw new RuntimeException("only regtest and testnet are supported for now")
}
// we use it as final payment address, so that funds are moved to the bitcoind wallet upon channel termination
val JString(finalAddress) = Await.result(bitcoinClient.client.invoke("getnewaddress"), 10 seconds)
logger.info(s"finaladdress=$finalAddress")
// TODO: we should use p2wpkh instead of p2pkh as soon as bitcoind supports it
//val finalScriptPubKey = OP_0 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: Nil
val finalScriptPubKey = Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(Base58Check.decode(finalAddress)._2) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
val nodeParams = NodeParams.makeNodeParams(datadir, config, chainHash, finalScriptPubKey)
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
Globals.blockCount.set(blockCount)
val defaultFeeratePerKw = config.getLong("default-feerate-perkw")
val feeratePerKw = if (chain == "regtest") defaultFeeratePerKw else {
val feeratePerKB = Await.result(bitcoinClient.estimateSmartFee(nodeParams.smartfeeNBlocks), 10 seconds)
if (feeratePerKB < 0) defaultFeeratePerKw else feerateKB2Kw(feeratePerKB)
}
logger.info(s"initial feeratePerKw=$feeratePerKw")
Globals.feeratePerKw.set(feeratePerKw)
def bootstrap: Future[Kit] = {
val zmqConnected = Promise[Boolean]()
val tcpBound = Promise[Unit]()
val zmq = system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
val watcher = system.actorOf(SimpleSupervisor.props(PeerWatcher.props(nodeParams, bitcoinClient), "watcher", SupervisorStrategy.Resume))
val defaultFeeratePerKB = config.getLong("default-feerate-perkB")
val feeProvider = chain match {
case "regtest" => new ConstantFeeProvider(defaultFeeratePerKB)
case _ => new BitpayInsightFeeProvider()
}
feeProvider.getFeeratePerKB.map {
case feeratePerKB =>
val feeratePerKw = feerateKB2Kw(feeratePerKB)
logger.info(s"initial feeratePerKw=$feeratePerKw")
Globals.feeratePerKw.set(feeratePerKw)
}
val watcher = bitcoin match {
case Left(bitcoinj) =>
zmqConnected.success(true)
bitcoinj.startAsync()
system.actorOf(SimpleSupervisor.props(SpvWatcher.props(nodeParams, bitcoinj), "watcher", SupervisorStrategy.Resume))
case Right(bitcoinClient) =>
system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), Some(zmqConnected))), "zmq", SupervisorStrategy.Restart))
system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(nodeParams, bitcoinClient), "watcher", SupervisorStrategy.Resume))
}
val wallet = bitcoin match {
case Left(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
case Right(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
}
wallet.getFinalAddress.map {
case address => logger.info(s"initial wallet address=$address")
}
val paymentHandler = system.actorOf(SimpleSupervisor.props(config.getString("payment-handler") match {
case "local" => LocalPaymentHandler.props(nodeParams)
case "noop" => Props[NoopPaymentHandler]
@ -99,14 +120,13 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
val register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume))
val relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams.privateKey, paymentHandler), "relayer", SupervisorStrategy.Resume))
val router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume))
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer), "switchboard", SupervisorStrategy.Resume))
val switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume))
val paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.privateKey.publicKey, router, register), "payment-initiator", SupervisorStrategy.Restart))
val server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, switchboard, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart))
val kit = Kit(
nodeParams = nodeParams,
system = system,
zmq = zmq,
watcher = watcher,
paymentHandler = paymentHandler,
register = register,
@ -124,22 +144,22 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
}
val httpBound = Http().bindAndHandle(api.route, config.getString("api.binding-ip"), config.getInt("api.port"))
val zmqTimeout = after(5 seconds, using = system.scheduler)(Future.failed(BitcoinZMQConnectionTimeoutException))
val tcpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(new TCPBindException(config.getInt("server.port"))))
val httpTimeout = after(5 seconds, using = system.scheduler)(Future.failed(throw new TCPBindException(config.getInt("api.port"))))
for {
_ <- zmqConnected.future
_ <- tcpBound.future
_ <- httpBound
_ <- Future.firstCompletedOf(zmqConnected.future :: zmqTimeout :: Nil)
_ <- Future.firstCompletedOf(tcpBound.future :: tcpTimeout :: Nil)
_ <- Future.firstCompletedOf(httpBound :: httpTimeout :: Nil)
} yield kit
//Try(Await.result(zmqConnected.future, 5 seconds)).recover { case _ => throw BitcoinZMQConnectionTimeoutException }.get
//Try(Await.result(tcpBound.future, 5 seconds)).recover { case _ => throw new TCPBindException(config.getInt("server.port")) }.get
//Try(Await.result(httpBound, 5 seconds)).recover { case _ => throw new TCPBindException(config.getInt("api.port")) }.get
}
}
case class Kit(nodeParams: NodeParams,
system: ActorSystem,
zmq: ActorRef,
watcher: ActorRef,
paymentHandler: ActorRef,
register: ActorRef,

View file

@ -37,7 +37,7 @@ case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "scala-client", met
case class Error(code: Int, message: String)
case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String)
case class Status(node_id: String)
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: String, blockHeight: Int)
case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int)
// @formatter:on
trait Service extends Logging {

View file

@ -15,5 +15,3 @@ case class NewTransaction(tx: Transaction) extends BlockchainEvent
case class CurrentBlockCount(blockCount: Long) extends BlockchainEvent
case class CurrentFeerate(feeratePerKw: Long) extends BlockchainEvent
case class MempoolTransaction(tx: Transaction)

View file

@ -0,0 +1,178 @@
package fr.acinq.eclair.blockchain
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.{Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.{Globals, NodeParams}
import org.bitcoinj.core.{Transaction => BitcoinjTransaction}
import org.bitcoinj.kits.WalletAppKit
import org.bitcoinj.script.Script
import scala.collection.SortedMap
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
final case class Hint(script: Script)
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 SpvWatcher(nodeParams: NodeParams, 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])
context.system.scheduler.schedule(10 seconds, 1 minute, self, 'tick)
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)
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel]): Receive = {
case event@NewConfidenceLevel(tx, blockHeight, confirmations) =>
log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=${Transaction.write(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))
case w@WatchConfirmed(_, txId, minDepth, event) if txId == tx.txid && confirmations == -1 =>
// the transaction watched was overriden by a competing tx
self ! TriggerEvent(w, WatchEventDoubleSpent(event))
}
context become watching(watches, block2tx, oldEvents.filterNot(_.tx.txid == tx.txid) :+ event)
case TriggerEvent(w, e) if watches.contains(w) =>
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)
if (!w.isInstanceOf[WatchSpent]) context.become(watching(watches - w, block2tx, oldEvents))
case CurrentBlockCount(count) => {
val toPublish = block2tx.filterKeys(_ <= count)
toPublish.values.flatten.map(tx => publish(tx))
context.become(watching(watches, block2tx -- toPublish.keys, oldEvents))
}
case hint: Hint => kit.wallet().addWatchedScripts(ImmutableList.of(hint.script))
case w: Watch if !watches.contains(w) =>
log.debug(s"adding watch $w for $sender")
log.warning(s"resending ${oldEvents.size} events in order!")
oldEvents.foreach(self ! _)
context.watch(w.channel)
context.become(watching(watches + w, block2tx, oldEvents))
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=${Transaction.write(tx)}")
self ! WatchConfirmed(self, parentTxid, 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, tx +: block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]))
context.become(watching(watches, block2tx1, oldEvents))
} 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, tx +: block2tx.getOrElse(absTimeout, Seq.empty[Transaction]))
context.become(watching(watches, block2tx1, oldEvents))
} else publish(tx)
case ParallelGetRequest(announcements) => sender ! ParallelGetResponse(announcements.map {
case c =>
log.info(s"blindly validating channel=$c")
val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(PublicKey(c.bitcoinKey1), PublicKey(c.bitcoinKey2))))
val fakeFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(Satoshi(0), pubkeyScript) :: Nil,
lockTime = 0)
IndividualResult(c, Some(fakeFundingTx), true)
})
case Terminated(channel) =>
// we remove watches associated to dead actor
val deprecatedWatches = watches.filter(_.channel == channel)
context.become(watching(watches -- deprecatedWatches, block2tx, oldEvents))
case 'watches => sender ! watches
}
def publish(tx: Transaction): Unit = broadcaster ! tx
}
object SpvWatcher {
def props(nodeParams: NodeParams, kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new SpvWatcher(nodeParams, 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) = {
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

@ -29,13 +29,12 @@ final case class WatchEventConfirmed(event: BitcoinEvent, blockHeight: Int, txIn
final case class WatchEventSpent(event: BitcoinEvent, tx: Transaction) extends WatchEvent
final case class WatchEventSpentBasic(event: BitcoinEvent) extends WatchEvent
final case class WatchEventLost(event: BitcoinEvent) extends WatchEvent
final case class WatchEventDoubleSpent(event: BitcoinEvent) extends WatchEvent
/**
* Publish the provided tx as soon as possible depending on locktime and csv
*/
final case class PublishAsap(tx: Transaction)
final case class MakeFundingTx(localCommitPub: PublicKey, remoteCommitPub: PublicKey, amount: Satoshi, feeRatePerKw: Long)
final case class MakeFundingTxResponse(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey)
final case class ParallelGetRequest(ann: Seq[ChannelAnnouncement])
final case class IndividualResult(c: ChannelAnnouncement, tx: Option[Transaction], unspent: Boolean)
final case class ParallelGetResponse(r: Seq[IndividualResult])

View file

@ -5,9 +5,10 @@ import java.util.concurrent.Executors
import akka.actor.{Actor, ActorLogging, Cancellable, Props, Terminated}
import akka.pattern.pipe
import fr.acinq.bitcoin._
import fr.acinq.eclair.blockchain.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.{Globals, NodeParams, feerateKB2Kw}
import fr.acinq.eclair.{Globals, NodeParams}
import scala.collection.SortedMap
import scala.concurrent.duration._
@ -20,16 +21,21 @@ import scala.util.Try
* - also uses bitcoin-core rpc api, most notably for tx confirmation count and blockcount (because reorgs)
* Created by PM on 21/02/2016.
*/
class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
class ZmqWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
// this is to initialize block count
self ! 'tick
case class TriggerEvent(w: Watch, e: WatchEvent)
def receive: Receive = watching(Set(), SortedMap(), None)
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], nextTick: Option[Cancellable]): Receive = {
case hint: Hint => {}
case NewTransaction(tx) =>
//log.debug(s"analyzing txid=${tx.txid} tx=${Transaction.write(tx)}")
watches.collect {
@ -55,14 +61,14 @@ class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implici
Globals.blockCount.set(count)
context.system.eventStream.publish(CurrentBlockCount(count))
}
client.estimateSmartFee(nodeParams.smartfeeNBlocks).map {
/*client.estimateSmartFee(nodeParams.smartfeeNBlocks).map {
case feeratePerKB if feeratePerKB > 0 =>
val feeratePerKw = feerateKB2Kw(feeratePerKB)
log.debug(s"setting feeratePerKB=$feeratePerKB -> feeratePerKw=$feeratePerKw")
Globals.feeratePerKw.set(feeratePerKw)
context.system.eventStream.publish(CurrentFeerate(feeratePerKw))
case _ => () // bitcoind cannot estimate feerate
}
}*/
// TODO: beware of the herd effect
watches.collect {
case w@WatchConfirmed(_, txId, minDepth, event) =>
@ -117,9 +123,6 @@ class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implici
context.become(watching(watches, block2tx1, None))
} else publish(tx)
case MakeFundingTx(ourCommitPub, theirCommitPub, amount, feeRatePerKw) =>
client.makeFundingTx(ourCommitPub, theirCommitPub, amount, feeRatePerKw).pipeTo(sender)
case ParallelGetRequest(ann) => client.getParallel(ann).pipeTo(sender)
case Terminated(channel) =>
@ -195,8 +198,8 @@ class PeerWatcher(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implici
}
object PeerWatcher {
object ZmqWatcher {
def props(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new PeerWatcher(nodeParams, client)(ec))
def props(nodeParams: NodeParams, client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new ZmqWatcher(nodeParams, client)(ec))
}

View file

@ -0,0 +1,33 @@
package fr.acinq.eclair.blockchain.fee
import fr.acinq.bitcoin.Btc
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import org.json4s.JsonAST.{JDouble, JInt}
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 09/07/2017.
*/
class BitcoinCoreFeeProvider(rpcClient: BitcoinJsonRPCClient, defaultFeeratePerKB: Long)(implicit ec: ExecutionContext) extends FeeProvider {
/**
* We need this to keep commitment tx fees in sync with the state of the network
*
* @param nBlocks number of blocks until tx is confirmed
* @return the current
*/
def estimateSmartFee(nBlocks: Int): Future[Long] =
rpcClient.invoke("estimatesmartfee", nBlocks).map(json => {
json \ "feerate" match {
case JDouble(feerate) => Btc(feerate).toLong
case JInt(feerate) if feerate.toLong < 0 => feerate.toLong
case JInt(feerate) => Btc(feerate.toLong).toLong
}
})
override def getFeeratePerKB: Future[Long] = estimateSmartFee(3).map {
case f if f < 0 => defaultFeeratePerKB
case f => f
}
}

View file

@ -0,0 +1,33 @@
package fr.acinq.eclair.blockchain.fee
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import fr.acinq.bitcoin.{Btc, Satoshi}
import org.json4s.JsonAST.{JDouble, JValue}
import org.json4s.{DefaultFormats, jackson}
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
import de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 09/07/2017.
*/
class BitpayInsightFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider {
implicit val materializer = ActorMaterializer()
val httpClient = Http(system)
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
override def getFeeratePerKB: Future[Long] =
for {
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://test-insight.bitpay.com/api/utils/estimatefee?nbBlocks=3"), method = HttpMethods.GET))
json <- Unmarshal(httpRes).to[JValue]
JDouble(fee_per_kb) = json \ "3"
} yield (Btc(fee_per_kb): Satoshi).amount
}

View file

@ -0,0 +1,10 @@
package fr.acinq.eclair.blockchain.fee
import scala.concurrent.Future
/**
* Created by PM on 09/07/2017.
*/
class ConstantFeeProvider(feeratePerKB: Long) extends FeeProvider {
override def getFeeratePerKB: Future[Long] = Future.successful(feeratePerKB)
}

View file

@ -0,0 +1,12 @@
package fr.acinq.eclair.blockchain.fee
import scala.concurrent.Future
/**
* Created by PM on 09/07/2017.
*/
trait FeeProvider {
def getFeeratePerKB: Future[Long]
}

View file

@ -1,11 +1,8 @@
package fr.acinq.eclair.blockchain
package fr.acinq.eclair.blockchain.rpc
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin._
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, JsonRPCError}
import fr.acinq.eclair.channel.Helpers
import fr.acinq.eclair.blockchain.{IndividualResult, ParallelGetResponse}
import fr.acinq.eclair.fromShortId
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.ChannelAnnouncement
import org.json4s.JsonAST._
@ -15,9 +12,7 @@ import scala.util.Try
/**
* Created by PM on 26/04/2016.
*/
class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
import ExtendedBitcoinClient._
class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
implicit val formats = org.json4s.DefaultFormats
@ -29,14 +24,14 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def hex2tx(hex: String): Transaction = Transaction.read(hex, protocolVersion)
def getTxConfirmations(txId: String)(implicit ec: ExecutionContext): Future[Option[Int]] =
client.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
.map(json => Some((json \ "confirmations").extractOrElse[Int](0)))
.recover {
case t: JsonRPCError if t.error.code == -5 => None
}
def getTxBlockHash(txId: String)(implicit ec: ExecutionContext): Future[Option[String]] =
client.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
rpcClient.invoke("getrawtransaction", txId, 1) // we choose verbose output to get the number of confirmations
.map(json => (json \ "blockhash").extractOpt[String])
.recover {
case t: JsonRPCError if t.error.code == -5 => None
@ -44,7 +39,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def getBlockHashesSinceBlockHash(blockHash: String, previous: Seq[String] = Nil)(implicit ec: ExecutionContext): Future[Seq[String]] =
for {
nextblockhash_opt <- client.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String]))
nextblockhash_opt <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String]))
res <- nextblockhash_opt match {
case Some(nextBlockHash) => getBlockHashesSinceBlockHash(nextBlockHash, previous :+ nextBlockHash)
case None => Future.successful(previous)
@ -53,7 +48,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def getTxsSinceBlockHash(blockHash: String, previous: Seq[Transaction] = Nil)(implicit ec: ExecutionContext): Future[Seq[Transaction]] =
for {
(nextblockhash_opt, txids) <- client.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String], (json \ "tx").extract[List[String]]))
(nextblockhash_opt, txids) <- rpcClient.invoke("getblock", blockHash).map(json => ((json \ "nextblockhash").extractOpt[String], (json \ "tx").extract[List[String]]))
next <- Future.sequence(txids.map(getTransaction(_)))
res <- nextblockhash_opt match {
case Some(nextBlockHash) => getTxsSinceBlockHash(nextBlockHash, previous ++ next)
@ -63,7 +58,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def getMempool()(implicit ec: ExecutionContext): Future[Seq[Transaction]] =
for {
txids <- client.invoke("getrawmempool").map(json => json.extract[List[String]])
txids <- rpcClient.invoke("getrawmempool").map(json => json.extract[List[String]])
txs <- Future.sequence(txids.map(getTransaction(_)))
} yield txs
@ -78,7 +73,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
* @return a Future[txid] where txid (a String) is the is of the tx that sends the bitcoins
*/
def sendFromAccount(account: String, destination: String, amount: Double)(implicit ec: ExecutionContext): Future[String] =
client.invoke("sendfrom", account, destination, amount) collect {
rpcClient.invoke("sendfrom", account, destination, amount) collect {
case JString(txid) => txid
}
@ -88,7 +83,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
* @return
*/
def getRawTransaction(txId: String)(implicit ec: ExecutionContext): Future[String] =
client.invoke("getrawtransaction", txId) collect {
rpcClient.invoke("getrawtransaction", txId) collect {
case JString(raw) => raw
}
@ -97,8 +92,8 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def getTransaction(height: Int, index: Int)(implicit ec: ExecutionContext): Future[Transaction] =
for {
hash <- client.invoke("getblockhash", height).map(json => json.extract[String])
json <- client.invoke("getblock", hash)
hash <- rpcClient.invoke("getblockhash", height).map(json => json.extract[String])
json <- rpcClient.invoke("getblock", hash)
JArray(txs) = json \ "tx"
txid = txs(index).extract[String]
tx <- getTransaction(txid)
@ -106,7 +101,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def isTransactionOuputSpendable(txId: String, ouputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] =
for {
json <- client.invoke("gettxout", txId, ouputIndex, includeMempool)
json <- rpcClient.invoke("gettxout", txId, ouputIndex, includeMempool)
} yield json != JNull
@ -120,7 +115,7 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
def getTransactionShortId(txId: String)(implicit ec: ExecutionContext): Future[(Int, Int)] = {
val future = for {
Some(blockHash) <- getTxBlockHash(txId)
json <- client.invoke("getblock", blockHash)
json <- rpcClient.invoke("getblock", blockHash)
JInt(height) = json \ "height"
JString(hash) = json \ "hash"
JArray(txs) = json \ "tx"
@ -130,60 +125,14 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
future
}
def fundTransaction(hex: String)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = {
client.invoke("fundrawtransaction", hex /*hex.take(4) + "0000" + hex.drop(4)*/).map(json => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDouble(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
})
}
def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[FundTransactionResponse] =
fundTransaction(tx2Hex(tx))
def signTransaction(hex: String)(implicit ec: ExecutionContext): Future[SignTransactionResponse] =
client.invoke("signrawtransaction", hex).map(json => {
val JString(hex) = json \ "hex"
val JBool(complete) = json \ "complete"
SignTransactionResponse(Transaction.read(hex), complete)
})
def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] =
signTransaction(tx2Hex(tx))
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
client.invoke("sendrawtransaction", hex) collect {
rpcClient.invoke("sendrawtransaction", hex) collect {
case JString(txid) => txid
}
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
publishTransaction(tx2Hex(tx))
def makeFundingTx(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, amount: Satoshi, feeRatePerKw: Long)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] =
for {
// ask for a new address and the corresponding private key
JString(address) <- client.invoke("getnewaddress")
JString(wif) <- client.invoke("dumpprivkey", address)
JString(segwitAddress) <- client.invoke("addwitnessaddress", address)
(prefix, raw) = Base58Check.decode(wif)
priv = PrivateKey(raw, compressed = true)
pub = priv.publicKey
// create a tx that sends money to a P2SH(WPKH) output that matches our private key
parentFee = Satoshi(250 * 2 * 2 * feeRatePerKw / 1024)
partialParentTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount + parentFee, Script.pay2sh(Script.pay2wpkh(pub))) :: Nil, lockTime = 0L)
FundTransactionResponse(unsignedParentTx, _, _) <- fundTransaction(partialParentTx)
// this is the first tx that we will publish, a standard tx which send money to our p2wpkh address
SignTransactionResponse(parentTx, true) <- signTransaction(unsignedParentTx)
// now we create the funding tx
(partialFundingTx, _) = Transactions.makePartialFundingTx(amount, localFundingPubkey, remoteFundingPubkey)
// and update it to spend from our segwit tx
pos = Transactions.findPubKeyScriptIndex(parentTx, Script.pay2sh(Script.pay2wpkh(pub)))
unsignedFundingTx = partialFundingTx.copy(txIn = TxIn(OutPoint(parentTx, pos), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil)
} yield Helpers.Funding.sign(MakeFundingTxResponse(parentTx, unsignedFundingTx, 0, priv))
/**
* We need this to compute absolute timeouts expressed in number of blocks (where getBlockCount would be equivalent
* to time.now())
@ -192,26 +141,10 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
* @return the current number of blocks in the active chain
*/
def getBlockCount(implicit ec: ExecutionContext): Future[Long] =
client.invoke("getblockcount") collect {
rpcClient.invoke("getblockcount") collect {
case JInt(count) => count.toLong
}
/**
* We need this to keep commitment tx fees in sync with the state of the network
*
* @param nBlocks number of blocks until tx is confirmed
* @param ec
* @return the current
*/
def estimateSmartFee(nBlocks: Int)(implicit ec: ExecutionContext): Future[Long] =
client.invoke("estimatesmartfee", nBlocks).map(json => {
json \ "feerate" match {
case JDouble(feerate) => Btc(feerate).toLong
case JInt(feerate) if feerate.toLong < 0 => feerate.toLong
case JInt(feerate) => Btc(feerate.toLong).toLong
}
})
def getParallel(awaiting: Seq[ChannelAnnouncement]): Future[ParallelGetResponse] = {
case class TxCoordinate(blockHeight: Int, txIndex: Int, outputIndex: Int)
@ -225,8 +158,8 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
implicit val formats = org.json4s.DefaultFormats
for {
blockHashes: Seq[String] <- client.invoke(coordinates.map(coord => ("getblockhash", coord._1.blockHeight :: Nil))).map(_.map(_.extractOrElse[String]("00" * 32)))
txids: Seq[String] <- client.invoke(blockHashes.map(h => ("getblock", h :: Nil)))
blockHashes: Seq[String] <- rpcClient.invoke(coordinates.map(coord => ("getblockhash", coord._1.blockHeight :: Nil))).map(_.map(_.extractOrElse[String]("00" * 32)))
txids: Seq[String] <- rpcClient.invoke(blockHashes.map(h => ("getblock", h :: Nil)))
.map(_.zipWithIndex)
.map(_.map {
case (json, idx) => Try {
@ -234,24 +167,16 @@ class ExtendedBitcoinClient(val client: BitcoinJsonRPCClient) {
txs(coordinates(idx)._1.txIndex).extract[String]
} getOrElse ("00" * 32)
})
txs <- client.invoke(txids.map(txid => ("getrawtransaction", txid :: Nil))).map(_.map {
txs <- rpcClient.invoke(txids.map(txid => ("getrawtransaction", txid :: Nil))).map(_.map {
case JString(raw) => Some(Transaction.read(raw))
case _ => None
})
unspent <- client.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull))
unspent <- rpcClient.invoke(txids.zipWithIndex.map(txid => ("gettxout", txid._1 :: coordinates(txid._2)._1.outputIndex :: true :: Nil))).map(_.map(_ != JNull))
} yield ParallelGetResponse(awaiting.zip(txs.zip(unspent)).map(x => IndividualResult(x._1, x._2._1, x._2._2)))
}
}
object ExtendedBitcoinClient {
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
}
/*object Test extends App {

View file

@ -0,0 +1,98 @@
package fr.acinq.eclair.blockchain.spv
import java.io.File
import java.util
import akka.actor.ActorSystem
import fr.acinq.bitcoin.{BinaryData, Transaction}
import fr.acinq.eclair.Globals
import fr.acinq.eclair.blockchain.spv.BitcoinjKit._
import fr.acinq.eclair.blockchain.{CurrentBlockCount, NewConfidenceLevel}
import grizzled.slf4j.Logging
import org.bitcoinj.core.TransactionConfidence.ConfidenceType
import org.bitcoinj.core.listeners.{NewBestBlockListener, OnTransactionBroadcastListener, PeerConnectedEventListener, TransactionConfidenceEventListener}
import org.bitcoinj.core.{NetworkParameters, Peer, StoredBlock, Transaction => BitcoinjTransaction}
import org.bitcoinj.kits.WalletAppKit
import org.bitcoinj.params.{RegTestParams, TestNet3Params}
import org.bitcoinj.script.Script
import org.bitcoinj.wallet.Wallet
import org.bitcoinj.wallet.listeners.ScriptsChangeEventListener
import scala.concurrent.Promise
/**
* Created by PM on 09/07/2017.
*/
class BitcoinjKit(chain: String, datadir: File)(implicit system: ActorSystem) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj") with Logging {
// so that we know when the peerGroup/chain/wallet are accessible
private val initializedPromise = Promise[Boolean]()
val initialized = initializedPromise.future
override def onSetupCompleted(): Unit = {
logger.info(s"peerGroup.getMaxConnections==${peerGroup().getMaxConnections}")
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
// 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 = {
val blockCount = peerGroup().getMostCommonChainHeight
// we wait for at least 3 peers before relying on current block height, but we trust localhost
if ((peer.getAddress.getAddr.isLoopbackAddress || peerCount > 3) && Globals.blockCount.get() < blockCount) {
logger.info(s"current blockchain height=$blockCount")
system.eventStream.publish(CurrentBlockCount(blockCount))
Globals.blockCount.set(blockCount)
}
}
})
peerGroup().addOnTransactionBroadcastListener(new OnTransactionBroadcastListener {
override def onTransaction(peer: Peer, t: BitcoinjTransaction): Unit = {
logger.info(s"txid=${t.getHashAsString} confidence=${t.getConfidence}")
}
})
chain().addNewBestBlockListener(new NewBestBlockListener {
override def notifyNewBestBlock(storedBlock: StoredBlock): Unit = {
// when synchronizing we don't want to advertise previous blocks
if (Globals.blockCount.get() < storedBlock.getHeight) {
logger.debug(s"new block height=${storedBlock.getHeight} hash=${storedBlock.getHeader.getHashAsString}")
system.eventStream.publish(CurrentBlockCount(storedBlock.getHeight))
Globals.blockCount.set(storedBlock.getHeight)
}
}
})
wallet().addTransactionConfidenceEventListener(new TransactionConfidenceEventListener {
override def onTransactionConfidenceChanged(wallet: Wallet, bitcoinjTx: BitcoinjTransaction): Unit = {
val tx = Transaction.read(bitcoinjTx.bitcoinSerialize())
logger.info(s"tx confidence changed for txid=${tx.txid} confidence=${bitcoinjTx.getConfidence}")
val depthInBlocks = bitcoinjTx.getConfidence.getConfidenceType match {
case ConfidenceType.DEAD => -1
case _ => bitcoinjTx.getConfidence.getDepthInBlocks
}
system.eventStream.publish(NewConfidenceLevel(tx, 0, depthInBlocks))
}
})
wallet.addScriptsChangeEventListener(new ScriptsChangeEventListener {
import scala.collection.JavaConversions._
override def onScriptsChanged(wallet: Wallet, scripts: util.List[Script], isAddingScripts: Boolean): Unit =
logger.info(s"watching scripts: ${scripts.map(_.getProgram).map(BinaryData(_)).mkString(",")}")
})
initializedPromise.success(true)
}
}
object BitcoinjKit {
def chain2Params(chain: String): NetworkParameters = chain match {
case "regtest" => RegTestParams.get()
case "test" => TestNet3Params.get()
}
}

View file

@ -0,0 +1,174 @@
package fr.acinq.eclair.blockchain.wallet
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58Check, BinaryData, Crypto, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.channel.{BITCOIN_INPUT_SPENT, BITCOIN_TX_CONFIRMED, Helpers}
import fr.acinq.eclair.transactions.Transactions
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JBool, JDouble, JInt, JString}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
/**
* Created by PM on 06/07/2017.
*/
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
override def getFinalAddress: Future[String] = rpcClient.invoke("getnewaddress").map(json => {
val JString(address) = json
address
})
case class FundTransactionResponse(tx: Transaction, changepos: Int, fee: Double)
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
case class MakeFundingTxResponseWithParent(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey)
def fundTransaction(hex: String): Future[FundTransactionResponse] = {
rpcClient.invoke("fundrawtransaction", hex).map(json => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDouble(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
})
}
def fundTransaction(tx: Transaction): Future[FundTransactionResponse] =
fundTransaction(Transaction.write(tx).toString())
def signTransaction(hex: String): Future[SignTransactionResponse] =
rpcClient.invoke("signrawtransaction", hex).map(json => {
val JString(hex) = json \ "hex"
val JBool(complete) = json \ "complete"
SignTransactionResponse(Transaction.read(hex), complete)
})
def signTransaction(tx: Transaction): Future[SignTransactionResponse] =
signTransaction(Transaction.write(tx).toString())
/**
*
* @param fundingTxResponse a funding tx response
* @return an updated funding tx response that is properly sign
*/
def sign(fundingTxResponse: MakeFundingTxResponseWithParent): MakeFundingTxResponseWithParent = {
// find the output that we are spending from
val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
val pub = fundingTxResponse.priv.publicKey
val pubKeyScript = Script.pay2pkh(pub)
val sig = Transaction.signInput(fundingTxResponse.fundingTx, 0, pubKeyScript, SIGHASH_ALL, utxo.amount, SigVersion.SIGVERSION_WITNESS_V0, fundingTxResponse.priv)
val witness = ScriptWitness(Seq(sig, pub.toBin))
val fundingTx1 = fundingTxResponse.fundingTx.updateSigScript(0, OP_PUSHDATA(Script.write(Script.pay2wpkh(pub))) :: Nil).updateWitness(0, witness)
Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
fundingTxResponse.copy(fundingTx = fundingTx1)
}
/**
*
* @param fundingTxResponse funding transaction response, which includes a funding tx, its parent, and the private key
* that we need to re-sign the funding
* @param newParentTx new parent tx
* @return an updated funding transaction response where the funding tx now spends from newParentTx
*/
def replaceParent(fundingTxResponse: MakeFundingTxResponseWithParent, newParentTx: Transaction): MakeFundingTxResponseWithParent = {
// find the output that we are spending from
val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
// check that it matches what we expect, which is a P2WPKH output to our public key
require(utxo.publicKeyScript == Script.write(Script.pay2sh(Script.pay2wpkh(fundingTxResponse.priv.publicKey))))
// update our tx input we the hash of the new parent
val input = fundingTxResponse.fundingTx.txIn(0)
val input1 = input.copy(outPoint = input.outPoint.copy(hash = newParentTx.hash))
val unsignedFundingTx = fundingTxResponse.fundingTx.copy(txIn = Seq(input1))
// and re-sign it
sign(MakeFundingTxResponseWithParent(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv))
}
def makeParentAndFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponseWithParent] =
for {
// ask for a new address and the corresponding private key
JString(address) <- rpcClient.invoke("getnewaddress")
JString(wif) <- rpcClient.invoke("dumpprivkey", address)
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
(prefix, raw) = Base58Check.decode(wif)
priv = PrivateKey(raw, compressed = true)
pub = priv.publicKey
// create a tx that sends money to a P2SH(WPKH) output that matches our private key
parentFee = Satoshi(250 * 2 * 2 * feeRatePerKw / 1024)
partialParentTx = Transaction(
version = 2,
txIn = Nil,
txOut = TxOut(amount + parentFee, Script.pay2sh(Script.pay2wpkh(pub))) :: Nil,
lockTime = 0L)
FundTransactionResponse(unsignedParentTx, _, _) <- fundTransaction(partialParentTx)
// this is the first tx that we will publish, a standard tx which send money to our p2wpkh address
SignTransactionResponse(parentTx, true) <- signTransaction(unsignedParentTx)
// now we create the funding tx
partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
// and update it to spend from our segwit tx
pos = Transactions.findPubKeyScriptIndex(parentTx, Script.pay2sh(Script.pay2wpkh(pub)))
unsignedFundingTx = partialFundingTx.copy(txIn = TxIn(OutPoint(parentTx, pos), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil)
} yield sign(MakeFundingTxResponseWithParent(parentTx, unsignedFundingTx, 0, priv))
/**
* This is a workaround for malleability
*
* @param pubkeyScript
* @param amount
* @param feeRatePerKw
* @return
*/
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = {
val promise = Promise[MakeFundingTxResponse]()
(for {
fundingTxResponse@MakeFundingTxResponseWithParent(parentTx, _, _, _) <- makeParentAndFundingTx(pubkeyScript, amount, feeRatePerKw)
_ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor")
tempActor = system.actorOf(Props(new Actor {
override def receive: Receive = {
case WatchEventSpent(BITCOIN_INPUT_SPENT(parentTx), spendingTx) =>
if (parentTx.txid != spendingTx.txid) {
// an input of our parent tx was spent by a tx that we're not aware of (i.e. a malleated version of our parent tx)
// set a new watch; if it is confirmed, we'll use it as the new parent for our funding tx
logger.warn(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}")
}
watcher ! WatchConfirmed(self, spendingTx.txid, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx))
case WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _) =>
// a potential parent for our funding tx has been confirmed, let's update our funding tx
val finalFundingTx = replaceParent(fundingTxResponse, tx)
promise.success(MakeFundingTxResponse(finalFundingTx.fundingTx, finalFundingTx.fundingTxOutputIndex))
}
}))
// we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar
input0 = parentTx.txIn.head
_ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, BITCOIN_INPUT_SPENT(parentTx))
// and we publish the parent tx
_ = logger.info(s"publishing parent tx: txid=${parentTx.txid} tx=${Transaction.write(parentTx)}")
// we use a small delay so that we are sure Publish doesn't race with WatchSpent (which is ok but generates unnecessary warnings)
_ = system.scheduler.scheduleOnce(100 milliseconds, watcher, PublishAsap(parentTx))
} yield {}) onFailure {
case t: Throwable => promise.failure(t)
}
promise.future
}
/**
* We don't manage double spends yet
* @param tx
* @return
*/
override def commit(tx: Transaction): Future[Boolean] = Future.successful(true)
}

View file

@ -0,0 +1,48 @@
package fr.acinq.eclair.blockchain.wallet
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Satoshi, Transaction}
import grizzled.slf4j.Logging
import org.bitcoinj.core.{Coin, Transaction => BitcoinjTransaction}
import org.bitcoinj.script.Script
import org.bitcoinj.wallet.KeyChain.KeyPurpose
import org.bitcoinj.wallet.{SendRequest, Wallet}
import scala.collection.JavaConversions._
import scala.concurrent.{ExecutionContext, Future}
/**
* Created by PM on 08/07/2017.
*/
class BitcoinjWallet(fWallet: Future[Wallet])(implicit ec: ExecutionContext) extends EclairWallet with Logging {
fWallet.map(wallet => wallet.allowSpendingUnconfirmedTransactions())
override def getFinalAddress: Future[String] = for {
wallet <- fWallet
} yield Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, wallet.freshSegwitAddress(KeyPurpose.RECEIVE_FUNDS).getHash160)
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for {
wallet <- fWallet
} yield {
logger.info(s"building funding tx")
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
bitcoinjTx = new org.bitcoinj.core.Transaction(wallet.getParams(), serializedTx)
canCommit = wallet.maybeCommitTx(bitcoinjTx)
_ = logger.info(s"commit txid=${tx.txid} result=$canCommit")
} yield canCommit
}
}

View file

@ -0,0 +1,20 @@
package fr.acinq.eclair.blockchain.wallet
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
import scala.concurrent.Future
/**
* Created by PM on 06/07/2017.
*/
trait EclairWallet {
def getFinalAddress: Future[String]
def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse]
def commit(tx: Transaction): Future[Boolean]
}
final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int)

View file

@ -1,16 +1,19 @@
package fr.acinq.eclair.channel
import akka.actor.{ActorRef, FSM, LoggingFSM, OneForOneStrategy, Props, Status, SupervisorStrategy}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import akka.pattern.pipe
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.wallet.{EclairWallet, MakeFundingTxResponse}
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain, Sphinx}
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._
@ -22,10 +25,10 @@ import scala.util.{Failure, Left, Success, Try}
*/
object Channel {
def props(nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef) = Props(new Channel(nodeParams, remoteNodeId, blockchain, router, relayer))
def props(nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef) = Props(new Channel(nodeParams, wallet, remoteNodeId, blockchain, router, relayer))
}
class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends LoggingFSM[State, Data] {
class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: PublicKey, blockchain: ActorRef, router: ActorRef, relayer: ActorRef)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends LoggingFSM[State, Data] {
val forwarder = context.actorOf(Props(new Forwarder(nodeParams)), "forwarder")
@ -101,6 +104,9 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
log.info(s"restoring channel $data")
context.system.eventStream.publish(ChannelRestored(self, context.parent, remoteNodeId, data.commitments.localParams.isFunder, data.channelId, data))
// TODO: should we wait for an acknowledgment from the watcher?
if (nodeParams.spv) {
blockchain ! Hint(new BitcoinjScript(data.commitments.commitInput.txOut.publicKeyScript))
}
blockchain ! WatchSpent(self, data.commitments.commitInput.outPoint.txid, data.commitments.commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT)
blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST)
data match {
@ -203,7 +209,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
val localFundingPubkey = localParams.fundingPrivKey.publicKey
// we assume that our funding parent tx is about 250 bytes, that the feereate-per-kb is 2*feerate-per-kw and we double the fee estimate
// to give the parent a hefty fee
blockchain ! MakeFundingTx(localFundingPubkey, remoteParams.fundingPubKey, Satoshi(fundingSatoshis), Globals.feeratePerKw.get())
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, Satoshi(fundingSatoshis), Globals.feeratePerKw.get()).pipeTo(self)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, open)
}
@ -215,57 +222,27 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
})
when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions {
case Event(fundingResponse@MakeFundingTxResponse(parentTx: Transaction, fundingTx: Transaction, fundingTxOutputIndex: Int, priv: PrivateKey), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, _)) =>
// we watch the first input of the parent tx, so that we can detect when it is spent by a malleated avatar
val input0 = parentTx.txIn.head
blockchain ! WatchSpent(self, input0.outPoint.txid, input0.outPoint.index.toInt, BITCOIN_INPUT_SPENT(parentTx))
// and we publish the parent tx
log.info(s"publishing parent tx: txid=${parentTx.txid} tx=${Transaction.write(parentTx)}")
// we use a small delay so that we are sure Publish doesn't race with WatchSpent (which is ok but generates unnecessary warnings)
context.system.scheduler.scheduleOnce(100 milliseconds, blockchain, PublishAsap(parentTx))
goto(WAIT_FOR_FUNDING_PARENT) using DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, Set(parentTx), data)
case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex), data@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, open)) =>
// let's create the first commitment tx that spends the yet uncommitted funding tx
val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch)
require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!")
val localSigOfRemoteTx = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey)
// signature of their initial commitment tx that pays remote pushMsat
val fundingCreated = FundingCreated(
temporaryChannelId = temporaryChannelId,
fundingTxid = fundingTx.hash,
fundingOutputIndex = fundingTxOutputIndex,
signature = localSigOfRemoteTx
)
val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex)
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId))
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, fundingCreated) sending fundingCreated
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
case Event(e: Error, _) => handleRemoteErrorNoCommitments(e)
case Event(INPUT_DISCONNECTED, _) => goto(CLOSED)
})
when(WAIT_FOR_FUNDING_PARENT)(handleExceptions {
case Event(WatchEventSpent(BITCOIN_INPUT_SPENT(parentTx), spendingTx), DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, parentCandidates, data)) =>
if (parentTx.txid != spendingTx.txid) {
// an input of our parent tx was spent by a tx that we're not aware of (i.e. a malleated version of our parent tx)
// set a new watch; if it is confirmed, we'll use it as the new parent for our funding tx
log.warning(s"parent tx has been malleated: originalParentTxid=${parentTx.txid} malleated=${spendingTx.txid}")
}
blockchain ! WatchConfirmed(self, spendingTx.txid, minDepth = 1, BITCOIN_TX_CONFIRMED(spendingTx))
stay using DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, parentCandidates + spendingTx, data)
case Event(WatchEventConfirmed(BITCOIN_TX_CONFIRMED(tx), _, _), DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse, _, data)) =>
// a potential parent for our funding tx has been confirmed, let's update our funding tx
Try(Helpers.Funding.replaceParent(fundingResponse, tx)) match {
case Success(MakeFundingTxResponse(_, fundingTx, fundingTxOutputIndex, _)) =>
// let's create the first commitment tx that spends the yet uncommitted funding tx
import data._
val (localSpec, localCommitTx, remoteSpec, remoteCommitTx) = Funding.makeFirstCommitTxs(localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint, nodeParams.maxFeerateMismatch)
val localSigOfRemoteTx = Transactions.sign(remoteCommitTx, localParams.fundingPrivKey)
// signature of their initial commitment tx that pays remote pushMsat
val fundingCreated = FundingCreated(
temporaryChannelId = temporaryChannelId,
fundingTxid = fundingTx.hash,
fundingOutputIndex = fundingTxOutputIndex,
signature = localSigOfRemoteTx
)
val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex)
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId))
goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), data.lastSent.channelFlags, fundingCreated) sending fundingCreated
case Failure(cause) =>
log.warning(s"confirmed tx ${tx.txid} is not an input to our funding tx")
stay()
}
case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) =>
log.error(t, s"wallet returned error: ")
val error = Error(d.temporaryChannelId, "aborting channel creation".getBytes)
goto(CLOSED) sending error
case Event(CMD_CLOSE(_), _) => goto(CLOSED)
@ -293,8 +270,6 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
val channelId = toLongId(fundingTxHash, fundingTxOutputIndex)
// watch the funding tx transaction
val commitInput = localCommitTx.input
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK)
val fundingSigned = FundingSigned(
channelId = channelId,
signature = localSigOfRemoteTx
@ -305,6 +280,11 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
commitInput, ShaChain.init, channelId = channelId)
if (nodeParams.spv) {
blockchain ! Hint(new BitcoinjScript(commitments.commitInput.txOut.publicKeyScript))
}
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK)
context.parent ! ChannelIdAssigned(self, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
context.system.eventStream.publish(ChannelIdAssigned(self, temporaryChannelId, channelId))
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
@ -323,7 +303,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
// we make sure that their sig checks out and that our first commit tx is spendable
val localSigOfLocalTx = Transactions.sign(localCommitTx, localParams.fundingPrivKey)
val signedLocalCommitTx = Transactions.addSigs(localCommitTx, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig)
Transactions.checkSpendable(signedLocalCommitTx) match {
Transactions.checkSpendable(fundingTx, signedLocalCommitTx.tx)
.flatMap(_ => Transactions.checkSpendable(signedLocalCommitTx)) match {
case Failure(cause) =>
log.error(cause, "their FundingSigned message contains an invalid signature")
val error = Error(channelId, cause.getMessage.getBytes)
@ -340,9 +321,18 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
// we do this to make sure that the channel state has been written to disk when we publish the funding tx
val nextState = store(DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, Left(fundingCreated)))
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK)
blockchain ! PublishAsap(fundingTx)
if (nodeParams.spv) {
blockchain ! Hint(new BitcoinjScript(commitments.commitInput.txOut.publicKeyScript))
}
log.info(s"committing txid=${fundingTx.txid}")
wallet.commit(fundingTx).map {
case true =>
blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher?
blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK)
blockchain ! PublishAsap(fundingTx)
case false =>
self ! WatchEventDoubleSpent(BITCOIN_FUNDING_DEPTHOK)
}
goto(WAIT_FOR_FUNDING_CONFIRMED) using nextState
}
@ -383,7 +373,9 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
// this clock will be used to detect htlc timeouts
context.system.eventStream.subscribe(self, classOf[CurrentBlockCount])
context.system.eventStream.subscribe(self, classOf[CurrentFeerate])
if (d.commitments.announceChannel) {
// NB: in spv 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
if (d.commitments.announceChannel && !nodeParams.spv) {
// used for announcement of channel (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly)
blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
}
@ -647,6 +639,11 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
log.info(s"received remote announcement signatures, delaying")
// our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message
context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs)
if (nodeParams.spv) {
log.warning(s"HACK: since we cannot get the tx index in spv mode, we copy the value sent by remote")
val (blockHeight, txIndex, _) = fromShortId(remoteAnnSigs.shortChannelId)
self ! WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex)
}
stay
}
@ -981,7 +978,7 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
log.info(s"re-sending fundingLocked")
val nextPerCommitmentPoint = Generators.perCommitPoint(d.commitments.localParams.shaSeed, 1)
val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint)
goto(WAIT_FOR_FUNDING_LOCKED) sending(fundingLocked)
goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked
case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) =>
@ -1003,7 +1000,9 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
}
// we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE
if (d.commitments.announceChannel && d.shortChannelId.isEmpty) {
// NB: in spv 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
if (d.commitments.announceChannel && d.shortChannelId.isEmpty && !nodeParams.spv) {
blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
}
@ -1072,7 +1071,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
// we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream
case Event(CurrentBlockCount(_), _) => stay
case Event("ok", _) => stay // noop handler
// we receive this when we send command to ourselves
case Event("ok", _) => stay
}
onTransition {
@ -1157,7 +1157,12 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
// NB: we do not watch for htlcs txes!!
// this may lead to some htlcs not been claimed because the channel will be considered close and deleted before the claiming txes are published
localCommitPublished.claimMainDelayedOutputTx match {
case Some(tx) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_LOCALCOMMIT_DONE)
case Some(tx) =>
if (nodeParams.spv) {
// we need to watch the corresponding public key script of the commit tx
blockchain ! Hint(new BitcoinjScript(localCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript))
}
blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_LOCALCOMMIT_DONE)
case None => blockchain ! WatchConfirmed(self, localCommitPublished.commitTx.txid, nodeParams.minDepthBlocks, BITCOIN_LOCALCOMMIT_DONE)
}
@ -1214,7 +1219,12 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
// NB: we do not watch for htlcs txes!!
// this may lead to some htlcs not been claimed because the channel will be considered close and deleted before the claiming txes are published
remoteCommitPublished.claimMainOutputTx match {
case Some(tx) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, event)
case Some(tx) =>
if (nodeParams.spv) {
// we need to watch the corresponding public key script of the commit tx
blockchain ! Hint(new BitcoinjScript(remoteCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript))
}
blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, event)
case None => blockchain ! WatchConfirmed(self, remoteCommitPublished.commitTx.txid, nodeParams.minDepthBlocks, event)
}
@ -1258,8 +1268,18 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
// if there is a main-penalty or a claim-main-output tx, we watch them, otherwise we watch the commit tx
// NB: we do not watch for htlcs txes, but we don't steal them currently anyway
(revokedCommitPublished.mainPenaltyTx, revokedCommitPublished.claimMainOutputTx) match {
case (Some(tx), _) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE)
case (None, Some(tx)) => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE)
case (Some(tx), _) =>
if (nodeParams.spv) {
// we need to watch the corresponding public key script of the revoked commit tx
blockchain ! Hint(new BitcoinjScript(revokedCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript))
}
blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE)
case (None, Some(tx)) =>
if (nodeParams.spv) {
// we need to watch the corresponding public key script of the revoked commit tx
blockchain ! Hint(new BitcoinjScript(revokedCommitPublished.commitTx.txOut(tx.txIn.head.outPoint.index.toInt).publicKeyScript))
}
blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE)
case _ => blockchain ! WatchConfirmed(self, revokedCommitPublished.commitTx.txid, nodeParams.minDepthBlocks, BITCOIN_PENALTY_DONE)
}
@ -1400,15 +1420,6 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
state
}
// def storing(): FSM.State[fr.acinq.eclair.channel.State, Data] = {
// state.stateData match {
// case d: HasCommitments =>
// log.debug(s"updating database record for channelId=${d.channelId} (state=$state)")
// nodeParams.channelsDb.put(d.channelId, d)
// case _ => {}
// }
// state
// }
}
// we let the peer decide what to do

View file

@ -4,7 +4,6 @@ import akka.actor.ActorRef
import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar}
import fr.acinq.bitcoin.{BinaryData, Transaction}
import fr.acinq.eclair.UInt64
import fr.acinq.eclair.blockchain.MakeFundingTxResponse
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.transactions.CommitmentSpec
import fr.acinq.eclair.transactions.Transactions.CommitTx
@ -32,7 +31,6 @@ case object WAIT_FOR_INIT_INTERNAL extends State
case object WAIT_FOR_OPEN_CHANNEL extends State
case object WAIT_FOR_ACCEPT_CHANNEL extends State
case object WAIT_FOR_FUNDING_INTERNAL extends State
case object WAIT_FOR_FUNDING_PARENT extends State
case object WAIT_FOR_FUNDING_CREATED extends State
case object WAIT_FOR_FUNDING_SIGNED extends State
case object WAIT_FOR_FUNDING_CONFIRMED extends State
@ -135,7 +133,6 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti
final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends Data
final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, lastSent: OpenChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_PARENT(fundingResponse: MakeFundingTxResponse, parentCandidates: Set[Transaction], data: DATA_WAIT_FOR_FUNDING_INTERNAL) extends Data
final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingSatoshis: Long, pushMsat: Long, initialFeeratePerKw: Long, remoteFirstPerCommitmentPoint: Point, channelFlags: Byte, lastSent: AcceptChannel) extends Data
final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams: LocalParams, remoteParams: RemoteParams, fundingTx: Transaction, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, channelFlags: Byte, lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments

View file

@ -3,7 +3,7 @@ package fr.acinq.eclair.channel
import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar, sha256}
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair.blockchain.MakeFundingTxResponse
import fr.acinq.eclair.blockchain.wallet.EclairWallet
import fr.acinq.eclair.crypto.Generators
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._
@ -12,6 +12,7 @@ import fr.acinq.eclair.wire.{ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Globals, NodeParams}
import grizzled.slf4j.Logging
import scala.concurrent.Await
import scala.util.{Failure, Success, Try}
/**
@ -31,7 +32,6 @@ object Helpers {
case d: DATA_WAIT_FOR_OPEN_CHANNEL => d.initFundee.temporaryChannelId
case d: DATA_WAIT_FOR_ACCEPT_CHANNEL => d.initFunder.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_INTERNAL => d.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_PARENT => d.data.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_CREATED => d.temporaryChannelId
case d: DATA_WAIT_FOR_FUNDING_SIGNED => d.channelId
case d: HasCommitments => d.channelId
@ -73,6 +73,16 @@ object Helpers {
remoteFeeratePerKw > 0 && feeRateMismatch(remoteFeeratePerKw, localFeeratePerKw) > maxFeerateMismatchRatio
}
def getFinalScriptPubKey(wallet: EclairWallet): BinaryData = {
import scala.concurrent.duration._
val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds)
val finalScriptPubKey = Base58Check.decode(finalAddress) match {
case (Base58.Prefix.PubkeyAddressTestnet, hash) => Script.write(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil)
case (Base58.Prefix.ScriptAddressTestnet, hash) => Script.write(OP_HASH160 :: OP_PUSHDATA(hash) :: OP_EQUAL :: Nil)
}
finalScriptPubKey
}
object Funding {
def makeFundingInputInfo(fundingTxId: BinaryData, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = {
@ -120,48 +130,6 @@ object Helpers {
(localSpec, localCommitTx, remoteSpec, remoteCommitTx)
}
/**
*
* @param fundingTxResponse funding transaction response, which includes a funding tx, its parent, and the private key
* that we need to re-sign the funding
* @param newParentTx new parent tx
* @return an updated funding transaction response where the funding tx now spends from newParentTx
*/
def replaceParent(fundingTxResponse: MakeFundingTxResponse, newParentTx: Transaction): MakeFundingTxResponse = {
// find the output that we are spending from
val utxo = newParentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
// check that it matches what we expect, which is a P2WPKH output to our public key
require(utxo.publicKeyScript == Script.write(Script.pay2sh(Script.pay2wpkh(fundingTxResponse.priv.publicKey))))
// update our tx input we the hash of the new parent
val input = fundingTxResponse.fundingTx.txIn(0)
val input1 = input.copy(outPoint = input.outPoint.copy(hash = newParentTx.hash))
val unsignedFundingTx = fundingTxResponse.fundingTx.copy(txIn = Seq(input1))
// and re-sign it
Helpers.Funding.sign(MakeFundingTxResponse(newParentTx, unsignedFundingTx, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.priv))
}
/**
*
* @param fundingTxResponse a funding tx response
* @return an updated funding tx response that is properly sign
*/
def sign(fundingTxResponse: MakeFundingTxResponse): MakeFundingTxResponse = {
// find the output that we are spending from
val utxo = fundingTxResponse.parentTx.txOut(fundingTxResponse.fundingTx.txIn(0).outPoint.index.toInt)
val pub = fundingTxResponse.priv.publicKey
val pubKeyScript = Script.pay2pkh(pub)
val sig = Transaction.signInput(fundingTxResponse.fundingTx, 0, pubKeyScript, SIGHASH_ALL, utxo.amount, SigVersion.SIGVERSION_WITNESS_V0, fundingTxResponse.priv)
val witness = ScriptWitness(Seq(sig, pub.toBin))
val fundingTx1 = fundingTxResponse.fundingTx.updateSigScript(0, OP_PUSHDATA(Script.write(Script.pay2wpkh(pub))) :: Nil).updateWitness(0, witness)
Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
fundingTxResponse.copy(fundingTx = fundingTx1)
}
}
object Closing extends Logging {

View file

@ -2,15 +2,16 @@ package fr.acinq.eclair.io
import java.net.InetSocketAddress
import akka.actor.{ActorRef, FSM, LoggingFSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated}
import akka.actor.{ActorRef, LoggingFSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, Crypto, DeterministicWallet}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.wallet.EclairWallet
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.TransportHandler.{HandshakeCompleted, Listener}
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
import fr.acinq.eclair.router.{Rebroadcast, SendRoutingState}
import fr.acinq.eclair.wire._
import fr.acinq.eclair._
import scala.concurrent.duration._
import scala.util.Random
@ -45,7 +46,7 @@ case class PeerRecord(id: PublicKey, address: InetSocketAddress)
/**
* Created by PM on 26/08/2016.
*/
class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, storedChannels: Set[HasCommitments]) extends LoggingFSM[State, Data] {
class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) extends LoggingFSM[State, Data] {
import Peer._
@ -92,7 +93,6 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
stay using d.copy(offlineChannels = offlineChannels + BrandNewChannel(c))
case Event(remoteInit: Init, InitializingData(transport, offlineChannels)) =>
import fr.acinq.eclair.Features._
log.info(s"$remoteNodeId has features: initialRoutingSync=${Features.initialRoutingSync(remoteInit.localFeatures)}")
if (Features.areSupported(remoteInit.localFeatures)) {
if (Features.initialRoutingSync(remoteInit.localFeatures)) {
@ -234,13 +234,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[
}
def createChannel(nodeParams: NodeParams, transport: ActorRef, funder: Boolean, fundingSatoshis: Long): (ActorRef, LocalParams) = {
val localParams = makeChannelParams(nodeParams, funder, fundingSatoshis)
val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet)
val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingSatoshis)
val channel = spawnChannel(nodeParams, transport)
(channel, localParams)
}
def spawnChannel(nodeParams: NodeParams, transport: ActorRef): ActorRef = {
val channel = context.actorOf(Channel.props(nodeParams, remoteNodeId, watcher, router, relayer))
val channel = context.actorOf(Channel.props(nodeParams, wallet, remoteNodeId, watcher, router, relayer))
context watch channel
channel
}
@ -256,11 +257,11 @@ object Peer {
val CHANNELID_ZERO = BinaryData("00" * 32)
def props(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, storedChannels: Set[HasCommitments]) = Props(new Peer(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, storedChannels))
def props(nodeParams: NodeParams, remoteNodeId: PublicKey, address_opt: Option[InetSocketAddress], watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, storedChannels: Set[HasCommitments]) = Props(new Peer(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet: EclairWallet, storedChannels))
def generateKey(nodeParams: NodeParams, keyPath: Seq[Long]): PrivateKey = DeterministicWallet.derivePrivateKey(nodeParams.extendedPrivateKey, keyPath).privateKey
def makeChannelParams(nodeParams: NodeParams, isFunder: Boolean, fundingSatoshis: Long): LocalParams = {
def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: BinaryData, isFunder: Boolean, fundingSatoshis: Long): LocalParams = {
// all secrets are generated from the main seed
// TODO: check this
val keyIndex = secureRandom.nextInt(1000).toLong
@ -276,7 +277,7 @@ object Peer {
revocationSecret = generateKey(nodeParams, keyIndex :: 1L :: Nil),
paymentKey = generateKey(nodeParams, keyIndex :: 2L :: Nil),
delayedPaymentKey = generateKey(nodeParams, keyIndex :: 3L :: Nil),
defaultFinalScriptPubKey = nodeParams.defaultFinalScriptPubKey,
defaultFinalScriptPubKey = defaultFinalScriptPubKey,
shaSeed = Crypto.sha256(generateKey(nodeParams, keyIndex :: 4L :: Nil).toBin), // TODO: check that
isFunder = isFunder,
globalFeatures = nodeParams.globalFeatures,

View file

@ -4,8 +4,9 @@ import java.net.InetSocketAddress
import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, Satoshi}
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.blockchain.wallet.EclairWallet
import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
import fr.acinq.eclair.router.Rebroadcast
@ -14,7 +15,7 @@ import fr.acinq.eclair.router.Rebroadcast
* Ties network connections to peers.
* Created by PM on 14/02/2017.
*/
class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef) extends Actor with ActorLogging {
class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends Actor with ActorLogging {
import Switchboard._
@ -80,7 +81,7 @@ class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, r
peers.get(remoteNodeId) match {
case Some(peer) => peer
case None =>
val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, offlineChannels), name = s"peer-$remoteNodeId")
val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, address_opt, watcher, router, relayer, wallet, offlineChannels), name = s"peer-$remoteNodeId")
context watch (peer)
peer
}
@ -92,7 +93,7 @@ class Switchboard(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, r
object Switchboard {
def props(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef) = Props(new Switchboard(nodeParams, watcher, router, relayer))
def props(nodeParams: NodeParams, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Switchboard(nodeParams, watcher, router, relayer, wallet))
// @formatter:off
case class NewChannel(fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelFlags: Option[Byte])

View file

@ -0,0 +1,196 @@
package fr.acinq.eclair.router
import akka.actor.{ActorRef, FSM, Props, Status}
import akka.pattern.pipe
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{BinaryData, Satoshi}
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.wire._
import org.jgrapht.alg.DijkstraShortestPath
import org.jgrapht.graph.{DefaultDirectedGraph, DefaultEdge}
import scala.collection.JavaConversions._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Success, Try}
/**
* Created by PM on 24/05/2016.
*/
class YesRouter(nodeParams: NodeParams, watcher: ActorRef) extends FSM[State, Data] {
import Router._
import ExecutionContext.Implicits.global
context.system.eventStream.subscribe(self, classOf[ChannelStateChanged])
setTimer("broadcast", 'tick_broadcast, nodeParams.routerBroadcastInterval, repeat = true)
setTimer("validate", 'tick_validate, nodeParams.routerValidateInterval, repeat = true)
startWith(NORMAL, Data(Map.empty, Map.empty, Map.empty, Nil, Nil, Nil, Map.empty, Map.empty))
when(NORMAL) {
case Event(ChannelStateChanged(_, _, _, _, channel.NORMAL, d: DATA_NORMAL), d1) =>
stay using d1.copy(localChannels = d1.localChannels + (d.commitments.channelId -> d.commitments.remoteParams.nodeId))
case Event(ChannelStateChanged(_, _, _, channel.NORMAL, _, d: DATA_NEGOTIATING), d1) =>
stay using d1.copy(localChannels = d1.localChannels - d.commitments.channelId)
case Event(c: ChannelStateChanged, _) => stay
case Event(SendRoutingState(remote), Data(nodes, channels, updates, _, _, _, _, _)) =>
log.debug(s"info sending all announcements to $remote: channels=${channels.size} nodes=${nodes.size} updates=${updates.size}")
channels.values.foreach(remote ! _)
nodes.values.foreach(remote ! _)
updates.values.foreach(remote ! _)
stay
case Event(c: ChannelAnnouncement, d) =>
log.debug(s"received channel announcement for shortChannelId=${c.shortChannelId} nodeId1=${c.nodeId1} nodeId2=${c.nodeId2}")
if (!Announcements.checkSigs(c)) {
log.error(s"bad signature for announcement $c")
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
stay
} else if (d.channels.containsKey(c.shortChannelId)) {
log.debug(s"ignoring $c (duplicate)")
stay
} else {
log.debug(s"added channel channelId=${c.shortChannelId}")
context.system.eventStream.publish(ChannelDiscovered(c, Satoshi(0)))
nodeParams.announcementsDb.put(channelKey(c.shortChannelId), c)
stay using d.copy(channels = d.channels + (c.shortChannelId -> c), origins = d.origins + (c -> sender))
}
case Event(n: NodeAnnouncement, d: Data) =>
if (!Announcements.checkSig(n)) {
log.error(s"bad signature for announcement $n")
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
stay
} else if (d.nodes.containsKey(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) {
log.debug(s"ignoring announcement $n (old timestamp or duplicate)")
stay
} else if (d.nodes.containsKey(n.nodeId)) {
log.debug(s"updated node nodeId=${n.nodeId}")
context.system.eventStream.publish(NodeUpdated(n))
nodeParams.announcementsDb.put(nodeKey(n.nodeId), n)
stay using d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast :+ n, origins = d.origins + (n -> sender))
} else if (d.channels.values.exists(c => isRelatedTo(c, n))) {
log.debug(s"added node nodeId=${n.nodeId}")
context.system.eventStream.publish(NodeDiscovered(n))
nodeParams.announcementsDb.put(nodeKey(n.nodeId), n)
stay using d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast :+ n, origins = d.origins + (n -> sender))
} else {
log.warning(s"ignoring $n (no related channel found)")
stay
}
case Event(u: ChannelUpdate, d: Data) =>
if (d.channels.contains(u.shortChannelId)) {
val c = d.channels(u.shortChannelId)
val desc = getDesc(u, c)
if (!Announcements.checkSig(u, getDesc(u, d.channels(u.shortChannelId)).a)) {
// TODO: (dirty) this will make the origin channel close the connection
log.error(s"bad signature for announcement $u")
sender ! Error(Peer.CHANNELID_ZERO, "bad announcement sig!!!".getBytes())
stay
} else if (d.updates.contains(desc) && d.updates(desc).timestamp >= u.timestamp) {
log.debug(s"ignoring $u (old timestamp or duplicate)")
stay
} else {
log.debug(s"added/updated $u")
context.system.eventStream.publish(ChannelUpdateReceived(u))
nodeParams.announcementsDb.put(channelUpdateKey(u.shortChannelId, u.flags), u)
stay using d.copy(updates = d.updates + (desc -> u), rebroadcast = d.rebroadcast :+ u, origins = d.origins + (u -> sender))
}
} else {
log.warning(s"ignoring announcement $u (unknown channel)")
stay
}
case Event(WatchEventSpentBasic(BITCOIN_FUNDING_OTHER_CHANNEL_SPENT(shortChannelId)), d)
if d.channels.containsKey(shortChannelId) =>
val lostChannel = d.channels(shortChannelId)
log.debug(s"funding tx of channelId=$shortChannelId has been spent")
log.debug(s"removed channel channelId=$shortChannelId")
context.system.eventStream.publish(ChannelLost(shortChannelId))
def isNodeLost(nodeId: PublicKey): Option[PublicKey] = {
// has nodeId still open channels?
if ((d.channels - shortChannelId).values.filter(c => c.nodeId1 == nodeId || c.nodeId2 == nodeId).isEmpty) {
context.system.eventStream.publish(NodeLost(nodeId))
log.debug(s"removed node nodeId=$nodeId")
Some(nodeId)
} else None
}
val lostNodes = isNodeLost(lostChannel.nodeId1).toSeq ++ isNodeLost(lostChannel.nodeId2).toSeq
nodeParams.announcementsDb.delete(channelKey(shortChannelId))
d.updates.values.filter(_.shortChannelId == shortChannelId).foreach(u => nodeParams.announcementsDb.delete(channelUpdateKey(u.shortChannelId, u.flags)))
lostNodes.foreach(id => nodeParams.announcementsDb.delete(s"ann-node-$id"))
stay using d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, updates = d.updates.filterKeys(_.id != shortChannelId))
case Event('tick_validate, d) => stay // ignored
case Event('tick_broadcast, d) =>
d.rebroadcast match {
case Nil => stay using d.copy(origins = Map.empty)
case _ =>
log.info(s"broadcasting ${d.rebroadcast.size} routing messages")
context.actorSelection(context.system / "*" / "switchboard") ! Rebroadcast(d.rebroadcast, d.origins)
stay using d.copy(rebroadcast = Nil, origins = Map.empty)
}
case Event('nodes, d) =>
sender ! d.nodes.values
stay
case Event('channels, d) =>
sender ! d.channels.values
stay
case Event('updates, d) =>
sender ! d.updates.values
stay
case Event('dot, d) =>
sender ! Status.Failure(???)
stay
case Event(RouteRequest(start, end, ignoreNodes, ignoreChannels), d) =>
val localNodeId = nodeParams.privateKey.publicKey
// TODO: HACK!!!!! the following is a workaround to make our routing work with private/not-yet-announced channels, that do not have a channelUpdate
val fakeUpdates = d.localChannels.map { case (channelId, remoteNodeId) =>
// note that this id is deterministic, so that filterUpdates method still works
val fakeShortId = BigInt(channelId.take(7).toArray).toLong
val channelDesc = ChannelDesc(fakeShortId, localNodeId, remoteNodeId)
// note that we store the channelId in the sig, other values are not used because this will be the first channel in the route
val channelUpdate = ChannelUpdate(signature = channelId, fakeShortId, 0, "0000", 0, 0, 0, 0)
(channelDesc -> channelUpdate)
}
// we replace local channelUpdates (we have them for regular public alread-announced channels) by the ones we just generated
val updates1 = d.updates.filterKeys(_.a != localNodeId) ++ fakeUpdates
val updates2 = filterUpdates(updates1, ignoreNodes, ignoreChannels)
log.info(s"finding a route $start->$end with ignoreNodes=${ignoreNodes.map(_.toBin).mkString(",")} ignoreChannels=${ignoreChannels.mkString(",")}")
findRoute(start, end, updates2).map(r => RouteResponse(r, ignoreNodes, ignoreChannels)) pipeTo sender
stay
}
onTransition {
case _ -> NORMAL => log.info(s"current status channels=${nextStateData.channels.size} nodes=${nextStateData.nodes.size} updates=${nextStateData.updates.size}")
}
initialize()
}
object YesRouter {
def props(nodeParams: NodeParams, watcher: ActorRef) = Props(new YesRouter(nodeParams, watcher))
}

View file

@ -278,24 +278,6 @@ object Transactions {
def makeHtlcPenaltyTx(commitTx: Transaction): HtlcPenaltyTx = ???
/**
* This generates a partial transaction that will be completed by bitcoind using a 'fundrawtransaction' rpc call.
* Since bitcoind may add a change output, we return the pubkeyScript so that we can do a lookup afterwards.
*
* @param amount
* @param localFundingPubkey
* @param remoteFundingPubkey
* @return (partialTx, pubkeyScript)
*/
def makePartialFundingTx(amount: Satoshi, localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): (Transaction, BinaryData) = {
val pubkeyScript = write(pay2wsh(Scripts.multiSig2of2(localFundingPubkey, remoteFundingPubkey)))
(Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0), pubkeyScript)
}
def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: BinaryData, remoteScriptPubKey: BinaryData, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = {
require(spec.htlcs.size == 0, "there shouldn't be any pending htlcs")
@ -381,6 +363,9 @@ object Transactions {
closingTx.copy(tx = closingTx.tx.updateWitness(0, witness))
}
def checkSpendable(parent: Transaction, child: Transaction): Try[Unit] =
Try(Transaction.correctlySpends(child, parent :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))
def checkSpendable(txinfo: TransactionWithInputInfo): Try[Unit] =
Try(Transaction.correctlySpends(txinfo.tx, Map(txinfo.tx.txIn(0).outPoint -> txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS))

View file

@ -1,16 +1,12 @@
package fr.acinq.eclair
import akka.actor.ActorSystem
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{BinaryData, Block, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.ExtendedBitcoinClient.SignTransactionResponse
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.bitcoin.{Block, Transaction}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
/**
* Created by PM on 26/04/2016.
@ -25,9 +21,6 @@ class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinC
override def run(): Unit = system.eventStream.publish(NewBlock(DUMMY_BLOCK)) // blocks are not actually interpreted
})
override def makeFundingTx(ourCommitPub: PublicKey, theirCommitPub: PublicKey, amount: Satoshi, feeRatePerKw: Long)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] =
Future.successful(TestBitcoinClient.makeDummyFundingTx(MakeFundingTx(ourCommitPub, theirCommitPub, amount, feeRatePerKw)))
override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = {
system.eventStream.publish(NewTransaction(tx))
Future.successful(tx.txid.toString())
@ -37,38 +30,6 @@ class TestBitcoinClient()(implicit system: ActorSystem) extends ExtendedBitcoinC
override def getTransaction(txId: String)(implicit ec: ExecutionContext): Future[Transaction] = ???
override def fundTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ExtendedBitcoinClient.FundTransactionResponse] = ???
override def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ???
override def getTransactionShortId(txId: String)(implicit ec: ExecutionContext): Future[(Int, Int)] = Future.successful((400000, 42))
}
object TestBitcoinClient {
def makeDummyFundingTx(makeFundingTx: MakeFundingTx): MakeFundingTxResponse = {
val priv = PrivateKey(BinaryData("01" * 32), compressed = true)
val parentTx = Transaction(version = 2,
txIn = TxIn(OutPoint("42" * 32, 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(makeFundingTx.amount, Script.pay2sh(Script.pay2wpkh(priv.publicKey))) :: Nil,
lockTime = 0)
val anchorTx = Transaction(version = 2,
txIn = TxIn(OutPoint(parentTx, 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(makeFundingTx.amount, Script.pay2wsh(Scripts.multiSig2of2(makeFundingTx.localCommitPub, makeFundingTx.remoteCommitPub))) :: Nil,
lockTime = 0)
MakeFundingTxResponse(parentTx, anchorTx, 0, priv)
}
def malleateTx(tx: Transaction): Transaction = {
val inputs1 = tx.txIn.map(input => Script.parse(input.signatureScript) match {
case OP_PUSHDATA(sig, _) :: OP_PUSHDATA(pub, _) :: Nil if pub.length == 33 && Try(Crypto.decodeSignature(sig)).isSuccess =>
val (r, s) = Crypto.decodeSignature(sig)
val s1 = Crypto.curve.getN.subtract(s)
val sig1 = Crypto.encodeSignature(r, s1)
input.copy(signatureScript = Script.write(OP_PUSHDATA(sig1) :: OP_PUSHDATA(pub) :: Nil))
})
val tx1 = tx.copy(txIn = inputs1)
tx1
}
}

View file

@ -41,7 +41,6 @@ object TestConstants {
feeProportionalMillionth = 10,
reserveToFundingRatio = 0.01, // note: not used (overriden below)
maxReserveToFundingRatio = 0.05,
defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(5), compressed = true).publicKey)),
channelsDb = Dbs.makeChannelDb(db),
peersDb = Dbs.makePeerDb(db),
announcementsDb = Dbs.makeAnnouncementDb(db),
@ -52,10 +51,12 @@ object TestConstants {
updateFeeMinDiffRatio = 0.1,
autoReconnect = false,
chainHash = Block.RegtestGenesisBlock.blockId,
channelFlags = 1)
channelFlags = 1,
spv = false)
def id = nodeParams.privateKey.publicKey
def channelParams = Peer.makeChannelParams(
nodeParams = nodeParams,
defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(4), compressed = true).publicKey)),
isFunder = true,
fundingSatoshis).copy(
channelReserveSatoshis = 10000 // Bob will need to keep that much satoshis as direct payment
@ -87,7 +88,6 @@ object TestConstants {
feeProportionalMillionth = 10,
reserveToFundingRatio = 0.01, // note: not used (overriden below)
maxReserveToFundingRatio = 0.05,
defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(5), compressed = true).publicKey)),
channelsDb = Dbs.makeChannelDb(db),
peersDb = Dbs.makePeerDb(db),
announcementsDb = Dbs.makeAnnouncementDb(db),
@ -98,10 +98,12 @@ object TestConstants {
updateFeeMinDiffRatio = 0.1,
autoReconnect = false,
chainHash = Block.RegtestGenesisBlock.blockId,
channelFlags = 1)
channelFlags = 1,
spv = false)
def id = nodeParams.privateKey.publicKey
def channelParams = Peer.makeChannelParams(
nodeParams = nodeParams,
defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(Array.fill[Byte](32)(5), compressed = true).publicKey)),
isFunder = false,
fundingSatoshis).copy(
channelReserveSatoshis = 20000 // Alice will need to keep that much satoshis as direct payment

View file

@ -2,7 +2,7 @@ package fr.acinq.eclair.blockchain
import akka.actor.ActorSystem
import com.typesafe.config.ConfigFactory
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import org.scalatest.FunSuite
import scala.concurrent.{Await, ExecutionContext}
@ -22,7 +22,7 @@ class ExtendedBitcoinClientSpec extends FunSuite {
implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global
val (chain, blockCount) = Await.result(client.client.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long])), 10 seconds)
val (chain, blockCount) = Await.result(client.rpcClient.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long])), 10 seconds)
assert(chain == "test", "you should be on testnet")
test("get transaction short id") {

View file

@ -1,81 +0,0 @@
package fr.acinq.eclair.blockchain
import akka.actor.ActorSystem
import akka.testkit.TestKit
import akka.util.Timeout
import fr.acinq.bitcoin.Script._
import fr.acinq.bitcoin.SigVersion._
import fr.acinq.bitcoin._
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.{TestConstants, randomKey}
import org.junit.runner.RunWith
import org.scalatest.FunSuiteLike
import org.scalatest.junit.JUnitRunner
import scala.concurrent.Await
import scala.concurrent.duration._
/**
* Created by PM on 22/02/2017.
*/
@RunWith(classOf[JUnitRunner])
class PeerWatcherSpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
ignore("publish a csv tx") {
import scala.concurrent.ExecutionContext.Implicits.global
implicit val formats = org.json4s.DefaultFormats
implicit val timeout = Timeout(30 seconds)
val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient("foo", "bar", port = 18332))
val (chain, blockCount, progress) = Await.result(bitcoin_client.client.invoke("getblockchaininfo").map(json => ((json \ "chain").extract[String], (json \ "blocks").extract[Long], (json \ "verificationprogress").extract[Double])), 10 seconds)
assert(chain == "regtest")
val watcher = system.actorOf(PeerWatcher.props(TestConstants.Alice.nodeParams, bitcoin_client))
// first we pick a random key
val localDelayedKey = randomKey
val revocationKey = randomKey
// then a delayed script
val delay = 10
val redeemScript = write(toLocalDelayed(revocationKey.publicKey, delay, localDelayedKey.publicKey))
val pubKeyScript = write(pay2wsh(redeemScript))
// and we generate a tx which pays to the delayed script
val amount = Satoshi(1000000)
val partialParentTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubKeyScript) :: Nil,
lockTime = 0)
// we ask bitcoind to fund the tx
val futureParentTx = for {
funded <- bitcoin_client.fundTransaction(partialParentTx).map(_.tx)
signed <- bitcoin_client.signTransaction(funded)
} yield signed.tx
val parentTx = Await.result(futureParentTx, 10 seconds)
val outputIndex = Transactions.findPubKeyScriptIndex(parentTx, pubKeyScript)
// we build a tx spending the parent tx
val finalPubKeyHash = Base58Check.decode("mkmJFtGN5QvVyYz2NLXPGW1p2SABo2LV9y")._2
val unsignedTx = Transaction(
version = 2,
txIn = TxIn(OutPoint(parentTx.hash, outputIndex), Array.emptyByteArray, delay) :: Nil,
txOut = TxOut(Satoshi(900000), Script.pay2pkh(finalPubKeyHash)) :: Nil,
lockTime = 0)
val sig = Transaction.signInput(unsignedTx, 0, redeemScript, SIGHASH_ALL, amount, SIGVERSION_WITNESS_V0, localDelayedKey)
val witness = witnessToLocalDelayedAfterDelay(sig, redeemScript)
val tx = unsignedTx.updateWitness(0, witness)
watcher ! NewBlock(Block(null, Nil))
watcher ! PublishAsap(tx)
Thread.sleep(5000)
watcher ! PublishAsap(parentTx)
// tester should manually generate blocks
while(true) {
Thread.sleep(5000)
watcher ! NewBlock(Block(null, Nil))
}
}
}

View file

@ -0,0 +1,43 @@
package fr.acinq.eclair.blockchain
import fr.acinq.bitcoin.{BinaryData, Crypto, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain.wallet.{EclairWallet, MakeFundingTxResponse}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
/**
* Created by PM on 06/07/2017.
*/
class TestWallet extends EclairWallet {
override def getFinalAddress: Future[String] = Future.successful("2MsRZ1asG6k94m6GYUufDGaZJMoJ4EV5JKs")
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount, feeRatePerKw))
override def commit(tx: Transaction): Future[Boolean] = Future.successful(true)
}
object TestWallet {
def makeDummyFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): MakeFundingTxResponse = {
val fundingTx = Transaction(version = 2,
txIn = TxIn(OutPoint("42" * 32, 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
MakeFundingTxResponse(fundingTx, 0)
}
def malleateTx(tx: Transaction): Transaction = {
val inputs1 = tx.txIn.map(input => Script.parse(input.signatureScript) match {
case OP_PUSHDATA(sig, _) :: OP_PUSHDATA(pub, _) :: Nil if pub.length == 33 && Try(Crypto.decodeSignature(sig)).isSuccess =>
val (r, s) = Crypto.decodeSignature(sig)
val s1 = Crypto.curve.getN.subtract(s)
val sig1 = Crypto.encodeSignature(r, s1)
input.copy(signatureScript = Script.write(OP_PUSHDATA(sig1) :: OP_PUSHDATA(pub) :: Nil))
})
val tx1 = tx.copy(txIn = inputs1)
tx1
}
}

View file

@ -22,7 +22,7 @@ class ThroughputSpec extends FunSuite {
ignore("throughput") {
implicit val system = ActorSystem()
val pipe = system.actorOf(Props[Pipe], "pipe")
val blockchain = system.actorOf(PeerWatcher.props(TestConstants.Alice.nodeParams, new TestBitcoinClient()), "blockchain")
val blockchain = system.actorOf(ZmqWatcher.props(TestConstants.Alice.nodeParams, new TestBitcoinClient()), "blockchain")
val paymentHandler = system.actorOf(Props(new Actor() {
val random = new Random()
@ -54,8 +54,9 @@ class ThroughputSpec extends FunSuite {
}), "payment-handler")
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams.privateKey, paymentHandler))
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams.privateKey, paymentHandler))
val alice = system.actorOf(Channel.props(Alice.nodeParams, Bob.id, blockchain, ???, relayerA), "a")
val bob = system.actorOf(Channel.props(Bob.nodeParams, Alice.id, blockchain, ???, relayerB), "b")
val wallet = new TestWallet
val alice = system.actorOf(Channel.props(Alice.nodeParams, wallet, Bob.id, blockchain, ???, relayerA), "a")
val bob = system.actorOf(Channel.props(Bob.nodeParams, wallet, Alice.id, blockchain, ???, relayerB), "b")
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty)

View file

@ -38,8 +38,9 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
val relayerA = system.actorOf(Relayer.props(Alice.nodeParams.privateKey, paymentHandlerA), "relayer-a")
val relayerB = system.actorOf(Relayer.props(Bob.nodeParams.privateKey, paymentHandlerB), "relayer-b")
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, Bob.id, alice2blockchain.ref, router.ref, relayerA))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, Alice.id, bob2blockchain.ref, router.ref, relayerB))
val wallet = new TestWallet
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.id, alice2blockchain.ref, router.ref, relayerA))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.id, bob2blockchain.ref, router.ref, relayerB))
within(30 seconds) {
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
@ -49,14 +50,6 @@ class FuzzySpec extends TestkitBaseClass with StateTestsHelperMethods with Loggi
alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte)
bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, pipe, aliceInit)
pipe ! (alice, bob)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42)
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.expectMsgType[PublishAsap]

View file

@ -1,17 +1,15 @@
package fr.acinq.eclair.channel.states
import akka.testkit.{TestFSMRef, TestKitBase, TestProbe}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, Crypto, OutPoint, Script, Transaction, TxIn, TxOut}
import fr.acinq.bitcoin.{BinaryData, Crypto}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.PaymentLifecycle
import fr.acinq.eclair.router.Hop
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, TestBitcoinClient, TestConstants}
import fr.acinq.eclair.{Globals, TestConstants}
import scala.util.Random
@ -41,8 +39,9 @@ trait StateTestsHelperMethods extends TestKitBase {
val router = TestProbe()
val nodeParamsA = TestConstants.Alice.nodeParams
val nodeParamsB = TestConstants.Bob.nodeParams
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, Bob.id, alice2blockchain.ref, router.ref, relayer.ref))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, Alice.id, bob2blockchain.ref, router.ref, relayer.ref))
val wallet = new TestWallet
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.id, alice2blockchain.ref, router.ref, relayer.ref))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.id, bob2blockchain.ref, router.ref, relayer.ref))
Setup(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayer)
}
@ -64,14 +63,6 @@ trait StateTestsHelperMethods extends TestKitBase {
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42)
alice2bob.expectMsgType[FundingCreated]
alice2bob.forward(bob)
bob2alice.expectMsgType[FundingSigned]

View file

@ -1,13 +1,11 @@
package fr.acinq.eclair.channel.states.b
import akka.actor.ActorRef
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -33,21 +31,19 @@ class WaitForFundingCreatedInternalStateSpec extends TestkitBaseClass with State
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL)
}
test((alice, alice2bob, bob2alice, alice2blockchain))
}
test("recv funding transaction") { case (alice, alice2bob, bob2alice, alice2blockchain) =>
/*test("recv MakeFundingTxResponse") { case (alice, alice2bob, bob2alice, alice2blockchain) =>
within(30 seconds) {
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
val dummyFundingTx = TestWallet.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
awaitCond(alice.stateName == WAIT_FOR_FUNDING_PARENT)
awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED)
}
}
}*/
test("recv Error") { case (bob, alice2bob, bob2alice, _) =>
within(30 seconds) {

View file

@ -8,7 +8,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.Tag
import org.scalatest.junit.JUnitRunner
@ -41,14 +41,6 @@ class WaitForFundingCreatedStateSpec extends TestkitBaseClass with StateTestsHel
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42)
awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED)
}
test((bob, alice2bob, bob2alice, bob2blockchain))

View file

@ -1,70 +0,0 @@
package fr.acinq.eclair.channel.states.b
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import scala.concurrent.duration._
/**
* Created by PM on 05/07/2016.
*/
@RunWith(classOf[JUnitRunner])
class WaitForFundingParentStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
type FixtureParam = Tuple5[TestFSMRef[State, Data, Channel], TestProbe, TestProbe, TestProbe, Transaction]
override def withFixture(test: OneArgTest) = {
val setup = init()
import setup._
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
within(30 seconds) {
alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, Alice.channelParams, alice2bob.ref, bobInit, ChannelFlags.Empty)
bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, bob2alice.ref, aliceInit)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
awaitCond(alice.stateName == WAIT_FOR_FUNDING_PARENT)
test((alice, alice2bob, bob2alice, alice2blockchain, dummyFundingTx.parentTx))
}
}
test("recv BITCOIN_INPUT_SPENT and then BITCOIN_TX_CONFIRMED") { case (alice, alice2bob, _, alice2blockchain, parentTx) =>
within(30 seconds) {
alice ! WatchEventSpent(BITCOIN_INPUT_SPENT(parentTx), parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(parentTx), 400000, 42)
alice2bob.expectMsgType[FundingCreated]
awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED)
}
}
test("recv Error") { case (bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
bob ! Error("00" * 32, "oops".getBytes)
awaitCond(bob.stateName == CLOSED)
}
}
test("recv CMD_CLOSE") { case (alice, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
alice ! CMD_CLOSE(None)
awaitCond(alice.stateName == CLOSED)
}
}
}

View file

@ -1,6 +1,5 @@
package fr.acinq.eclair.channel.states.b
import akka.actor.ActorRef
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.BinaryData
import fr.acinq.eclair.TestConstants.{Alice, Bob}
@ -8,7 +7,7 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -34,14 +33,6 @@ class WaitForFundingSignedStateSpec extends TestkitBaseClass with StateTestsHelp
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42)
alice2bob.expectMsgType[FundingCreated]
alice2bob.forward(bob)
awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED)

View file

@ -1,13 +1,12 @@
package fr.acinq.eclair.channel.states.c
import akka.actor.ActorRef
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.wire.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel}
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -33,14 +32,6 @@ class WaitForFundingConfirmedStateSpec extends TestkitBaseClass with StateTestsH
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42)
alice2bob.expectMsgType[FundingCreated]
alice2bob.forward(bob)
bob2alice.expectMsgType[FundingSigned]

View file

@ -1,13 +1,12 @@
package fr.acinq.eclair.channel.states.c
import akka.actor.ActorRef
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{TestBitcoinClient, TestConstants, TestkitBaseClass}
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
@ -33,14 +32,6 @@ class WaitForFundingLockedStateSpec extends TestkitBaseClass with StateTestsHelp
alice2bob.forward(bob)
bob2alice.expectMsgType[AcceptChannel]
bob2alice.forward(alice)
val makeFundingTx = alice2blockchain.expectMsgType[MakeFundingTx]
val dummyFundingTx = TestBitcoinClient.makeDummyFundingTx(makeFundingTx)
alice ! dummyFundingTx
val w = alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[PublishAsap]
alice ! WatchEventSpent(w.event, dummyFundingTx.parentTx)
alice2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(dummyFundingTx.parentTx), 400000, 42)
alice2bob.expectMsgType[FundingCreated]
alice2bob.forward(bob)
bob2alice.expectMsgType[FundingSigned]

View file

@ -8,10 +8,10 @@ import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.pattern.pipe
import akka.testkit.{TestKit, TestProbe}
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Crypto, MilliSatoshi, Satoshi, Script}
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, Watch, WatchConfirmed}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script}
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
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
@ -107,7 +107,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
}
val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name"))
val kit = Await.result(setup.bootstrap, 10 seconds)
finalAddresses = finalAddresses + (name -> setup.finalAddress)
finalAddresses = finalAddresses + (name -> "")
nodes = nodes + (name -> kit)
}
@ -119,7 +119,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
test("starting eclair nodes") {
import collection.JavaConversions._
val commonConfig = ConfigFactory.parseMap(Map("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))
val commonConfig = ConfigFactory.parseMap(Map("eclair.spv" -> false, "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))
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))
@ -145,7 +145,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// funder transitions
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_ACCEPT_CHANNEL)
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_INTERNAL)
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_PARENT)
// fundee transitions
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_OPEN_CHANNEL)
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CREATED)
@ -354,6 +353,17 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
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 _ => ???
}
test("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
@ -376,6 +386,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
val finalAddressF = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
// we then kill the connection between C and F
sender.send(nodes("F1").switchboard, 'peers)
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
@ -403,7 +418,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
res.filter(_ \ "address" == JString(finalAddresses("F1"))).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
}, max = 30 seconds, interval = 1 second)
// we then generate enough blocks so that C gets its main delayed output
sender.send(bitcoincli, BitcoinReq("generate", 145))
@ -412,7 +427,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 1
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
@ -440,6 +455,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
val finalAddressF = scriptPubKeyToAddress(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]]
@ -466,13 +486,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
res.filter(_ \ "address" == JString(finalAddresses("F2"))).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
}, max = 30 seconds, interval = 1 second)
// and C will have its main output
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 1
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
@ -500,6 +520,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
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 finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
// we then generate enough blocks to make the htlc timeout
sender.send(bitcoincli, BitcoinReq("generate", 11))
sender.expectMsgType[JValue](10 seconds)
@ -512,7 +535,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
@ -540,6 +563,9 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
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 finalAddressC = scriptPubKeyToAddress(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
@ -554,7 +580,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
val receivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)

View file

@ -5,12 +5,12 @@ import java.util.concurrent.{CountDownLatch, TimeUnit}
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
import fr.acinq.eclair.Globals
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain.PeerWatcher
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.channel._
import fr.acinq.eclair.payment.NoopPaymentHandler
import fr.acinq.eclair.wire.Init
import fr.acinq.eclair.{Globals, TestBitcoinClient, TestConstants}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, Matchers, fixture}
@ -30,14 +30,15 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix
Globals.blockCount.set(0)
val latch = new CountDownLatch(1)
val pipe: ActorRef = system.actorOf(Props(new SynchronizationPipe(latch)))
val blockchainA = system.actorOf(PeerWatcher.props(TestConstants.Alice.nodeParams, new TestBitcoinClient()))
val blockchainB = system.actorOf(PeerWatcher.props(TestConstants.Bob.nodeParams, new TestBitcoinClient()))
val alice2blockchain = TestProbe()
val bob2blockchain = TestProbe()
val paymentHandler = system.actorOf(Props(new NoopPaymentHandler()))
// we just bypass the relayer for this test
val relayer = paymentHandler
val router = TestProbe()
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, Bob.id, blockchainA, router.ref, relayer))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, Alice.id, blockchainB, router.ref, relayer))
val wallet = new TestWallet
val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.id, alice2blockchain.ref, router.ref, relayer))
val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(Bob.nodeParams, wallet, Alice.id, bob2blockchain.ref, router.ref, relayer))
val aliceInit = Init(Alice.channelParams.globalFeatures, Alice.channelParams.localFeatures)
val bobInit = Init(Bob.channelParams.globalFeatures, Bob.channelParams.localFeatures)
// alice and bob will both have 1 000 000 sat
@ -46,6 +47,15 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix
bob ! INPUT_INIT_FUNDEE("00" * 32, Bob.channelParams, pipe, aliceInit)
pipe ! (alice, bob)
within(30 seconds) {
alice2blockchain.expectMsgType[WatchSpent]
alice2blockchain.expectMsgType[WatchConfirmed]
alice2blockchain.expectMsgType[PublishAsap]
bob2blockchain.expectMsgType[WatchSpent]
bob2blockchain.expectMsgType[WatchConfirmed]
alice ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42)
bob ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, 400000, 42)
alice2blockchain.expectMsgType[WatchLost]
bob2blockchain.expectMsgType[WatchLost]
awaitCond(alice.stateName == NORMAL)
awaitCond(bob.stateName == NORMAL)
}

View file

@ -2,9 +2,10 @@ package fr.acinq.eclair.router
import akka.actor.ActorSystem
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, MakeFundingTxResponse}
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
import fr.acinq.bitcoin.{BinaryData, Satoshi, Script, Transaction}
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.wallet.BitcoinCoreWallet
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire.ChannelAnnouncement
import fr.acinq.eclair.{randomKey, toShortId}
import org.junit.runner.RunWith
@ -56,20 +57,22 @@ object AnnouncementsBatchValidationSpec {
case class SimulatedChannel(node1Key: PrivateKey, node2Key: PrivateKey, node1FundingKey: PrivateKey, node2FundingKey: PrivateKey, amount: Satoshi, fundingTx: Transaction, fundingOutputIndex: Int)
def generateBlocks(numBlocks: Int)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext) =
Await.result(extendedBitcoinClient.client.invoke("generate", numBlocks), 10 seconds)
Await.result(extendedBitcoinClient.rpcClient.invoke("generate", numBlocks), 10 seconds)
def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): SimulatedChannel = {
def simulateChannel()(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext, system: ActorSystem): SimulatedChannel = {
val node1Key = randomKey
val node2Key = randomKey
val node1BitcoinKey = randomKey
val node2BitcoinKey = randomKey
val amount = Satoshi(1000000)
// first we publish the funding tx
val fundingTxFuture = extendedBitcoinClient.makeFundingTx(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey, amount, 10000)
val MakeFundingTxResponse(parentTx, fundingTx, fundingOutputIndex, _) = Await.result(fundingTxFuture, 10 seconds)
Await.result(extendedBitcoinClient.publishTransaction(parentTx), 10 seconds)
Await.result(extendedBitcoinClient.publishTransaction(fundingTx), 10 seconds)
SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, fundingTx, fundingOutputIndex)
val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient, null)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey)))
val fundingTxFuture = wallet.makeParentAndFundingTx(fundingPubkeyScript, amount, 10000)
val res = Await.result(fundingTxFuture, 10 seconds)
Await.result(extendedBitcoinClient.publishTransaction(res.parentTx), 10 seconds)
Await.result(extendedBitcoinClient.publishTransaction(res.fundingTx), 10 seconds)
SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res.fundingTx, res.fundingTxOutputIndex)
}
def makeChannelAnnouncement(c: SimulatedChannel)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): ChannelAnnouncement = {

View file

@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-SNAPSHOT</version>
<version>0.2-spv-SNAPSHOT</version>
</parent>
<artifactId>eclair-node-javafx_2.11</artifactId>

View file

@ -19,6 +19,7 @@ import fr.acinq.eclair.router.NetworkEvent
import grizzled.slf4j.Logging
import scala.concurrent.Promise
import scala.util.{Success, Failure}
/**
@ -31,59 +32,57 @@ class FxApp extends Application with Logging {
}
override def start(primaryStage: Stage): Unit = {
val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false)
primaryStage.getIcons.add(icon)
new Thread(new Runnable {
override def run(): Unit = {
try {
val datadir = new File(getParameters.getUnnamed.get(0))
implicit val system = ActorSystem("system")
val setup = new Setup(datadir)
val pKit = Promise[Kit]()
val handlers = new Handlers(pKit.future)
val controller = new MainController(handlers, setup, getHostServices)
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvents])
Platform.runLater(new Runnable {
override def run(): Unit = {
val mainFXML = new FXMLLoader(getClass.getResource("/gui/main/main.fxml"))
mainFXML.setController(controller)
val mainRoot = mainFXML.load[Parent]
val scene = new Scene(mainRoot)
primaryStage.setTitle("Eclair")
primaryStage.setMinWidth(600)
primaryStage.setWidth(960)
primaryStage.setMinHeight(400)
primaryStage.setHeight(640)
primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] {
override def handle(event: WindowEvent) {
System.exit(0)
}
})
notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful"))
primaryStage.setScene(scene)
primaryStage.show
initNotificationStage(primaryStage, handlers)
pKit.completeWith(setup.bootstrap)
}
})
} catch {
case TCPBindException(port) =>
val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false)
primaryStage.getIcons.add(icon)
val mainFXML = new FXMLLoader(getClass.getResource("/gui/main/main.fxml"))
val pKit = Promise[Kit]()
val handlers = new Handlers(pKit.future)
val controller = new MainController(handlers, getHostServices)
mainFXML.setController(controller)
val mainRoot = mainFXML.load[Parent]
val datadir = new File(getParameters.getUnnamed.get(0))
implicit val system = ActorSystem("system")
val setup = new Setup(datadir)
val guiUpdater = setup.system.actorOf(SimpleSupervisor.props(Props(classOf[GUIUpdater], controller), "gui-updater", SupervisorStrategy.Resume))
setup.system.eventStream.subscribe(guiUpdater, classOf[ChannelEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent])
setup.system.eventStream.subscribe(guiUpdater, classOf[ZMQEvents])
pKit.completeWith(setup.bootstrap)
import scala.concurrent.ExecutionContext.Implicits.global
pKit.future.onComplete {
case Success(_) =>
Platform.runLater(new Runnable {
override def run(): Unit = {
val scene = new Scene(mainRoot)
primaryStage.setTitle("Eclair")
primaryStage.setMinWidth(600)
primaryStage.setWidth(960)
primaryStage.setMinHeight(400)
primaryStage.setHeight(640)
primaryStage.setOnCloseRequest(new EventHandler[WindowEvent] {
override def handle(event: WindowEvent) {
System.exit(0)
}
})
controller.initInfoFields(setup)
primaryStage.setScene(scene)
primaryStage.show
notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful"))
initNotificationStage(primaryStage, handlers)
}
})
case Failure(TCPBindException(port)) =>
notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null))
case BitcoinRPCConnectionException =>
case Failure(BitcoinRPCConnectionException) =>
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using JSON-RPC.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and RPC parameters are correct."))
case BitcoinZMQConnectionTimeoutException =>
case Failure(BitcoinZMQConnectionTimeoutException) =>
notifyPreloader(new ErrorNotification("Setup", "Could not connect to Bitcoin Core using ZMQ.", null))
notifyPreloader(new AppNotification(InfoAppNotification, "Make sure that Bitcoin Core is up and running and ZMQ parameters are correct."))
case t: Throwable =>
case Failure(t) =>
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
}
}

View file

@ -36,7 +36,7 @@ case class ChannelInfo(val announcement: ChannelAnnouncement, var isNode1Enabled
/**
* Created by DPA on 22/09/2016.
*/
class MainController(val handlers: Handlers, val setup: Setup, val hostServices: HostServices) extends Logging {
class MainController(val handlers: Handlers, val hostServices: HostServices) extends Logging {
@FXML var root: AnchorPane = _
var contextMenu: ContextMenu = _
@ -119,23 +119,6 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
*/
@FXML def initialize = {
// init status bar
labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}")
labelAlias.setText(s"${setup.nodeParams.alias}")
rectRGB.setFill(Color.rgb(setup.nodeParams.color._1 & 0xFF, setup.nodeParams.color._2 & 0xFF, setup.nodeParams.color._3 & 0xFF))
labelApi.setText(s"${setup.config.getInt("api.port")}")
labelServer.setText(s"${setup.config.getInt("server.port")}")
bitcoinVersion.setText(s"v${setup.bitcoinVersion}")
bitcoinChain.setText(s"${setup.chain.toUpperCase()}")
bitcoinChain.getStyleClass.add(setup.chain)
// init context
contextMenu = ContextMenuUtils.buildCopyContext(
List(
Some(new CopyAction("Copy Pubkey", s"${setup.nodeParams.privateKey.publicKey}")),
setup.nodeParams.publicAddresses.headOption.map(address => new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${address.getHostString}:${address.getPort}"))
).flatten)
// init channels tab
if (channelBox.getChildren.size() > 0) {
channelInfo.setScaleY(0)
@ -212,6 +195,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
val directionImage = new ImageView
directionImage.setFitWidth(20)
directionImage.setFitHeight(20)
override def updateItem(item: String, empty: Boolean): Unit = {
super.updateItem(item, empty)
if (this.getIndex >= 0 && this.getIndex < networkChannelsList.size) {
@ -294,6 +278,25 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
paymentRelayedTable.setRowFactory(paymentRowFactory)
}
def initInfoFields(setup: Setup) = {
// init status bar
labelNodeId.setText(s"${setup.nodeParams.privateKey.publicKey}")
labelAlias.setText(s"${setup.nodeParams.alias}")
rectRGB.setFill(Color.rgb(setup.nodeParams.color._1 & 0xFF, setup.nodeParams.color._2 & 0xFF, setup.nodeParams.color._3 & 0xFF))
labelApi.setText(s"${setup.config.getInt("api.port")}")
labelServer.setText(s"${setup.config.getInt("server.port")}")
bitcoinVersion.setText(s"v0.0.0")
//bitcoinVersion.setText(s"v${setup.bitcoinVersion}")
bitcoinChain.setText(s"${setup.chain.toUpperCase()}")
bitcoinChain.getStyleClass.add(setup.chain)
contextMenu = ContextMenuUtils.buildCopyContext(
List(
Some(new CopyAction("Copy Pubkey", s"${setup.nodeParams.privateKey.publicKey}")),
setup.nodeParams.publicAddresses.headOption.map(address => new CopyAction("Copy URI", s"${setup.nodeParams.privateKey.publicKey}@${address.getHostString}:${address.getPort}"))
).flatten)
}
private def updateTabHeader(tab: Tab, prefix: String, items: ObservableList[_]) = {
Platform.runLater(new Runnable() {
override def run = tab.setText(s"$prefix (${items.size})")
@ -440,6 +443,7 @@ class MainController(val handlers: Handlers, val setup: Setup, val hostServices:
t.setDelay(Duration.millis(200))
t.play
}
def hideBlockerModal = {
val ftCover = new FadeTransition(Duration.millis(400))
ftCover.setFromValue(1)

View file

@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-SNAPSHOT</version>
<version>0.2-spv-SNAPSHOT</version>
</parent>
<artifactId>eclair-node_2.11</artifactId>

View file

@ -18,6 +18,15 @@
</encoder>
</appender>
<appender name="ORANGE" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<withJansi>false</withJansi>
<encoder>
<pattern>%boldYellow(${HOSTNAME} %d) %highlight(%-5level) %logger{36} %X{akkaSource} - %yellow(%msg) %ex{12}%n
</pattern>
</encoder>
</appender>
<appender name="RED" class="ch.qos.logback.core.ConsoleAppender">
<target>System.out</target>
<withJansi>false</withJansi>
@ -75,10 +84,26 @@
<appender-ref ref="GREEN"/>
</logger>
<logger name="fr.acinq.eclair.blockchain.PeerWatcher" level="DEBUG" additivity="false">
<logger name="fr.acinq.eclair.blockchain.ZmqWatcher" level="DEBUG" additivity="false">
<appender-ref ref="YELLOW"/>
</logger>
<logger name="fr.acinq.eclair.blockchain.SpvWatcher" level="DEBUG" additivity="false">
<appender-ref ref="YELLOW"/>
</logger>
<logger name="fr.acinq.eclair.blockchain.Broadcaster" level="DEBUG" additivity="false">
<appender-ref ref="YELLOW"/>
</logger>
<logger name="fr.acinq.eclair.blockchain.spv" level="DEBUG" additivity="false">
<appender-ref ref="YELLOW"/>
</logger>
<logger name="fr.acinq.eclair.blockchain.wallet" level="DEBUG" additivity="false">
<appender-ref ref="ORANGE"/>
</logger>
<logger name="fr.acinq.eclair.router" level="DEBUG" additivity="false">
<appender-ref ref="CYAN"/>
</logger>

View file

@ -0,0 +1,47 @@
package fr.acinq.eclair
import java.io.File
import akka.actor.{ActorSystem, Props, SupervisorStrategy}
import fr.acinq.bitcoin.{Satoshi, Script}
import fr.acinq.eclair.blockchain.ZmqWatcher
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.wallet.BitcoinCoreWallet
import fr.acinq.eclair.blockchain.zmq.ZMQActor
import fr.acinq.eclair.transactions.Scripts
import grizzled.slf4j.Logging
import scala.concurrent.{Await, ExecutionContext}
import scala.concurrent.duration._
/**
* Created by PM on 06/07/2017.
*/
object BitcoinCoreWalletTest extends App with Logging {
implicit val system = ActorSystem("system")
val datadir = new File(".")
val config = NodeParams.loadConfiguration(datadir)
val nodeParams = NodeParams.makeNodeParams(datadir, config, "")
val bitcoinClient = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport")))
implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global
val zmq = system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmq"), None)), "zmq", SupervisorStrategy.Restart))
val watcher = system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(nodeParams, bitcoinClient), "watcher", SupervisorStrategy.Resume))
val wallet = new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
logger.info("hello")
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(1000000L), 20000), 30 minutes)
println(result)
}

View file

@ -4,7 +4,7 @@
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.11</artifactId>
<version>0.2-SNAPSHOT</version>
<version>0.2-spv-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>