Compare commits
11 Commits
applicatio
...
temp-node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9377c0eb1d | ||
|
|
d462ad6ae9 | ||
|
|
2d5d68bf3f | ||
|
|
ba25821e85 | ||
|
|
86133db5f7 | ||
|
|
40b18aed8b | ||
|
|
8b151eb5c0 | ||
|
|
6304041d77 | ||
|
|
bf0423c020 | ||
|
|
3f9297f100 | ||
|
|
5cbfb6f964 |
@ -44,6 +44,12 @@ 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).
|
||||
|
||||
@ -28,13 +28,16 @@ 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"}, \"something\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
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"
|
||||
;;
|
||||
"send")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"send\", \"params\" : [\"${2?"missing request"}\"] }' $URL" | jq -r "if .error == null then .result else .error.message end"
|
||||
;;
|
||||
"network")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"network\", \"params\" : [] }' $URL" | jq ".result"
|
||||
"allnodes")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"allnodes\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"allchannels")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"allchannels\", \"params\" : [] }' $URL" | jq ".result"
|
||||
;;
|
||||
"peers")
|
||||
eval curl "$CURL_OPTS -d '{ \"method\": \"peers\", \"params\" : [] }' $URL" | jq ".result"
|
||||
|
||||
@ -81,4 +81,21 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,12 +70,17 @@ 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])
|
||||
} yield (progress, chainHash, bitcoinVersion)
|
||||
unspentAddresses <- bitcoinClient.listUnspentAddresses
|
||||
} yield (progress, chainHash, bitcoinVersion, unspentAddresses)
|
||||
// blocking sanity checks
|
||||
val (progress, chainHash, bitcoinVersion) = Await.result(future, 10 seconds)
|
||||
val (progress, chainHash, bitcoinVersion, unspentAddresses) = 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!!!")
|
||||
@ -107,8 +112,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 EarnDotComFeeProvider() :: new BitcoinCoreFeeProvider(client.rpcClient, defaultFeerates) :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
case _ => new FallbackFeeProvider(new EarnDotComFeeProvider() :: new ConstantFeeProvider(defaultFeerates) :: Nil) // order matters!
|
||||
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!
|
||||
}
|
||||
system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map {
|
||||
case feerates: FeeratesPerByte =>
|
||||
@ -131,7 +136,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")
|
||||
|
||||
@ -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, extractPublicKeyScript(tx.txIn.head.witness), minDepth, event)
|
||||
def apply(channel: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(channel, tx.txid, tx.txOut.map(_.publicKeyScript).headOption.getOrElse(""), minDepth, event)
|
||||
|
||||
def extractPublicKeyScript(witness: ScriptWitness): BinaryData = Try(PublicKey(witness.stack.last)) match {
|
||||
case Success(pubKey) =>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
}
|
||||
@ -173,23 +173,18 @@ 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))
|
||||
}*/
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
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))
|
||||
|
||||
}
|
||||
@ -133,10 +133,10 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
|
||||
case d: HasCommitments =>
|
||||
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)
|
||||
relayer ! channelUpdate
|
||||
case DATA_NORMAL(_, _, ann, _, _, _) =>
|
||||
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, ann.shortChannelId))
|
||||
//val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, ann.shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
|
||||
//drelayer ! channelUpdate
|
||||
case _ => ()
|
||||
}
|
||||
goto(OFFLINE) using d
|
||||
@ -409,7 +409,9 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
log.warning("using hardcoded short id for testing with bitcoinj!!!!!")
|
||||
context.system.scheduler.scheduleOnce(5 seconds, self, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, Random.nextInt(100), Random.nextInt(100)))
|
||||
}
|
||||
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), None, None, None, None))
|
||||
val tmpShortChannelId = BigInt(d.channelId.take(7).toArray).toLong
|
||||
val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, tmpShortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
|
||||
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), None, initialChannelUpdate, None, None, None))
|
||||
|
||||
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel =>
|
||||
log.info(s"received remote announcement signatures, delaying")
|
||||
@ -443,15 +445,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
// note: spec would allow us to keep sending new htlcs after having received their shutdown (and not sent ours)
|
||||
// but we want to converge as fast as possible and they would probably not route them anyway
|
||||
val error = ClosingInProgress(d.channelId)
|
||||
handleCommandAddError(error, origin(c))
|
||||
handleCommandError(AddHtlcFailed(error, origin(c), Some(d.channelUpdate)))
|
||||
|
||||
case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) =>
|
||||
Try(Commitments.sendAdd(d.commitments, c, origin(c))) match {
|
||||
case Success(Right((commitments1, add))) =>
|
||||
if (c.commit) self ! CMD_SIGN
|
||||
handleCommandSuccess(sender, d.copy(commitments = commitments1)) sending add
|
||||
case Success(Left(error)) => handleCommandAddError(error, origin(c))
|
||||
case Failure(cause) => handleCommandError(cause)
|
||||
case Success(Left(error)) => handleCommandError(AddHtlcFailed(error, origin(c), Some(d.channelUpdate)))
|
||||
case Failure(cause) => handleCommandError(AddHtlcFailed(cause, origin(c), Some(d.channelUpdate)))
|
||||
}
|
||||
|
||||
case Event(add: UpdateAddHtlc, d: DATA_NORMAL) =>
|
||||
@ -669,7 +671,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
case _ => stay
|
||||
}
|
||||
|
||||
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex), d: DATA_NORMAL) if d.commitments.announceChannel && d.shortChannelId.isEmpty =>
|
||||
case Event(WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, blockHeight, txIndex), d: DATA_NORMAL) if d.commitments.announceChannel && d.channelAnnouncement.isEmpty =>
|
||||
val shortChannelId = toShortId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt)
|
||||
log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex, sending announcements")
|
||||
val annSignatures = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)
|
||||
@ -678,7 +680,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel =>
|
||||
// channels are publicly announced if both parties want it (defined as feature bit)
|
||||
d.localAnnouncementSignatures match {
|
||||
case Some(localAnnSigs) if d.shortChannelId.isDefined =>
|
||||
case Some(localAnnSigs) if d.channelAnnouncement.isDefined =>
|
||||
// this can happen if our announcement_signatures was lost during a disconnection
|
||||
// specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message"
|
||||
// current implementation always replies to announcement_signatures, not only the first time
|
||||
@ -701,7 +703,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
context.system.scheduler.scheduleOnce(3 seconds, router, TickBroadcast)
|
||||
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, localAnnSigs.shortChannelId))
|
||||
// we acknowledge our AnnouncementSignatures message
|
||||
stay using store(d.copy(shortChannelId = Some(localAnnSigs.shortChannelId))) // note: we don't clear our announcement sigs because we may need to re-send them
|
||||
stay using store(d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate)) // note: we don't clear our announcement sigs because we may need to re-send them
|
||||
case None =>
|
||||
log.info(s"received remote announcement signatures, delaying")
|
||||
// our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message
|
||||
@ -715,14 +717,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
stay
|
||||
}
|
||||
|
||||
case Event(TickRefreshChannelUpdate, d: DATA_NORMAL) if d.shortChannelId.isDefined =>
|
||||
d.shortChannelId match {
|
||||
case Some(shortChannelId) => // periodic refresh is used as a keep alive
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
|
||||
router ! channelUpdate
|
||||
stay
|
||||
case None => stay // channel is not announced
|
||||
}
|
||||
case Event(TickRefreshChannelUpdate, d: DATA_NORMAL) =>
|
||||
// periodic refresh is used as a keep alive
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth)
|
||||
router ! channelUpdate
|
||||
stay using d.copy(channelUpdate = channelUpdate)
|
||||
|
||||
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
|
||||
|
||||
@ -731,18 +730,14 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) => handleRemoteSpentOther(tx, d)
|
||||
|
||||
case Event(INPUT_DISCONNECTED, d: DATA_NORMAL) =>
|
||||
// we disable the channel
|
||||
log.info(s"disabling the channel (disconnected)")
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
|
||||
router ! channelUpdate
|
||||
d.commitments.localChanges.proposed.collect {
|
||||
case add: UpdateAddHtlc => relayer ! ForwardLocalFail(ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id))
|
||||
case add: UpdateAddHtlc => relayer ! ForwardLocalFail(ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate))
|
||||
}
|
||||
d.shortChannelId match {
|
||||
case Some(shortChannelId) =>
|
||||
// if channel has be announced, we disable it
|
||||
log.info(s"disabling the channel (disconnected)")
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
|
||||
router ! channelUpdate
|
||||
case None => {}
|
||||
}
|
||||
goto(OFFLINE)
|
||||
goto(OFFLINE) using d.copy(channelUpdate = channelUpdate)
|
||||
|
||||
case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d)
|
||||
|
||||
@ -1040,7 +1035,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
case add if ripemd160(add.paymentHash) == extracted =>
|
||||
val origin = d.commitments.originChannels(add.id)
|
||||
log.warning(s"found a match between paymentHash160=$extracted and origin=$origin: htlc timed out")
|
||||
relayer ! ForwardLocalFail(HtlcTimedout(d.channelId), origin)
|
||||
relayer ! ForwardLocalFail(HtlcTimedout(d.channelId), origin, None)
|
||||
}
|
||||
// TODO: should we handle local htlcs here as well? currently timed out htlcs that we sent will never have an answer
|
||||
// TODO: we do not handle the case where htlcs transactions end up being unconfirmed this can happen if an htlc-success tx is published right before a htlc timed out
|
||||
@ -1159,17 +1154,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
|
||||
}
|
||||
// rfc: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message
|
||||
if (d.localAnnouncementSignatures.isDefined && d.shortChannelId.isEmpty) {
|
||||
if (d.localAnnouncementSignatures.isDefined && d.channelAnnouncement.isEmpty) {
|
||||
forwarder ! d.localAnnouncementSignatures.get
|
||||
}
|
||||
|
||||
d.shortChannelId.map {
|
||||
case shortChannelId =>
|
||||
// we re-enable the channel
|
||||
log.info(s"enabling the channel (reconnected)")
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true)
|
||||
router ! channelUpdate
|
||||
}
|
||||
// we re-enable the channel
|
||||
log.info(s"enabling the channel (reconnected)")
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = true)
|
||||
router ! channelUpdate
|
||||
|
||||
goto(NORMAL) using d.copy(commitments = commitments1)
|
||||
|
||||
case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) =>
|
||||
@ -1231,7 +1224,15 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
case Event(c: CMD_ADD_HTLC, d: HasCommitments) =>
|
||||
log.info(s"rejecting htlc request in state=$stateName")
|
||||
val error = ChannelUnavailable(d.channelId)
|
||||
handleCommandAddError(error, origin(c))
|
||||
d match {
|
||||
case dn: DATA_NORMAL =>
|
||||
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, 0, nodeParams.expiryDeltaBlocks, nodeParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, enable = false)
|
||||
handleCommandError(AddHtlcFailed(error, origin(c), Some(channelUpdate)))
|
||||
case _ =>
|
||||
handleCommandError(AddHtlcFailed(error, origin(c), None))
|
||||
}
|
||||
|
||||
|
||||
|
||||
// we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream
|
||||
case Event(CurrentBlockCount(_), _) => stay
|
||||
@ -1273,18 +1274,10 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
|
||||
stay using newData replying "ok"
|
||||
}
|
||||
|
||||
def handleCommandAddError(cause: Throwable, origin: Origin) = {
|
||||
relayer ! ForwardLocalFail(cause, origin)
|
||||
cause match {
|
||||
case _: ChannelException => log.error(s"$cause")
|
||||
case _ => log.error(cause, "")
|
||||
}
|
||||
stay
|
||||
}
|
||||
|
||||
def handleCommandError(cause: Throwable) = {
|
||||
cause match {
|
||||
case _: ChannelException => log.error(s"$cause")
|
||||
case e: AddHtlcFailed => log.error(s"${e.t}")
|
||||
case _ => log.error(cause, "")
|
||||
}
|
||||
stay replying Status.Failure(cause)
|
||||
|
||||
@ -2,42 +2,46 @@ package fr.acinq.eclair.channel
|
||||
|
||||
import fr.acinq.bitcoin.BinaryData
|
||||
import fr.acinq.eclair.UInt64
|
||||
import fr.acinq.eclair.payment.Origin
|
||||
import fr.acinq.eclair.wire.ChannelUpdate
|
||||
|
||||
/**
|
||||
* Created by PM on 11/04/2017.
|
||||
*/
|
||||
|
||||
class ChannelException(channelId: BinaryData, message: String) extends RuntimeException(message)
|
||||
class ChannelException(val channelId: BinaryData, message: String) extends RuntimeException(message)
|
||||
// @formatter:off
|
||||
case class DebugTriggeredException (channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
|
||||
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")
|
||||
case class CannotCloseWithUnsignedOutgoingHtlcs(channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
|
||||
case class ChannelUnavailable (channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
|
||||
case class InvalidFinalScript (channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
|
||||
case class HtlcTimedout (channelId: BinaryData) extends ChannelException(channelId, s"one or more htlcs timed out")
|
||||
case class FeerateTooDifferent (channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
case class InvalidCloseSignature (channelId: BinaryData) extends ChannelException(channelId, "cannot verify their close signature")
|
||||
case class InvalidCommitmentSignature (channelId: BinaryData) extends ChannelException(channelId, "invalid commitment signature")
|
||||
case class ForcedLocalCommit (channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
|
||||
case class UnexpectedHtlcId (channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
|
||||
case class InvalidPaymentHash (channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
|
||||
case class ExpiryTooSmall (channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
|
||||
case class ExpiryCannotBeInThePast (channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
|
||||
case class HtlcValueTooSmall (channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
|
||||
case class HtlcValueTooHighInFlight (channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
|
||||
case class TooManyAcceptedHtlcs (channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
|
||||
case class InsufficientFunds (channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
|
||||
case class InvalidHtlcPreimage (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
|
||||
case class UnknownHtlcId (channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
|
||||
case class FundeeCannotSendUpdateFee (channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
|
||||
case class CannotAffordFees (channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
|
||||
case class CannotSignWithoutChanges (channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
|
||||
case class CannotSignBeforeRevocation (channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
|
||||
case class UnexpectedRevocation (channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
|
||||
case class InvalidRevocation (channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
|
||||
case class CommitmentSyncError (channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
|
||||
case class RevocationSyncError (channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
|
||||
case class InvalidFailureCode (channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
|
||||
// @formatter:on
|
||||
case class DebugTriggeredException (override val channelId: BinaryData) extends ChannelException(channelId, "debug-mode triggered failure")
|
||||
case class ChannelReserveTooHigh (override val channelId: BinaryData, channelReserveSatoshis: Long, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double) extends ChannelException(channelId, s"channelReserveSatoshis too high: reserve=$channelReserveSatoshis fundingRatio=$reserveToFundingRatio maxFundingRatio=$maxReserveToFundingRatio")
|
||||
case class ClosingInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "cannot send new htlcs, closing in progress")
|
||||
case class ClosingAlreadyInProgress (override val channelId: BinaryData) extends ChannelException(channelId, "closing already in progress")
|
||||
case class CannotCloseWithUnsignedOutgoingHtlcs(override val channelId: BinaryData) extends ChannelException(channelId, "cannot close when there are unsigned outgoing htlcs")
|
||||
case class ChannelUnavailable (override val channelId: BinaryData) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
|
||||
case class InvalidFinalScript (override val channelId: BinaryData) extends ChannelException(channelId, "invalid final script")
|
||||
case class HtlcTimedout (override val channelId: BinaryData) extends ChannelException(channelId, s"one or more htlcs timed out")
|
||||
case class FeerateTooDifferent (override val channelId: BinaryData, localFeeratePerKw: Long, remoteFeeratePerKw: Long) extends ChannelException(channelId, s"local/remote feerates are too different: remoteFeeratePerKw=$remoteFeeratePerKw localFeeratePerKw=$localFeeratePerKw")
|
||||
case class InvalidCloseSignature (override val channelId: BinaryData) extends ChannelException(channelId, "cannot verify their close signature")
|
||||
case class InvalidCommitmentSignature (override val channelId: BinaryData) extends ChannelException(channelId, "invalid commitment signature")
|
||||
case class ForcedLocalCommit (override val channelId: BinaryData, reason: String) extends ChannelException(channelId, s"forced local commit: reason")
|
||||
case class UnexpectedHtlcId (override val channelId: BinaryData, expected: Long, actual: Long) extends ChannelException(channelId, s"unexpected htlc id: expected=$expected actual=$actual")
|
||||
case class InvalidPaymentHash (override val channelId: BinaryData) extends ChannelException(channelId, "invalid payment hash")
|
||||
case class ExpiryTooSmall (override val channelId: BinaryData, minimum: Long, actual: Long, blockCount: Long) extends ChannelException(channelId, s"expiry too small: required=$minimum actual=$actual blockCount=$blockCount")
|
||||
case class ExpiryCannotBeInThePast (override val channelId: BinaryData, expiry: Long, blockCount: Long) extends ChannelException(channelId, s"expiry can't be in the past: expiry=$expiry blockCount=$blockCount")
|
||||
case class HtlcValueTooSmall (override val channelId: BinaryData, minimum: Long, actual: Long) extends ChannelException(channelId, s"htlc value too small: minimum=$minimum actual=$actual")
|
||||
case class HtlcValueTooHighInFlight (override val channelId: BinaryData, maximum: UInt64, actual: UInt64) extends ChannelException(channelId, s"in-flight htlcs hold too much value: maximum=$maximum actual=$actual")
|
||||
case class TooManyAcceptedHtlcs (override val channelId: BinaryData, maximum: Long) extends ChannelException(channelId, s"too many accepted htlcs: maximum=$maximum")
|
||||
case class InsufficientFunds (override val channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis")
|
||||
case class InvalidHtlcPreimage (override val channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id")
|
||||
case class UnknownHtlcId (override val channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id")
|
||||
case class FundeeCannotSendUpdateFee (override val channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages")
|
||||
case class CannotAffordFees (override val channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis")
|
||||
case class CannotSignWithoutChanges (override val channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes")
|
||||
case class CannotSignBeforeRevocation (override val channelId: BinaryData) extends ChannelException(channelId, "cannot sign until next revocation hash is received")
|
||||
case class UnexpectedRevocation (override val channelId: BinaryData) extends ChannelException(channelId, "received unexpected RevokeAndAck message")
|
||||
case class InvalidRevocation (override val channelId: BinaryData) extends ChannelException(channelId, "invalid revocation")
|
||||
case class CommitmentSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "commitment sync error")
|
||||
case class RevocationSyncError (override val channelId: BinaryData) extends ChannelException(channelId, "revocation sync error")
|
||||
case class InvalidFailureCode (override val channelId: BinaryData) extends ChannelException(channelId, "UpdateFailMalformedHtlc message doesn't have BADONION bit set")
|
||||
// @formatter:on
|
||||
|
||||
case class AddHtlcFailed(t: Throwable, origin: Origin, channelUpdate: Option[ChannelUpdate]) extends RuntimeException(t)
|
||||
@ -7,7 +7,7 @@ import fr.acinq.eclair.UInt64
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.transactions.CommitmentSpec
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.wire.{AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc}
|
||||
|
||||
|
||||
/**
|
||||
@ -135,7 +135,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: BinaryData, localParams
|
||||
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments
|
||||
final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, lastSent: FundingLocked) extends Data with HasCommitments
|
||||
final case class DATA_NORMAL(commitments: Commitments,
|
||||
shortChannelId: Option[Long],
|
||||
channelAnnouncement: Option[ChannelAnnouncement],
|
||||
channelUpdate: ChannelUpdate,
|
||||
localAnnouncementSignatures: Option[AnnouncementSignatures],
|
||||
localShutdown: Option[Shutdown],
|
||||
remoteShutdown: Option[Shutdown]) extends Data with HasCommitments
|
||||
|
||||
@ -64,4 +64,12 @@ 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")
|
||||
}
|
||||
@ -279,6 +279,10 @@ object PaymentRequest {
|
||||
}
|
||||
}
|
||||
|
||||
case class UnknownTag(tag: Int5, int5s: Seq[Int5]) extends Tag {
|
||||
override def toInt5s = tag +: (writeSize(int5s.size) ++ int5s)
|
||||
}
|
||||
|
||||
object Amount {
|
||||
|
||||
/**
|
||||
@ -313,7 +317,7 @@ object PaymentRequest {
|
||||
}
|
||||
|
||||
object Tag {
|
||||
def parse(input: Seq[Byte]): Tag = {
|
||||
def parse(input: Seq[Int5]): Tag = {
|
||||
val tag = input(0)
|
||||
val len = input(1) * 32 + input(2)
|
||||
tag match {
|
||||
@ -345,6 +349,8 @@ 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,8 @@ case class Local(sender: Option[ActorRef]) extends Origin // we don't persist re
|
||||
case class Relayed(originChannelId: BinaryData, originHtlcId: Long, amountMsatIn: Long, amountMsatOut: Long) extends Origin
|
||||
|
||||
case class ForwardAdd(add: UpdateAddHtlc)
|
||||
case class ForwardLocalFail(error: Throwable, to: Origin, channelUpdate: Option[ChannelUpdate]) // happens when the failure happened in a local channel (and not in some downstream channel)
|
||||
case class ForwardFulfill(fulfill: UpdateFulfillHtlc, to: Origin)
|
||||
case class ForwardLocalFail(error: Throwable, to: Origin) // happens when the failure happened in a local channel (and not in some downstream channel)
|
||||
case class ForwardFail(fail: UpdateFailHtlc, to: Origin)
|
||||
case class ForwardFailMalformed(fail: UpdateFailMalformedHtlc, to: Origin)
|
||||
|
||||
@ -45,7 +45,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
||||
case ChannelStateChanged(channel, _, _, _, NORMAL | SHUTDOWN | CLOSING, d: HasCommitments) =>
|
||||
import d.channelId
|
||||
preimagesDb.listPreimages(channelId) match {
|
||||
case Nil => {}
|
||||
case Nil => ()
|
||||
case preimages =>
|
||||
log.info(s"re-sending ${preimages.size} unacked fulfills to channel $channelId")
|
||||
preimages.map(p => CMD_FULFILL_HTLC(p._2, p._3, commit = false)).foreach(channel ! _)
|
||||
@ -75,11 +75,11 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
||||
paymentHandler forward add
|
||||
}
|
||||
case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) =>
|
||||
val channelUpdate_opt = channelUpdates.get(perHopPayload.channel_id)
|
||||
channelUpdate_opt match {
|
||||
channelUpdates.get(perHopPayload.channel_id) match {
|
||||
case None =>
|
||||
// TODO: clarify what we're supposed to do in the specs
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(TemporaryNodeFailure), commit = true)
|
||||
// if we don't (yet?) have a channel_update for the next channel, we consider the channel doesn't exist
|
||||
// TODO: use a different channel to the same peer instead?
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)
|
||||
case Some(channelUpdate) if !Announcements.isEnabled(channelUpdate.flags) =>
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.flags, channelUpdate)), commit = true)
|
||||
case Some(channelUpdate) if add.amountMsat < channelUpdate.htlcMinimumMsat =>
|
||||
@ -90,7 +90,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
||||
sender ! CMD_FAIL_HTLC(add.id, Right(ExpiryTooSoon(channelUpdate)), commit = true)
|
||||
case _ =>
|
||||
log.info(s"forwarding htlc #${add.id} to shortChannelId=${perHopPayload.channel_id}")
|
||||
register forward Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true))
|
||||
register ! Register.ForwardShortId(perHopPayload.channel_id, CMD_ADD_HTLC(perHopPayload.amtToForward, add.paymentHash, perHopPayload.outgoingCltvValue, nextPacket.serialize, upstream_opt = Some(add), commit = true))
|
||||
}
|
||||
case Success((Attempt.Failure(cause), _, _)) =>
|
||||
log.error(s"couldn't parse payload: $cause")
|
||||
@ -119,13 +119,15 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR
|
||||
log.debug(s"fulfill acked for channelId=$channelId htlcId=$htlcId")
|
||||
preimagesDb.removePreimage(channelId, htlcId)
|
||||
|
||||
case ForwardLocalFail(error, Local(Some(sender))) =>
|
||||
case ForwardLocalFail(error, Local(Some(sender)), _) =>
|
||||
sender ! Status.Failure(error)
|
||||
|
||||
case ForwardLocalFail(error, Relayed(originChannelId, originHtlcId, _, _)) =>
|
||||
// TODO: clarify what we're supposed to do in the specs depending on the error
|
||||
val failure = error match {
|
||||
case HtlcTimedout(_) => PermanentChannelFailure
|
||||
case ForwardLocalFail(error, Relayed(originChannelId, originHtlcId, _, _), channelUpdate_opt) =>
|
||||
val failure = (error, channelUpdate_opt) match {
|
||||
case (_: ChannelUnavailable, Some(channelUpdate)) if !Announcements.isEnabled(channelUpdate.flags) => ChannelDisabled(channelUpdate.flags, channelUpdate)
|
||||
case (_: InsufficientFunds, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
|
||||
case (_: TooManyAcceptedHtlcs, Some(channelUpdate)) => TemporaryChannelFailure(channelUpdate)
|
||||
case (_: HtlcTimedout, _) => PermanentChannelFailure
|
||||
case _ => TemporaryNodeFailure
|
||||
}
|
||||
val cmd = CMD_FAIL_HTLC(originHtlcId, Right(failure), commit = true)
|
||||
|
||||
@ -200,7 +200,8 @@ object ChannelCodecs {
|
||||
|
||||
val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = (
|
||||
("commitments" | commitmentsCodec) ::
|
||||
("shortChannelId" | optional(bool, uint64)) ::
|
||||
("channelAnnouncement" | optional(bool, channelAnnouncementCodec)) ::
|
||||
("channelUpdate" | channelUpdateCodec) ::
|
||||
("localAnnouncementSignatures" | optional(bool, announcementSignaturesCodec)) ::
|
||||
("localShutdown" | optional(bool, shutdownCodec)) ::
|
||||
("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL]
|
||||
|
||||
@ -1,47 +1,104 @@
|
||||
package fr.acinq.eclair.blockchain
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
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._
|
||||
import fr.acinq.bitcoin.Crypto.PrivateKey
|
||||
import fr.acinq.eclair.Kit
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
|
||||
import org.scalatest.FunSuite
|
||||
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 scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, ExecutionContext}
|
||||
import ExecutionContext.Implicits.global
|
||||
import scala.sys.process._
|
||||
|
||||
// this test is not run automatically
|
||||
class ExtendedBitcoinClientSpec extends FunSuite {
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike with BeforeAndAfterAll with Logging {
|
||||
|
||||
implicit lazy val system = ActorSystem()
|
||||
val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}"
|
||||
logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR")
|
||||
|
||||
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))
|
||||
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")
|
||||
|
||||
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")
|
||||
var bitcoind: Process = null
|
||||
var bitcoinrpcclient: BitcoinJsonRPCClient = null
|
||||
var bitcoincli: ActorRef = null
|
||||
var client: ExtendedBitcoinClient = _
|
||||
|
||||
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)
|
||||
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 by short id") {
|
||||
val tx = Await.result(client.getTransaction(150002, 7), 5 seconds)
|
||||
assert(tx.txid.toString() == "7b2184f8539af648d51cc11d2a83630dd10fdf2a40a1824777d7f8da8e0d4b9e")
|
||||
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("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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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.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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,13 +4,14 @@ import akka.actor.Status.Failure
|
||||
import akka.testkit.{TestFSMRef, TestProbe}
|
||||
import fr.acinq.bitcoin.Crypto.Scalar
|
||||
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, ScriptFlags, Transaction}
|
||||
import fr.acinq.eclair.TestConstants.Bob
|
||||
import fr.acinq.eclair.TestConstants.{Alice, Bob}
|
||||
import fr.acinq.eclair.UInt64.Conversions._
|
||||
import fr.acinq.eclair.blockchain._
|
||||
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
|
||||
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
|
||||
import fr.acinq.eclair.channel.{Data, State, _}
|
||||
import fr.acinq.eclair.payment._
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.{IN, OUT}
|
||||
import fr.acinq.eclair.wire.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
|
||||
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass}
|
||||
@ -90,14 +91,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
}
|
||||
}
|
||||
|
||||
test("recv CMD_ADD_HTLC (invalid payment hash)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
test("recv CMD_ADD_HTLC (invalid payment hash)") { case (alice, _, alice2bob, _, _, _, _) =>
|
||||
within(30 seconds) {
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val sender = TestProbe()
|
||||
val add = CMD_ADD_HTLC(500000000, "11" * 42, expiry = 400144)
|
||||
sender.send(alice, add)
|
||||
val error = InvalidPaymentHash(channelId(alice))
|
||||
//sender.expectMsg(Failure(InvalidPaymentHash(channelId(alice))))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -105,11 +106,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (expiry too small)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000)
|
||||
sender.send(alice, add)
|
||||
val error = ExpiryCannotBeInThePast(channelId(alice), 300000, 400000)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -117,11 +118,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (value too small)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val add = CMD_ADD_HTLC(50, "11" * 32, 400144)
|
||||
sender.send(alice, add)
|
||||
val error = HtlcValueTooSmall(channelId(alice), 1000, 50)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -129,11 +130,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (insufficient funds)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val add = CMD_ADD_HTLC(Int.MaxValue, "11" * 32, 400144)
|
||||
sender.send(alice, add)
|
||||
val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -141,6 +142,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
sender.send(alice, CMD_ADD_HTLC(500000000, "11" * 32, 400144))
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
@ -153,8 +155,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val add = CMD_ADD_HTLC(1000000, "44" * 32, 400144)
|
||||
sender.send(alice, add)
|
||||
val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -162,6 +163,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs 2/2)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
sender.send(alice, CMD_ADD_HTLC(300000000, "11" * 32, 400144))
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[UpdateAddHtlc]
|
||||
@ -171,8 +173,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val add = CMD_ADD_HTLC(500000000, "33" * 32, 400144)
|
||||
sender.send(alice, add)
|
||||
val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -180,11 +181,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (over max inflight htlc value)") { case (_, bob, _, bob2alice, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = bob.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val add = CMD_ADD_HTLC(151000000, "11" * 32, 400144)
|
||||
sender.send(bob, add)
|
||||
val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
bob2alice.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -192,6 +193,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (over max accepted htlcs)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
// Bob accepts a maximum of 30 htlcs
|
||||
for (i <- 0 until 30) {
|
||||
sender.send(alice, CMD_ADD_HTLC(10000000, "11" * 32, 400144))
|
||||
@ -201,15 +203,15 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val add = CMD_ADD_HTLC(10000000, "33" * 32, 400144)
|
||||
sender.send(alice, add)
|
||||
val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30)
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
|
||||
test("recv CMD_ADD_HTLC (while waiting for a revocation)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
test("recv CMD_ADD_HTLC (over capacity)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "11" * 32, 400144)
|
||||
sender.send(alice, add1)
|
||||
sender.expectMsg("ok")
|
||||
@ -220,8 +222,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
// this is over channel-capacity
|
||||
val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, "22" * 32, 400144)
|
||||
sender.send(alice, add2)
|
||||
//sender.expectMsgType[Failure]
|
||||
relayer.expectMsgType[ForwardLocalFail]
|
||||
val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680)
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -229,6 +231,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (after having sent Shutdown)") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
sender.send(alice, CMD_CLOSE(None))
|
||||
sender.expectMsg("ok")
|
||||
alice2bob.expectMsgType[Shutdown]
|
||||
@ -238,8 +241,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144)
|
||||
sender.send(alice, add)
|
||||
val error = ClosingInProgress(channelId(alice))
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
@ -247,6 +249,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
test("recv CMD_ADD_HTLC (after having received Shutdown)") { case (alice, bob, alice2bob, bob2alice, _, _, relayer) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
|
||||
// let's make alice send an htlc
|
||||
val add1 = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 400144)
|
||||
sender.send(alice, add1)
|
||||
@ -263,8 +266,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
bob2alice.forward(alice)
|
||||
sender.send(alice, add2)
|
||||
val error = ClosingInProgress(channelId(alice))
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), Some(initialState.channelUpdate))))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1658,9 +1660,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures]
|
||||
sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
|
||||
val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
|
||||
import initialState.commitments.localParams
|
||||
import initialState.commitments.remoteParams
|
||||
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
|
||||
val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
|
||||
// actual test starts here
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annSigsB.shortChannelId), localAnnouncementSignatures = Some(annSigsA)))
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate, localAnnouncementSignatures = Some(annSigsA)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1672,8 +1678,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures]
|
||||
sender.send(bob, WatchEventConfirmed(BITCOIN_FUNDING_DEEPLYBURIED, 42, 10))
|
||||
val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures]
|
||||
import initialState.commitments.localParams
|
||||
import initialState.commitments.remoteParams
|
||||
val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, localParams.nodeId, remoteParams.nodeId, localParams.fundingPrivKey.publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature)
|
||||
val channelUpdate = Announcements.makeChannelUpdate(Alice.nodeParams.chainHash, Alice.nodeParams.privateKey, remoteParams.nodeId, annSigsA.shortChannelId, Alice.nodeParams.expiryDeltaBlocks, Alice.nodeParams.htlcMinimumMsat, Alice.nodeParams.feeBaseMsat, Alice.nodeParams.feeProportionalMillionth)
|
||||
bob2alice.forward(alice)
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(shortChannelId = Some(annSigsB.shortChannelId), localAnnouncementSignatures = Some(annSigsA)))
|
||||
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL] == initialState.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate, localAnnouncementSignatures = Some(annSigsA)))
|
||||
|
||||
// actual test starts here
|
||||
// simulate bob re-sending its sigs
|
||||
|
||||
@ -78,14 +78,13 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
|
||||
}
|
||||
}
|
||||
|
||||
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _, relayer) =>
|
||||
test("recv CMD_ADD_HTLC") { case (alice, _, alice2bob, _, _, _, _) =>
|
||||
within(30 seconds) {
|
||||
val sender = TestProbe()
|
||||
val add = CMD_ADD_HTLC(500000000, "11" * 32, expiry = 300000)
|
||||
sender.send(alice, add)
|
||||
val error = ChannelUnavailable(channelId(alice))
|
||||
//sender.expectMsg(Failure(error))
|
||||
relayer.expectMsg(ForwardLocalFail(error, Local(Some(sender.ref))))
|
||||
sender.expectMsg(Failure(AddHtlcFailed(error, Local(Some(sender.ref)), None)))
|
||||
alice2bob.expectNoMsg(200 millis)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,10 @@ import fr.acinq.eclair.channel.Helpers.Funding
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
|
||||
import fr.acinq.eclair.payment.{Local, Relayed}
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.transactions.Transactions.CommitTx
|
||||
import fr.acinq.eclair.transactions._
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.wire.{ChannelCodecs, ChannelUpdate, UpdateAddHtlc}
|
||||
import fr.acinq.eclair.{UInt64, randomKey}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
@ -96,5 +97,7 @@ object ChannelStateSpec {
|
||||
remoteNextCommitInfo = Right(randomKey.publicKey),
|
||||
commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init, channelId = "00" * 32)
|
||||
|
||||
val normal = DATA_NORMAL(commitments, Some(42), None, None, None)
|
||||
val channelUpdate = Announcements.makeChannelUpdate("11" * 32, randomKey, randomKey.publicKey, 142553, 42, 15, 575, 53)
|
||||
|
||||
val normal = DATA_NORMAL(commitments, None, channelUpdate, None, None, None)
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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.destroy()
|
||||
bitcoind.exitValue()
|
||||
nodes.foreach {
|
||||
case (name, setup) =>
|
||||
logger.info(s"stopping node $name")
|
||||
@ -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 _ => ???
|
||||
}
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ package fr.acinq.eclair.payment
|
||||
import java.nio.ByteOrder
|
||||
|
||||
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
|
||||
import fr.acinq.bitcoin.{BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest.{Amount, ExtraHop, RoutingInfoTag}
|
||||
import fr.acinq.bitcoin.{Bech32, BinaryData, Block, Btc, Crypto, MilliBtc, MilliSatoshi, Protocol, Satoshi}
|
||||
import fr.acinq.eclair.payment.PaymentRequest._
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.FunSuite
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
@ -180,4 +180,30 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,9 @@ import fr.acinq.bitcoin.{BinaryData, Crypto, MilliSatoshi}
|
||||
import fr.acinq.eclair.channel._
|
||||
import fr.acinq.eclair.crypto.Sphinx
|
||||
import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand
|
||||
import fr.acinq.eclair.router.Announcements
|
||||
import fr.acinq.eclair.wire._
|
||||
import fr.acinq.eclair.{TestConstants, TestkitBaseClass}
|
||||
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass}
|
||||
import org.junit.runner.RunWith
|
||||
import org.scalatest.junit.JUnitRunner
|
||||
|
||||
@ -64,7 +65,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when there is no available upstream channel") { case (relayer, register, paymentHandler) =>
|
||||
test("fail to relay an htlc-add when we have no channel_update for the next channel") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
@ -76,15 +77,33 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(UnknownNextPeer))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when the requested channel is disabled") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
val channelUpdate_bc_disabled = channelUpdate_bc.copy(flags = Announcements.makeFlags(Announcements.isNode1(channelUpdate_bc.flags), enable = false))
|
||||
relayer ! channelUpdate_bc_disabled
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fail = sender.expectMsgType[CMD_FAIL_HTLC]
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(ChannelDisabled(channelUpdate_bc_disabled.flags, channelUpdate_bc_disabled)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when the onion is malformed") { case (relayer, register, paymentHandler) =>
|
||||
|
||||
// TODO: we should use the new update_fail_malformed_htlc message (see BOLT 2)
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
@ -107,7 +126,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
val sender = TestProbe()
|
||||
|
||||
// we use this to build a valid onion
|
||||
val (cmd, secrets) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0))))
|
||||
val (cmd, _) = buildCommand(channelUpdate_bc.htlcMinimumMsat - 1, finalExpiry, paymentHash, hops.map(hop => hop.copy(lastUpdate = hop.lastUpdate.copy(feeBaseMsat = 0, feeProportionalMillionths = 0))))
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
@ -126,7 +145,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
val sender = TestProbe()
|
||||
|
||||
val hops1 = hops.updated(1, hops(1).copy(lastUpdate = hops(1).lastUpdate.copy(cltvExpiryDelta = 0)))
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
@ -144,7 +163,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
test("fail to relay an htlc-add when expiry is too soon") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, 0, paymentHash, hops)
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, 0, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
@ -164,7 +183,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
// to simulate this we use a zero-hop route A->B where A is the 'attacker'
|
||||
val hops1 = hops.head :: Nil
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
// and then manually build an htlc with a wrong expiry
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat - 1, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
@ -184,7 +203,7 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
|
||||
// to simulate this we use a zero-hop route A->B where A is the 'attacker'
|
||||
val hops1 = hops.head :: Nil
|
||||
val (cmd, secrets) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, finalExpiry, paymentHash, hops1)
|
||||
// and then manually build an htlc with a wrong expiry
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry - 1, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
@ -199,6 +218,78 @@ class RelayerSpec extends TestkitBaseClass {
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when next channel's balance is too low") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, Globals.blockCount.get().toInt + 10, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
|
||||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, ForwardLocalFail(new InsufficientFunds(channelId_bc, cmd.amountMsat, 100, 0, 0), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc)))
|
||||
|
||||
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(TemporaryChannelFailure(channelUpdate_bc)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when next channel has too many inflight htlcs") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, Globals.blockCount.get().toInt + 10, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
|
||||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, ForwardLocalFail(new TooManyAcceptedHtlcs(channelId_bc, 30), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc)))
|
||||
|
||||
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(TemporaryChannelFailure(channelUpdate_bc)))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("fail to relay an htlc-add when next channel has a timed out htlc (and is thus closing)") { case (relayer, register, paymentHandler) =>
|
||||
val sender = TestProbe()
|
||||
|
||||
val (cmd, _) = buildCommand(finalAmountMsat, Globals.blockCount.get().toInt + 10, paymentHash, hops)
|
||||
// and then manually build an htlc
|
||||
val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.expiry, cmd.onion)
|
||||
relayer ! channelUpdate_bc
|
||||
|
||||
sender.send(relayer, ForwardAdd(add_ab))
|
||||
|
||||
val fwd = register.expectMsgType[Register.ForwardShortId[CMD_ADD_HTLC]]
|
||||
assert(fwd.shortChannelId === channelUpdate_bc.shortChannelId)
|
||||
assert(fwd.message.upstream_opt === Some(add_ab))
|
||||
|
||||
sender.send(relayer, ForwardLocalFail(new HtlcTimedout(channelId_bc), Relayed(add_ab.channelId, add_ab.id, add_ab.amountMsat, cmd.amountMsat), Some(channelUpdate_bc)))
|
||||
|
||||
val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message
|
||||
assert(fail.id === add_ab.id)
|
||||
assert(fail.reason == Right(PermanentChannelFailure))
|
||||
|
||||
register.expectNoMsg(500 millis)
|
||||
paymentHandler.expectNoMsg(500 millis)
|
||||
}
|
||||
|
||||
test("relay an htlc-fulfill") { case (relayer, register, _) =>
|
||||
val sender = TestProbe()
|
||||
val eventListener = TestProbe()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user