Skip to content

Commit

Permalink
Add messages for when startup fails due to wrong bitcoind wallet(s) l…
Browse files Browse the repository at this point in the history
…oaded (#2209)

Eclair fails at startup when eclair.bitcoind.wallet is mis-configured or not loaded by bitcoind. We now show different error messages for specific failure situations and report the current wallet configuration and loaded wallets where appropriate.
  • Loading branch information
remyers authored Mar 18, 2022
1 parent 18ba900 commit 5af042a
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 13 deletions.
22 changes: 15 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,22 @@ class Setup(val datadir: File,
val future = for {
json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) }
// Make sure wallet support is enabled in bitcoind.
_ <- bitcoinClient.invoke("getbalance").recover { case e => throw BitcoinWalletDisabledException(e) }
wallets <- bitcoinClient.invoke("listwallets").recover { case e => throw BitcoinWalletDisabledException(e) }
.collect {
case JArray(values) => values.map(value => value.extract[String])
}
progress = (json \ "verificationprogress").extract[Double]
ibd = (json \ "initialblockdownload").extract[Boolean]
blocks = (json \ "blocks").extract[Long]
headers = (json \ "headers").extract[Long]
chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => ByteVector32.fromValidHex(s)).map(_.reverse)
bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => json \ "version").map(_.extract[Int])
unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) =>
values
.filter(value => (value \ "spendable").extract[Boolean])
.map(value => (value \ "address").extract[String])
}
unspentAddresses <- bitcoinClient.invoke("listunspent").recover { _ => if (wallet.isEmpty && wallets.length > 1) throw BitcoinDefaultWalletException(wallets) else throw BitcoinWalletNotLoadedException(wallet.getOrElse(""), wallets) }
.collect { case JArray(values) =>
values
.filter(value => (value \ "spendable").extract[Boolean])
.map(value => (value \ "address").extract[String])
}
_ <- chain match {
case "mainnet" => bitcoinClient.invoke("getrawtransaction", "2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000
case "testnet" => bitcoinClient.invoke("getrawtransaction", "8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000
Expand Down Expand Up @@ -402,7 +406,11 @@ case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could

case class BitcoinRPCConnectionException(e: Throwable) extends RuntimeException("could not connect to bitcoind using json-rpc", e)

case class BitcoinWalletDisabledException(e: Throwable) extends RuntimeException("bitcoind wallet not available", e)
case class BitcoinWalletDisabledException(e: Throwable) extends RuntimeException("bitcoind wallet support disabled", e)

case class BitcoinDefaultWalletException(loaded: List[String]) extends RuntimeException(s"no bitcoind wallet configured, but multiple wallets loaded: ${loaded.map("\"" + _ + "\"").mkString("[", ",", "]")}")

case class BitcoinWalletNotLoadedException(wallet: String, loaded: List[String]) extends RuntimeException(s"configured wallet \"$wallet\" not in the set of loaded bitcoind wallets: ${loaded.map("\"" + _ + "\"").mkString("[", ",", "]")}")

case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ trait BitcoindService extends Logging {
var bitcoinrpcauthmethod: BitcoinJsonRPCAuthMethod = _
var bitcoincli: ActorRef = _

def startBitcoind(useCookie: Boolean = false): Unit = {
def startBitcoind(useCookie: Boolean = false, startupFlags: String = ""): Unit = {
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) {
val is = classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf")
Expand All @@ -87,7 +87,7 @@ trait BitcoindService extends Logging {
Files.writeString(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath, conf)
}

bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run()
bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR $startupFlags".run()
bitcoinrpcauthmethod = if (useCookie) {
SafeCookie(s"$PATH_BITCOIND_DATADIR/regtest/.cookie")
} else {
Expand All @@ -111,12 +111,14 @@ trait BitcoindService extends Logging {
bitcoind.exitValue()
}

def restartBitcoind(sender: TestProbe = TestProbe(), useCookie: Boolean = false): Unit = {
def restartBitcoind(sender: TestProbe = TestProbe(), useCookie: Boolean = false, startupFlags: String = "", loadWallet: Boolean = true): Unit = {
stopBitcoind()
startBitcoind(useCookie = useCookie)
startBitcoind(useCookie = useCookie, startupFlags = startupFlags)
waitForBitcoindUp(sender)
sender.send(bitcoincli, BitcoinReq("loadwallet", defaultWallet))
sender.expectMsgType[JValue]
if (loadWallet) {
sender.send(bitcoincli, BitcoinReq("loadwallet", defaultWallet))
sender.expectMsgType[JValue]
}
}

private def waitForBitcoindUp(sender: TestProbe): Unit = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2022 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.integration

import akka.testkit.TestProbe
import com.typesafe.config.ConfigFactory
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.{Error, JsonRPCError}
import fr.acinq.eclair.{BitcoinDefaultWalletException, BitcoinWalletDisabledException, BitcoinWalletNotLoadedException, TestUtils}

import scala.jdk.CollectionConverters._

/**
* Created by remyers on 16/03/2022.
*/

class StartupIntegrationSpec extends IntegrationSpec {

test("no bitcoind wallet configured and one wallet loaded") {
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
}

test("no bitcoind wallet configured and two wallets loaded") {
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("createwallet", ""))
sender.expectMsgType[Any]
val thrown = intercept[BitcoinDefaultWalletException] {
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
}
assert(thrown === BitcoinDefaultWalletException(List(defaultWallet, "")))
}

test("explicit bitcoind wallet configured and two wallets loaded") {
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("createwallet", ""))
sender.expectMsgType[Any]
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
}

test("explicit bitcoind wallet configured but not loaded") {
val sender = TestProbe()
sender.send(bitcoincli, BitcoinReq("createwallet", ""))
sender.expectMsgType[Any]
val thrown = intercept[BitcoinWalletNotLoadedException] {
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "notloaded", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
}
assert(thrown === BitcoinWalletNotLoadedException("notloaded", List(defaultWallet, "")))
}

test("bitcoind started with wallets disabled") {
restartBitcoind(startupFlags = "-disablewallet", loadWallet = false)
val thrown = intercept[BitcoinWalletDisabledException] {
instantiateEclairNode("F", ConfigFactory.load().getConfig("eclair").withFallback(withDefaultCommitment).withFallback(commonConfig))
}
assert(thrown === BitcoinWalletDisabledException(e = JsonRPCError(Error(-32601, "Method not found"))))
}
}

0 comments on commit 5af042a

Please sign in to comment.