From af91698ba654050db866460170be2e54c0a117ef Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 21 Jun 2023 16:32:11 +0200 Subject: [PATCH] Create eclair-backed wallet automatically on startup When eclair starts, if it is configured to manage bitcoin core's onchain key, and the configured wallet does not exist yet, and eclair descriptor's timestamps are less then 2 hours old, eclair will automatically create the configured wallet with the appropriate options and import its descriptors. --- docs/BitcoinCoreKeys.md | 113 +++++++++++------- docs/Configure.md | 2 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 18 +-- .../main/scala/fr/acinq/eclair/Setup.scala | 23 +++- .../bitcoind/rpc/BitcoinCoreClient.scala | 31 ++++- .../keymanager/LocalOnchainKeyManager.scala | 4 +- .../bitcoind/BitcoinCoreClientSpec.scala | 15 +-- .../blockchain/bitcoind/BitcoindService.scala | 40 ++++--- .../publish/ReplaceableTxPublisherSpec.scala | 11 +- .../eclair/integration/IntegrationSpec.scala | 7 +- .../acinq/eclair/api/handlers/OnChain.scala | 12 +- 11 files changed, 179 insertions(+), 97 deletions(-) diff --git a/docs/BitcoinCoreKeys.md b/docs/BitcoinCoreKeys.md index 9040f45c71..b26a16902b 100644 --- a/docs/BitcoinCoreKeys.md +++ b/docs/BitcoinCoreKeys.md @@ -1,11 +1,14 @@ # Using Eclair to manage your Bitcoin Core wallet's private keys -You can configure Eclair to control (and never expose) the private keys of your Bitcoin Core wallet. This is very useful if your Bitcoin and Eclair nodes run on different machines for example, with a setup for the Bitcoin host that +You can configure Eclair to control (and never expose) the private keys of your Bitcoin Core wallet. This feature was designed to take advantage of deployements where your Eclair node runs in a +"trusted" runtime environment, but is also very useful if your Bitcoin and Eclair nodes run on different machines for example, with a setup for the Bitcoin host that is less secure than for Eclair (because it is shared among several services for example). +## Configuring Eclair and Bitcoin Core to use a new Eclair-backed bitcoin wallet + Follow these steps to delegate onchain key management to eclair: -1) Generate or import a BIP39 mnemonic code and passphrase +1) Generate a BIP39 mnemonic code and passphrase You can use any BIP39-compatible tool, including most hardware wallets. @@ -24,46 +27,82 @@ This is an example of `eclair-signer.conf` configuration file: ```hocon { - eclair { - signer { - wallet = "eclair" - mnemonics = "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title" - passphrase = "" - timestamp = 1686055705 - } - } + eclair { + signer { + wallet = "eclair" + mnemonics = "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title" + passphrase = "" + timestamp = 1686055705 + } + } } ``` -You must set `eclair.signer.wallet` to a name that is different from your current Bitcoin Core wallet. +3) Configure Eclair to handle private keys for this wallet + +Set `eclair.bitcoind.wallet` to the name of the wallet just created (`eclair` in the example above) and restart Eclair. +Eclair will automatically create a new, empty, descriptor-enabled, watch-only wallet in Bitcoin Core and import its descriptors. + +:warning: Eclair will not import descriptors if the timestamp set in your `eclair-signer.conf` is more than 2 hours old. If the mnmeonics and +passphrase that your are using are new, you can safely update this timestamp, but if they have been used before you will need to follow +the steps described in the next section. + +You now have a Bitcoin Core watch-only wallet for which only your Eclair node can sign transactions. This Bitcoin Core wallet can +safely be copied to another Bitcoin Core node to monitor your onchain funds. + +You can also use `eclair-cli getmasterxpub` to get a BIP32 extended public key that you can import into any compatible Bitcoin wallet +to create a watch-only wallet (Electrum for example) that you can use to monitor your Bitcoin Core balance. + +:warning: this means that your Bitcoin Core wallet cannot send funds on its own (since it cannot access private keys to sign transactions). +To send funds onchain you must use `eclair-cli sendonchain`. + +:warning: to backup the private keys of this wallet you must either backup your mnemonic code and passphrase, or backup the `eclair-signer.conf` file in your eclair +directory (default is `~/.eclair`) along with your channels and node seed files. + +:warning: You can also initialise a backup onchain wallet with the same mnemonic code and passphrase (on a hardware wallet for example), but be warned that using them may interfere with your node's operations (for example you may end up +double-spending funding transactions generated by your node). + +## Importing an existing Eclair-backed bitcoin core wallet + +The steps above describe how you can simply switch to a new Eclair-backed bitcoin wallet. +If you are already using an Eclair-backed bitcoin wallet that you want to move to another setup, you have several options: -3) Create an empty, descriptor-enabled, watch-only wallet in Bitcoin Core: +### Copy the bitcoin core wallet and `eclair-signer.conf` + +Copy your wallet file to a new Bitcoin Core node, and load it with `bitcoin-cli loadwallet`. Bitcoin Core may need to scan the blockchain which could take some time. +Copy `eclair-signer.conf` to your Eclair data directory and set `eclair.bitcoind.wallet` to the name of the wallet configured in `eclair-signer.conf` (and which should also be the name of your Bitcoin Core wallet). +Once your wallet has been imported, just restart Eclair. + +### Create an empty wallet manually and import Eclair descriptors + +1) Create an empty, descriptor-enabled, watch-only wallet in Bitcoin Core: :warning: The name must match the one that you set in `eclair-signer.conf` (here we use "eclair") ```shell $ bitcoin-cli -named createwallet wallet_name=eclair disable_private_keys=true blank=true descriptors=true load_on_startup=true ``` -4) Import public descriptors generated by Eclair +2) Import public descriptors generated by Eclair + +Copy `eclair-signer.conf` to your Eclair data directory but do not change `eclair.bitcoind.wallet`, and restart Eclair. `eclair-cli listdescriptors` will return public wallet descriptors in a format that is compatible with Bitcoin Core, and that you can import with `bitcoin-cli -rpcwallet=eclair importdescriptors` -For now, this descriptors follow the BIP84 standard (p2wpkh outputs). This is an example of descriptors generated by Eclair: ```json [ - { - "desc": "wpkh([0d9250da/84h/1h/0h]tpubDDGF9PnrXww2h1mKNjKiXoqdDFGEcZGCZUNq7g26LdzKXKiE31RrFWsogPy1uMLrbG8ksQ8eJS6u6KFLjYUUSVJRuwmMD2SYCr8uG1TcRgM/0/*)#jz5n2pcp", - "internal": false, - "timestamp": 1686055705, - "active": true - }, - { - "desc": "wpkh([0d9250da/84h/1h/0h]tpubDDGF9PnrXww2h1mKNjKiXoqdDFGEcZGCZUNq7g26LdzKXKiE31RrFWsogPy1uMLrbG8ksQ8eJS6u6KFLjYUUSVJRuwmMD2SYCr8uG1TcRgM/1/*)#rk3jh5ge", - "internal": true, - "timestamp": 1686055705, - "active": true - } + { + "desc": "wpkh([0d9250da/84h/1h/0h]tpubDDGF9PnrXww2h1mKNjKiXoqdDFGEcZGCZUNq7g26LdzKXKiE31RrFWsogPy1uMLrbG8ksQ8eJS6u6KFLjYUUSVJRuwmMD2SYCr8uG1TcRgM/0/*)#jz5n2pcp", + "internal": false, + "timestamp": 1686055705, + "active": true + }, + { + "desc": "wpkh([0d9250da/84h/1h/0h]tpubDDGF9PnrXww2h1mKNjKiXoqdDFGEcZGCZUNq7g26LdzKXKiE31RrFWsogPy1uMLrbG8ksQ8eJS6u6KFLjYUUSVJRuwmMD2SYCr8uG1TcRgM/1/*)#rk3jh5ge", + "internal": true, + "timestamp": 1686055705, + "active": true + } ] ``` @@ -73,24 +112,8 @@ You can combine the generation and import of descriptors with: $ eclair-cli getdescriptors | jq --raw-output -c | xargs -0 bitcoin-cli -rpcwallet=eclair importdescriptors ``` -:warning: If you are restoring an existing `eclair-signer.conf` file with a timestamp that is fairly old, importing descriptors can take a long time, and your -Bitcoin Core node will not be usable until it's done - -5) Configure Eclair to handle private keys for this wallet - -Set `eclair.bitcoind.wallet` to the name of the wallet just created (`eclair` in the example above) and restart Eclair. - -You now have a Bitcoin Core watch-only wallet for which only your Eclair node can sign transactions. This Bitcoin Core wallet can -safely be copied to another Bitcoin Core node to monitor your onchain funds. - -You can also use `eclair-cli getmasterxpub` to get a BIP32 extended public key that you can import into any compatible Bitcoin wallet -to create a watch-only wallet (Electrum for example) that you can use to monitor your Bitcoin Core balance. +:warning: Importing descriptors can take a long time, and your Bitcoin Core node will not be usable until it's done -:warning: this means that your Bitcoin Core wallet cannot send funds on its on (since it cannot access private keys to sign transactions). -To send funds onchain you must use `eclair-cli sendonchain`. +3) Configure Eclair to handle private keys for this wallet -:warning: to backup the private keys of this wallet you must either backup your mnemonic code and passphrase, or backup the `eclair-signer.conf` file in your eclair -directory (default is `~/.eclair`) along with your channels and node seed files. - -:warning: You can also initialise a backup onchain wallet with the same mnemonic code and passphrase (a hardware wallet for example), but be warned that using them may interfere with your node's operations (for example you may end up -double-spending funding transactions generated by your node). +Set `eclair.bitcoind.wallet` to the name of the wallet in `eclair-signer.conf` restart Eclair. diff --git a/docs/Configure.md b/docs/Configure.md index c11375718b..3e3626610f 100644 --- a/docs/Configure.md +++ b/docs/Configure.md @@ -4,7 +4,7 @@ * [Configuration file](#configuration-file) * [Changing the data directory](#changing-the-data-directory) - * [Splitting the configuration](#splitting-the-configuration)\ + * [Splitting the configuration](#splitting-the-configuration) * [Options reference](#options-reference) * [Customize features](#customize-features) * [Customize feerate tolerance](#customize-feerate-tolerance) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index f4b49cb8db..a60b9782d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -128,7 +128,7 @@ trait Eclair { def sentInfo(id: PaymentIdentifier)(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] - def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] + def sendOnChain(address: String, amount: Satoshi, confirmationTargetOrFeerate: Either[Long, FeeratePerByte]): Future[ByteVector32] def cpfpBumpFees(targetFeeratePerByte: FeeratePerByte, outpoints: Set[OutPoint]): Future[ByteVector32] @@ -341,13 +341,17 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] = { - val feeRate = if (confirmationTarget < 3) appKit.nodeParams.currentFeerates.fast - else if (confirmationTarget > 6) appKit.nodeParams.currentFeerates.slow - else appKit.nodeParams.currentFeerates.medium - + override def sendOnChain(address: String, amount: Satoshi, confirmationTargetOrFeerate: Either[Long, FeeratePerByte]): Future[ByteVector32] = { + val feeRatePerKw = confirmationTargetOrFeerate match { + case Left(blocks) => + if (blocks < 3) + appKit.nodeParams.currentFeerates.fast + else if (blocks > 6) appKit.nodeParams.currentFeerates.slow + else appKit.nodeParams.currentFeerates.medium + case Right(feeratePerByte) => FeeratePerKw(feeratePerByte) + } appKit.wallet match { - case w: BitcoinCoreClient => w.sendToPubkeyScript(Script.write(addressToPublicKeyScript(appKit.nodeParams.chainHash, address)), amount, feeRate) + case w: BitcoinCoreClient => w.sendToPubkeyScript(Script.write(addressToPublicKeyScript(appKit.nodeParams.chainHash, address)), amount, feeRatePerKw) case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend")) } } 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 e0114e403b..a22ffe643f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -75,8 +75,7 @@ import scala.util.{Failure, Success} class Setup(val datadir: File, pluginParams: Seq[PluginParams], seeds_opt: Option[Seeds] = None, - db: Option[Databases] = None, - onchainKeyManager_opt: Option[LocalOnchainKeyManager] = None)(implicit system: ActorSystem) extends Logging { + db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging { implicit val timeout: Timeout = Timeout(30 seconds) implicit val formats: org.json4s.Formats = org.json4s.DefaultFormats @@ -122,6 +121,8 @@ class Setup(val datadir: File, // early checks PortChecker.checkAvailable(serverBindingAddress) + val onchainKeyManager_opt = LocalOnchainKeyManager.load(datadir, NodeParams.hashFromChain(chain)) + val (bitcoin, bitcoinChainHash) = { val wallet = { val name = config.getString("bitcoind.wallet") @@ -140,6 +141,19 @@ class Setup(val datadir: File, host = config.getString("bitcoind.host"), port = config.getInt("bitcoind.rpcport"), wallet = wallet) + + def createDescriptorWallet(wallets: List[String]): Future[Boolean] = { + if (wallet.exists(name => wallets.contains(name))) { + // wallet already exists + Future.successful(true) + } else { + new BitcoinCoreClient(bitcoinClient, onchainKeyManager_opt).createDescriptorWallet().recover { case e => + logger.error(s"cannot create descriptor wallet", e) + throw BitcoinWalletNotCreatedException(wallet.getOrElse("")) + } + } + } + val future = for { json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) } // Make sure wallet support is enabled in bitcoind. @@ -147,6 +161,7 @@ class Setup(val datadir: File, .collect { case JArray(values) => values.map(value => value.extract[String]) } + true <- createDescriptorWallet(wallets) progress = (json \ "verificationprogress").extract[Double] ibd = (json \ "initialblockdownload").extract[Boolean] blocks = (json \ "blocks").extract[Long] @@ -247,7 +262,7 @@ class Setup(val datadir: File, finalPubkey = new AtomicReference[PublicKey](null) pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS) - bitcoinClient = new BitcoinCoreClient(bitcoin, onchainKeyManager_opt.orElse(LocalOnchainKeyManager.load(datadir, nodeParams.chainHash))) with OnchainPubkeyCache { + bitcoinClient = new BitcoinCoreClient(bitcoin, onchainKeyManager_opt) with OnchainPubkeyCache { val refresher: typed.ActorRef[OnchainPubkeyRefresher.Command] = system.spawn(Behaviors.supervise(OnchainPubkeyRefresher(this, finalPubkey, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") override def getP2wpkhPubkey(renew: Boolean): PublicKey = { @@ -463,6 +478,8 @@ case class BitcoinDefaultWalletException(loaded: List[String]) extends RuntimeEx 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 class BitcoinWalletNotCreatedException(wallet: String) extends RuntimeException(s"configured wallet \"$wallet\" does not exist and could not be created.") + case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api") case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index a4d4fc3022..7d7c08ab2f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -35,6 +35,7 @@ import org.json4s.JsonAST._ import scodec.bits.ByteVector import java.util.Base64 +import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.{Failure, Success, Try} @@ -58,6 +59,34 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManag val useEclairSigner = onchainKeyManager_opt.exists(m => rpcClient.wallet.contains(m.wallet)) + //------------------------- WALLET -------------------------// + def importDescriptors(descriptors: Seq[Descriptor])(implicit ec: ExecutionContext): Future[Boolean] = { + rpcClient.invoke("importdescriptors", descriptors).collect { + case JArray(results) => results.forall(item => { + val JBool(success) = item \ "success" + val JArray(_) = item \ "warnings" + success + }) + } + } + + def createDescriptorWallet()(implicit ec: ExecutionContext): Future[Boolean] = { + onchainKeyManager_opt match { + case None => Future.successful(true) // no eclair-backed wallet is configured + case Some(onchainKeyManager) if !rpcClient.wallet.contains(onchainKeyManager.wallet) => Future.successful(true) // configured wallet has a different name + case Some(onchainKeyManager) if !onchainKeyManager.getDescriptors(0).descriptors.forall(desc => TimestampSecond(desc.timestamp) >= TimestampSecond.now() - 2.hours) => + logger.warn(s"descriptors are too old, you will need to manually import them and select how far back to rescan") + Future.failed(new RuntimeException("Could not import descriptors, please check logs for details")) + case Some(onchainKeyManager) => + logger.info(s"Creating a new on-chain eclair-backed wallet in bitcoind: ${onchainKeyManager.wallet}") + for { + _ <- rpcClient.invoke("createwallet", onchainKeyManager.wallet, true, true, "", false, true, true) + _ = logger.info(s"importing new descriptors ${onchainKeyManager.getDescriptors(0).descriptors}") + result <- importDescriptors(onchainKeyManager.getDescriptors(0).descriptors) + } yield result + } + } + //------------------------- TRANSACTIONS -------------------------// def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] = @@ -783,7 +812,7 @@ object BitcoinCoreClient { def toSatoshi(btcAmount: BigDecimal): Satoshi = Satoshi(btcAmount.bigDecimal.scaleByPowerOfTen(8).longValue) - case class Descriptor(desc: String, internal: Boolean = false, timestamp: Either[String, Long] = Left("now"), active: Boolean = true) + case class Descriptor(desc: String, internal: Boolean = false, timestamp: Long, active: Boolean = true) case class Descriptors(wallet_name: String, descriptors: Seq[Descriptor]) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala index 405a69cabd..6da6e901a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManager.scala @@ -82,8 +82,8 @@ class LocalOnchainKeyManager(override val wallet: String, seed: ByteVector, time val changeDesc = s"wpkh([${this.fingerPrintHex}/$keyPath]${encode(accountPub, prefix)}/1/*)" Descriptors(wallet_name = wallet, descriptors = List( - Descriptor(desc = s"$receiveDesc#${descriptorChecksum(receiveDesc)}", internal = false, active = true, timestamp = Right(timestamp.toLong)), - Descriptor(desc = s"$changeDesc#${descriptorChecksum(changeDesc)}", internal = true, active = true, timestamp = Right(timestamp.toLong)), + Descriptor(desc = s"$receiveDesc#${descriptorChecksum(receiveDesc)}", internal = false, active = true, timestamp = timestamp.toLong), + Descriptor(desc = s"$changeDesc#${descriptorChecksum(changeDesc)}", internal = true, active = true, timestamp = timestamp.toLong), )) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 257db569a7..070290ed89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -83,7 +83,6 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A sender.expectMsgType[JValue] } - test("fund transactions") { val sender = TestProbe() val bitcoinClient = makeBitcoinCoreClient @@ -1436,10 +1435,10 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val master = DeterministicWallet.generate(seed) val onchainKeyManager = new LocalOnchainKeyManager(s"eclair_$hex", seed, TimestampSecond.now(), Block.RegtestGenesisBlock.hash) - bitcoinrpcclient.invoke("createwallet", s"eclair_$hex", true, false, "", false, true, true, false).pipeTo(sender.ref) - sender.expectMsgType[JValue] val jsonRpcClient = new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(s"eclair_$hex")) - importEclairDescriptors(jsonRpcClient, onchainKeyManager) + val wallet1 = new BitcoinCoreClient(jsonRpcClient, Some(onchainKeyManager)) + wallet1.createDescriptorWallet().pipeTo(sender.ref) + sender.expectMsg(true) // this account xpub can be used to create a watch-only wallet val accountXPub = DeterministicWallet.encode( @@ -1447,7 +1446,6 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { DeterministicWallet.vpub) assert(onchainKeyManager.getOnchainMasterPubKey(0) == accountXPub) - val wallet1 = new BitcoinCoreClient(jsonRpcClient, Some(onchainKeyManager)) def getBip32Path(address: String): DeterministicWallet.KeyPath = { wallet1.rpcClient.invoke("getaddressinfo", address).pipeTo(sender.ref) @@ -1477,18 +1475,15 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val seed = randomBytes32() val hex = seed.toString() val onchainKeyManager = new LocalOnchainKeyManager(s"eclair_$hex", seed, TimestampSecond.now(), Block.RegtestGenesisBlock.hash) - bitcoinrpcclient.invoke("createwallet", s"eclair_$hex", true, false, "", false, true, true, false).pipeTo(sender.ref) - sender.expectMsgType[JValue] - val jsonRpcClient = new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(s"eclair_$hex")) - importEclairDescriptors(jsonRpcClient, onchainKeyManager) val wallet1 = new BitcoinCoreClient(jsonRpcClient, Some(onchainKeyManager)) + wallet1.createDescriptorWallet().pipeTo(sender.ref) + sender.expectMsg(true) wallet1.getReceiveAddress().pipeTo(sender.ref) val address = sender.expectMsgType[String] // we can send to an onchain address if eclair signs the transactions sendToAddress(address, 100_0000.sat) - generateBlocks(1) // but bitcoin core's sendtoaddress RPC call will fail because wallets uses an external signer 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 f337a6e478..bf50598fc5 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 @@ -21,17 +21,16 @@ import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcAmount, MilliBtc, Satoshi, Transaction, TxOut, computeP2WpkhAddress} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcAmount, MilliBtc, MnemonicCode, Satoshi, Transaction, TxOut, computeP2WpkhAddress} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.PreviousTx import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.{SafeCookie, UserPassword} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCAuthMethod, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKB, FeeratePerKw} -import fr.acinq.eclair.crypto.keymanager.{LocalOnchainKeyManager, OnchainKeyManager} +import fr.acinq.eclair.crypto.keymanager.LocalOnchainKeyManager import fr.acinq.eclair.integration.IntegrationSpec import fr.acinq.eclair.{BlockHeight, TestUtils, TimestampSecond, addressToPublicKeyScript, randomKey} import grizzled.slf4j.Logging import org.json4s.JsonAST._ -import scodec.bits.ByteVector import sttp.client3.okhttp.OkHttpFutureBackend import java.io.File @@ -73,7 +72,25 @@ trait BitcoindService extends Logging { var bitcoinrpcclient: BitcoinJsonRPCClient = _ var bitcoinrpcauthmethod: BitcoinJsonRPCAuthMethod = _ var bitcoincli: ActorRef = _ - val onchainKeyManager = new LocalOnchainKeyManager("eclair", ByteVector.fromValidHex("01" * 32), TimestampSecond.now(), Block.RegtestGenesisBlock.hash) + val mnemonics = "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title" + val passphrase = "" + val eclairSignerConf = + s""" + |{ + | eclair { + | signer { + | wallet = "eclair" + | mnemonics = $mnemonics + | passphrase = "$passphrase" + | timestamp = ${TimestampSecond.now().toLong} + | } + | } + |} + |""".stripMargin + val onchainKeyManager = { + new LocalOnchainKeyManager("eclair", MnemonicCode.toSeed(mnemonics, passphrase), TimestampSecond.now(), Block.RegtestGenesisBlock.hash) + } + def startBitcoind(useCookie: Boolean = false, defaultAddressType_opt: Option[String] = None, mempoolSize_opt: Option[Int] = None, // mempool size in MB @@ -157,12 +174,8 @@ trait BitcoindService extends Logging { val sender = TestProbe() waitForBitcoindUp(sender) if (useEclairSigner) { - // wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup, external_signer - bitcoinrpcclient.invoke("createwallet", defaultWallet, true, false, "", false, true, true, false).pipeTo(sender.ref) - sender.expectMsgType[JValue] - - val jsonRpcClient = new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(defaultWallet)) - importEclairDescriptors(jsonRpcClient, onchainKeyManager) + makeBitcoinCoreClient.createDescriptorWallet().pipeTo(sender.ref) + sender.expectMsg(true) } else { sender.send(bitcoincli, BitcoinReq("createwallet", defaultWallet)) sender.expectMsgType[JValue] @@ -172,13 +185,6 @@ trait BitcoindService extends Logging { awaitCond(currentBlockHeight(sender) >= BlockHeight(150), max = 3 minutes, interval = 2 second) } - - def importEclairDescriptors(jsonRpcClient: BitcoinJsonRPCClient, keyManager: OnchainKeyManager, probe: TestProbe = TestProbe()): Unit = { - val descriptors = keyManager.getDescriptors(0).descriptors - jsonRpcClient.invoke("importdescriptors", descriptors).pipeTo(probe.ref) - probe.expectMsgType[JValue] - } - def generateBlocks(blockCount: Int, address: Option[String] = None, timeout: FiniteDuration = 10 seconds)(implicit system: ActorSystem): Unit = { val sender = TestProbe() val addressToUse = address match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index a9fc08575e..adbfbaadd1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -41,7 +41,6 @@ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck} import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} -import org.json4s.JValue import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -1657,20 +1656,18 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS val entropy = ByteVector.fromValidHex("01" * 32) val seed = MnemonicCode.toSeed(MnemonicCode.toMnemonics(entropy), walletName) val keyManager = new LocalOnchainKeyManager(walletName, seed, TimestampSecond.now(), Block.RegtestGenesisBlock.hash) - bitcoinrpcclient.invoke("createwallet", walletName, true, false, "", false, true, true, false).pipeTo(probe.ref) - probe.expectMsgType[JValue] - val walletRpcClient = new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(walletName)) - importEclairDescriptors(walletRpcClient, keyManager) val walletClient = new BitcoinCoreClient(walletRpcClient, Some(keyManager)) with OnchainPubkeyCache { - val pubkey = { + lazy val pubkey = { getP2wpkhPubkey().pipeTo(probe.ref) probe.expectMsgType[PublicKey] } override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey } - + walletClient.createDescriptorWallet().pipeTo(probe.ref) + probe.expectMsg(true) + (walletRpcClient, walletClient) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index dd6baf7413..a4f6a3bf1a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -35,6 +35,7 @@ import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike import java.io.File +import java.nio.file.Files import java.util.Properties import scala.concurrent.Await import scala.concurrent.duration._ @@ -146,8 +147,12 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit def instantiateEclairNode(name: String, config: Config): Unit = { val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name") datadir.mkdirs() + if (useEclairSigner) { + Files.writeString(datadir.toPath.resolve("eclair-signer.conf"), eclairSignerConf) + } implicit val system: ActorSystem = ActorSystem(s"system-$name", config) - val setup = new Setup(datadir, pluginParams = Seq.empty, seeds_opt = Some(Setup.Seeds(randomBytes32(), randomBytes32())), onchainKeyManager_opt = Some(onchainKeyManager)) + + val setup = new Setup(datadir, pluginParams = Seq.empty, seeds_opt = Some(Setup.Seeds(randomBytes32(), randomBytes32()))) val kit = Await.result(setup.bootstrap, 10 seconds) nodes = nodes + (name -> kit) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala index 9ed4366d05..51772a2d3f 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala @@ -34,9 +34,15 @@ trait OnChain { } val sendOnChain: Route = postRequest("sendonchain") { implicit t => - formFields("address".as[String], "amountSatoshis".as[Satoshi], "confirmationTarget".as[Long]) { - (address, amount, confirmationTarget) => - complete(eclairApi.sendOnChain(address, amount, confirmationTarget)) + formFields("address".as[String], "amountSatoshis".as[Satoshi], "confirmationTarget".as[Long].?, "feeRatePerByte".as[Int].?) { + (address, amount, confirmationTarget_opt, feeratePerByte_opt) => { + val confirmationTargetOrFeerate = (feeratePerByte_opt, confirmationTarget_opt) match { + case (Some(feeratePerByte), _) => Right(FeeratePerByte(Satoshi(feeratePerByte))) + case (None, Some(confirmationTarget)) => Left(confirmationTarget) + case _ => throw new IllegalArgumentException("You must provide a confirmation target (in blocks) or a fee rate (in sat/vb)") + } + complete(eclairApi.sendOnChain(address, amount, confirmationTargetOrFeerate)) + } } }