mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-14 03:48:13 +01:00
merged from master, added tests for spv mode
This commit is contained in:
parent
45a3993e7a
commit
be2cdf44c5
58 changed files with 2277 additions and 782 deletions
|
@ -2,7 +2,7 @@ sudo: required
|
|||
dist: trusty
|
||||
language: scala
|
||||
scala:
|
||||
- 2.11.8
|
||||
- 2.11.11
|
||||
env:
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib
|
||||
script:
|
||||
|
|
|
@ -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>fr.acinq</groupId>
|
||||
<artifactId>bitcoinj-core</artifactId>
|
||||
<version>${bitcoinj.version}</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 = false
|
||||
chain = "test"
|
||||
|
||||
server {
|
||||
public-ips = [] // external ips, will be announced on the network
|
||||
binding-ip = "0.0.0.0"
|
||||
|
@ -16,13 +20,17 @@ eclair {
|
|||
zmq = "tcp://127.0.0.1:29000"
|
||||
}
|
||||
|
||||
bitcoinj {
|
||||
# port = 28333 // used in integration tests to override default port
|
||||
}
|
||||
|
||||
node-alias = "eclair"
|
||||
node-color = "49daaa"
|
||||
global-features = ""
|
||||
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-per-kb = 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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,17 +9,19 @@ import akka.pattern.after
|
|||
import akka.stream.{ActorMaterializer, BindFailedException}
|
||||
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, EclairWallet}
|
||||
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}
|
||||
|
@ -34,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")
|
||||
|
||||
// early check
|
||||
PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port"))
|
||||
|
||||
|
@ -44,59 +48,76 @@ 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("getnetworkinfo").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
|
||||
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")
|
||||
val (chain, chainHash, bitcoin) = if (spv) {
|
||||
logger.warn("EXPERIMENTAL 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, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
(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(_))
|
||||
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
|
||||
} yield (chain, progress, chainHash, bitcoinVersion)
|
||||
val (chain, progress, chainHash, bitcoinVersion) = Try(Await.result(future, 10 seconds)).recover { case _ => throw BitcoinRPCConnectionException }.get
|
||||
assert(progress > 0.99, "bitcoind should be synchronized")
|
||||
(chain, chainHash, Right(bitcoinClient))
|
||||
}
|
||||
// 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)
|
||||
val nodeParams = NodeParams.makeNodeParams(datadir, config, chainHash)
|
||||
logger.info(s"using chain=$chain chainHash=$chainHash")
|
||||
logger.info(s"nodeid=${nodeParams.privateKey.publicKey.toBin} alias=${nodeParams.alias}")
|
||||
|
||||
DBCompatChecker.checkDBCompatibility(nodeParams)
|
||||
|
||||
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-per-kb")
|
||||
Globals.feeratePerKw.set(feerateKb2Kw(defaultFeeratePerKb))
|
||||
logger.info(s"initial feeratePerKw=${Globals.feeratePerKw.get()}")
|
||||
val feeProvider = chain match {
|
||||
case "regtest" => new ConstantFeeProvider(defaultFeeratePerKb)
|
||||
case _ => new BitpayInsightFeeProvider()
|
||||
}
|
||||
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeeratePerKB.map {
|
||||
case feeratePerKB =>
|
||||
Globals.feeratePerKw.set(feerateKb2Kw(feeratePerKB))
|
||||
logger.info(s"current feeratePerKw=${Globals.feeratePerKw.get()}")
|
||||
})
|
||||
|
||||
val watcher = bitcoin match {
|
||||
case Left(bitcoinj) =>
|
||||
zmqConnected.success(true)
|
||||
bitcoinj.startAsync()
|
||||
system.actorOf(SimpleSupervisor.props(SpvWatcher.props(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(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]
|
||||
|
@ -104,14 +125,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,
|
||||
|
@ -119,7 +139,8 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
|||
router = router,
|
||||
switchboard = switchboard,
|
||||
paymentInitiator = paymentInitiator,
|
||||
server = server)
|
||||
server = server,
|
||||
wallet = wallet)
|
||||
|
||||
val api = new Service {
|
||||
|
||||
|
@ -147,7 +168,6 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
|
|||
|
||||
case class Kit(nodeParams: NodeParams,
|
||||
system: ActorSystem,
|
||||
zmq: ActorRef,
|
||||
watcher: ActorRef,
|
||||
paymentHandler: ActorRef,
|
||||
register: ActorRef,
|
||||
|
@ -155,7 +175,8 @@ case class Kit(nodeParams: NodeParams,
|
|||
router: ActorRef,
|
||||
switchboard: ActorRef,
|
||||
paymentInitiator: ActorRef,
|
||||
server: ActorRef)
|
||||
server: ActorRef,
|
||||
wallet: EclairWallet)
|
||||
|
||||
case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could not connect to bitcoind using zeromq")
|
||||
|
||||
|
|
|
@ -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)
|
||||
case class ChannelInfo(shortChannelId: String, nodeId1: PublicKey , nodeId2: PublicKey)
|
||||
// @formatter:on
|
||||
|
||||
|
@ -103,10 +103,10 @@ trait Service extends Logging {
|
|||
for {
|
||||
req <- Future(PaymentRequest.read(paymentRequest))
|
||||
amount = (req.amount, rest) match {
|
||||
case (Some(amt), Nil) => amt.amount
|
||||
case (Some(_), JInt(amt) :: Nil) => amt.toLong // overriding payment request amount with the one provided
|
||||
case (Some(amt), _) => amt.amount
|
||||
case (None, JInt(amt) :: Nil) => amt.toLong // amount wasn't specified in request, using custom one
|
||||
case (None, Nil) => throw new RuntimeException("you need to manually specify an amount for this payment request")
|
||||
case (None, _) => throw new RuntimeException("you need to manually specify an amount for this payment request")
|
||||
}
|
||||
res <- (paymentInitiator ? SendPayment(amount, req.paymentHash, req.nodeId)).mapTo[PaymentResult]
|
||||
} yield res
|
||||
|
|
|
@ -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,184 @@
|
|||
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.{Globals, fromShortId}
|
||||
import fr.acinq.eclair.channel.BITCOIN_PARENT_TX_CONFIRMED
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import org.bitcoinj.core.{Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.script.Script
|
||||
|
||||
import scala.collection.SortedMap
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.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(val kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging {
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[BlockchainEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[NewConfidenceLevel])
|
||||
|
||||
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, Nil)
|
||||
|
||||
def watching(watches: Set[Watch], block2tx: SortedMap[Long, Seq[Transaction]], oldEvents: Seq[NewConfidenceLevel], sent: Seq[TriggerEvent]): Receive = {
|
||||
|
||||
case event@NewConfidenceLevel(tx, blockHeight, confirmations) =>
|
||||
log.debug(s"analyzing txid=${tx.txid} confirmations=$confirmations tx=${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, sent)
|
||||
|
||||
case t@TriggerEvent(w, e) if watches.contains(w) && !sent.contains(t) =>
|
||||
log.info(s"triggering $w")
|
||||
w.channel ! e
|
||||
// NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx
|
||||
// They are never cleaned up but it is not a big deal for now (1 channel == 1 watch)
|
||||
val newWatches = if (!w.isInstanceOf[WatchSpent]) watches - w else watches
|
||||
context.become(watching(newWatches, block2tx, oldEvents, sent :+ t))
|
||||
|
||||
case CurrentBlockCount(count) => {
|
||||
val toPublish = block2tx.filterKeys(_ <= count)
|
||||
toPublish.values.flatten.map(tx => publish(tx))
|
||||
context.become(watching(watches, block2tx -- toPublish.keys, oldEvents, sent))
|
||||
}
|
||||
|
||||
case hint: Hint => {
|
||||
Context.propagate(kit.wallet.getContext)
|
||||
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, sent))
|
||||
|
||||
case PublishAsap(tx) =>
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val cltvTimeout = Scripts.cltvTimeout(tx)
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
if (csvTimeout > 0) {
|
||||
require(tx.txIn.size == 1, s"watcher only supports tx with 1 input, this tx has ${tx.txIn.size} inputs")
|
||||
val parentTxid = tx.txIn(0).outPoint.txid
|
||||
log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parenttxid=$parentTxid tx=${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, sent))
|
||||
} else publish(tx)
|
||||
|
||||
case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(tx), blockHeight, _) =>
|
||||
log.info(s"parent tx of txid=${tx.txid} has been confirmed")
|
||||
val blockCount = Globals.blockCount.get()
|
||||
val csvTimeout = Scripts.csvTimeout(tx)
|
||||
val absTimeout = blockHeight + csvTimeout
|
||||
if (absTimeout > blockCount) {
|
||||
log.info(s"delaying publication of txid=${tx.txid} until block=$absTimeout (curblock=$blockCount)")
|
||||
val block2tx1 = block2tx.updated(absTimeout, tx +: block2tx.getOrElse(absTimeout, Seq.empty[Transaction]))
|
||||
context.become(watching(watches, block2tx1, oldEvents, sent))
|
||||
} 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 (_, _, outputIndex) = fromShortId(c.shortChannelId)
|
||||
val fakeFundingTx = Transaction(
|
||||
version = 2,
|
||||
txIn = Seq.empty[TxIn],
|
||||
txOut = List.fill(outputIndex + 1)(TxOut(Satoshi(0), pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format
|
||||
lockTime = 0)
|
||||
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, sent))
|
||||
|
||||
case 'watches => sender ! watches
|
||||
|
||||
}
|
||||
|
||||
def publish(tx: Transaction): Unit = broadcaster ! tx
|
||||
|
||||
}
|
||||
|
||||
object SpvWatcher {
|
||||
|
||||
def props(kit: WalletAppKit)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new SpvWatcher(kit)(ec))
|
||||
|
||||
}
|
||||
|
||||
class Broadcaster(kit: WalletAppKit) extends Actor with ActorLogging {
|
||||
|
||||
override def receive: Receive = {
|
||||
case tx: Transaction =>
|
||||
broadcast(tx)
|
||||
context become waiting(Nil)
|
||||
}
|
||||
|
||||
def waiting(stash: Seq[Transaction]): Receive = {
|
||||
case BroadcastResult(tx, result) =>
|
||||
result match {
|
||||
case Success(_) => log.info(s"broadcast success for txid=${tx.txid}")
|
||||
case Failure(t) => log.error(t, s"broadcast failure for txid=${tx.txid}: ")
|
||||
}
|
||||
stash match {
|
||||
case head :: rest =>
|
||||
broadcast(head)
|
||||
context become waiting(rest)
|
||||
case Nil => context become receive
|
||||
}
|
||||
case tx: Transaction =>
|
||||
log.info(s"stashing txid=${tx.txid} for broadcast")
|
||||
context become waiting(stash :+ tx)
|
||||
}
|
||||
|
||||
case class BroadcastResult(tx: Transaction, result: Try[Boolean])
|
||||
def broadcast(tx: Transaction) = {
|
||||
Context.propagate(kit.wallet().getContext)
|
||||
val bitcoinjTx = new org.bitcoinj.core.Transaction(kit.wallet().getParams, Transaction.write(tx))
|
||||
log.info(s"broadcasting txid=${tx.txid}")
|
||||
Futures.addCallback(kit.peerGroup().broadcastTransaction(bitcoinjTx).future(), new FutureCallback[BitcoinjTransaction] {
|
||||
override def onFailure(t: Throwable): Unit = self ! BroadcastResult(tx, Failure(t))
|
||||
|
||||
override def onSuccess(v: BitcoinjTransaction): Unit = self ! BroadcastResult(tx, Success(true))
|
||||
}, context.dispatcher)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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(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(client: ExtendedBitcoinClient)(implicit ec: ExecutionContext = ExecutionContext.global) = Props(new ZmqWatcher(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,140 @@
|
|||
package fr.acinq.eclair.blockchain.spv
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import com.google.common.util.concurrent.{FutureCallback, Futures}
|
||||
import fr.acinq.bitcoin.Transaction
|
||||
import fr.acinq.eclair.Globals
|
||||
import fr.acinq.eclair.blockchain.spv.BitcoinjKit._
|
||||
import fr.acinq.eclair.blockchain.{CurrentBlockCount, NewConfidenceLevel}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.TransactionConfidence.ConfidenceType
|
||||
import org.bitcoinj.core.listeners._
|
||||
import org.bitcoinj.core.{Block, FilteredBlock, NetworkParameters, Peer, PeerAddress, StoredBlock, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.kits.WalletAppKit
|
||||
import org.bitcoinj.params.{RegTestParams, TestNet3Params}
|
||||
import org.bitcoinj.utils.Threading
|
||||
import org.bitcoinj.wallet.Wallet
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Created by PM on 09/07/2017.
|
||||
*/
|
||||
class BitcoinjKit(chain: String, datadir: File, staticPeers: List[InetSocketAddress] = Nil)(implicit system: ActorSystem) extends WalletAppKit(chain2Params(chain), datadir, "bitcoinj", true) with Logging {
|
||||
|
||||
if (staticPeers.size > 0) {
|
||||
logger.info(s"using staticPeers=${staticPeers.mkString(",")}")
|
||||
setPeerNodes(staticPeers.map(addr => new PeerAddress(params, addr)).head)
|
||||
}
|
||||
|
||||
// tells us when the peerGroup/chain/wallet are accessible
|
||||
private val initializedPromise = Promise[Boolean]()
|
||||
val initialized = initializedPromise.future
|
||||
|
||||
// tells us as soon as we know the current block height
|
||||
private val atCurrentHeightPromise = Promise[Boolean]()
|
||||
val atCurrentHeight = atCurrentHeightPromise.future
|
||||
|
||||
// tells us when we are at current block height
|
||||
// private val syncedPromise = Promise[Boolean]()
|
||||
// val synced = syncedPromise.future
|
||||
|
||||
private def updateBlockCount(blockCount: Int) = {
|
||||
// when synchronizing we don't want to advertise previous blocks
|
||||
if (Globals.blockCount.get() < blockCount) {
|
||||
logger.debug(s"current blockchain height=$blockCount")
|
||||
system.eventStream.publish(CurrentBlockCount(blockCount))
|
||||
Globals.blockCount.set(blockCount)
|
||||
}
|
||||
}
|
||||
|
||||
override def onSetupCompleted(): Unit = {
|
||||
|
||||
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
|
||||
logger.info(s"peerGroup.getMinBroadcastConnections==${peerGroup().getMinBroadcastConnections}")
|
||||
|
||||
// setDownloadListener(new DownloadProgressTracker {
|
||||
// override def doneDownload(): Unit = {
|
||||
// super.doneDownload()
|
||||
// // may be called multiple times
|
||||
// syncedPromise.trySuccess(true)
|
||||
// }
|
||||
// })
|
||||
|
||||
// we set the blockcount to the previous stored block height
|
||||
updateBlockCount(chain().getBestChainHeight)
|
||||
|
||||
// as soon as we are connected the peers will tell us their current height and we will advertise it immediately
|
||||
peerGroup().addConnectedEventListener(new PeerConnectedEventListener {
|
||||
override def onPeerConnected(peer: Peer, peerCount: Int): Unit =
|
||||
// we wait for at least 3 peers before relying on the information they are giving, but we trust localhost
|
||||
if (peer.getAddress.getAddr.isLoopbackAddress || peerCount > 3) {
|
||||
updateBlockCount(peerGroup().getMostCommonChainHeight)
|
||||
// may be called multiple times
|
||||
atCurrentHeightPromise.trySuccess(true)
|
||||
}
|
||||
})
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener(new BlocksDownloadedEventListener {
|
||||
override def onBlocksDownloaded(peer: Peer, block: Block, filteredBlock: FilteredBlock, blocksLeft: Int): Unit = {
|
||||
logger.info(s"received block=${block.getHashAsString} (size=${block.bitcoinSerialize().size} txs=${Try(block.getTransactions.size).getOrElse(-1)}) filteredBlock=${Try(filteredBlock.getHash.toString).getOrElse("N/A")} (size=${Try(block.bitcoinSerialize().size).getOrElse(-1)} txs=${Try(filteredBlock.getTransactionCount).getOrElse(-1)})")
|
||||
Try {
|
||||
if (filteredBlock.getAssociatedTransactions.size() > 0) {
|
||||
logger.info(s"retrieving full block ${block.getHashAsString}")
|
||||
Futures.addCallback(peer.getBlock(block.getHash), new FutureCallback[Block] {
|
||||
override def onFailure(throwable: Throwable) = logger.error(s"could not retrieve full block=${block.getHashAsString}")
|
||||
|
||||
override def onSuccess(fullBlock: Block) = {
|
||||
Try {
|
||||
fullBlock.getTransactions.foreach {
|
||||
case tx =>
|
||||
logger.info(s"received tx=${tx.getHashAsString} witness=${Transaction.read(tx.bitcoinSerialize()).txIn(0).witness.stack.size}} from fullBlock=${fullBlock.getHash} confidence=${tx.getConfidence}")
|
||||
val depthInBlocks = tx.getConfidence.getConfidenceType match {
|
||||
case ConfidenceType.DEAD => -1
|
||||
case _ => tx.getConfidence.getDepthInBlocks
|
||||
}
|
||||
system.eventStream.publish(NewConfidenceLevel(Transaction.read(tx.bitcoinSerialize()), 0, depthInBlocks))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, Threading.USER_THREAD)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
chain().addNewBestBlockListener(new NewBestBlockListener {
|
||||
override def notifyNewBestBlock(storedBlock: StoredBlock): Unit =
|
||||
updateBlockCount(storedBlock.getHeight)
|
||||
})
|
||||
|
||||
wallet().addTransactionConfidenceEventListener(new TransactionConfidenceEventListener {
|
||||
override def onTransactionConfidenceChanged(wallet: Wallet, bitcoinjTx: BitcoinjTransaction): Unit = {
|
||||
val tx = Transaction.read(bitcoinjTx.bitcoinSerialize())
|
||||
logger.info(s"tx confidence changed for txid=${tx.txid} confidence=${bitcoinjTx.getConfidence} witness=${bitcoinjTx.getWitness(0)}")
|
||||
val (blockHeight, confirmations) = bitcoinjTx.getConfidence.getConfidenceType match {
|
||||
case ConfidenceType.DEAD => (-1, -1)
|
||||
case ConfidenceType.BUILDING => (bitcoinjTx.getConfidence.getAppearedAtChainHeight, bitcoinjTx.getConfidence.getDepthInBlocks)
|
||||
case _ => (-1, bitcoinjTx.getConfidence.getDepthInBlocks)
|
||||
}
|
||||
system.eventStream.publish(NewConfidenceLevel(tx, blockHeight, confirmations))
|
||||
}
|
||||
})
|
||||
|
||||
initializedPromise.success(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object BitcoinjKit {
|
||||
|
||||
def chain2Params(chain: String): NetworkParameters = chain match {
|
||||
case "regtest" => RegTestParams.get()
|
||||
case "test" => TestNet3Params.get()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
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 getBalance: Future[Satoshi] = ???
|
||||
|
||||
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,59 @@
|
|||
package fr.acinq.eclair.blockchain.wallet
|
||||
|
||||
import fr.acinq.bitcoin.{BinaryData, Satoshi, Transaction}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.{Coin, Context, Transaction => BitcoinjTransaction}
|
||||
import org.bitcoinj.script.Script
|
||||
import org.bitcoinj.wallet.{SendRequest, Wallet}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/**
|
||||
* Created by PM on 08/07/2017.
|
||||
*/
|
||||
class BitcoinjWallet(val fWallet: Future[Wallet])(implicit ec: ExecutionContext) extends EclairWallet with Logging {
|
||||
|
||||
fWallet.map(wallet => wallet.allowSpendingUnconfirmedTransactions())
|
||||
|
||||
override def getBalance: Future[Satoshi] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
//Context.propagate(wallet.getContext)
|
||||
Satoshi(wallet.getBalance.longValue())
|
||||
}
|
||||
|
||||
override def getFinalAddress: Future[String] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
//Context.propagate(wallet.getContext)
|
||||
wallet.currentReceiveAddress().toBase58
|
||||
}
|
||||
|
||||
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = for {
|
||||
wallet <- fWallet
|
||||
} yield {
|
||||
logger.info(s"building funding tx")
|
||||
//Context.propagate(wallet.getContext)
|
||||
val script = new Script(pubkeyScript)
|
||||
val tx = new BitcoinjTransaction(wallet.getParams)
|
||||
tx.addOutput(Coin.valueOf(amount.amount), script)
|
||||
val req = SendRequest.forTx(tx)
|
||||
wallet.completeTx(req)
|
||||
val txOutputIndex = tx.getOutputs.find(_.getScriptPubKey.equals(script)).get.getIndex
|
||||
MakeFundingTxResponse(Transaction.read(tx.bitcoinSerialize()), txOutputIndex)
|
||||
}
|
||||
|
||||
override def commit(tx: Transaction): Future[Boolean] = {
|
||||
// we make sure that we haven't double spent our own tx (eg by opening 2 channels at the same time)
|
||||
val serializedTx = Transaction.write(tx)
|
||||
logger.info(s"committing tx: txid=${tx.txid} tx=$serializedTx")
|
||||
for {
|
||||
wallet <- fWallet
|
||||
//_ = Context.propagate(wallet.getContext)
|
||||
bitcoinjTx = new org.bitcoinj.core.Transaction(wallet.getParams(), serializedTx)
|
||||
canCommit = wallet.maybeCommitTx(bitcoinjTx)
|
||||
_ = logger.info(s"commit txid=${tx.txid} result=$canCommit")
|
||||
} yield canCommit
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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 getBalance: Future[Satoshi]
|
||||
|
||||
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,21 +1,24 @@
|
|||
package fr.acinq.eclair.channel
|
||||
|
||||
import akka.actor.{ActorRef, DiagnosticActorLogging, FSM, LoggingFSM, OneForOneStrategy, Props, Status, SupervisorStrategy}
|
||||
import akka.actor.{ActorRef, FSM, LoggingFSM, OneForOneStrategy, Props, Status, SupervisorStrategy}
|
||||
import akka.event.Logging.MDC
|
||||
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._
|
||||
import scala.util.{Failure, Left, Success, Try}
|
||||
import scala.util.{Failure, Left, Random, Success, Try}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,10 +26,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] with FSMDiagnosticActorLogging[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] with FSMDiagnosticActorLogging[State, Data] {
|
||||
|
||||
val forwarder = context.actorOf(Props(new Forwarder(nodeParams)), "forwarder")
|
||||
|
||||
|
@ -102,6 +105,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 {
|
||||
|
@ -202,9 +208,8 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
|||
localFeatures = remoteInit.localFeatures)
|
||||
log.debug(s"remote params: $remoteParams")
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -216,57 +221,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)
|
||||
|
||||
|
@ -294,8 +269,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
|
||||
|
@ -307,6 +280,11 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
|||
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)
|
||||
log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}")
|
||||
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))
|
||||
|
@ -325,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)
|
||||
|
@ -343,9 +322,18 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
|||
log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}")
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -386,9 +374,15 @@ 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)
|
||||
} else if (d.commitments.announceChannel && nodeParams.spv && d.commitments.localParams.isFunder && System.getProperty("spvtest") != null) {
|
||||
// hard coded id for testing
|
||||
log.warning("using hardcoded short id for testing!!!!!")
|
||||
context.system.scheduler.scheduleOnce(5 seconds, self, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, Random.nextInt(100), Random.nextInt(100)))
|
||||
}
|
||||
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), None, None, None, None))
|
||||
|
||||
|
@ -650,6 +644,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
|
||||
}
|
||||
|
||||
|
@ -995,7 +994,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) =>
|
||||
|
||||
|
@ -1017,7 +1016,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)
|
||||
}
|
||||
|
||||
|
@ -1089,7 +1090,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(CurrentFeerate(_), _) => stay
|
||||
|
||||
case Event("ok", _) => stay // noop handler
|
||||
// we receive this when we send command to ourselves
|
||||
case Event("ok", _) => stay
|
||||
}
|
||||
|
||||
onTransition {
|
||||
|
@ -1174,10 +1176,21 @@ 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)
|
||||
}
|
||||
|
||||
// we need to watch the htlc-success outputs in order to be notified when they can be spent by claim-delayed-output txes
|
||||
if (nodeParams.spv) {
|
||||
// we need to watch the corresponding public key script of the htlc-success tx
|
||||
localCommitPublished.htlcSuccessTxs.map(_.txIn(0).outPoint.index.toInt).map(outputIndex => blockchain ! Hint(new BitcoinjScript(localCommitPublished.commitTx.txOut(outputIndex).publicKeyScript)))
|
||||
}
|
||||
|
||||
localCommitPublished.claimMainDelayedOutputTx.foreach(tx => blockchain ! PublishAsap(tx))
|
||||
localCommitPublished.htlcSuccessTxs.foreach(tx => blockchain ! PublishAsap(tx))
|
||||
localCommitPublished.htlcTimeoutTxs.foreach(tx => blockchain ! PublishAsap(tx))
|
||||
|
@ -1187,6 +1200,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
|||
localCommitPublished.htlcTimeoutTxs.foreach(tx => {
|
||||
require(tx.txIn.size == 1, s"an htlc-timeout tx must have exactly 1 input (has ${tx.txIn.size})")
|
||||
val outpoint = tx.txIn(0).outPoint
|
||||
if (nodeParams.spv) {
|
||||
// we need to watch the corresponding public key script of the commit tx
|
||||
blockchain ! Hint(new BitcoinjScript(localCommitPublished.commitTx.txOut(outpoint.index.toInt).publicKeyScript))
|
||||
}
|
||||
log.info(s"watching output ${outpoint.index} of commit tx ${outpoint.txid}")
|
||||
blockchain ! WatchSpent(relayer, outpoint.txid, outpoint.index.toInt, BITCOIN_HTLC_SPENT)
|
||||
})
|
||||
|
@ -1231,7 +1248,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)
|
||||
}
|
||||
|
||||
|
@ -1243,6 +1265,10 @@ class Channel(val nodeParams: NodeParams, remoteNodeId: PublicKey, blockchain: A
|
|||
remoteCommitPublished.claimHtlcTimeoutTxs.foreach(tx => {
|
||||
require(tx.txIn.size == 1, s"a claim-htlc-timeout tx must have exactly 1 input (has ${tx.txIn.size})")
|
||||
val outpoint = tx.txIn(0).outPoint
|
||||
if (nodeParams.spv) {
|
||||
// we need to watch the corresponding public key script of the commit tx
|
||||
blockchain ! Hint(new BitcoinjScript(remoteCommitPublished.commitTx.txOut(outpoint.index.toInt).publicKeyScript))
|
||||
}
|
||||
log.info(s"watching output ${outpoint.index} of commit tx ${outpoint.txid}")
|
||||
blockchain ! WatchSpent(relayer, outpoint.txid, outpoint.index.toInt, BITCOIN_HTLC_SPENT)
|
||||
})
|
||||
|
@ -1275,8 +1301,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)
|
||||
}
|
||||
|
||||
|
@ -1416,15 +1452,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
|
||||
// }
|
||||
}
|
||||
|
||||
override def mdc(currentMessage: Any): MDC = {
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -58,10 +58,10 @@ package object eclair {
|
|||
/**
|
||||
* Converts fee-rate-per-kB to fee-rate-per-kw, *based on a standard commit tx*
|
||||
*
|
||||
* @param feeratePerKB
|
||||
* @param feeratePerKb
|
||||
* @return
|
||||
*/
|
||||
def feerateKB2Kw(feeratePerKB: Long): Long = feeratePerKB / 2
|
||||
def feerateKb2Kw(feeratePerKb: Long): Long = feeratePerKb / 2
|
||||
|
||||
|
||||
}
|
|
@ -218,6 +218,9 @@ class Relayer(nodeSecret: PrivateKey, paymentHandler: ActorRef) extends Actor wi
|
|||
log.warning(s"extracted paymentHash160=$paymentHash160 from tx=${Transaction.write(tx)} (claim-htlc-timeout)")
|
||||
paymentHash160
|
||||
}
|
||||
// TODO: should we handle local htlcs here as well? currently timed out htlcs that we sent will never have an answer
|
||||
// TODO: we do not handle the case where htlcs transactions end up beeing unconfirmed this can happen if an htlc-success
|
||||
// tx is published right before a htlc timed out
|
||||
val htlcsOut = bindings.collect {
|
||||
case b@(htlcOut, Relayed(upstream, htlcIn)) if htlcIn.paymentHash == sha256(extracted) =>
|
||||
log.warning(s"found a match between preimage=$extracted and origin htlc=$htlcIn")
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
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.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 scala.collection.JavaConversions._
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
|
||||
/**
|
||||
* 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, chainHash = nodeParams.chainHash, 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))
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
</encoder>
|
||||
</appender-->
|
||||
|
||||
<!--logger name="fr.acinq.eclair.channel" level="DEBUG"/-->
|
||||
|
||||
<root level="INFO">
|
||||
<!--appender-ref ref="FILE"/>
|
||||
<appender-ref ref="CONSOLEWARN"/-->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{Satoshi, Script}
|
||||
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
|
||||
import fr.acinq.eclair.blockchain.spv.BitcoinjKit
|
||||
import fr.acinq.eclair.blockchain.wallet.BitcoinjWallet
|
||||
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
|
||||
import fr.acinq.eclair.randomKey
|
||||
import fr.acinq.eclair.transactions.Scripts
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.{Coin, Transaction}
|
||||
import org.bitcoinj.script.{Script => BitcoinjScript}
|
||||
import org.bitcoinj.wallet.{SendRequest, Wallet}
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.sys.process.{Process, _}
|
||||
import scala.util.Random
|
||||
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/bitcoinj-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BitcoinjSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
|
||||
ignore("bitcoinj wallet commit") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 60 seconds, interval = 1 second)
|
||||
|
||||
logger.info(s"generating blocks")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 10))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
val fundingPubkeyScript1 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result1 = Await.result(wallet.makeFundingTx(fundingPubkeyScript1, Satoshi(10000L), 20000), 10 seconds)
|
||||
val fundingPubkeyScript2 = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result2 = Await.result(wallet.makeFundingTx(fundingPubkeyScript2, Satoshi(10000L), 20000), 10 seconds)
|
||||
|
||||
assert(Await.result(wallet.commit(result1.fundingTx), 10 seconds) == true)
|
||||
assert(Await.result(wallet.commit(result2.fundingTx), 10 seconds) == false)
|
||||
|
||||
}
|
||||
|
||||
/*def ticket() = {
|
||||
val wallet: Wallet = ???
|
||||
|
||||
def makeTx(amount: Coin, script: BitcoinjScript): Transaction = {
|
||||
val tx = new Transaction(wallet.getParams)
|
||||
tx.addOutput(amount, script)
|
||||
val req = SendRequest.forTx(tx)
|
||||
wallet.completeTx(req)
|
||||
tx
|
||||
}
|
||||
|
||||
val tx1 = makeTx(amount1, script1)
|
||||
val tx2 = makeTx(amount2, script2)
|
||||
|
||||
// everything is fine until here, and as expected tx1 and tx2 spend the same input
|
||||
|
||||
wallet.maybeCommitTx(tx1) // returns true as expected
|
||||
wallet.maybeCommitTx(tx2) // returns true! how come?
|
||||
}*/
|
||||
|
||||
ignore("manual publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val watcher = system.actorOf(Props(new SpvWatcher(bitcoinjKit)), name = "spv-watcher")
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second)
|
||||
|
||||
logger.info(s"generating blocks")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 10))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
val listener = TestProbe()
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds)
|
||||
watcher ! Hint(new BitcoinjScript(fundingPubkeyScript))
|
||||
assert(Await.result(wallet.commit(result.fundingTx), 10 seconds))
|
||||
watcher ! WatchSpent(listener.ref, result.fundingTx.txid, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT)
|
||||
watcher ! WatchConfirmed(listener.ref, result.fundingTx.txid, 3, BITCOIN_FUNDING_DEPTHOK)
|
||||
watcher ! PublishAsap(result.fundingTx)
|
||||
|
||||
logger.info(s"waiting for confirmation of ${result.fundingTx.txid}")
|
||||
val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds)
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
|
||||
ignore("multiple publish/watch") {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
|
||||
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
|
||||
bitcoinjKit.startAsync()
|
||||
bitcoinjKit.awaitRunning()
|
||||
|
||||
val sender = TestProbe()
|
||||
val watcher = system.actorOf(Props(new SpvWatcher(bitcoinjKit)), name = "spv-watcher")
|
||||
val wallet = new BitcoinjWallet(Future.successful(bitcoinjKit.wallet()))
|
||||
|
||||
val address = Await.result(wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond(Await.result(wallet.getBalance, 10 seconds) > Satoshi(0), max = 30 seconds, interval = 1 second)
|
||||
|
||||
def send() = {
|
||||
val count = Random.nextInt(20)
|
||||
val listeners = (0 to count).map {
|
||||
case i =>
|
||||
val listener = TestProbe()
|
||||
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
|
||||
val result = Await.result(wallet.makeFundingTx(fundingPubkeyScript, Satoshi(10000L), 20000), 10 seconds)
|
||||
watcher ! Hint(new BitcoinjScript(fundingPubkeyScript))
|
||||
assert(Await.result(wallet.commit(result.fundingTx), 10 seconds))
|
||||
watcher ! WatchSpent(listener.ref, result.fundingTx.txid, result.fundingTxOutputIndex, BITCOIN_FUNDING_SPENT)
|
||||
watcher ! WatchConfirmed(listener.ref, result.fundingTx.txid, 3, BITCOIN_FUNDING_DEPTHOK)
|
||||
watcher ! PublishAsap(result.fundingTx)
|
||||
(result.fundingTx.txid, listener)
|
||||
}
|
||||
system.scheduler.scheduleOnce(2 seconds, new Runnable {
|
||||
override def run() = {
|
||||
logger.info(s"generating one block")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 3))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
})
|
||||
for ((txid, listener) <- listeners) {
|
||||
logger.info(s"waiting for confirmation of $txid")
|
||||
val event = listener.expectMsgType[WatchEventConfirmed](1000 seconds)
|
||||
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
|
||||
}
|
||||
}
|
||||
|
||||
for (i <- 0 to 10) send()
|
||||
|
||||
}
|
||||
}
|
|
@ -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,45 @@
|
|||
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 getBalance: Future[Satoshi] = ???
|
||||
|
||||
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(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]
|
||||
|
|
|
@ -0,0 +1,653 @@
|
|||
package fr.acinq.eclair.integration
|
||||
|
||||
import java.io.{File, PrintWriter}
|
||||
import java.nio.file.Files
|
||||
import java.util.{Properties, UUID}
|
||||
|
||||
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
|
||||
import akka.pattern.pipe
|
||||
import akka.testkit.{TestKit, TestProbe}
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script}
|
||||
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import fr.acinq.eclair.blockchain.wallet.BitcoinjWallet
|
||||
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
|
||||
import fr.acinq.eclair.channel.Register.Forward
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx.ErrorPacket
|
||||
import fr.acinq.eclair.io.Disconnect
|
||||
import fr.acinq.eclair.io.Switchboard.{NewChannel, NewConnection}
|
||||
import fr.acinq.eclair.payment.{State => _, _}
|
||||
import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec}
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{Globals, Kit, Setup}
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoinj.core.Transaction
|
||||
import org.json4s.DefaultFormats
|
||||
import org.json4s.JsonAST.JValue
|
||||
import org.junit.Ignore
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
|
||||
|
||||
import scala.collection.JavaConversions._
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.sys.process._
|
||||
|
||||
/**
|
||||
* Created by PM on 15/03/2017.
|
||||
*/
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
@Ignore
|
||||
class BasicIntegrationSpvSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
System.setProperty("spvtest", "true")
|
||||
|
||||
val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoind")
|
||||
val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin")
|
||||
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var nodes: Map[String, Kit] = Map()
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
case class BitcoinReq(method: String, params: Any*)
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
|
||||
Files.copy(classOf[BasicIntegrationSpvSpec].getResourceAsStream("/integration/bitcoin.conf"), new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)
|
||||
|
||||
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
bitcoinrpcclient = new BitcoinJsonRPCClient(user = "foo", password = "bar", host = "localhost", port = 28332)
|
||||
bitcoincli = system.actorOf(Props(new Actor {
|
||||
override def receive: Receive = {
|
||||
case BitcoinReq(method) => bitcoinrpcclient.invoke(method) pipeTo sender
|
||||
case BitcoinReq(method, params) => bitcoinrpcclient.invoke(method, params) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2) => bitcoinrpcclient.invoke(method, param1, param2) pipeTo sender
|
||||
case BitcoinReq(method, param1, param2, param3) => bitcoinrpcclient.invoke(method, param1, param2, param3) pipeTo sender
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
// gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging
|
||||
logger.info(s"stopping bitcoind")
|
||||
val sender = TestProbe()
|
||||
sender.send(bitcoincli, BitcoinReq("stop"))
|
||||
sender.expectMsgType[JValue]
|
||||
//bitcoind.destroy()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
setup.system.terminate()
|
||||
}
|
||||
// logger.warn(s"starting bitcoin-qt")
|
||||
// val PATH_BITCOINQT = new File(System.getProperty("buildDirectory"), "bitcoin-0.14.0/bin/bitcoin-qt").toPath
|
||||
// bitcoind = s"$PATH_BITCOINQT -datadir=$PATH_BITCOIND_DATADIR".run()
|
||||
}
|
||||
|
||||
test("wait bitcoind ready") {
|
||||
val sender = TestProbe()
|
||||
logger.info(s"waiting for bitcoind to initialize...")
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("getnetworkinfo"))
|
||||
sender.receiveOne(5 second).isInstanceOf[JValue]
|
||||
}, max = 30 seconds, interval = 500 millis)
|
||||
logger.info(s"generating initial blocks...")
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 500))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
|
||||
def instantiateEclairNode(name: String, config: Config) = {
|
||||
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name")
|
||||
datadir.mkdirs()
|
||||
new PrintWriter(new File(datadir, "eclair.conf")) {
|
||||
write(config.root().render());
|
||||
close
|
||||
}
|
||||
val setup = new Setup(datadir, actorSystem = ActorSystem(s"system-$name"))
|
||||
val kit = Await.result(setup.bootstrap, 10 seconds)
|
||||
setup.bitcoin.left.get.awaitRunning()
|
||||
nodes = nodes + (name -> kit)
|
||||
}
|
||||
|
||||
def javaProps(props: Seq[(String, String)]) = {
|
||||
val properties = new Properties()
|
||||
props.foreach(p => properties.setProperty(p._1, p._2))
|
||||
properties
|
||||
}
|
||||
|
||||
test("starting eclair nodes") {
|
||||
import collection.JavaConversions._
|
||||
val commonConfig = ConfigFactory.parseMap(Map("eclair.spv" -> true, "eclair.chain" -> "regtest", "eclair.bitcoinj.port" -> 28333, "eclair.server.public-ips.1" -> "localhost", "eclair.bitcoind.port" -> 28333, "eclair.bitcoind.rpcport" -> 28332, "eclair.bitcoind.zmq" -> "tcp://127.0.0.1:28334", "eclair.router-broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false, "eclair.delay-blocks" -> 6))
|
||||
//instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 29730, "eclair.api.port" -> 28080)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 29731, "eclair.api.port" -> 28081)).withFallback(commonConfig))
|
||||
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 29732, "eclair.api.port" -> 28082)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 29733, "eclair.api.port" -> 28083)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 29734, "eclair.api.port" -> 28084)).withFallback(commonConfig))
|
||||
//instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.payment-handler" -> "noop")).withFallback(commonConfig)) // NB: eclair.payment-handler = noop allows us to manually fulfill htlcs
|
||||
//instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
|
||||
}
|
||||
|
||||
def sendFunds(node: Kit) = {
|
||||
val sender = TestProbe()
|
||||
val address = Await.result(node.wallet.getFinalAddress, 10 seconds)
|
||||
logger.info(s"sending funds to $address")
|
||||
sender.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
awaitCond({
|
||||
node.wallet.getBalance.pipeTo(sender.ref)
|
||||
sender.expectMsgType[Satoshi] > Satoshi(0)
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
test("fund eclair wallets") {
|
||||
//sendFunds(nodes("A"))
|
||||
//sendFunds(nodes("B"))
|
||||
sendFunds(nodes("C"))
|
||||
//sendFunds(nodes("D"))
|
||||
//sendFunds(nodes("E"))
|
||||
}
|
||||
|
||||
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
|
||||
val eventListener1 = TestProbe()
|
||||
val eventListener2 = TestProbe()
|
||||
node1.system.eventStream.subscribe(eventListener1.ref, classOf[ChannelStateChanged])
|
||||
node2.system.eventStream.subscribe(eventListener2.ref, classOf[ChannelStateChanged])
|
||||
val sender = TestProbe()
|
||||
sender.send(node1.switchboard, NewConnection(
|
||||
remoteNodeId = node2.nodeParams.privateKey.publicKey,
|
||||
address = node2.nodeParams.publicAddresses.head,
|
||||
newChannel_opt = Some(NewChannel(Satoshi(fundingSatoshis), MilliSatoshi(pushMsat), None))))
|
||||
sender.expectMsgAnyOf(10 seconds, "connected", s"already connected to nodeId=${node2.nodeParams.privateKey.publicKey.toBin}")
|
||||
awaitCond(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds)
|
||||
awaitCond(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds, interval = 1 seconds)
|
||||
}
|
||||
|
||||
test("connect nodes") {
|
||||
//
|
||||
// A ---- B ---- C ---- D
|
||||
// | / \
|
||||
// --E--' F{1,2,3,4}
|
||||
//
|
||||
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
|
||||
|
||||
//connect(nodes("A"), nodes("B"), 10000000, 0)
|
||||
//connect(nodes("B"), nodes("C"), 2000000, 0)
|
||||
//connect(nodes("C"), nodes("D"), 5000000, 0)
|
||||
//connect(nodes("B"), nodes("E"), 5000000, 0)
|
||||
//connect(nodes("E"), nodes("C"), 5000000, 0)
|
||||
//connect(nodes("C"), nodes("F1"), 5000000, 0)
|
||||
//connect(nodes("C"), nodes("F2"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F3"), 5000000, 0)
|
||||
connect(nodes("C"), nodes("F4"), 5000000, 0)
|
||||
|
||||
// a channel has two endpoints
|
||||
val channelEndpointsCount = nodes.values.foldLeft(0) {
|
||||
case (sum, setup) =>
|
||||
sender.send(setup.register, 'channels)
|
||||
val channels = sender.expectMsgType[Map[BinaryData, ActorRef]]
|
||||
sum + channels.size
|
||||
}
|
||||
|
||||
// we make sure all channels have set up their WatchConfirmed for the funding tx
|
||||
awaitCond({
|
||||
nodes.values.foldLeft(Set.empty[Watch]) {
|
||||
case (watches, setup) =>
|
||||
sender.send(setup.watcher, 'watches)
|
||||
watches ++ sender.expectMsgType[Set[Watch]]
|
||||
}.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
|
||||
}, max = 10 seconds, interval = 1 second)
|
||||
|
||||
// confirming the funding tx
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 2))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
||||
within(60 seconds) {
|
||||
var count = 0
|
||||
while (count < channelEndpointsCount) {
|
||||
if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == NORMAL) count = count + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int) = {
|
||||
val sender = TestProbe()
|
||||
subset.foreach {
|
||||
case (_, setup) =>
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'nodes)
|
||||
sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
awaitCond({
|
||||
sender.send(setup.router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].size == updates
|
||||
}, max = 60 seconds, interval = 1 second)
|
||||
}
|
||||
}
|
||||
|
||||
test("wait for network announcements") {
|
||||
val sender = TestProbe()
|
||||
// generating more blocks so that all funding txes are buried under at least 6 blocks
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 4))
|
||||
sender.expectMsgType[JValue]
|
||||
awaitAnnouncements(nodes, 3, 2, 4)
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D") {
|
||||
val sender = TestProbe()
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
// first we retrieve a payment hash from D
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
sender.send(nodes("A").paymentInitiator,
|
||||
SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey))
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an invalid expiry delta for C") {
|
||||
val sender = TestProbe()
|
||||
// to simulate this, we will update C's relay params
|
||||
// first we find out the short channel id for channel C-D, easiest way is to ask D's register which has only one channel
|
||||
sender.send(nodes("D").register, 'shortIds)
|
||||
val shortIdCD = sender.expectMsgType[Map[Long, BinaryData]].keys.head
|
||||
val channelUpdateCD = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.blockId, nodes("C").nodeParams.privateKey, nodes("D").nodeParams.privateKey.publicKey, shortIdCD, nodes("D").nodeParams.expiryDeltaBlocks + 1, nodes("D").nodeParams.htlcMinimumMsat, nodes("D").nodeParams.feeBaseMsat, nodes("D").nodeParams.feeProportionalMillionth)
|
||||
sender.send(nodes("C").relayer, channelUpdateCD)
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(4200000)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the actual payment
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will receive an error from C that include the updated channel update, then will retry the payment
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
// in the meantime, the router will have updated its state
|
||||
awaitCond({
|
||||
sender.send(nodes("A").router, 'updates)
|
||||
sender.expectMsgType[Iterable[ChannelUpdate]].toSeq.contains(channelUpdateCD)
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// finally we retry the same payment, this time successfully
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an amount greater than capacity of C-D") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D
|
||||
val amountMsat = MilliSatoshi(300000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
// then we make the payment (C-D has a smaller capacity than A-B and B-C)
|
||||
val sendReq = SendPayment(amountMsat.amount, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
sender.expectMsgType[PaymentSucceeded](5 seconds)
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with an unknown payment hash") {
|
||||
val sender = TestProbe()
|
||||
val pr = SendPayment(100000000L, "42" * 32, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, pr)
|
||||
|
||||
// A will first receive an error from C, then retry and route around C: A->B->E->C->D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, UnknownPaymentHash))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with a lower amount than requested") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of only 1 mBTC
|
||||
val sendReq = SendPayment(100000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with too much overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 6 mBTC
|
||||
val sendReq = SendPayment(600000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
|
||||
// A will first receive an IncorrectPaymentAmount error from D
|
||||
val failed = sender.expectMsgType[PaymentFailed]
|
||||
assert(failed.paymentHash === pr.paymentHash)
|
||||
assert(failed.failures.size === 1)
|
||||
assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.privateKey.publicKey, IncorrectPaymentAmount))
|
||||
}
|
||||
|
||||
ignore("send an HTLC A->D with a reasonable overpayment") {
|
||||
val sender = TestProbe()
|
||||
// first we retrieve a payment hash from D for 2 mBTC
|
||||
val amountMsat = MilliSatoshi(200000000L)
|
||||
sender.send(nodes("D").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
|
||||
val pr = sender.expectMsgType[PaymentRequest]
|
||||
|
||||
// A send payment of 3 mBTC, more than asked but it should still be accepted
|
||||
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("D").nodeParams.privateKey.publicKey)
|
||||
sender.send(nodes("A").paymentInitiator, sendReq)
|
||||
sender.expectMsgType[PaymentSucceeded]
|
||||
}
|
||||
|
||||
/**
|
||||
* We currently use p2pkh script Helpers.getFinalScriptPubKey
|
||||
*
|
||||
* @param scriptPubKey
|
||||
* @return
|
||||
*/
|
||||
def scriptPubKeyToAddress(scriptPubKey: BinaryData) = Script.parse(scriptPubKey) match {
|
||||
case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)
|
||||
case OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUAL :: Nil =>
|
||||
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, pubKeyHash)
|
||||
case _ => ???
|
||||
}
|
||||
|
||||
def incomingTxes(node: Kit) = {
|
||||
val sender = TestProbe()
|
||||
(for {
|
||||
w <- nodes("F1").wallet.asInstanceOf[BitcoinjWallet].fWallet
|
||||
txes = w.getWalletTransactions
|
||||
incomingTxes = txes.toSet.filter(tx => tx.getTransaction.getValueSentToMe(w).longValue() > 0)
|
||||
} yield incomingTxes).pipeTo(sender.ref)
|
||||
sender.expectMsgType[Set[Transaction]]
|
||||
}
|
||||
|
||||
ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
val initialTxesC = incomingTxes(nodes("C"))
|
||||
val initialTxesF1 = incomingTxes(nodes("F1"))
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F1").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F1").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F1").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then have C unilateral close the channel (which will make F redeem the htlc onchain)
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then wait for F to detect the unilateral close and go to CLOSING state
|
||||
awaitCond({
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == CLOSING
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// we then fulfill the htlc, which will make F redeem it on-chain
|
||||
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have received the on-chain tx corresponding to the redeemed htlc
|
||||
awaitCond({
|
||||
incomingTxes(nodes("F1")).size - initialTxesF1.size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// we then generate enough blocks so that C gets its main delayed output
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// and C will have its main output
|
||||
awaitCond({
|
||||
incomingTxes(nodes("C")).size - initialTxesC.size == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
|
||||
}
|
||||
|
||||
ignore("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F2").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F2").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("A").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F2").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
peers(nodes("C").nodeParams.privateKey.publicKey) ! Disconnect
|
||||
// we then wait for F to be in disconnected state
|
||||
awaitCond({
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_GETSTATE))
|
||||
sender.expectMsgType[State] == OFFLINE
|
||||
}, max = 20 seconds, interval = 1 second)
|
||||
// then we have F unilateral close the channel
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain)
|
||||
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
|
||||
// we then generate one block so that the htlc success tx gets written to the blockchain
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
// C will extract the preimage from the blockchain and fulfill the payment upstream
|
||||
paymentSender.expectMsgType[PaymentSucceeded](30 seconds)
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc
|
||||
// we then generate enough blocks so that F gets its htlc-success delayed output
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point F should have 1 recv transactions: the redeemed htlc and C will have its main output
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 1 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 1
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F3").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F3").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("C").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F3").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
for (i <- 0 until 11) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// this will fail the htlc
|
||||
//val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
//assert(failed.paymentHash === paymentHash)
|
||||
//assert(failed.failures.size === 1)
|
||||
//assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
|
||||
}
|
||||
|
||||
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
|
||||
val sender = TestProbe()
|
||||
// first we make sure we are in sync with current blockchain height
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
sender.send(bitcoincli, BitcoinReq("getbestblockhash"))
|
||||
val currentBlockHash = sender.expectMsgType[JValue](10 seconds).extract[String]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
nodes("F4").paymentHandler ! htlcReceiver.ref
|
||||
val preimage: BinaryData = "42" * 32
|
||||
val paymentHash = Crypto.sha256(preimage)
|
||||
// A sends a payment to F
|
||||
val paymentReq = SendPayment(100000000L, paymentHash, nodes("F4").nodeParams.privateKey.publicKey, maxAttempts = 1)
|
||||
val paymentSender = TestProbe()
|
||||
paymentSender.send(nodes("C").paymentInitiator, paymentReq)
|
||||
// F gets the htlc
|
||||
val htlc = htlcReceiver.expectMsgType[UpdateAddHtlc]
|
||||
// now that we have the channel id, we retrieve channels default final addresses
|
||||
sender.send(nodes("C").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyC = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, CMD_GETSTATEDATA))
|
||||
val finalScriptPubkeyF = sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey
|
||||
// then we ask F to unilaterally close the channel
|
||||
sender.send(nodes("F4").register, Forward(htlc.channelId, INPUT_PUBLISH_LOCALCOMMIT))
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
for (i <- 0 until 11) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
// this will fail the htlc
|
||||
//val failed = paymentSender.expectMsgType[PaymentFailed](30 seconds)
|
||||
//assert(failed.paymentHash === paymentHash)
|
||||
//assert(failed.failures.size === 1)
|
||||
//assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.privateKey.publicKey, PermanentChannelFailure))
|
||||
// we then generate enough blocks to confirm all delayed transactions
|
||||
for (i <- 0 until 7) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
val ext = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
awaitCond({
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
ext.getTxsSinceBlockHash(currentBlockHash).pipeTo(sender.ref)
|
||||
val txes = sender.expectMsgType[Seq[fr.acinq.bitcoin.Transaction]].filterNot(fr.acinq.bitcoin.Transaction.isCoinbase(_))
|
||||
// at this point C should have 2 recv transactions: its main output and the htlc timeout
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyF) == 0 &&
|
||||
txes.count(tx => tx.txOut(0).publicKeyScript == finalScriptPubkeyC) == 2
|
||||
}, max = 30 seconds, interval = 1 second)
|
||||
// TODO: awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)
|
||||
}
|
||||
|
||||
ignore("generate and validate lots of channels") {
|
||||
implicit val extendedClient = new ExtendedBitcoinClient(bitcoinrpcclient)
|
||||
// we simulate fake channels by publishing a funding tx and sending announcement messages to a node at random
|
||||
logger.info(s"generating fake channels")
|
||||
val sender = TestProbe()
|
||||
val channels = for (i <- 0 until 242) yield {
|
||||
// let's generate a block every 10 txs so that we can compute short ids
|
||||
if (i % 10 == 0) {
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
}
|
||||
AnnouncementsBatchValidationSpec.simulateChannel
|
||||
}
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 1))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
logger.info(s"simulated ${channels.size} channels")
|
||||
// then we make the announcements
|
||||
val announcements = channels.map(c => AnnouncementsBatchValidationSpec.makeChannelAnnouncement(c))
|
||||
announcements.foreach(ann => nodes("A").router ! ann)
|
||||
awaitCond({
|
||||
sender.send(nodes("D").router, 'channels)
|
||||
sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 5 // 5 remaining channels because D->F{1-F4} have disappeared
|
||||
}, max = 120 seconds, interval = 1 second)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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, Block, 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, Block, 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
|
||||
|
@ -49,7 +49,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var nodes: Map[String, Kit] = Map()
|
||||
var finalAddresses: Map[String, String] = Map()
|
||||
|
||||
implicit val formats = DefaultFormats
|
||||
|
||||
|
@ -107,7 +106,6 @@ 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)
|
||||
nodes = nodes + (name -> kit)
|
||||
}
|
||||
|
||||
|
@ -119,7 +117,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 +143,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)
|
||||
|
@ -363,16 +360,23 @@ 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
|
||||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
|
@ -385,6 +389,15 @@ 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F1").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
|
@ -412,7 +425,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))
|
||||
|
@ -421,7 +434,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)
|
||||
|
@ -433,10 +446,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
|
@ -449,6 +458,15 @@ 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// we then kill the connection between C and F
|
||||
sender.send(nodes("F2").switchboard, 'peers)
|
||||
val peers = sender.expectMsgType[Map[PublicKey, ActorRef]]
|
||||
|
@ -475,13 +493,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)
|
||||
|
@ -493,10 +511,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
|
@ -509,6 +523,13 @@ 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// we then generate enough blocks to make the htlc timeout
|
||||
sender.send(bitcoincli, BitcoinReq("generate", 11))
|
||||
sender.expectMsgType[JValue](10 seconds)
|
||||
|
@ -524,7 +545,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)
|
||||
|
@ -536,10 +557,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
|
|||
sender.send(bitcoincli, BitcoinReq("getblockcount"))
|
||||
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
|
||||
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
|
||||
// we also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddresses("C"))).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// NB: F has a no-op payment handler, allowing us to manually fulfill htlcs
|
||||
val htlcReceiver = TestProbe()
|
||||
// we register this probe as the final payment handler
|
||||
|
@ -552,6 +569,13 @@ 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 also retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
|
||||
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
|
||||
val res = sender.expectMsgType[JValue](10 seconds)
|
||||
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
|
||||
// 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
|
||||
|
@ -569,7 +593,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, Block, Satoshi, Transaction}
|
||||
import fr.acinq.eclair.blockchain.{ExtendedBitcoinClient, MakeFundingTxResponse}
|
||||
import fr.acinq.eclair.blockchain.rpc.BitcoinJsonRPCClient
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, 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-gui_2.11</artifactId>
|
||||
|
|
|
@ -31,78 +31,69 @@ class FxApp extends Application with Logging {
|
|||
logger.debug("initializing application...")
|
||||
}
|
||||
|
||||
def onError(t: Throwable): Unit = t match {
|
||||
case TCPBindException(port) =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null))
|
||||
case 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 =>
|
||||
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 IncompatibleDBException =>
|
||||
notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", null))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
|
||||
case t: Throwable =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
|
||||
}
|
||||
|
||||
override def start(primaryStage: Stage): Unit = {
|
||||
val icon = new Image(getClass.getResource("/gui/commons/images/eclair-square.png").toExternalForm, false)
|
||||
primaryStage.getIcons.add(icon)
|
||||
|
||||
def onError(t: Throwable): Unit = t match {
|
||||
case TCPBindException(port) =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Could not bind to port $port", null))
|
||||
case 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 =>
|
||||
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 IncompatibleDBException =>
|
||||
notifyPreloader(new ErrorNotification("Setup", "Breaking changes!", null))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Eclair is still in alpha, and under heavy development. Last update was not backward compatible."))
|
||||
notifyPreloader(new AppNotification(InfoAppNotification, "Please reset your datadir."))
|
||||
case t: Throwable =>
|
||||
notifyPreloader(new ErrorNotification("Setup", s"Internal error: ${t.toString}", t))
|
||||
}
|
||||
|
||||
new Thread(new Runnable {
|
||||
override def run(): Unit = {
|
||||
try {
|
||||
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 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)
|
||||
}
|
||||
})
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
setup.bootstrap onComplete {
|
||||
case Success(kit) =>
|
||||
Platform.runLater(new Runnable {
|
||||
override def run(): Unit = {
|
||||
notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful"))
|
||||
primaryStage.setScene(scene)
|
||||
primaryStage.show
|
||||
initNotificationStage(primaryStage, handlers)
|
||||
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)
|
||||
}
|
||||
})
|
||||
pKit.success(kit)
|
||||
case Failure(t) => onError(t)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
controller.initInfoFields(setup)
|
||||
primaryStage.setScene(scene)
|
||||
primaryStage.show
|
||||
notifyPreloader(new AppNotification(SuccessAppNotification, "Init successful"))
|
||||
initNotificationStage(primaryStage, handlers)
|
||||
}
|
||||
})
|
||||
case Failure(t) => onError(t)
|
||||
}
|
||||
} catch {
|
||||
case t: Throwable => onError(t)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ case class PaymentRelayedRecord(event: PaymentRelayed, date: LocalDateTime) exte
|
|||
/**
|
||||
* 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 = _
|
||||
|
@ -130,23 +130,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)
|
||||
|
@ -323,6 +306,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})")
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration debug="false">
|
||||
|
||||
<!--appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.out</target>
|
||||
<withJansi>false</withJansi>
|
||||
<encoder>
|
||||
<pattern>%yellow(${HOSTNAME} %d) %highlight(%-5level) %logger{36} %X{akkaSource} - %msg%ex{12}%n</pattern>
|
||||
</encoder>
|
||||
</appender-->
|
||||
</appender>
|
||||
|
||||
<!--appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${user.home}/.eclair/eclair.log</file>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<logger name="fr.acinq.eclair.router" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<!--appender-ref ref="CONSOLE"/-->
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<!--appender-ref ref="FILE"/-->
|
||||
</root>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true" debug="false">
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.out</target>
|
||||
<encoder>
|
||||
<pattern>%date{HH:mm:ss.SSS} %highlight(%-5level) %X{akkaSource} - %msg%ex{12}%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!--appender name="CONSOLEWARN" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<target>System.out</target>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>WARN</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
|
||||
</encoder>
|
||||
</appender-->
|
||||
|
||||
<!--appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>eclair.log</file>
|
||||
<append>false</append>
|
||||
<encoder>
|
||||
<pattern>%-5level %X{akkaSource} - %msg%ex{12}%n</pattern>
|
||||
</encoder>
|
||||
</appender-->
|
||||
|
||||
<logger name="fr.acinq.eclair.Pipe" level="DEBUG" />
|
||||
<logger name="fr.acinq.eclair.crypto.TransportHandler" level="DEBUG" />
|
||||
|
||||
<root level="INFO">
|
||||
<!--appender-ref ref="FILE"/>
|
||||
<appender-ref ref="CONSOLEWARN"/-->
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
3
pom.xml
3
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>
|
||||
|
@ -48,6 +48,7 @@
|
|||
<scala.version.short>2.11</scala.version.short>
|
||||
<akka.version>2.4.18</akka.version>
|
||||
<bitcoinlib.version>0.9.13</bitcoinlib.version>
|
||||
<bitcoinj.version>0.15-SNAPSHOT</bitcoinj.version>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
|
|
Loading…
Add table
Reference in a new issue