diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 7c74fca89e..1f49f6fa67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -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 @@ -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") 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 c4ad31b42b..1135514485 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 @@ -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") @@ -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 { @@ -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 = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala new file mode 100644 index 0000000000..1aeddcad3f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala @@ -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")))) + } +} \ No newline at end of file