diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 34667720d..9de1de4aa 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -79,10 +79,10 @@ true - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-x86_64-linux-gnu.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-x86_64-linux-gnu.tar.gz - c371e383f024c6c45fb255d528a6beec - e6d8ab1f7661a6654fd81e236b9b5fd35a3d4dcb + 724043999e2b5ed0c088e8db34f15d43 + 546ee35d4089c7ccc040a01cdff3362599b8bc53 @@ -93,10 +93,10 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-osx64.tar.gz + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-osx64.tar.gz - bacd87d0c3f65a5acd666e33d094a59e - 62cc5bd9ced610bb9e8d4a854396bfe2139e3d0f + b5a792c6142995faa42b768273a493bd + 8bd51c7024d71de07df381055993e9f472013db8 @@ -107,9 +107,9 @@ - https://bitcoin.org/bin/bitcoin-core-0.16.3/bitcoin-0.16.3-win64.zip - bbde9b1206956d19298034319e9f405e - 85e3dc8a9c6f93b1b20cb79fa5850b5ce81da221 + https://bitcoin.org/bin/bitcoin-core-0.17.1/bitcoin-0.17.1-win64.zip + b0e824e9dd02580b5b01f073f3c89858 + 4e17bad7d08c465b444143a93cd6eb1c95076e3f diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index 9a7f3bc4d..008326fa0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -19,10 +19,12 @@ package fr.acinq.eclair.blockchain.bitcoind import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, JsonRPCError} +import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, JsonRPCError} import fr.acinq.eclair.transactions.Transactions import grizzled.slf4j.Logging +import org.json4s.DefaultFormats import org.json4s.JsonAST._ +import org.json4s.jackson.Serialization import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} @@ -47,9 +49,13 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: Long): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw) def signTransaction(hex: String): Future[SignTransactionResponse] = - rpcClient.invoke("signrawtransaction", hex).map(json => { + rpcClient.invoke("signrawtransactionwithwallet", hex).map(json => { val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" + if (!complete) { + val message = (json \ "errors" \\ classOf[JString]).mkString(",") + throw new JsonRPCError(Error(-1, message)) + } SignTransactionResponse(Transaction.read(hex), complete) }) @@ -74,7 +80,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = { val f = signTransaction(tx) - // if signature fails (e.g. because wallet is uncrypted) we need to unlock the utxos + // if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos f.recoverWith { case _ => unlockOutpoints(tx.txIn.map(_.outPoint)) .recover { case t: Throwable => logger.warn(s"Cannot unlock failed transaction's UTXOs txid=${tx.txid}", t); t } // no-op, just add a log in case of failure @@ -94,7 +100,7 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC // we ask bitcoin core to add inputs to the funding tx, and use the specified change address FundTransactionResponse(unsignedFundingTx, _, fee) <- fundTransaction(partialFundingTx, lockUnspents = true, feeRatePerKw) // now let's sign the funding tx - SignTransactionResponse(fundingTx, _) <- signTransactionOrUnlock(unsignedFundingTx) + SignTransactionResponse(fundingTx, true) <- signTransactionOrUnlock(unsignedFundingTx) // there will probably be a change output, so we need to find which output is ours outputIndex = Transactions.findPubKeyScriptIndex(fundingTx, pubkeyScript, outputsAlreadyUsed = Set.empty, amount_opt = None) _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$outputIndex fee=$fee") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 38bc96e8a..56952e30e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -97,8 +97,6 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu context.system.eventStream.subscribe(self, classOf[CurrentBlockCount]) // this will be used to make sure the current commitment fee is up-to-date context.system.eventStream.subscribe(self, classOf[CurrentFeerates]) - // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network - setTimer(TickRefreshChannelUpdate.toString, TickRefreshChannelUpdate, timeout = REFRESH_CHANNEL_UPDATE_INTERVAL, repeat = true) /* 8888888 888b 888 8888888 88888888888 @@ -201,9 +199,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu blockchain ! WatchSpent(self, data.commitments.commitInput.outPoint.txid, data.commitments.commitInput.outPoint.index.toInt, data.commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) blockchain ! WatchLost(self, data.commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId)) - // we rebuild a channel_update for two reasons: - // - we want to reload values from configuration - // - if eclair was previously killed, it might not have had time to publish a channel_update with enable=false + // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, normal.channelUpdate.shortChannelId, nodeParams.expiryDeltaBlocks, normal.commitments.remoteParams.htlcMinimumMsat, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.localCommit.spec.totalFunds, enable = false) @@ -820,7 +816,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu Some(Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)) } else None // we use GOTO instead of stay because we want to fire transitions - goto(NORMAL) using manualTransition(d, store(d.copy(shortChannelId = shortChannelId, buried = true, channelUpdate = channelUpdate))) sending localAnnSigs_opt.toSeq + goto(NORMAL) using manualTransition(NORMAL, NORMAL, d, store(d.copy(shortChannelId = shortChannelId, buried = true, channelUpdate = channelUpdate))) sending localAnnSigs_opt.toSeq case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel => // channels are publicly announced if both parties want it (defined as feature bit) @@ -835,7 +831,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu import d.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, keyManager.fundingPublicKey(localParams.channelKeyPath).publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) // we use GOTO instead of stay because we want to fire transitions - goto(NORMAL) using manualTransition(d, store(d.copy(channelAnnouncement = Some(channelAnn)))) + goto(NORMAL) using manualTransition(NORMAL, NORMAL, d, store(d.copy(channelAnnouncement = Some(channelAnn)))) case Some(_) => // they have sent their announcement sigs, but we already have a valid channel announcement // this can happen if our announcement_signatures was lost during a disconnection @@ -858,7 +854,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu log.info(s"sending channel_update announcement (refresh)") val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) // we use GOTO instead of stay because we want to fire transitions - goto(NORMAL) using manualTransition(d, store(d.copy(channelUpdate = channelUpdate))) + goto(NORMAL) using manualTransition(NORMAL, NORMAL, d, store(d.copy(channelUpdate = channelUpdate))) case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths) @@ -879,6 +875,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! Status.Failure(AddHtlcFailed(d.channelId, add.paymentHash, ChannelUnavailable(d.channelId), d.commitments.originChannels(add.id), Some(channelUpdate), None)) } + // disable the channel_update refresh timer + cancelTimer(TickRefreshChannelUpdate.toString) goto(OFFLINE) using d.copy(channelUpdate = channelUpdate) case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d) @@ -1453,7 +1451,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu } } // re-enable the channel - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + val timestamp = Platform.currentTime / 1000 match { + case ts if ts == d.channelUpdate.timestamp => ts + 1 // corner case: in case of quick reconnection, we bump the timestamp of the new channel_update, otherwise it will get ignored by the network + case ts => ts + } + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments), timestamp = timestamp) + // we will refresh need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network + setTimer(TickRefreshChannelUpdate.toString, TickRefreshChannelUpdate, timeout = REFRESH_CHANNEL_UPDATE_INTERVAL, repeat = true) goto(NORMAL) using d.copy(commitments = commitments1, channelUpdate = channelUpdate) } @@ -1559,7 +1563,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu // we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream case Event(CurrentFeerates(_), _) => stay - // we only care about this event in NORMAL state, and the scheduler is never disabled + // we only care about this event in NORMAL state, and the scheduler is not cancelled in some cases (e.g. NORMAL->CLOSING) case Event(TickRefreshChannelUpdate, _) => stay // we receive this when we send command to ourselves @@ -1579,7 +1583,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu /** * This is needed because of a bug in akka where onTransition event are not fired for same-state transition */ - def manualTransition(stateData: Data, nextStateData: Data): Data = { + def manualTransition(state: channel.State, nextState: channel.State, stateData: Data, nextStateData: Data): Data = { // if channel is private, we send the channel_update directly to remote // they need it "to learn the other end's forwarding parameters" (BOLT 7) (stateData, nextStateData) match { @@ -1592,15 +1596,19 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case _ => () } - (stateData, nextStateData) match { - case (d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate == d2.channelUpdate && d1.channelAnnouncement == d2.channelAnnouncement => + (state, nextState, stateData, nextStateData) match { + // ORDER MATTERS! + case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate == d2.channelUpdate && d1.channelAnnouncement == d2.channelAnnouncement => // don't do anything if neither the channel_update nor the channel_announcement didn't change () - case (_, normal: DATA_NORMAL) => - // whenever we go to a state with NORMAL data (can be OFFLINE or NORMAL), we send out the new channel_update (most of the time it will just be to enable/disable the channel) + case (WAIT_FOR_FUNDING_LOCKED | NORMAL | SYNCING, NORMAL | OFFLINE, _, normal: DATA_NORMAL) => + // when we do WAIT_FOR_FUNDING_LOCKED->NORMAL or NORMAL->NORMAL or SYNCING->NORMAL or NORMAL->OFFLINE, we send out the new channel_update (most of the time it will just be to enable/disable the channel) context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, normal.commitments)) - case (normal: DATA_NORMAL, _) => - // when we finally leave the NORMAL state (or OFFLINE with NORMAL data) to got to SHUTDOWN/NEGOTIATING/CLOSING/ERR*, we advertise the fact that channel can't be used for payments anymore + case (_, _, _: DATA_NORMAL, _: DATA_NORMAL) => + // in any other case (e.g. WAIT_FOR_INIT_INTERNAL->OFFLINE) we do nothing + () + case (_, _, normal: DATA_NORMAL, _) => + // when we finally leave the NORMAL state (or OFFLINE with NORMAL data) to go to SHUTDOWN/NEGOTIATING/CLOSING/ERR*, we advertise the fact that channel can't be used for payments anymore // if the channel is private we don't really need to tell the counterparty because it is already aware that the channel is being closed context.system.eventStream.publish(LocalChannelDown(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId)) case _ => () @@ -1624,7 +1632,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu cancelTimer(RevocationTimeout.toString) } - manualTransition(stateData, nextStateData) + manualTransition(state, nextState, stateData, nextStateData) } /* diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 4392c757c..20a2a8e00 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -304,7 +304,7 @@ object Graph { // NB we're guaranteed to have weightRatios and factors > 0 val factor = (cltvFactor * wr.cltvDeltaFactor) + (ageFactor * wr.ageFactor) + (capFactor * wr.capacityFactor) - val edgeWeight = if (isNeighborTarget) prev.weight else edgeCost * factor + val edgeWeight = if (isNeighborTarget) prev.weight else prev.weight + edgeCost * factor RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + channelCltvDelta, weight = edgeWeight) } diff --git a/eclair-core/src/test/resources/integration/bitcoin.conf b/eclair-core/src/test/resources/integration/bitcoin.conf index da4dd59a0..29775744a 100644 --- a/eclair-core/src/test/resources/integration/bitcoin.conf +++ b/eclair-core/src/test/resources/integration/bitcoin.conf @@ -1,7 +1,7 @@ regtest=1 +noprinttoconsole=1 server=1 port=28333 -rpcport=28332 rpcuser=foo rpcpassword=bar txindex=1 @@ -9,3 +9,5 @@ zmqpubrawblock=tcp://127.0.0.1:28334 zmqpubrawtx=tcp://127.0.0.1:28335 rpcworkqueue=64 addresstype=bech32 +[regtest] +rpcport=28332 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index c04a01a4b..11792b7f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,7 +21,7 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, MilliBtc, Satoshi, Script, Transaction} +import fr.acinq.bitcoin.{ByteVector32, Block, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} @@ -41,7 +41,14 @@ import scala.util.{Random, Try} class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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 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.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString @@ -94,6 +101,39 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe } } + test("handle errors when signing transactions") { + val bitcoinClient = new BasicBitcoinJsonRPCClient( + 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() + + // create a transaction that spends UTXOs that don't exist + wallet.getFinalAddress.pipeTo(sender.ref) + val address = sender.expectMsgType[String] + val unknownTxids = Seq( + ByteVector32.fromValidHex("01" * 32), + ByteVector32.fromValidHex("02" * 32), + ByteVector32.fromValidHex("03" * 32) + ) + val unsignedTx = Transaction(version = 2, + txIn = Seq( + TxIn(OutPoint(unknownTxids(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), + TxIn(OutPoint(unknownTxids(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL), + TxIn(OutPoint(unknownTxids(2), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) + ), + txOut = TxOut(Satoshi(1000000), addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, + lockTime = 0) + + // signing it should fail, and the error message should contain the txids of the UTXOs that could not be used + wallet.signTransaction(unsignedTx).pipeTo(sender.ref) + val Failure(JsonRPCError(error)) = sender.expectMsgType[Failure] + unknownTxids.foreach(id => assert(error.message.contains(id.toString()))) + } + test("create/commit/rollback funding txes") { val bitcoinClient = new BasicBitcoinJsonRPCClient( user = config.getString("bitcoind.rpcuser"), @@ -142,10 +182,9 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.send(bitcoincli, BitcoinReq("getrawtransaction", fundingTxes(2).txid.toString())) assert(sender.expectMsgType[JString](10 seconds).s === fundingTxes(2).toString()) - // NB: bitcoin core doesn't clear the locks when a tx is published + // NB: from 0.17.0 on bitcoin core will clear locks when a tx is published sender.send(bitcoincli, BitcoinReq("listlockunspent")) - assert(sender.expectMsgType[JValue](10 seconds).children.size === 2) - + assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) } test("encrypt wallet") { @@ -178,7 +217,8 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey))) wallet.makeFundingTx(pubkeyScript, MilliBtc(50), 10000).pipeTo(sender.ref) - assert(sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error.message.contains("Please enter the wallet passphrase with walletpassphrase first.")) + val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error + assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first")) sender.send(bitcoincli, BitcoinReq("listlockunspent")) assert(sender.expectMsgType[JValue](10 seconds).children.size === 0) @@ -213,14 +253,14 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe bitcoinClient.invoke("fundrawtransaction", noinputTx1).pipeTo(sender.ref) val json = sender.expectMsgType[JValue] val JString(unsignedtx1) = json \ "hex" - bitcoinClient.invoke("signrawtransaction", unsignedtx1).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx1).pipeTo(sender.ref) val JString(signedTx1) = sender.expectMsgType[JValue] \ "hex" val tx1 = Transaction.read(signedTx1) // let's then generate another tx that double spends the first one val inputs = tx1.txIn.map(txIn => Map("txid" -> txIn.outPoint.txid.toString, "vout" -> txIn.outPoint.index)).toArray bitcoinClient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount.toLong).sum * 1.0 / 1e8)).pipeTo(sender.ref) val JString(unsignedtx2) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransaction", unsignedtx2).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" val tx2 = Transaction.read(signedTx2) @@ -237,4 +277,4 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.expectMsg(true) } -} \ No newline at end of file +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 42f7f949c..479072f4e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -44,7 +44,7 @@ trait BitcoindService extends Logging { val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}") logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.16.3/bin/bitcoind") + val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.17.1/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala index d730b2bf1..18a08923d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ExtendedBitcoinClientSpec.scala @@ -34,7 +34,14 @@ import scala.concurrent.ExecutionContext.Implicits.global class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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 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.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") implicit val formats = DefaultFormats @@ -67,7 +74,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val json = sender.expectMsgType[JValue] val JString(unsignedtx) = json \ "hex" val JInt(changePos) = json \ "changepos" - bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" val tx = Transaction.read(signedTx) val txid = tx.txid.toString() @@ -92,7 +99,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi val pos = if (changePos == 0) 1 else 0 bitcoinClient.invoke("createrawtransaction", Array(Map("txid" -> txid, "vout" -> pos)), Map(address -> 5.99999)).pipeTo(sender.ref) val JString(unsignedtx) = sender.expectMsgType[JValue] - bitcoinClient.invoke("signrawtransaction", unsignedtx).pipeTo(sender.ref) + bitcoinClient.invoke("signrawtransactionwithwallet", unsignedtx).pipeTo(sender.ref) val JString(signedTx) = sender.expectMsgType[JValue] \ "hex" signedTx } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 82c292166..7b9126b2a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -38,7 +38,14 @@ import scala.util.Random class BitcoinCoreFeeProviderSpec extends TestKit(ActorSystem("test")) with BitcoindService with FunSuiteLike with BeforeAndAfterAll with Logging { - 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 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.router-broadcast-interval" -> "2 second", + "eclair.auto-reconnect" -> false)) val config = ConfigFactory.load(commonConfig).getConfig("eclair") val walletPassword = Random.alphanumeric.take(8).mkString diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index bfd84d675..e1f7842d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -366,4 +366,42 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) } + test("bump timestamp in case of quick reconnection") { f => + import f._ + val sender = TestProbe() + + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + + // alice and bob announce that their channel is OFFLINE + val channelUpdate_alice_disabled = channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate + val channelUpdate_bob_disabled = channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate + assert(Announcements.isEnabled(channelUpdate_alice_disabled.channelFlags) == false) + assert(Announcements.isEnabled(channelUpdate_bob_disabled.channelFlags) == false) + + // we immediately reconnect them + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) + + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + bob2alice.expectMsgType[ChannelReestablish] + alice2bob.forward(bob) + bob2alice.forward(alice) + + // both nodes reach NORMAL state, and broadcast a new channel_update + val channelUpdate_alice_enabled = channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate + val channelUpdate_bob_enabled = channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate + assert(Announcements.isEnabled(channelUpdate_alice_enabled.channelFlags) == true) + assert(Announcements.isEnabled(channelUpdate_bob_enabled.channelFlags) == true) + + // let's check that the two successive channel_update have a different timestamp + assert(channelUpdate_alice_enabled.timestamp > channelUpdate_alice_disabled.timestamp) + assert(channelUpdate_bob_enabled.timestamp > channelUpdate_bob_disabled.timestamp) + + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 3b270ebc6..2169a9062 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -837,6 +837,36 @@ class RouteCalculationSpec extends FunSuite { assert(hops2Nodes(routeScoreOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) } + + test("cost function is monotonic") { + + // This test have a channel (542280x2156x0) that according to heuristics is very convenient but actually useless to reach the target, + // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. + val updates = List( + ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")) -> ChannelUpdate(ByteVector32.Zeroes.bytes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = 0, feeBaseMsat = 1000, 100, Some(15000000000L)), + ChannelDesc(ShortChannelId("565643x1216x0"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector32.Zeroes.bytes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 0.toByte, 14, htlcMinimumMsat = 1, 1000, 10, Some(4294967295L)), + ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")) -> ChannelUpdate(ByteVector32.Zeroes.bytes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 1.toByte, 144, htlcMinimumMsat = 1000, feeBaseMsat = 1000, 100, Some(16777000000L)), + ChannelDesc(ShortChannelId("542280x2156x0"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector32.Zeroes.bytes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = 1, 667, 1, Some(16777000000L)), + ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96")) -> ChannelUpdate(ByteVector32.Zeroes.bytes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 3.toByte, 144, htlcMinimumMsat = 1, 1000, 100, Some(230000000L)), + ChannelDesc(ShortChannelId("565779x2711x0"), PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")) -> ChannelUpdate(ByteVector32.Zeroes.bytes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 0.toByte, 144, htlcMinimumMsat = 1, 1000, 100, Some(230000000L)) + ).toMap + + val g = DirectedGraph.makeGraph(updates) + + val params = RouteParams(randomize = false, maxFeeBaseMsat = 21000, maxFeePct = 0.03, routeMaxCltv = 1008, routeMaxLength = 6, ratios = Some( + WeightRatios(cltvDeltaFactor = 0.15, ageFactor = 0.35, capacityFactor = 0.5) + )) + val thisNode = PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96") + val targetNode = PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca") + val amount = 351000 + + Globals.blockCount.set(567634) // simulate mainnet block for heuristic + val Success(route) = Router.findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, params) + + assert(route.size == 2) + assert(route.last.nextNodeId == targetNode) + } + } object RouteCalculationSpec { diff --git a/pom.xml b/pom.xml index b9dfc46fd..e46032cf7 100644 --- a/pom.xml +++ b/pom.xml @@ -214,6 +214,7 @@ ${project.build.directory} + -Xmx1024m diff --git a/travis/builddeps.sh b/travis/builddeps.sh deleted file mode 100755 index bfff183d5..000000000 --- a/travis/builddeps.sh +++ /dev/null @@ -1,25 +0,0 @@ -pushd . -# lightning deps -sudo add-apt-repository -y ppa:chris-lea/libsodium -sudo apt-get update -sudo apt-get install -y libsodium-dev libgmp-dev libsqlite3-dev -cd -git clone https://github.com/luke-jr/libbase58.git -cd libbase58 -./autogen.sh && ./configure && make && sudo make install -# lightning -cd -git clone https://github.com/ElementsProject/lightning.git -cd lightning -git checkout fce9ee29e3c37b4291ebb050e6a687cfaa7df95a -git submodule init -git submodule update -make -# bitcoind -cd -wget https://bitcoin.org/bin/bitcoin-core-0.13.0/bitcoin-0.13.0-x86_64-linux-gnu.tar.gz -echo "bcc1e42d61f88621301bbb00512376287f9df4568255f8b98bc10547dced96c8 bitcoin-0.13.0-x86_64-linux-gnu.tar.gz" > sha256sum.asc -sha256sum -c sha256sum.asc -tar xzvf bitcoin-0.13.0-x86_64-linux-gnu.tar.gz -popd -