1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 14:40:34 +01:00

Removed bitcoin-core parent-tx hack (#231)

This was a workaround because bitcoin-core could produce malleable funding
transactions.

We now:
1) assume that all existing funds in bitcoin core are in segwit P2S addresses
2) manually create segwit change addresses when we create new transactions

Also disabled unused/unreliable bitcoinj tests
This commit is contained in:
Pierre-Marie Padiou 2017-11-27 15:05:25 +01:00 committed by GitHub
parent 6304041d77
commit 8b151eb5c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 205 additions and 217 deletions

View file

@ -131,7 +131,7 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
}
val wallet = bitcoin match {
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient)
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
case Electrum(electrumClient) =>
val electrumSeedPath = new File(datadir, "electrum_seed.dat")

View file

@ -1,55 +1,32 @@
package fr.acinq.eclair.blockchain.bitcoind
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Base58Check, BinaryData, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import akka.actor.ActorSystem
import fr.acinq.bitcoin.{BinaryData, OutPoint, Satoshi, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError}
import fr.acinq.eclair.channel.{BITCOIN_OUTPUT_SPENT, BITCOIN_TX_CONFIRMED}
import fr.acinq.eclair.transactions.Transactions
import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JBool, JDouble, JInt, JString}
import org.json4s.JsonAST._
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.concurrent.{ExecutionContext, Future}
/**
* Due to bitcoin-core wallet not fully supporting segwit txes yet, our current scheme is:
* utxos <- parent-tx <- funding-tx
*
* With:
* - utxos may be non-segwit
* - parent-tx pays to a p2wpkh segwit output
* - funding-tx is a segwit tx
*
* Created by PM on 06/07/2017.
*/
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
override def getBalance: Future[Satoshi] = ???
import BitcoinCoreWallet._
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, lockUnspents: Boolean): Future[FundTransactionResponse] = {
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(lockUnspents)).map(json => {
def fundTransaction(hex: String, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(changeAddress, lockUnspents)).map(json => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDouble(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), (fee * 10e8).toLong)
})
}
def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] =
fundTransaction(Transaction.write(tx).toString(), lockUnspents)
def fundTransaction(tx: Transaction, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toString(), changeAddress, lockUnspents)
def signTransaction(hex: String): Future[SignTransactionResponse] =
rpcClient.invoke("signrawtransaction", hex).map(json => {
@ -58,157 +35,66 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(impl
SignTransactionResponse(Transaction.read(hex), complete)
})
def signTransaction(tx: Transaction): Future[SignTransactionResponse] =
signTransaction(Transaction.write(tx).toString())
def signTransaction(tx: Transaction): Future[SignTransactionResponse] = signTransaction(Transaction.write(tx).toString())
def getTransaction(txid: BinaryData): Future[Transaction] = {
rpcClient.invoke("getrawtransaction", txid.toString()).map(json => {
val JString(hex) = json
Transaction.read(hex)
})
}
def getTransaction(txid: BinaryData): Future[Transaction] = rpcClient.invoke("getrawtransaction", txid.toString()) collect { case JString(hex) => Transaction.read(hex) }
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] =
publishTransaction(Transaction.write(tx).toString())
def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[String] = publishTransaction(Transaction.write(tx).toString())
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] =
rpcClient.invoke("sendrawtransaction", hex) collect {
case JString(txid) => txid
}
def publishTransaction(hex: String)(implicit ec: ExecutionContext): Future[String] = rpcClient.invoke("sendrawtransaction", hex) collect { case JString(txid) => txid }
/**
*
* @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)
def unlockOutpoint(outPoints: List[OutPoint])(implicit ec: ExecutionContext): Future[Boolean] = rpcClient.invoke("lockunspent", true, outPoints.map(outPoint => Utxo(outPoint.txid.toString, outPoint.index))) collect { case JBool(result) => result }
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)
}
override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDouble(balance) => Satoshi((balance * 10e8).toLong) }
/**
*
* @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)
override def getFinalAddress: Future[String] = for {
JString(address) <- rpcClient.invoke("getnewaddress")
// we want bitcoind to only use segwit addresses to avoid malleability issues
JString(segwitAddress) <- rpcClient.invoke("addwitnessaddress", address)
} yield segwitAddress
// 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] =
override def makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
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, lockUnspents = true)
// 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
// we create a new segwit change address (we don't want bitcoin core to use regular malleable outputs)
JString(changeAddress) <- rpcClient.invoke("getnewaddress")
JString(segwitChangeAddress) <- rpcClient.invoke("addwitnessaddress", changeAddress)
_ = logger.debug(s"using segwitChangeAddress=$segwitChangeAddress")
// partial 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)
input0 = parentTx.txIn.head
parentOfParentTx <- getTransaction(input0.outPoint.txid)
_ = logger.debug(s"built parentTxid=${parentTx.txid}, initializing temporary actor")
tempActor = system.actorOf(Props(new Actor {
override def receive: Receive = {
case WatchEventSpent(BITCOIN_OUTPUT_SPENT, 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, spendingTx.txOut(0).publicKeyScript, 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
_ = watcher ! WatchSpent(tempActor, input0.outPoint.txid, input0.outPoint.index.toInt, parentOfParentTx.txOut(input0.outPoint.index.toInt).publicKeyScript, BITCOIN_OUTPUT_SPENT)
// 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 ask bitcoin core to add inputs to the funding tx, and use the specified change address
FundTransactionResponse(unsignedFundingTx, changepos, fee) <- fundTransaction(partialFundingTx, segwitChangeAddress, lockUnspents = true)
// now let's sign the funding tx
SignTransactionResponse(fundingTx, _) <- signTransaction(unsignedFundingTx)
// there will probably be a change output, so we need to find which output is ours
outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript)
_ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee")
} yield MakeFundingTxResponse(fundingTx, outputIndex)
override def commit(tx: Transaction): Future[Boolean] = publishTransaction(tx)
.map(_ => true) // if bitcoind says OK, then we consider the tx succesfully published
.recoverWith { case JsonRPCError(_) => getTransaction(tx.txid).map(_ => true).recover { case _ => false } } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
.recoverWith { case JsonRPCError(e) =>
logger.warn(s"txid=${tx.txid} error=$e")
getTransaction(tx.txid).map(_ => true).recover { case _ => false } // if we get a parseable error from bitcoind AND the tx is NOT in the mempool/blockchain, then we consider that the tx was not published
}
.recover { case _ => true } // in all other cases we consider that the tx has been published
override def rollback(tx: Transaction): Future[Boolean] = unlockOutpoint(tx.txIn.map(_.outPoint).toList) // we unlock all utxos used by the tx
/**
* We currently only put a lock on the parent tx inputs, and we publish the parent tx immediately so there is nothing
* to do here.
*
* @param tx
* @return
*/
override def rollback(tx: Transaction): Future[Boolean] = Future.successful(true)
}
object BitcoinCoreWallet {
case class Options(lockUnspents: Boolean)
// @formatter:off
case class Options(changeAddress: String, lockUnspents: Boolean)
case class Utxo(txid: String, vout: Long)
case class FundTransactionResponse(tx: Transaction, changepos: Int, feeSatoshis: Long)
case class SignTransactionResponse(tx: Transaction, complete: Boolean)
// @formatter:on
}

