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:
parent
f321d3e4ec
commit
8245f61f1f
51 changed files with 1337 additions and 708 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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])
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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]
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue