Compare commits

..

1 Commits

Author SHA1 Message Date
pm47
00894bae64 moved akka conf to eclair-node application.conf 2017-11-24 16:15:12 +01:00
31 changed files with 336 additions and 721 deletions

View File

@ -1,3 +0,0 @@
Dockerfile
.dockerignore
target/

View File

@ -1,65 +0,0 @@
FROM openjdk:8u121-jdk-alpine as BUILD
# Setup maven, we don't use https://hub.docker.com/_/maven/ as it declare .m2 as volume, we loose all mvn cache
# We can alternatively do as proposed by https://github.com/carlossg/docker-maven#packaging-a-local-repository-with-the-image
# this was meant to make the image smaller, but we use multi-stage build so we don't care
# We can remove dependency on git by setting <failOnNoGitDirectory>false</failOnNoGitDirectory> is set on git-commit-id-plugin
RUN apk add --no-cache curl tar bash git
ARG MAVEN_VERSION=3.5.2
ARG USER_HOME_DIR="/root"
ARG SHA=707b1f6e390a65bde4af4cdaf2a24d45fc19a6ded00fff02e91626e3e42ceaff
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
# Let's fetch eclair dependencies by copying and running poms without sources first.
# Docker will not have to fetch dependencies again if only the source code change
WORKDIR /usr/src
COPY pom.xml pom.xml
COPY eclair-core/pom.xml eclair-core/pom.xml
COPY eclair-node/pom.xml eclair-node/pom.xml
COPY eclair-node-gui/pom.xml eclair-node-gui/pom.xml
# We can remove dependency on git by setting <failOnNoGitDirectory>false</failOnNoGitDirectory> is set on git-commit-id-plugin
RUN git init && \
git config user.email "you@example.com" && \
git config user.name "Your Name" && \
git commit --allow-empty-message -m "" --allow-empty && \
# Get offline dependencies
mvn dependency:go-offline --fail-never && \
# For some reasons mvn miss dependencies which can be downloaded automatically if there is something to build
mkdir eclair-core/src && mkdir eclair-core/src/main && mkdir eclair-core/src/main/scala && touch eclair-core/src/main/scala/dummy.scala && \
mvn install -DskipTests -f eclair-core/pom.xml --fail-never && \
rm -rf eclair-core/src && mvn clean && \
# Same stuff with eclair-node
mkdir eclair-node/src && mkdir eclair-node/src/main && mkdir eclair-node/src/main/scala && touch eclair-node/src/main/scala/dummy.scala && \
mvn install -DskipTests -f eclair-node/pom.xml --fail-never && \
rm -rf eclair-node/src && mvn clean && \
# For some reasons, this fetch more dependencies
mvn package -pl eclair-node -am -DskipTests && mvn clean && \
rm -rf .git
# Phew, we have all the dependencies in local now. We can now copy sources and build offline
COPY . .
RUN mvn package -pl eclair-node -am -DskipTests -o
# It might be good idea to run the tests here, so that the docker build fail if the code is bugged
# Remove artifact unecessary to run
RUN cd eclair-node/target && ls -1 | grep -v 'node-' | xargs rm -rf
# We are only interested into the target for running eclair
FROM openjdk:8u151-jre-slim
WORKDIR /app
COPY --from=BUILD /usr/src/eclair-node/target .
RUN ln `ls` eclair-node
ENTRYPOINT [ "java", "-jar", "eclair-node" ]

View File

@ -44,12 +44,6 @@ zmqpubrawblock=tcp://127.0.0.1:29000
zmqpubrawtx=tcp://127.0.0.1:29000
```
On **__testnet__**, you also need to make sure that all your UTXOs are `p2sh-of-p2wpkh`.
To do this, use the debug console, create a new address with `getnewaddress`, import it as a witness address with `addwitnessaddress`, and
send all your balance to this witness address.
If you need to create and send funds manually, don't forget to create and specify a witness address for the change output (this option is avaliable on the GUI once you set the `Enable coin control features` wallet option).
### Installing Eclair
The released binaries can be downloaded [here](https://github.com/ACINQ/eclair/releases).

View File

@ -28,16 +28,13 @@ case $1 in
eval curl "$CURL_OPTS -d '{ \"method\": \"close\", \"params\" : [\"${2?"missing channel id"}\"] }' $URL"
;;
"receive")
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"${3?"missing description"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
eval curl "$CURL_OPTS -d '{ \"method\": \"receive\", \"params\" : [${2?"missing amount"}, \"something\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"send")
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
;;
"allnodes")
eval curl "$CURL_OPTS -d '{ \"method\": \"allnodes\", \"params\" : [] }' $URL" | jq ".result"
;;
"allchannels")
eval curl "$CURL_OPTS -d '{ \"method\": \"allchannels\", \"params\" : [] }' $URL" | jq ".result"
"network")
eval curl "$CURL_OPTS -d '{ \"method\": \"network\", \"params\" : [] }' $URL" | jq ".result"
;;
"peers")
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"

View File

@ -48,7 +48,7 @@ eclair {
global-features = ""
local-features = "08" // initial_routing_sync
channel-flags = 1 // announce channels
dust-limit-satoshis = 546
dust-limit-satoshis = 542
default-feerate-per-kb = 20000 // default bitcoin core value
max-htlc-value-in-flight-msat = 100000000000 // 1 BTC ~= unlimited
@ -81,21 +81,4 @@ eclair {
auto-reconnect = true
payment-handler = "local"
}
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
http {
host-connection-pool {
max-open-requests = 64
}
}
}

View File

@ -11,7 +11,6 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey
import fr.acinq.bitcoin.{BinaryData, Block, DeterministicWallet}
import fr.acinq.eclair.NodeParams.WatcherType
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.db._
import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqliteNetworkDb, SqlitePeersDb, SqlitePreimagesDb}
@ -115,12 +114,6 @@ object NodeParams {
case _ => BITCOIND
}
val dustLimitSatoshis = config.getLong("dust-limit-satoshis")
require(dustLimitSatoshis >= Channel.MIN_DUSTLIMIT, s"dust limit must be greater than ${Channel.MIN_DUSTLIMIT}")
val maxAcceptedHtlcs = config.getInt("max-accepted-htlcs")
require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}")
NodeParams(
extendedPrivateKey = extendedPrivateKey,
privateKey = extendedPrivateKey.privateKey,
@ -129,9 +122,9 @@ object NodeParams {
publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(ip, config.getInt("server.port"))),
globalFeatures = BinaryData(config.getString("global-features")),
localFeatures = BinaryData(config.getString("local-features")),
dustLimitSatoshis = dustLimitSatoshis,
dustLimitSatoshis = config.getLong("dust-limit-satoshis"),
maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")),
maxAcceptedHtlcs = maxAcceptedHtlcs,
maxAcceptedHtlcs = config.getInt("max-accepted-htlcs"),
expiryDeltaBlocks = config.getInt("expiry-delta-blocks"),
htlcMinimumMsat = config.getInt("htlc-minimum-msat"),
delayBlocks = config.getInt("delay-blocks"),

View File

@ -70,17 +70,12 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
progress = (json \ "verificationprogress").extract[Double]
chainHash <- bitcoinClient.rpcClient.invoke("getblockhash", 0).map(_.extract[String]).map(BinaryData(_)).map(x => BinaryData(x.reverse))
bitcoinVersion <- bitcoinClient.rpcClient.invoke("getnetworkinfo").map(json => (json \ "version")).map(_.extract[String])
unspentAddresses <- bitcoinClient.listUnspentAddresses
} yield (progress, chainHash, bitcoinVersion, unspentAddresses)
} yield (progress, chainHash, bitcoinVersion)
// blocking sanity checks
val (progress, chainHash, bitcoinVersion, unspentAddresses) = Await.result(future, 10 seconds)
val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds)
assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)")
if (chainHash == Block.TestnetGenesisBlock.hash) {
assert(unspentAddresses.forall(isSegwitAddress), "In testnet mode, make sure that all your UTXOs are p2sh-of-p2wpkh (check out our README for more details)")
}
assert(progress > 0.99, "bitcoind should be synchronized")
// TODO: add a check on bitcoin version?
Bitcoind(bitcoinClient)
case BITCOINJ =>
logger.warn("EXPERIMENTAL BITCOINJ MODE ENABLED!!!")
@ -112,8 +107,8 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
logger.info(s"initial feeratesPerByte=${Globals.feeratesPerByte.get()}")
val feeProvider = (chain, bitcoin) match {
case ("regtest", _) => new ConstantFeeProvider(defaultFeerates)
case (_, Bitcoind(client)) => new FallbackFeeProvider(new BitgoFeeProvider() :: new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
case _ => new FallbackFeeProvider(new BitgoFeeProvider() :: new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
case (_, Bitcoind(client)) => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
case _ => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
}
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
case feerates: FeeratesPerByte =>
@ -136,7 +131,7 @@ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), act
}
val wallet = bitcoin match {
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient)
case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient.rpcClient, watcher)
case Bitcoinj(bitcoinj) => new BitcoinjWallet(bitcoinj.initialized.map(_ => bitcoinj.wallet()))
case Electrum(electrumClient) =>
val electrumSeedPath = new File(datadir, "electrum_seed.dat")

View File

@ -23,7 +23,7 @@ final case class WatchConfirmed(channel: ActorRef, txId: BinaryData, publicKeySc
object WatchConfirmed {
// if we have the entire transaction, we can get the redeemScript from the witness, and re-compute the publicKeyScript
// we support both p2pkh and p2wpkh scripts
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, tx.txOut.map(_.publicKeyScript).headOption.getOrElse(""), minDepth, event)
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, extractPublicKeyScript(tx.txIn.head.witness), minDepth, event)
def extractPublicKeyScript(witness: ScriptWitness): BinaryData = Try(PublicKey(witness.stack.last)) match {
case Success(pubKey) =>

View File

@ -1,32 +1,55 @@
package fr.acinq.eclair.blockchain.bitcoind
import akka.actor.ActorSystem
import fr.acinq.bitcoin.{BinaryData, OutPoint, Satoshi, Transaction, TxIn, TxOut}
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 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._
import org.json4s.JsonAST.{JBool, JDouble, JInt, JString}
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
/**
* 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)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient, watcher: ActorRef)(implicit system: ActorSystem, ec: ExecutionContext) extends EclairWallet with Logging {
import BitcoinCoreWallet._
override def getBalance: Future[Satoshi] = ???
def fundTransaction(hex: String, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = {
rpcClient.invoke("fundrawtransaction", hex, BitcoinCoreWallet.Options(changeAddress, lockUnspents)).map(json => {
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 => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDouble(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), (fee * 10e8).toLong)
FundTransactionResponse(Transaction.read(hex), changepos.intValue(), fee)
})
}
def fundTransaction(tx: Transaction, changeAddress: String, lockUnspents: Boolean): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toString(), changeAddress, lockUnspents)
def fundTransaction(tx: Transaction, lockUnspents: Boolean): Future[FundTransactionResponse] =
fundTransaction(Transaction.write(tx).toString(), lockUnspents)
def signTransaction(hex: String): Future[SignTransactionResponse] =
rpcClient.invoke("signrawtransaction", hex).map(json => {
@ -35,66 +58,157 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit system: ActorS
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()) collect { case JString(hex) => Transaction.read(hex) }
def getTransaction(txid: BinaryData): Future[Transaction] = {
rpcClient.invoke("getrawtransaction", txid.toString()).map(json => {
val JString(hex) = json
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
}
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 }
/**
*
* @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)
override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDouble(balance) => Satoshi((balance * 10e8).toLong) }
Transaction.correctlySpends(fundingTx1, fundingTxResponse.parentTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
fundingTxResponse.copy(fundingTx = fundingTx1)
}
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
/**
*
* @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 makeFundingTx(pubkeyScript: BinaryData, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] =
// 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 {
// 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
// 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
partialFundingTx = Transaction(
version = 2,
txIn = Seq.empty[TxIn],
txOut = TxOut(amount, pubkeyScript) :: Nil,
lockTime = 0)
// 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)
// 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
}
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(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
}
.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
.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 {
// @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
case class Options(lockUnspents: Boolean)
}

View File

@ -173,18 +173,23 @@ class ExtendedBitcoinClient(val rpcClient: BitcoinJsonRPCClient) {
})
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)))
}
/**
*
* @return the list of bitcoin addresses for which the wallet has UTXOs
*/
def listUnspentAddresses: Future[Seq[String]] = {
import ExecutionContext.Implicits.global
implicit val formats = org.json4s.DefaultFormats
rpcClient.invoke("listunspent").collect {
case JArray(values) => values.map(value => (value \ "address").extract[String])
}
}
}
/*object Test extends App {
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
implicit val system = ActorSystem()
implicit val timeout = Timeout(30 seconds)
val bitcoin_client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = "foo",
password = "bar",
host = "localhost",
port = 28332))
println(Await.result(bitcoin_client.getTxBlockHash("dcb0abfa822402ce379fedd7bbbb2c824e53ef300313594c39282da1efd35f17"), 10 seconds))
}*/

View File

@ -128,16 +128,9 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, items), data) =>
log.debug(s"scriptHash=$scriptHash has history=$items")
val shadow_items = data.history.get(scriptHash) match {
case Some(existing_items) => existing_items.filterNot(item => items.exists(_.tx_hash == item.tx_hash))
case None => Nil
}
shadow_items.foreach(item => log.warning(s"keeping shadow item for txid=${item.tx_hash}"))
val items0 = items ++ shadow_items
val (heights1, pendingTransactionRequests1) = items0.foldLeft((data.heights, data.pendingTransactionRequests)) {
case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, history), data) =>
log.debug(s"scriptHash=$scriptHash has history=$history")
val (heights1, pendingTransactionRequests1) = history.foldLeft((data.heights, data.pendingTransactionRequests)) {
case ((heights, hashes), item) if !data.transactions.contains(item.tx_hash) && !data.pendingTransactionRequests.contains(item.tx_hash) =>
// we retrieve the tx if we don't have it and haven't yet requested it
client ! GetTransaction(item.tx_hash)
@ -164,7 +157,7 @@ class ElectrumWallet(mnemonics: Seq[String], client: ActorRef, params: ElectrumW
// no reorg, nothing to do
}
}
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> items0), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
val data1 = data.copy(heights = heights1, history = data.history + (scriptHash -> history), pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, pendingTransactionRequests = pendingTransactionRequests1)
goto(stateName) using data1 // goto instead of stay because we want to fire transitions
case Event(GetTransactionResponse(tx), data) =>
@ -417,9 +410,6 @@ object ElectrumWallet {
* @param heights transactions heights
* @param history script hash -> history
* @param locks transactions which lock some of our utxos.
* @param pendingHistoryRequests requests pending a response from the electrum server
* @param pendingTransactionRequests requests pending a response from the electrum server
* @param pendingTransactions transactions received but not yet connected to their parents
*/
case class Data(tip: ElectrumClient.Header,
accountKeys: Vector[ExtendedPrivateKey],

View File

@ -1,58 +0,0 @@
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 de.heikoseeberger.akkahttpjson4s.Json4sSupport._
import org.json4s.JsonAST.{JArray, JInt, JValue}
import org.json4s.{DefaultFormats, jackson}
import scala.concurrent.{ExecutionContext, Future}
class BitgoFeeProvider(implicit system: ActorSystem, ec: ExecutionContext) extends FeeProvider {
import BitgoFeeProvider._
implicit val materializer = ActorMaterializer()
val httpClient = Http(system)
implicit val serialization = jackson.Serialization
implicit val formats = DefaultFormats
override def getFeerates: Future[FeeratesPerByte] =
for {
httpRes <- httpClient.singleRequest(HttpRequest(uri = Uri("https://www.bitgo.com/api/v1/tx/fee"), method = HttpMethods.GET))
json <- Unmarshal(httpRes).to[JValue]
feeRanges = parseFeeRanges(json)
} yield extractFeerates(feeRanges)
}
object BitgoFeeProvider {
case class BlockTarget(block: Int, fee: Long)
def parseFeeRanges(json: JValue): Seq[BlockTarget] = {
val blockTargets = json \ "feeByBlockTarget"
blockTargets.foldField(Seq.empty[BlockTarget]) {
case (list, (strBlockTarget, JInt(feePerKb))) => list :+ BlockTarget(strBlockTarget.toInt, feePerKb.longValue() / 1024)
}
}
def extractFeerate(feeRanges: Seq[BlockTarget], maxBlockDelay: Int): Long = {
// first we keep only fee ranges with a max block delay below the limit
val belowLimit = feeRanges.filter(_.block <= maxBlockDelay)
// out of all the remaining fee ranges, we select the one with the minimum higher bound
belowLimit.map(_.fee).min
}
def extractFeerates(feeRanges: Seq[BlockTarget]): FeeratesPerByte =
FeeratesPerByte(
block_1 = extractFeerate(feeRanges, 1),
blocks_2 = extractFeerate(feeRanges, 2),
blocks_6 = extractFeerate(feeRanges, 6),
blocks_12 = extractFeerate(feeRanges, 12),
blocks_36 = extractFeerate(feeRanges, 36),
blocks_72 = extractFeerate(feeRanges, 72))
}

View File

@ -31,14 +31,6 @@ object Channel {
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements
val ANNOUNCEMENTS_MINCONF = 6
// https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
val MAX_FUNDING_SATOSHIS = 16777216 // = 2^24
val MIN_FUNDING_SATOSHIS = 1000
val MAX_ACCEPTED_HTLCS = 483
// we don't want the counterparty to use a dust limit lower than that, because they wouldn't only hurt themselves we may need them to publish their commit tx in certain cases (backup/restore)
val MIN_DUSTLIMIT = 546
case object TickRefreshChannelUpdate
}
@ -143,7 +135,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
d match {
case DATA_NORMAL(_, Some(shortChannelId), _, _, _) =>
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId))
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
relayer ! channelUpdate
case _ => ()
}
@ -153,7 +145,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions {
case Event(open: OpenChannel, DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(temporaryChannelId, localParams, _, remoteInit))) =>
Try(Helpers.validateParamsFundee(nodeParams, open)) match {
Try(Helpers.validateParamsFundee(temporaryChannelId, nodeParams, open.channelReserveSatoshis, open.fundingSatoshis, open.chainHash, open.feeratePerKw)) match {
case Failure(t) =>
log.warning(t.getMessage)
val error = Error(open.temporaryChannelId, t.getMessage.getBytes)
@ -205,7 +197,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
case Event(accept: AcceptChannel, DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, localParams, _, remoteInit, _), open)) =>
Try(Helpers.validateParamsFunder(nodeParams, open, accept)) match {
Try(Helpers.validateParamsFunder(temporaryChannelId, nodeParams, accept.channelReserveSatoshis, fundingSatoshis)) match {
case Failure(t) =>
log.warning(t.getMessage)
val error = Error(temporaryChannelId, t.getMessage.getBytes)

View File

@ -10,11 +10,6 @@ import fr.acinq.eclair.UInt64
class ChannelException(channelId: BinaryData, message: String) extends RuntimeException(message)
// @formatter:off
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
case class InvalidChainHash (channelId: BinaryData, local: BinaryData, remote: BinaryData) extends ChannelException(channelId, s"invalid chain_hash (local=$local remote=$remote)")
case class InvalidFundingAmount (channelId: BinaryData, fundingSatoshis: Long, min: Long, max: Long) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingSatoshis (min=$min max=$max)")
case class InvalidPushAmount (channelId: BinaryData, pushMsat: Long, max: Long) extends ChannelException(channelId, s"invalid push_msat=$pushMsat (max=$max)")
case class InvalidMaxAcceptedHtlcs (channelId: BinaryData, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
case class InvalidDustLimit (channelId: BinaryData, dustLimitSatoshis: Long, min: Long) extends ChannelException(channelId, s"invalid dust_limit_satoshis=$dustLimitSatoshis (min=$min)")
case class ChannelReserveTooHigh (channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
case class ClosingInProgress (channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
case class ClosingAlreadyInProgress (channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")

View File

@ -9,7 +9,7 @@ import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, UpdateAddHtlc, UpdateFulfillHtlc}
import fr.acinq.eclair.{Globals, NodeParams}
import grizzled.slf4j.Logging
@ -38,27 +38,21 @@ object Helpers {
case d: HasCommitments => d.channelId
}
/**
* Called by the fundee
*/
def validateParamsFundee(nodeParams: NodeParams, open: OpenChannel): Unit = {
if (nodeParams.chainHash != open.chainHash) throw new InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)
if (open.fundingSatoshis < Channel.MIN_FUNDING_SATOSHIS || open.fundingSatoshis >= Channel.MAX_FUNDING_SATOSHIS) throw new InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS)
if (open.pushMsat > 1000 * open.fundingSatoshis) throw new InvalidPushAmount(open.temporaryChannelId, open.pushMsat, 1000 * open.fundingSatoshis)
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
if (isFeeDiffTooHigh(open.feeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) throw new FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.feeratePerKw)
val reserveToFundingRatio = open.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1)
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw new ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
def validateParamsFunder(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long): Unit = {
val reserveToFundingRatio = channelReserveSatoshis.toDouble / fundingSatoshis
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) {
throw new ChannelReserveTooHigh(temporaryChannelId, channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
}
}
/**
* Called by the funder
*/
def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Unit = {
if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) throw new InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)
if (accept.dustLimitSatoshis < Channel.MIN_DUSTLIMIT) throw new InvalidDustLimit(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUSTLIMIT)
val reserveToFundingRatio = accept.channelReserveSatoshis.toDouble / Math.max(open.fundingSatoshis, 1)
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) throw new ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)
def validateParamsFundee(temporaryChannelId: BinaryData, nodeParams: NodeParams, channelReserveSatoshis: Long, fundingSatoshis: Long, chainHash: BinaryData, initialFeeratePerKw: Long): Unit = {
require(nodeParams.chainHash == chainHash, s"invalid chain hash $chainHash (we are on ${nodeParams.chainHash})")
val localFeeratePerKw = Globals.feeratesPerKw.get.block_1
// we are fundee => initialFeeratePerKw has been set by remote
if (isFeeDiffTooHigh(initialFeeratePerKw, localFeeratePerKw, nodeParams.maxFeerateMismatch)) {
throw new FeerateTooDifferent(temporaryChannelId, localFeeratePerKw, initialFeeratePerKw)
}
validateParamsFunder(temporaryChannelId, nodeParams, channelReserveSatoshis, fundingSatoshis)
}
/**

View File

@ -7,7 +7,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.channel.{Channel, HasCommitments}
import fr.acinq.eclair.channel.HasCommitments
import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted
import fr.acinq.eclair.router.Rebroadcast
@ -101,10 +101,7 @@ object Switchboard {
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]) {
require(fundingSatoshis.amount < Channel.MAX_FUNDING_SATOSHIS, s"fundingSatoshis must be less than ${Channel.MAX_FUNDING_SATOSHIS}")
require(pushMsat.amount <= 1000 * fundingSatoshis.amount, s"pushMsat must be less or equal to fundingSatoshis")
}
case class NewChannel(fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelFlags: Option[Byte])
case class NewConnection(remoteNodeId: PublicKey, address: InetSocketAddress, newChannel_opt: Option[NewChannel])
// @formatter:on

View File

@ -64,12 +64,4 @@ package object eclair {
def feerateByte2Kw(feeratePerByte: Long): Long = feeratePerByte * 1024 / 4
/**
*
* @param address bitcoin Base58 address
* @return true if the address is a segwit address i.e. a p2sh-of-p2wpkh address.
* We approximate this be returning true if the address is a p2sh address, there is no
* way to tell what the script is.
*/
def isSegwitAddress(address: String) : Boolean = address.startsWith("2") || address.startsWith("3")
}

View File

@ -279,10 +279,6 @@ object PaymentRequest {
}
}
case class UnknownTag(tag: Int5, int5s: Seq[Int5]) extends Tag {
override def toInt5s = tag +: (writeSize(int5s.size) ++ int5s)
}
object Amount {
/**
@ -317,7 +313,7 @@ object PaymentRequest {
}
object Tag {
def parse(input: Seq[Int5]): Tag = {
def parse(input: Seq[Byte]): Tag = {
val tag = input(0)
val len = input(1) * 32 + input(2)
tag match {
@ -349,8 +345,6 @@ object PaymentRequest {
case c if c == Bech32.map('c') =>
val expiry = readUnsignedLong(len, input.drop(3).take(len))
MinFinalCltvExpiryTag(expiry)
case _ =>
UnknownTag(tag, input.drop(3).take(len))
}
}
}

View File

@ -1,104 +1,47 @@
package fr.acinq.eclair.blockchain
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 akka.actor.ActorSystem
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin._
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.eclair.Kit
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.integration.IntegrationSpec
import grizzled.slf4j.Logging
import org.json4s.DefaultFormats
import org.json4s.JsonAST.JValue
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, FunSuite, FunSuiteLike}
import org.scalatest.FunSuite
import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext}
import ExecutionContext.Implicits.global
import scala.sys.process._
@RunWith(classOf[JUnitRunner])
class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
// this test is not run automatically
class ExtendedBitcoinClientSpec extends FunSuite {
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
implicit lazy val system = ActorSystem()
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")
val config = ConfigFactory.load()
val client = new ExtendedBitcoinClient(new BitcoinJsonRPCClient(
user = config.getString("eclair.bitcoind.rpcuser"),
password = config.getString("eclair.bitcoind.rpcpassword"),
host = config.getString("eclair.bitcoind.host"),
port = 18332))
var bitcoind: Process = null
var bitcoinrpcclient: BitcoinJsonRPCClient = null
var bitcoincli: ActorRef = null
var client: ExtendedBitcoinClient = _
implicit val formats = org.json4s.DefaultFormats
implicit val ec = ExecutionContext.Implicits.global
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")
implicit val formats = DefaultFormats
case class BitcoinReq(method: String, params: Any*)
override def beforeAll(): Unit = {
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
Files.copy(classOf[IntegrationSpec].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
}
}))
client = new ExtendedBitcoinClient(bitcoinrpcclient)
test("get transaction short id") {
val txid = "7b2184f8539af648d51cc11d2a83630dd10fdf2a40a1824777d7f8da8e0d4b9e"
val conf = Await.result(client.getTxConfirmations(txid), 5 seconds)
val (height, index) = Await.result(client.getTransactionShortId(txid), 5 seconds)
assert(height == 150002)
assert(index == 7)
}
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.exitValue()
test("get transaction by short id") {
val tx = Await.result(client.getTransaction(150002, 7), 5 seconds)
assert(tx.txid.toString() == "7b2184f8539af648d51cc11d2a83630dd10fdf2a40a1824777d7f8da8e0d4b9e")
}
test("list unspent addresss") {
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)
val future = for {
count <- client.getBlockCount
_ = assert(count == 500)
unspentAddresses <- client.listUnspentAddresses
// coinbase txs need 100 confirmations to be spendable
_ = assert(unspentAddresses.length == 500 - 100)
// generate and import a new private key
priv = PrivateKey("01" * 32)
wif = Base58Check.encode(Base58.Prefix.SecretKeyTestnet, priv.toBin)
_ = sender.send(bitcoincli, BitcoinReq("importprivkey", wif))
_ = sender.expectMsgType[JValue](10 seconds)
// send money to our private key
address = Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, priv.publicKey.hash160)
_ = client.sendFromAccount("", address, 1.0)
_ = sender.send(bitcoincli, BitcoinReq("generate", 1))
_ = sender.expectMsgType[JValue](10 seconds)
// and check that we find a utxo four our private key
unspentAddresses1 <- client.listUnspentAddresses
_ = assert(unspentAddresses1 contains address)
} yield ()
Await.result(future, 10 seconds)
test("is tx output spendable") {
val result = Await.result(client.isTransactionOuputSpendable("48ebfd0c0fe043b76eee09fcd8ea1e9248ffe1553fa30040fb7f7112ba3a202f", 0, true), 5 seconds)
assert(result)
val result1 = Await.result(client.isTransactionOuputSpendable("48ebfd0c0fe043b76eee09fcd8ea1e9248ffe1553fa30040fb7f7112ba3a202f", 5, true), 5 seconds)
assert(!result1)
}
}
}

View File

@ -1,137 +0,0 @@
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.exitValue()
// 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,7 +18,6 @@ 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}
@ -29,7 +28,6 @@ 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 {
@ -86,7 +84,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
sender.expectMsgType[JValue](30 seconds)
}
test("bitcoinj wallet commit") {
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()
@ -135,7 +133,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
wallet.maybeCommitTx(tx2) // returns true! how come?
}*/
test("manual publish/watch") {
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()
@ -168,7 +166,7 @@ class BitcoinjSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with B
assert(event.event === BITCOIN_FUNDING_DEPTHOK)
}
test("multiple publish/watch") {
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()

View File

@ -1,64 +0,0 @@
package fr.acinq.eclair.blockchain.fee
import akka.actor.ActorSystem
import akka.util.Timeout
import org.json4s.DefaultFormats
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
import scala.concurrent.Await
/**
* Created by PM on 27/01/2017.
*/
@RunWith(classOf[JUnitRunner])
class BitgoFeeProviderSpec extends FunSuite {
import BitgoFeeProvider._
import org.json4s.jackson.JsonMethods.parse
implicit val formats = DefaultFormats
val sample_response =
"""
{"feePerKb":136797,"cpfpFeePerKb":136797,"numBlocks":2,"confidence":80,"multiplier":1,"feeByBlockTarget":{"1":149453,"2":136797,"5":122390,"6":105566,"8":100149,"9":96254,"10":122151,"13":116855,"15":110860,"17":87402,"27":82635,"33":71098,"42":105782,"49":68182,"73":59207,"97":17336,"121":16577,"193":13545,"313":12268,"529":11122,"553":9139,"577":5395,"793":5070}}
"""
test("parse test") {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
assert(feeRanges.size === 23)
}
test("extract fee for a particular block delay") {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
val fee = extractFeerate(feeRanges, 6)
assert(fee === 103)
}
test("extract all fees") {
val json = parse(sample_response)
val feeRanges = parseFeeRanges(json)
val feerates = extractFeerates(feeRanges)
val ref = FeeratesPerByte(
block_1 = 145,
blocks_2 = 133,
blocks_6 = 103,
blocks_12 = 93,
blocks_36 = 69,
blocks_72 = 66)
assert(feerates === ref)
}
test("make sure API hasn't changed") {
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
implicit val system = ActorSystem()
implicit val timeout = Timeout(30 seconds)
val provider = new BitgoFeeProvider()
Await.result(provider.getFeerates, 10 seconds)
}
}

View File

@ -42,30 +42,6 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp
}
}
test("recv AcceptChannel (invalid max accepted htlcs)") { case (alice, alice2bob, bob2alice, _) =>
within(30 seconds) {
val accept = bob2alice.expectMsgType[AcceptChannel]
// spec says max = 483
val invalidMaxAcceptedHtlcs = 484
alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs)
val error = alice2bob.expectMsgType[Error]
assert(error === Error(accept.temporaryChannelId, new InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage.getBytes("UTF-8")))
awaitCond(alice.stateName == CLOSED)
}
}
test("recv AcceptChannel (invalid dust limit)") { case (alice, alice2bob, bob2alice, _) =>
within(30 seconds) {
val accept = bob2alice.expectMsgType[AcceptChannel]
// we don't want their dust limit to be below 546
val lowDustLimitSatoshis = 545
alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis)
val error = alice2bob.expectMsgType[Error]
assert(error === Error(accept.temporaryChannelId, new InvalidDustLimit(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUSTLIMIT).getMessage.getBytes("UTF-8")))
awaitCond(alice.stateName == CLOSED)
}
}
test("recv AcceptChannel (reserve too high)") { case (alice, alice2bob, bob2alice, _) =>
within(30 seconds) {
val accept = bob2alice.expectMsgType[AcceptChannel]
@ -73,7 +49,7 @@ class WaitForAcceptChannelStateSpec extends TestkitBaseClass with StateTestsHelp
val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong
alice ! accept.copy(channelReserveSatoshis = reserveTooHigh)
val error = alice2bob.expectMsgType[Error]
assert(error === Error(accept.temporaryChannelId, new ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage.getBytes("UTF-8")))
assert(new String(error.data) === "channelReserveSatoshis too high: reserve=300000 fundingRatio=0.3 maxFundingRatio=0.05")
awaitCond(alice.stateName == CLOSED)
}
}

View File

@ -1,7 +1,6 @@
package fr.acinq.eclair.channel.states.a
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Block
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
@ -41,51 +40,6 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper
}
}
test("recv OpenChannel (invalid chain)") { case (bob, alice2bob, bob2alice, _) =>
within(30 seconds) {
val open = alice2bob.expectMsgType[OpenChannel]
// using livenet genesis block
val livenetChainHash = Block.LivenetGenesisBlock.hash
bob ! open.copy(chainHash = livenetChainHash)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage.getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}
test("recv OpenChannel (funding too low)") { case (bob, alice2bob, bob2alice, _) =>
within(30 seconds) {
val open = alice2bob.expectMsgType[OpenChannel]
val lowFundingMsat = 100
bob ! open.copy(fundingSatoshis = lowFundingMsat)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, lowFundingMsat, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}
test("recv OpenChannel (funding too high)") { case (bob, alice2bob, bob2alice, _) =>
within(30 seconds) {
val open = alice2bob.expectMsgType[OpenChannel]
val highFundingMsat = 100000000
bob ! open.copy(fundingSatoshis = highFundingMsat)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Channel.MIN_FUNDING_SATOSHIS, Channel.MAX_FUNDING_SATOSHIS).getMessage.getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}
test("recv OpenChannel (invalid push_msat)") { case (bob, alice2bob, bob2alice, _) =>
within(30 seconds) {
val open = alice2bob.expectMsgType[OpenChannel]
val invalidPushMsat = 100000000000L
bob ! open.copy(pushMsat = invalidPushMsat)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, 1000 * open.fundingSatoshis).getMessage.getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}
test("recv OpenChannel (reserve too high)") { case (bob, alice2bob, bob2alice, _) =>
within(30 seconds) {
val open = alice2bob.expectMsgType[OpenChannel]
@ -93,7 +47,7 @@ class WaitForOpenChannelStateSpec extends TestkitBaseClass with StateTestsHelper
val reserveTooHigh = (0.3 * TestConstants.fundingSatoshis).toLong
bob ! open.copy(channelReserveSatoshis = reserveTooHigh)
val error = bob2alice.expectMsgType[Error]
assert(error === Error(open.temporaryChannelId, new ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage.getBytes("UTF-8")))
assert(error === Error(open.temporaryChannelId, "channelReserveSatoshis too high: reserve=300000 fundingRatio=0.3 maxFundingRatio=0.05".getBytes("UTF-8")))
awaitCond(bob.stateName == CLOSED)
}
}

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_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
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.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
import fr.acinq.eclair.channel.Register.Forward
@ -74,7 +74,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("stop"))
sender.expectMsgType[JValue]
bitcoind.exitValue()
//bitcoind.destroy()
nodes.foreach {
case (name, setup) =>
logger.info(s"stopping node $name")
@ -131,25 +131,31 @@ 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,5}
// --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)
@ -161,19 +167,51 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
connect(nodes("C"), nodes("F4"), 5000000, 0)
connect(nodes("C"), nodes("F5"), 5000000, 0)
val numberOfChannels = 10
val channelEndpointsCount = 2 * numberOfChannels
val sender = TestProbe()
val eventListener = TestProbe()
nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged]))
// we make sure all channels have set up their WatchConfirmed for the funding tx
// 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]
awaitCond({
val watches = nodes.values.foldLeft(Set.empty[Watch]) {
watches1 = nodes.values.foldLeft(Set.empty[Watch]) {
case (watches, setup) =>
sender.send(setup.watcher, 'watches)
watches ++ sender.expectMsgType[Set[Watch]]
}
watches.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount
watches1.count(_.isInstanceOf[WatchConfirmed]) == channelEndpointsCount / 2
}, 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)
@ -333,8 +371,6 @@ 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

@ -3,8 +3,8 @@ package fr.acinq.eclair.payment
import java.nio.ByteOrder
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Bech32, BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
import fr.acinq.eclair.payment.PaymentRequest._
import fr.acinq.bitcoin.{BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
import fr.acinq.eclair.payment.PaymentRequest.{Amount, ExtraHop, RoutingInfoTag}
import org.junit.runner.RunWith
import org.scalatest.FunSuite
import org.scalatest.junit.JUnitRunner
@ -180,30 +180,4 @@ class PaymentRequestSpec extends FunSuite {
val pr1 = PaymentRequest read serialized
assert(pr.expiry === Some(21600))
}
test("ignore unknown tags") {
// create a new tag that we don't know about
class MyExpiryTag(override val seconds: Long) extends ExpiryTag(seconds) {
// replace the tag with 'j' which is not used yet
override def toInt5s = super.toInt5s.updated(0, Bech32.map('j'))
}
val pr = PaymentRequest(
prefix = "lntb",
amount = Some(MilliSatoshi(100000L)),
timestamp = System.currentTimeMillis() / 1000L,
nodeId = nodeId,
tags = List(
PaymentHashTag(BinaryData("01" * 32)),
DescriptionTag("description"),
new MyExpiryTag(42L)
),
signature = BinaryData.empty).sign(priv)
val serialized = PaymentRequest write pr
val pr1 = PaymentRequest read serialized
val Some(unknownTag) = pr1.tags.collectFirst { case u: UnknownTag => u }
assert(unknownTag.tag == Bech32.map('j'))
assert(unknownTag.toInt5s == (new MyExpiryTag(42L)).toInt5s)
}
}

View File

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

View File

@ -65,7 +65,7 @@ class TransactionsSpec extends FunSuite {
val remoteHtlcPriv = PrivateKey(BinaryData("eb" * 32), compressed = true)
val localFinalPriv = PrivateKey(BinaryData("ff" * 32), compressed = true)
val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(BinaryData("fe" * 32), compressed = true).publicKey))
val localDustLimit = Satoshi(546)
val localDustLimit = Satoshi(542)
val toLocalDelay = 144
val feeratePerKw = 1000
@ -141,7 +141,7 @@ class TransactionsSpec extends FunSuite {
val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(BinaryData("a9" * 32), true).publicKey))
val commitInput = Funding.makeFundingInputInfo(BinaryData("a0" * 32), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey)
val toLocalDelay = 144
val localDustLimit = Satoshi(546)
val localDustLimit = Satoshi(542)
val feeratePerKw = 22000

View File

@ -73,7 +73,7 @@
</VBox>
<TextField fx:id="pushMsat" prefWidth="313.0" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Label fx:id="pushMsatError" opacity="0.0" styleClass="text-error, text-error-downward"
text="Generic Invalid Push" GridPane.columnSpan="2"
text="Generic Invalid Push"
GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<CheckBox fx:id="publicChannel" mnemonicParsing="true" selected="true" styleClass="text-sm"
text="Public Channel"

View File

@ -8,7 +8,7 @@ import javafx.scene.control._
import javafx.stage.Stage
import fr.acinq.bitcoin.{MilliSatoshi, Satoshi}
import fr.acinq.eclair.channel.{Channel, ChannelFlags}
import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.gui.Handlers
import fr.acinq.eclair.gui.utils.GUIValidators
import fr.acinq.eclair.io.Switchboard.NewChannel
@ -19,6 +19,15 @@ import grizzled.slf4j.Logging
*/
class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Logging {
/**
* Funding must be less than {@code 2^24} satoshi.
* PushMsat must not be greater than 1000 * Max funding
*
* https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#requirements
*/
val maxFunding = 16777216L
val maxPushMsat = 1000L * maxFunding
@FXML var host: TextField = _
@FXML var hostError: Label = _
@FXML var simpleConnection: CheckBox = _
@ -56,12 +65,11 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo
case "Satoshi" => Satoshi(rawFunding)
case "milliSatoshi" => Satoshi(rawFunding / 1000L)
}
if (GUIValidators.validate(fundingSatoshisError, f"Capacity must be less than ${Channel.MAX_FUNDING_SATOSHIS}%d satoshis", smartFunding.toLong < Channel.MAX_FUNDING_SATOSHIS)) {
if (GUIValidators.validate(fundingSatoshisError, "Funding must be 16 777 216 satoshis (~0.167 BTC) or less", smartFunding.toLong < maxFunding)) {
if (!pushMsat.getText.isEmpty) {
// pushMsat is optional, so we validate field only if it isn't empty
import fr.acinq.bitcoin._
if (GUIValidators.validate(pushMsat.getText, pushMsatError, "Push msat must be numeric", GUIValidators.amountRegex)
&& GUIValidators.validate(pushMsatError, "Push msat must be less or equal to capacity", pushMsat.getText.toLong <= satoshi2millisatoshi(smartFunding).amount)) {
&& GUIValidators.validate(pushMsatError, "Push msat must be 16 777 216 000 msat (~0.167 BTC) or less", pushMsat.getText.toLong <= maxPushMsat)) {
val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
handlers.open(host.getText, Some(NewChannel(smartFunding, MilliSatoshi(pushMsat.getText.toLong), Some(channelFlags))))
stage.close

View File

@ -0,0 +1,17 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
actor {
debug {
# enable DEBUG logging of all LoggingFSMs for events, transitions and timers
fsm = on
}
}
http {
host-connection-pool {
max-open-requests = 64
}
}
}