View file

@ -0,0 +1,137 @@
package fr.acinq.eclair.blockchain.bitcoind
import java.io.File
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 com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.{MilliBtc, Satoshi, Script, Transaction}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient
import fr.acinq.eclair.randomKey
import fr.acinq.eclair.transactions.Scripts
import grizzled.slf4j.Logging
import org.bitcoinj.script.{Script => BitcoinjScript}
import org.json4s.JsonAST.JValue
import org.json4s.{DefaultFormats, JString}
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.sys.process.{Process, _}
@RunWith(classOf[JUnitRunner])
class BitcoinCoreWalletSpec 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[BitcoinCoreWalletSpec].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](30 seconds)
}
test("create/commit/rollback funding txes") {
import collection.JavaConversions._
val commonConfig = ConfigFactory.parseMap(Map("eclair.chain" -> "regtest", "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))
val config = ConfigFactory.load(commonConfig).getConfig("eclair")
val bitcoinClient = new BitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport"))
val wallet = new BitcoinCoreWallet(bitcoinClient)
val sender = TestProbe()
wallet.getBalance.pipeTo(sender.ref)
assert(sender.expectMsgType[Satoshi] > Satoshi(0))
wallet.getFinalAddress.pipeTo(sender.ref)
assert(sender.expectMsgType[String].startsWith("2"))
val fundingTxes = for (i <- 0 to 3) yield {
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref)
val MakeFundingTxResponse(fundingTx, _) = sender.expectMsgType[MakeFundingTxResponse]
fundingTx
}
sender.send(bitcoincli, BitcoinReq("listlockunspent"))
assert(sender.expectMsgType[JValue](10 seconds).children.size === 4)
wallet.commit(fundingTxes(0)).pipeTo(sender.ref)
assert(sender.expectMsgType[Boolean])
wallet.rollback(fundingTxes(1)).pipeTo(sender.ref)
assert(sender.expectMsgType[Boolean])
wallet.commit(fundingTxes(2)).pipeTo(sender.ref)
assert(sender.expectMsgType[Boolean])
wallet.rollback(fundingTxes(3)).pipeTo(sender.ref)
assert(sender.expectMsgType[Boolean])
sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(0).txid.toString()))
assert(sender.expectMsgType[JString](10 seconds).s === Transaction.write(fundingTxes(0)).toString())
sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString()))
assert(sender.expectMsgType[JString](10 seconds).s === Transaction.write(fundingTxes(2)).toString())
// NB: bitcoin core doesn't clear the locks when a tx is published
sender.send(bitcoincli, BitcoinReq("listlockunspent"))
assert(sender.expectMsgType[JValue](10 seconds).children.size === 2)
}
}

