From 0e116a6d41f3dc5947b40be386db5a91ef48a809 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 30 May 2023 10:20:36 +0200 Subject: [PATCH] Add a separate configuration file for Eclair's onchain signer Eclair's onchain signer now has its own `eclair-signer.conf` configuration file in HOCON format. It includes BIP39 mnemonic codes and passphrase, a wallet name and a timestamp. When an `eclair-signer.conf` file is found, Eclair's API will return descriptors that can be imported into an empty watch-only Bitcoin Wallet. When wallet name in `eclair-signer.conf` matches the name of the Bitcoin Core wallet defined in `eclair.conf` (`eclair.bitcoind.wallet`), Eclair will bypass Bitcoin Core and sign onchain transactions directly. --- README.md | 73 +------------- docs/BitcoinCoreKeys.md | 96 +++++++++++++++++++ docs/Configure.md | 2 +- docs/Guides.md | 1 + .../main/scala/fr/acinq/eclair/Eclair.scala | 14 ++- .../scala/fr/acinq/eclair/NodeParams.scala | 10 +- .../main/scala/fr/acinq/eclair/Setup.scala | 17 ++-- .../rpc/BasicBitcoinJsonRPCClient.scala | 2 +- .../rpc/BatchingBitcoinJsonRPCClient.scala | 1 + .../bitcoind/rpc/BitcoinCoreClient.scala | 25 +++-- .../bitcoind/rpc/BitcoinJsonRPCClient.scala | 1 + .../keymanager/LocalOnchainKeyManager.scala | 40 +++++--- .../crypto/keymanager/OnchainKeyManager.scala | 5 +- .../scala/fr/acinq/eclair/StartupSpec.scala | 7 +- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +- .../bitcoind/BitcoinCoreClientSpec.scala | 14 +-- .../blockchain/bitcoind/BitcoindService.scala | 11 +-- .../fee/BitcoinCoreFeeProviderSpec.scala | 2 +- .../channel/InteractiveTxBuilderSpec.scala | 2 +- .../publish/ReplaceableTxPublisherSpec.scala | 8 +- .../LocalChannelKeyManagerSpec.scala | 2 +- .../keymanager/LocalNodeKeyManagerSpec.scala | 2 +- .../LocalOnchainKeyManagerSpec.scala | 10 +- .../integration/ChannelIntegrationSpec.scala | 13 +-- .../eclair/integration/IntegrationSpec.scala | 3 +- .../basic/fixtures/MinimalNodeFixture.scala | 1 - .../acinq/eclair/api/handlers/OnChain.scala | 20 +--- 27 files changed, 211 insertions(+), 177 deletions(-) create mode 100644 docs/BitcoinCoreKeys.md diff --git a/README.md b/README.md index 9e85675ac3..f1e43b008e 100644 --- a/README.md +++ b/README.md @@ -165,78 +165,7 @@ Setting these parameters lets you unblock long chains of unconfirmed channel fun With the default `bitcoind` parameters, if your node created a chain of 25 unconfirmed funding transactions with a low-feerate, you wouldn't be able to use CPFP to raise their fees because your CPFP transaction would likely be rejected by the rest of the network. -#### 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 -is less secure than for Eclair (because it is shared among several services for example). - -1) Create an empty, descriptor-enabled, watch-only wallet in Bitcoin Core: - -```shell -$ bitcoin-cli -named createwallet wallet_name=eclair disable_private_keys=true blank=true descriptors=true load_on_startup=true -``` - -2) Import public descriptors generated by 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` - -This is an example of descriptors generated by Eclair: - -```json -{ - "wallet_name": "eclair", - "descriptors": [ - { - "desc": "wpkh([0f09f381/84'/1'/0']tpubDCAVQRxWnkXjyYbsHdUsHA7krYSRyVS8EuWeWE6K2V34goMaUCdCTjfSFS8ZkE5iESaWQsZoM9HL7ZANi5bW7Ly3EqDqthEyvdZHSrBTNHq/0/*)#gj3mq2sl", - "timestamp": 1684150749, - "active": true, - "internal": false, - "range": [ - 0, - 1003 - ], - "next": 4 - }, - { - "desc": "wpkh([0f09f381/84'/1'/0']tpubDCAVQRxWnkXjyYbsHdUsHA7krYSRyVS8EuWeWE6K2V34goMaUCdCTjfSFS8ZkE5iESaWQsZoM9HL7ZANi5bW7Ly3EqDqthEyvdZHSrBTNHq/1/*)#ex56alq8", - "timestamp": 1684150749, - "active": true, - "internal": true, - "range": [ - 0, - 1001 - ], - "next": 2 - } - ] -} -``` - -You can combine the generation and import of descriptors with: - -```shell -$ eclair-cli getdescriptors | jq --raw-output -c | xargs -0 bitcoin-cli -rpcwallet=eclair importdescriptors -``` - -3) Configure Eclair to handle private keys for this wallet - -Add the following lines to your `eclair.conf` file: - -``` -eclair.bitcoind.wallet = eclair -eclair.bitcoind.use-eclair-signer = true -``` - -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). - -: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`. - -:warning: to backup the private keys of this wallet you must backup the onchain seed file `onchain_seed.dat` that is located in your eclair -directory (default is `~/.eclair`) along with your channels and node seed files. +You can also configure Eclair to manage Bitcoin Core's private keys, see our [guides](./docs/Guides.md) for more details. ### Java Environment Variables diff --git a/docs/BitcoinCoreKeys.md b/docs/BitcoinCoreKeys.md new file mode 100644 index 0000000000..9040f45c71 --- /dev/null +++ b/docs/BitcoinCoreKeys.md @@ -0,0 +1,96 @@ +# 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 +is less secure than for Eclair (because it is shared among several services for example). + +Follow these steps to delegate onchain key management to eclair: + +1) Generate or import a BIP39 mnemonic code and passphrase + +You can use any BIP39-compatible tool, including most hardware wallets. + +2) Create an `eclair-signer.conf` configuration file add it to eclair's data directory + +A signer configuration file uses the HOCON format that we already use for `eclair.conf` and must include the following options: + + key | description +--------------------------|-------------------------------------------------------------------------- + eclair.signer.wallet | wallet name + eclair.signer.mnemonics | BIP39 mnemonic words + eclair.signer.passphrase | passphrase + eclair.signer.timestamp | wallet creation UNIX timestamp. Set to the current time for new wallets. + +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 + } + } +} +``` + +You must set `eclair.signer.wallet` to a name that is different from your current Bitcoin Core wallet. + +3) 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 + +`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 + } +] +``` + +You can combine the generation and import of descriptors with: + +```shell +$ 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: 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`. + +: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). diff --git a/docs/Configure.md b/docs/Configure.md index 3e3626610f..c11375718b 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/docs/Guides.md b/docs/Guides.md index 6f7a19d0a9..85c07ed300 100644 --- a/docs/Guides.md +++ b/docs/Guides.md @@ -4,6 +4,7 @@ This section contains how-to guides for more advanced scenarios: * [Customize Logging](./Logging.md) * [Customize Features](./Features.md) +* [Manage Bitcoin Core's private keys](./BitcoinCoreKeys.md) * [Use Tor with Eclair](./Tor.md) * [Multipart Payments](./MultipartPayments.md) * [Trampoline Payments](./TrampolinePayments.md) 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 ccf04bdecf..f4b49cb8db 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} import fr.acinq.eclair.blockchain.OnChainWallet.OnChainBalance import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.WalletTx +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{Descriptors, WalletTx} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx @@ -180,7 +180,7 @@ trait Eclair { def getOnchainMasterPubKey(account: Long): String - def getDescriptors(account: Long): (List[String], List[String]) + def getDescriptors(account: Long): Descriptors def stop(): Future[Unit] } @@ -705,9 +705,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { payOfferInternal(offer, amount, quantity, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true).mapTo[PaymentEvent] } - override def getDescriptors(account: Long): (List[String], List[String]) = this.appKit.nodeParams.onchainKeyManager.getDescriptors(account) + override def getDescriptors(account: Long): Descriptors = appKit.wallet match { + case bitcoinCoreClient: BitcoinCoreClient if bitcoinCoreClient.onchainKeyManager_opt.isDefined => bitcoinCoreClient.onchainKeyManager_opt.get.getDescriptors(account) + case _ => throw new RuntimeException("onchain seed is not configured") + } - override def getOnchainMasterPubKey(account: Long): String = this.appKit.nodeParams.onchainKeyManager.getOnchainMasterPubKey(account) + override def getOnchainMasterPubKey(account: Long): String = appKit.wallet match { + case bitcoinCoreClient: BitcoinCoreClient if bitcoinCoreClient.onchainKeyManager_opt.isDefined => bitcoinCoreClient.onchainKeyManager_opt.get.getOnchainMasterPubKey(account) + case _ => throw new RuntimeException("onchain seed is not configured") + } override def stop(): Future[Unit] = { // README: do not make this smarter or more complex ! diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index f6e5f1e6be..9b7ce7535c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy} import fr.acinq.eclair.crypto.Noise.KeyPair -import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnchainKeyManager} +import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager} import fr.acinq.eclair.db._ import fr.acinq.eclair.io.MessageRelay.{RelayAll, RelayChannelsOnly, RelayPolicy} import fr.acinq.eclair.io.PeerConnection @@ -54,7 +54,6 @@ import scala.jdk.CollectionConverters._ */ case class NodeParams(nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, - onchainKeyManager: OnchainKeyManager, instanceId: UUID, // a unique instance ID regenerated after each restart private val blockHeight: AtomicLong, private val feerates: AtomicReference[FeeratesPerKw], @@ -164,7 +163,6 @@ object NodeParams extends Logging { val oldSeedPath = new File(datadir, "seed.dat") val nodeSeedFilename: String = "node_seed.dat" val channelSeedFilename: String = "channel_seed.dat" - val onchainSeedFilename: String = "onchain_seed.dat" def getSeed(filename: String): ByteVector = { val seedPath = new File(datadir, filename) @@ -182,8 +180,7 @@ object NodeParams extends Logging { val nodeSeed = getSeed(nodeSeedFilename) val channelSeed = getSeed(channelSeedFilename) - val onchainSeed = getSeed(onchainSeedFilename) - Seeds(nodeSeed, channelSeed, onchainSeed) + Seeds(nodeSeed, channelSeed) } private val chain2Hash: Map[String, ByteVector32] = Map( @@ -214,7 +211,7 @@ object NodeParams extends Logging { } } - def makeNodeParams(config: Config, instanceId: UUID, nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, onchainKeyManager: OnchainKeyManager, + def makeNodeParams(config: Config, instanceId: UUID, nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feerates: AtomicReference[FeeratesPerKw], pluginParams: Seq[PluginParams] = Nil): NodeParams = { // check configuration for keys that have been renamed @@ -478,7 +475,6 @@ object NodeParams extends Logging { NodeParams( nodeKeyManager = nodeKeyManager, channelKeyManager = channelKeyManager, - onchainKeyManager = onchainKeyManager, instanceId = instanceId, blockHeight = blockHeight, feerates = feerates, 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 307a458477..e0114e403b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -75,7 +75,8 @@ import scala.util.{Failure, Success} class Setup(val datadir: File, pluginParams: Seq[PluginParams], seeds_opt: Option[Seeds] = None, - db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging { + db: Option[Databases] = None, + onchainKeyManager_opt: Option[LocalOnchainKeyManager] = None)(implicit system: ActorSystem) extends Logging { implicit val timeout: Timeout = Timeout(30 seconds) implicit val formats: org.json4s.Formats = org.json4s.DefaultFormats @@ -95,17 +96,13 @@ class Setup(val datadir: File, datadir.mkdirs() val config = system.settings.config.getConfig("eclair") - val Seeds(nodeSeed, channelSeed, onchainSeed) = seeds_opt.getOrElse(NodeParams.getSeeds(datadir)) + val Seeds(nodeSeed, channelSeed) = seeds_opt.getOrElse(NodeParams.getSeeds(datadir)) val chain = config.getString("chain") val chaindir = new File(datadir, chain) chaindir.mkdirs() val nodeKeyManager = new LocalNodeKeyManager(nodeSeed, NodeParams.hashFromChain(chain)) val channelKeyManager = new LocalChannelKeyManager(channelSeed, NodeParams.hashFromChain(chain)) - val onchainKeyManager = { - val passphrase = if (config.hasPath("bitcoind.eclair-signer-passphrase")) config.getString("bitcoind.eclair-signer-passphrase") else "" - new LocalOnchainKeyManager(onchainSeed, NodeParams.hashFromChain(chain), passphrase) - } /** * This counter holds the current blockchain height. @@ -188,7 +185,7 @@ class Setup(val datadir: File, logger.info(s"connecting to database with instanceId=$instanceId") val databases = Databases.init(config.getConfig("db"), instanceId, chaindir, db) - val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, onchainKeyManager, initTor(), databases, blockHeight, feeratesPerKw, pluginParams) + val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, initTor(), databases, blockHeight, feeratesPerKw, pluginParams) logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") assert(bitcoinChainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$bitcoinChainHash)") @@ -250,8 +247,7 @@ class Setup(val datadir: File, finalPubkey = new AtomicReference[PublicKey](null) pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS) - useEclairSigner = if (config.hasPath("bitcoind.use-eclair-signer")) config.getBoolean("bitcoind.use-eclair-signer") else false - bitcoinClient = new BitcoinCoreClient(bitcoin, if (useEclairSigner) Some(onchainKeyManager) else None) with OnchainPubkeyCache { + bitcoinClient = new BitcoinCoreClient(bitcoin, onchainKeyManager_opt.orElse(LocalOnchainKeyManager.load(datadir, nodeParams.chainHash))) 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 = { @@ -260,6 +256,7 @@ class Setup(val datadir: File, key } } + _ = if (bitcoinClient.useEclairSigner) logger.info("using eclair to sign bitcoin core transactions") initialPubkey <- bitcoinClient.getP2wpkhPubkey() _ = finalPubkey.set(initialPubkey) @@ -426,7 +423,7 @@ class Setup(val datadir: File, object Setup { - final case class Seeds(nodeSeed: ByteVector, channelSeed: ByteVector, onchainSeed: ByteVector) + final case class Seeds(nodeSeed: ByteVector, channelSeed: ByteVector) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala index 4886d66ac4..d9c1731c30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala @@ -32,7 +32,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} -class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, wallet: Option[String] = None)(implicit sb: SttpBackend[Future, _]) extends BitcoinJsonRPCClient { +class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, override val wallet: Option[String] = None)(implicit sb: SttpBackend[Future, _]) extends BitcoinJsonRPCClient { implicit val formats: Formats = DefaultFormats.withBigDecimal + ByteVector32Serializer + ByteVector32KmpSerializer diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BatchingBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BatchingBitcoinJsonRPCClient.scala index 16da82573b..6e36041234 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BatchingBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BatchingBitcoinJsonRPCClient.scala @@ -27,6 +27,7 @@ import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} class BatchingBitcoinJsonRPCClient(rpcClient: BasicBitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends BitcoinJsonRPCClient { + override def wallet: Option[String] = rpcClient.wallet implicit val timeout: Timeout = Timeout(1 hour) 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 989ad7bd78..a4d4fc3022 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 @@ -47,7 +47,7 @@ import scala.util.{Failure, Success, Try} * The Bitcoin Core client provides some high-level utility methods to interact with Bitcoin Core. * * @param rpcClient bitcoin core JSON rpc client - * @param onchainKeyManager_opt optional onchain key manager. If provided, it will be used to sign transactions (it is assumed that bitcoin + * @param onchainKeyManager_opt optional onchain key manager. If provided and its wallet name matcher our rpcClient wallet's name, it will be used to sign transactions (it is assumed that bitcoin * core uses a watch-only wallet with descriptors generated by Eclair with this onchain key manager) */ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManager_opt: Option[OnchainKeyManager] = None) extends OnChainWallet with Logging { @@ -56,6 +56,8 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManag implicit val formats: Formats = org.json4s.DefaultFormats + val useEclairSigner = onchainKeyManager_opt.exists(m => rpcClient.wallet.contains(m.wallet)) + //------------------------- TRANSACTIONS -------------------------// def getTransaction(txid: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] = @@ -427,12 +429,14 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManag def signPsbt(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int])(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = { - def sign(psbt: Psbt): Future[ProcessPsbtResponse] = onchainKeyManager_opt match { - case Some(keyManager) => Try(keyManager.signPsbt(psbt, ourInputs, ourOutputs)) match { + def sign(psbt: Psbt): Future[ProcessPsbtResponse] = if (useEclairSigner) { + val onchainKeyManager = onchainKeyManager_opt.getOrElse(throw new RuntimeException("We are configured to use an eclair signer has not been loaded")) // this should not be possible + Try(onchainKeyManager.signPsbt(psbt, ourInputs, ourOutputs)) match { case Success(signedPsbt) => Future.successful(ProcessPsbtResponse(signedPsbt, signedPsbt.extract().isRight)) case Failure(error) => Future.failed(error) } - case None => processPsbt(psbt, sign = true) + } else { + processPsbt(psbt, sign = true) } for { @@ -526,9 +530,12 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onchainKeyManag val extracted = PublicKey(ByteVector.fromValidHex(rawKey)) // check that when we manage private keys we can re-compute the public key we got from bitcoin core // and that the address and public key match - val keyPath1 = keyPath.replace('h', '\'') // our bitcoin lib expects a ' suffix for hardened indexes and does not yet accept the h suffix - val computed_opt = this.onchainKeyManager_opt.map(_.getPublicKey(DeterministicWallet.KeyPath(keyPath1))) - require(computed_opt.forall(_ == (extracted, address)), "cannot recompute pubkey generated by bitcoin core") + if (useEclairSigner) { + val onchainKeyManager = onchainKeyManager_opt.getOrElse(throw new RuntimeException("We are configured to use an eclair signer has not been loaded")) // this should not be possible + val keyPath1 = keyPath.replace('h', '\'') // our bitcoin lib expects a ' suffix for hardened indexes and does not yet accept the h suffix + val computed = onchainKeyManager.getPublicKey(DeterministicWallet.KeyPath(keyPath1)) + require(computed == (extracted, address), "cannot recompute pubkey generated by bitcoin core") + } extracted } } @@ -776,5 +783,7 @@ object BitcoinCoreClient { def toSatoshi(btcAmount: BigDecimal): Satoshi = Satoshi(btcAmount.bigDecimal.scaleByPowerOfTen(8).longValue) - case class Descriptor(desc: String, internal: Boolean = false, timestamp: String = "now", active: Boolean = true) + case class Descriptor(desc: String, internal: Boolean = false, timestamp: Either[String, Long] = Left("now"), 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/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala index 5373cc7947..63468d9ca9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala @@ -22,6 +22,7 @@ import java.io.IOException import scala.concurrent.{ExecutionContext, Future} trait BitcoinJsonRPCClient { + def wallet: Option[String] def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] 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 22217780c7..405a69cabd 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 @@ -1,20 +1,40 @@ package fr.acinq.eclair.crypto.keymanager +import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.ScriptWitness import fr.acinq.bitcoin.psbt.{Psbt, SignPsbtResult} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, DeterministicWallet, MnemonicCode, computeBIP84Address} import fr.acinq.bitcoin.utils.EitherKt +import fr.acinq.eclair.TimestampSecond +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{Descriptor, Descriptors} import grizzled.slf4j.Logging import scodec.bits.ByteVector +import java.io.File import scala.jdk.CollectionConverters.MapHasAsScala -object LocalOnchainKeyManager { +object LocalOnchainKeyManager extends Logging { def descriptorChecksum(span: String): String = fr.acinq.bitcoin.Descriptor.checksum(span) + + def load(datadir: File, chainHash: ByteVector32): Option[LocalOnchainKeyManager] = { + val file = new File(datadir, "eclair-signer.conf") + if (file.exists()) { + val config = ConfigFactory.parseFile(file) + val wallet = config.getString("eclair.signer.wallet") + val mnemonics = config.getString("eclair.signer.mnemonics") + val passphrase = config.getString("eclair.signer.passphrase") + val timestamp = config.getLong("eclair.signer.timestamp") + val keyManager = new LocalOnchainKeyManager(wallet, MnemonicCode.toSeed(mnemonics, passphrase), TimestampSecond(timestamp), chainHash) + logger.info(s"using onchain key manager wallet=${wallet} xpub=${keyManager.getOnchainMasterPubKey(0)}") + Some(keyManager) + } else { + None + } + } } -class LocalOnchainKeyManager(entropy: ByteVector, chainHash: ByteVector32, passphrase: String = "") extends OnchainKeyManager with Logging { +class LocalOnchainKeyManager(override val wallet: String, seed: ByteVector, timestamp: TimestampSecond, chainHash: ByteVector32) extends OnchainKeyManager with Logging { import LocalOnchainKeyManager._ @@ -24,8 +44,6 @@ class LocalOnchainKeyManager(entropy: ByteVector, chainHash: ByteVector32, passp // by Eclair to fund transactions (only Eclair will be able to sign wallet inputs). // m / purpose' / coin_type' / account' / change / address_index - private val mnemonics = MnemonicCode.toMnemonics(entropy) - private val seed = MnemonicCode.toSeed(mnemonics, passphrase) private val master = DeterministicWallet.generate(seed) private val fingerprint = DeterministicWallet.fingerprint(master) & 0xFFFFFFFFL private val fingerPrintHex = String.format("%8s", fingerprint.toHexString).replace(' ', '0') @@ -45,14 +63,13 @@ class LocalOnchainKeyManager(entropy: ByteVector, chainHash: ByteVector32, passp case Block.LivenetGenesisBlock.hash => zpub case _ => throw new IllegalArgumentException(s"invalid chain hash ${chainHash}") } - // master pubkey for account 0 is m/84'/{0'/1'}/0' val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKey, hardened(account))) DeterministicWallet.encode(accountPub, prefix) } - override def getDescriptors(account: Long): (List[String], List[String]) = { - val keyPath = s"$rootPath/$account'" + override def getDescriptors(account: Long): Descriptors = { + val keyPath = s"$rootPath/$account'".replace('\'', 'h') // Bitcoin Core understands both ' and h suffix for hardened derivation, and h is much easier to parse for external tools val prefix: Int = chainHash match { case Block.LivenetGenesisBlock.hash => xpub case _ => tpub @@ -63,10 +80,11 @@ class LocalOnchainKeyManager(entropy: ByteVector, chainHash: ByteVector32, passp // 84'/{0'/1'}/0'/1/* for change addresses val receiveDesc = s"wpkh([${this.fingerPrintHex}/$keyPath]${encode(accountPub, prefix)}/0/*)" val changeDesc = s"wpkh([${this.fingerPrintHex}/$keyPath]${encode(accountPub, prefix)}/1/*)" - ( - List(s"$receiveDesc#${descriptorChecksum(receiveDesc)}"), - List(s"$changeDesc#${descriptorChecksum(changeDesc)}") - ) + + 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)), + )) } override def signPsbt(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int]): Psbt = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala index 22803ec280..0cc7b876aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnchainKeyManager.scala @@ -3,8 +3,11 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.Descriptors trait OnchainKeyManager { + def wallet: String + /** * * @param account account number (0 is used by most wallets) @@ -24,7 +27,7 @@ trait OnchainKeyManager { * @param account account number * @return a pair of (main, change) wallet descriptors that can be imported into an onchain wallet */ - def getDescriptors(account: Long): (List[String], List[String]) + def getDescriptors(account: Long): Descriptors /** * diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 042798d1a8..ae8b9cbf32 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -22,10 +22,8 @@ import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants.feeratePerKw -import fr.acinq.eclair.blockchain.fee.{DustTolerance, FeeratePerByte, FeeratePerKw, FeerateTolerance, FeeratesPerKw} +import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} -import fr.acinq.eclair.blockchain.fee.{DustTolerance, FeeratePerByte, FeeratePerKw, FeerateTolerance} -import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager, LocalOnchainKeyManager} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -43,9 +41,8 @@ class StartupSpec extends AnyFunSuite { val feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)) val nodeKeyManager = new LocalNodeKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) - val onchainKeyManager = new LocalOnchainKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) val db = TestDatabases.inMemoryDb() - NodeParams.makeNodeParams(conf, UUID.fromString("01234567-0123-4567-89ab-0123456789ab"), nodeKeyManager, channelKeyManager, onchainKeyManager, None, db, blockCount, feerates) + NodeParams.makeNodeParams(conf, UUID.fromString("01234567-0123-4567-89ab-0123456789ab"), nodeKeyManager, channelKeyManager, None, db, blockCount, feerates) } test("check configuration") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 30d91d4778..21bd98694c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, RemoteRbfLimits, UnhandledExceptionStrategy} import fr.acinq.eclair.channel.{ChannelFlags, LocalParams} -import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager, LocalOnchainKeyManager} +import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.MessageRelay.RelayAll import fr.acinq.eclair.io.{OpenChannelInterceptor, PeerConnection} import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig @@ -76,13 +76,11 @@ object TestConstants { val seed: ByteVector32 = ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3") // 02aaaa... val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) - val onchainKeyManager = new LocalOnchainKeyManager(ByteVector.fromValidHex("01" * 32), Block.RegtestGenesisBlock.hash) // This is a function, and not a val! When called will return a new NodeParams def nodeParams: NodeParams = NodeParams( nodeKeyManager, channelKeyManager, - onchainKeyManager, blockHeight = new AtomicLong(defaultBlockHeight), feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)), alias = "alice", @@ -242,12 +240,10 @@ object TestConstants { val seed: ByteVector32 = ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492") // 02bbbb... val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) - val onchainKeyManager = new LocalOnchainKeyManager(seed, Block.RegtestGenesisBlock.hash) def nodeParams: NodeParams = NodeParams( nodeKeyManager, channelKeyManager, - onchainKeyManager, blockHeight = new AtomicLong(defaultBlockHeight), feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)), alias = "bob", 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 e29591b427..257db569a7 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 @@ -34,7 +34,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, Bitco import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.crypto.keymanager.LocalOnchainKeyManager import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, addressFromPublicKeyScript, addressToPublicKeyScript, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, TimestampSecond, addressFromPublicKeyScript, addressToPublicKeyScript, randomBytes32, randomKey} import grizzled.slf4j.Logging import org.json4s.JsonAST._ import org.json4s.{DefaultFormats, Formats} @@ -157,6 +157,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A def makeEvilBitcoinClient(changePosMod: (Int) => Int, txMod: Transaction => Transaction): BitcoinCoreClient = { val badRpcClient = new BitcoinJsonRPCClient { + override def wallet: Option[String] = if (useEclairSigner) Some("eclair") else None + override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] = method match { case "fundrawtransaction" => bitcoinClient.rpcClient.invoke(method, params: _*)(ec).map(json => json.mapField { case ("changepos", JInt(pos)) => ("changepos", JInt(changePosMod(pos.toInt))) @@ -1423,7 +1425,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { - override val useEclairSigner = true + override def useEclairSigner = true test("wallets managed by eclair implement BIP84") { val sender = TestProbe() @@ -1433,7 +1435,7 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val seed = MnemonicCode.toSeed(mnemmonics, "") val master = DeterministicWallet.generate(seed) - val onchainKeyManager = new LocalOnchainKeyManager(entropy, Block.RegtestGenesisBlock.hash, passphrase = "") + 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")) @@ -1472,9 +1474,9 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val sender = TestProbe() (1 to 10).foreach { _ => - val entropy = randomBytes32() - val hex = entropy.toString() - val onchainKeyManager = new LocalOnchainKeyManager(entropy, Block.RegtestGenesisBlock.hash, passphrase = "") + 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] 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 acc777eabc..f337a6e478 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 @@ -22,13 +22,13 @@ 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.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{Descriptor, PreviousTx} +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.integration.IntegrationSpec -import fr.acinq.eclair.{BlockHeight, TestUtils, addressToPublicKeyScript, randomKey} +import fr.acinq.eclair.{BlockHeight, TestUtils, TimestampSecond, addressToPublicKeyScript, randomKey} import grizzled.slf4j.Logging import org.json4s.JsonAST._ import scodec.bits.ByteVector @@ -73,7 +73,7 @@ trait BitcoindService extends Logging { var bitcoinrpcclient: BitcoinJsonRPCClient = _ var bitcoinrpcauthmethod: BitcoinJsonRPCAuthMethod = _ var bitcoincli: ActorRef = _ - val onchainKeyManager = new LocalOnchainKeyManager(ByteVector.fromValidHex("01" * 32), Block.RegtestGenesisBlock.hash) + val onchainKeyManager = new LocalOnchainKeyManager("eclair", ByteVector.fromValidHex("01" * 32), TimestampSecond.now(), Block.RegtestGenesisBlock.hash) def startBitcoind(useCookie: Boolean = false, defaultAddressType_opt: Option[String] = None, mempoolSize_opt: Option[Int] = None, // mempool size in MB @@ -119,7 +119,7 @@ trait BitcoindService extends Logging { })) } - def makeBitcoinCoreClient: BitcoinCoreClient = new BitcoinCoreClient(bitcoinrpcclient, if (useEclairSigner) Some(onchainKeyManager) else None) + def makeBitcoinCoreClient: BitcoinCoreClient = new BitcoinCoreClient(bitcoinrpcclient, Some(onchainKeyManager)) def stopBitcoind(): Unit = { // gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging @@ -174,8 +174,7 @@ trait BitcoindService extends Logging { def importEclairDescriptors(jsonRpcClient: BitcoinJsonRPCClient, keyManager: OnchainKeyManager, probe: TestProbe = TestProbe()): Unit = { - val (main, change) = keyManager.getDescriptors(0) - val descriptors = main.map(d => Descriptor(d)) ++ change.map(d => Descriptor(d, internal = true)) + val descriptors = keyManager.getDescriptors(0).descriptors jsonRpcClient.invoke("importdescriptors", descriptors).pipeTo(probe.ref) probe.expectMsgType[JValue] } 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 254c121ee2..5987527da7 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 @@ -34,7 +34,7 @@ import scala.concurrent.{ExecutionContext, Future} class BitcoinCoreFeeProviderSpec extends TestKitBaseClass with BitcoindService with AnyFunSuiteLike with BeforeAndAfterAll with Logging { - override val useEclairSigner = false + override def useEclairSigner = false override def beforeAll(): Unit = { startBitcoind() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 454ee04e54..ac192adc70 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -2563,5 +2563,5 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } class InteractiveTxBuilderWithEclairSignerSpec extends InteractiveTxBuilderSpec { - override val useEclairSigner = true + override def useEclairSigner = true } \ No newline at end of file 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 e214d31002..a9fc08575e 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 @@ -22,7 +22,7 @@ import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, ByteVector32, MilliBtcDouble, OutPoint, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, ByteVector32, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -40,7 +40,7 @@ import fr.acinq.eclair.crypto.keymanager.LocalOnchainKeyManager 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, randomKey} +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 @@ -1654,7 +1654,9 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS override def createTestWallet(walletName: String) = { val probe = TestProbe() // we use the wallet name as a passphrase to make sure we get a new empty wallet - val keyManager = new LocalOnchainKeyManager(ByteVector.fromValidHex("01" * 32), Block.RegtestGenesisBlock.hash, walletName) + 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] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala index 62e6f17775..3aebdf2783 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala @@ -134,7 +134,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" val seedDatFile = TestUtils.createSeedFile("seed.dat", seed.toArray) - val Seeds(_, _, _) = NodeParams.getSeeds(seedDatFile.getParentFile) + val Seeds(_, _) = NodeParams.getSeeds(seedDatFile.getParentFile) val channelSeedDatFile = new File(seedDatFile.getParentFile, "channel_seed.dat") assert(channelSeedDatFile.exists()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala index 522ab6c6ed..d6384f4a6e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala @@ -54,7 +54,7 @@ class LocalNodeKeyManagerSpec extends AnyFunSuite { val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" val seedDatFile = TestUtils.createSeedFile("seed.dat", seed.toArray) - val Seeds(_, _, _) = NodeParams.getSeeds(seedDatFile.getParentFile) + val Seeds(_, _) = NodeParams.getSeeds(seedDatFile.getParentFile) val nodeSeedDatFile = new File(seedDatFile.getParentFile, "node_seed.dat") assert(nodeSeedDatFile.exists()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManagerSpec.scala index 4024136911..a59282d7e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnchainKeyManagerSpec.scala @@ -1,8 +1,9 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.psbt.{KeyPathWithMaster, Psbt} -import fr.acinq.bitcoin.scalacompat.{Block, DeterministicWallet, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.bitcoin.{ScriptFlags, SigHash} +import fr.acinq.eclair.TimestampSecond import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -11,8 +12,9 @@ import scala.jdk.CollectionConverters.SeqHasAsJava class LocalOnchainKeyManagerSpec extends AnyFunSuite { test("sign psbt (non-reg test)") { - val seed = ByteVector.fromValidHex("01" * 32) - val onchainKeyManager = new LocalOnchainKeyManager(seed, Block.TestnetGenesisBlock.hash) + val entropy = ByteVector.fromValidHex("01" * 32) + val seed = MnemonicCode.toSeed(MnemonicCode.toMnemonics(entropy), "") + val onchainKeyManager = new LocalOnchainKeyManager("eclair", seed, TimestampSecond.now(), Block.TestnetGenesisBlock.hash) // data generated by bitcoin core on regtest val psbt = Psbt.read( Base64.getDecoder.decode("cHNidP8BAHECAAAAAfZo4nGIyTg77MFmEBkQH1Au3Jl8vzB2WWQGGz/MbyssAAAAAAD9////ArAHPgUAAAAAFgAU6j9yVvLg66Zu3GM/xHbmXT0yvyiAlpgAAAAAABYAFODscQh3N7lmDYyV5yrHpGL2Zd4JAAAAAAABAH0CAAAAAaNdmqUNlziIjSaif3JUcvJWdyF0U5bYq13NMe+LbaBZAAAAAAD9////AjSp1gUAAAAAFgAUjfFMfBg8ulo/874n3+0ode7ka0BAQg8AAAAAACIAIPUn/XU17DfnvDkj8gn2twG3jtr2Z7sthy9K2MPTdYkaAAAAAAEBHzSp1gUAAAAAFgAUjfFMfBg8ulo/874n3+0ode7ka0AiBgM+PDdyxsVisa66SyBxiUvhEam8lEP64yujvVsEcGaqIxgPCfOBVAAAgAEAAIAAAACAAQAAAAMAAAAAIgIDWmAhb/sCV9+HjwFpPuy2TyEBi/Y11wrEHZUihe3N80EYDwnzgVQAAIABAACAAAAAgAEAAAAFAAAAAAA=") @@ -28,7 +30,7 @@ class LocalOnchainKeyManagerSpec extends AnyFunSuite { import fr.acinq.bitcoin.utils.EitherKt val seed = ByteVector.fromValidHex("01" * 32) - val onchainKeyManager = new LocalOnchainKeyManager(seed, Block.TestnetGenesisBlock.hash) + val onchainKeyManager = new LocalOnchainKeyManager("eclair", seed, TimestampSecond.now(), Block.TestnetGenesisBlock.hash) // create a watch-only BIP84 wallet from our key manager xpub val (_, accountPub) = DeterministicWallet.ExtendedPublicKey.decode(onchainKeyManager.getOnchainMasterPubKey(0)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index dd6e49c9fc..29adfe7718 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -455,14 +455,9 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { test("start eclair nodes") { - var mapA = Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> TestUtils.availablePort, "eclair.api.port" -> TestUtils.availablePort) - var mapB = Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> TestUtils.availablePort, "eclair.api.port" -> TestUtils.availablePort) - var mapC = Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> TestUtils.availablePort, "eclair.api.port" -> TestUtils.availablePort) - if (useEclairSigner) { - mapA = mapA + ("eclair.bitcoind.use-eclair-signer" -> true) - mapB = mapB + ("eclair.bitcoind.use-eclair-signer" -> true) - mapC = mapC + ("eclair.bitcoind.use-eclair-signer" -> true) - } + val mapA = Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> TestUtils.availablePort, "eclair.api.port" -> TestUtils.availablePort) + val mapB = Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> TestUtils.availablePort, "eclair.api.port" -> TestUtils.availablePort) + val mapC = Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> TestUtils.availablePort, "eclair.api.port" -> TestUtils.availablePort) instantiateEclairNode("A", ConfigFactory.parseMap(mapA.asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) instantiateEclairNode("C", ConfigFactory.parseMap(mapB.asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) instantiateEclairNode("F", ConfigFactory.parseMap(mapC.asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) @@ -637,7 +632,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { } class StandardChannelIntegrationWithEclairSignerSpec extends StandardChannelIntegrationSpec { - override val useEclairSigner: Boolean = true + override def useEclairSigner: Boolean = true } abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { 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 7cfe95a76a..dd6baf7413 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 @@ -33,7 +33,6 @@ import grizzled.slf4j.Logging import org.json4s.{DefaultFormats, Formats} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector import java.io.File import java.util.Properties @@ -148,7 +147,7 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name") datadir.mkdirs() implicit val system: ActorSystem = ActorSystem(s"system-$name", config) - val setup = new Setup(datadir, pluginParams = Seq.empty, seeds_opt = Some(Setup.Seeds(randomBytes32(), randomBytes32(), ByteVector.fromValidHex("01" * 32)))) + val setup = new Setup(datadir, pluginParams = Seq.empty, seeds_opt = Some(Setup.Seeds(randomBytes32(), randomBytes32())), onchainKeyManager_opt = Some(onchainKeyManager)) val kit = Await.result(setup.bootstrap, 10 seconds) nodes = nodes + (name -> kit) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index de91090822..6fb7853f27 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -70,7 +70,6 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat instanceId = UUID.randomUUID(), nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash), channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash), - onchainKeyManager = new LocalOnchainKeyManager(seed, Block.RegtestGenesisBlock.hash), torAddress_opt = None, database = TestDatabases.inMemoryDb(), blockHeight = new AtomicLong(400_000), 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 6aff8642e8..9ed4366d05 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 @@ -22,7 +22,7 @@ import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.blockchain.fee.FeeratePerByte -import org.json4s.{JArray, JBool, JObject, JString} +import org.json4s.{JObject, JString} trait OnChain { this: Service with EclairDirectives => @@ -75,22 +75,8 @@ trait OnChain { val getdescriptors: Route = postRequest("getdescriptors") { implicit t => formFields("account".as[Long].?) { (account_opt) => - val (receiveDescs, internalDescs) = this.eclairApi.getDescriptors(account_opt.getOrElse(0L)) - - // format JSON result to be compatible with Bitcoin Core's importdescriptors RPC call - val receive = receiveDescs.map(desc => JObject( - "desc" -> JString(desc), - "active" -> JBool(true), - "timestamp" -> JString("now") - )) - val change = internalDescs.map(desc => JObject( - "desc" -> JString(desc), - "active" -> JBool(true), - "timestamp" -> JString("now"), - "internal" -> JBool(true) - )) - val json = JArray(receive ++ change) - complete(json) + val descriptors = this.eclairApi.getDescriptors(account_opt.getOrElse(0L)) + complete(descriptors.descriptors) } }