View file

@ -18,6 +18,7 @@ import grizzled.slf4j.Logging
import org.bitcoinj.script.{Script => BitcoinjScript}
import org.json4s.DefaultFormats
import org.json4s.JsonAST.JValue
import org.junit.Ignore
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
@ -28,6 +29,7 @@ import scala.concurrent.{Await, Future}
import scala.sys.process.{Process, _}
import scala.util.Random
@Ignore
@RunWith(classOf[JUnitRunner])
class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
@ -84,7 +86,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
sender.expectMsgType[JValue](30 seconds)
}
ignore("bitcoinj wallet commit") {
test("bitcoinj wallet commit") {
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
bitcoinjKit.startAsync()
@ -133,7 +135,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
wallet.maybeCommitTx(tx2) // returns true! how come?
}*/
ignore("manual publish/watch") {
test("manual publish/watch") {
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
bitcoinjKit.startAsync()
@ -166,7 +168,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
}
ignore("multiple publish/watch") {
test("multiple publish/watch") {
val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-bitcoinj")
val bitcoinjKit = new BitcoinjKit("regtest", datadir, staticPeers = new InetSocketAddress("localhost", 28333) :: Nil)
bitcoinjKit.startAsync()

View file

@ -9,7 +9,7 @@ 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_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
import fr.acinq.eclair.channel.Register.Forward
@ -131,31 +131,25 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
}
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}")
// funder transitions
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_ACCEPT_CHANNEL)
assert(eventListener1.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_INTERNAL)
// fundee transitions
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_OPEN_CHANNEL)
assert(eventListener2.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CREATED)
}
test("connect nodes") {
//
// A ---- B ---- C ---- D
// | / \
// --E--' F{1,2,3,4}
// --E--' F{1,2,3,4,5}
//
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)
@ -167,51 +161,19 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
connect(nodes("C"), nodes("F4"), 5000000, 0)
connect(nodes("C"), nodes("F5"), 5000000, 0)
val sender = TestProbe()
val eventListener = TestProbe()
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
val numberOfChannels = 10
val channelEndpointsCount = 2 * numberOfChannels
// 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
}
// each funder sets up a WatchConfirmed on the parent tx, we need to make sure it has been received by the watcher
var watches1 = Set.empty[Watch]
// we make sure all channels have set up their WatchConfirmed for the funding tx
awaitCond({
watches1 = nodes.values.foldLeft(Set.empty[Watch]) {
val watches = nodes.values.foldLeft(Set.empty[Watch]) {
case (watches, setup) =>
sender.send(setup.watcher, 'watches)
watches ++ sender.expectMsgType[Set[Watch]]
}
watches1.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount / 2
watches.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
}, max = 10 seconds, interval = 1 second)
// confirming the parent tx of the funding
sender.send(bitcoincli, BitcoinReq("generate", 1))
sender.expectMsgType[JValue](10 seconds)
within(30 seconds) {
var count = 0
while (count < channelEndpointsCount) {
if (eventListener.expectMsgType[ChannelStateChanged](10 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED) count = count + 1
}
}
// we make sure all channels have set up their WatchConfirmed for the funding tx
awaitCond({
val watches2 = nodes.values.foldLeft(Set.empty[Watch]) {
case (watches, setup) =>
sender.send(setup.watcher, 'watches)
watches ++ sender.expectMsgType[Set[Watch]]
}
(watches2 -- watches1).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)
@ -371,6 +333,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
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(scriptHash, _) :: OP_EQUAL :: Nil =>
Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash)
case _ => ???
}

View file

@ -66,11 +66,10 @@ object AnnouncementsBatchValidationSpec {
val node2BitcoinKey = randomKey
val amount = Satoshi(1000000)
// first we publish the funding tx
val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient, null)
val wallet = new BitcoinCoreWallet(extendedBitcoinClient.rpcClient)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey)))
val fundingTxFuture = wallet.makeParentAndFundingTx(fundingPubkeyScript, amount, 10000)
val fundingTxFuture = wallet.makeFundingTx(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)
}