Skip to content

Commit 0a1bbec

Browse files
committed
Merge bitcoin/bitcoin#32489: wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet
a15bdc0 doc: update offline-signing-tutorial to use exportwatchonlywallet rpc (Pol Espinasa) a388076 test: Test for exportwatchonlywallet (Ava Chow) d053e3e wallet, rpc: Add exportwatchonlywallet RPC (Ava Chow) 444878e wallet: Add CWallet::ExportWatchOnly (Ava Chow) f9273f0 wallet: Move listdescriptors retrieving from RPC to CWallet (Ava Chow) a1c8378 wallet: Write new descriptor's cache in AddWalletDescriptor (Ava Chow) 1e99664 wallet: Use Descriptor::CanSelfExpand() in CanGetAddresses() (Ava Chow) d2ee922 descriptor: Add CanSelfExpand() (Ava Chow) Pull request description: Currently, if a user wants to use an airgapped setup, they need to manually create the watchonly wallet that will live on the online node by importing the public descriptors. This PR introduces `exportwatchonlywallet` which will create a wallet file with the public descriptors to avoid exposing the specific internals to the user. Additionally, this RPC will copy any existing labels, transactions, and wallet flags. This ensures that the exported watchonly wallet is almost entirely a copy of the original wallet but without private keys. ACKs for top commit: polespinasa: lgtm ACK a15bdc0 Sjors: re-utACK a15bdc0 pablomartin4btc: re-ACK [a15bdc0](bitcoin/bitcoin@a15bdc0) w0xlt: lgtm reACK a15bdc0 Tree-SHA512: cfc59415ad9aa13d1445cf2a85db1c051215496b6edcf5a8db463499b2b51b92ee7bf840b709035dff7635f9d0c533423bceb58c851f220500e1ea254d12f3b8
2 parents 4a00712 + a15bdc0 commit 0a1bbec

15 files changed

Lines changed: 688 additions & 102 deletions

doc/offline-signing-tutorial.md

Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ This workflow uses [Partially Signed Bitcoin Transactions](/doc/psbt.md) (PSBTs)
1010
> While this tutorial demonstrates the process using `signet` network, you should omit the `-signet` flag in the provided commands when working with `mainnet`.
1111
1212
## Overview
13-
In this tutorial we have two hosts, both running Bitcoin v25.0
13+
In this tutorial we have two hosts, both running Bitcoin v32.0
1414

1515
* `offline` host which is disconnected from all networks (internet, Tor, wifi, bluetooth etc.) and does not have, or need, a copy of the blockchain.
1616
* `online` host which is a regular online node with a synced blockchain.
1717

18-
We are going to first create an `offline_wallet` on the offline host. We will then create a `watch_only_wallet` on the online host using public key descriptors exported from the `offline_wallet`. Next we will receive some coins into the wallet. In order to spend these coins we'll create an unsigned PSBT using the `watch_only_wallet`, sign the PSBT using the private keys in the `offline_wallet`, and finally broadcast the signed PSBT using the online host.
18+
We are going to first create an `offline_wallet` on the offline host. We will then create a `watch_only_wallet` on the online host using a wallet file exported from the `offline_wallet`. Next we will receive some coins into the wallet. In order to spend these coins we'll create an unsigned PSBT using the `watch_only_wallet`, sign the PSBT using the private keys in the `offline_wallet`, and finally broadcast the signed PSBT using the online host.
1919

2020
### Requirements
2121
- [jq](https://jqlang.github.io/jq/) installation - This tutorial uses jq to process certain fields from JSON RPC responses, but this convenience is optional.
@@ -39,72 +39,30 @@ We are going to first create an `offline_wallet` on the offline host. We will th
3939
> [!NOTE]
4040
> The use of a passphrase is crucial to encrypt the wallet.dat file. This encryption ensures that even if an unauthorized individual gains access to the offline host, they won't be able to access the wallet's contents. Further details about securing your wallet can be found in [Managing the Wallet](/doc/managing-wallets.md#12-encrypting-the-wallet)
4141
42-
2. Export the public key-only descriptors from the offline host to a JSON file named `descriptors.json`. We use `jq` here to extract the `.descriptors` field from the full RPC response.
43-
42+
2. Export the wallet in a watch-only format to a .dat file named `watch_only_wallet.dat`.
4443
```sh
45-
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" listdescriptors \
46-
| jq -r '.descriptors' \
47-
>> /path/to/descriptors.json
44+
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" -named exportwatchonlywallet \
45+
destination=/path/to/watch_only_wallet.dat
4846
```
4947

5048
> [!NOTE]
51-
> The `descriptors.json` file will be transferred to the online machine (e.g. using a USB flash drive) where it can be imported to create a related watch-only wallet.
49+
> The `watch_only_wallet.dat` file will be transferred to the online machine (e.g. using a USB flash drive) where it can be imported to create a related watch-only wallet.
5250
5351
### Create the online `watch_only_wallet`
5452

55-
1. On the online machine create a blank watch-only wallet which has private keys disabled and is named `watch_only_wallet`. This is achieved by using the `createwallet` options: `disable_private_keys=true, blank=true`.
56-
53+
On the online machine import the watch-only wallet. This wallet will have the private keys disabled and is named `watch_only_wallet`. This is achieved by using the `restorewallet` rpc call.
5754
The `watch_only_wallet` wallet will be used to track and validate incoming transactions, create unsigned PSBTs when spending coins, and broadcast signed and finalized PSBTs.
5855

59-
> [!NOTE]
60-
> `disable_private_keys` indicates that the wallet should refuse to import private keys, i.e. will be a dedicated watch-only wallet.
61-
6256
```sh
63-
[online]$ ./build/bin/bitcoin-cli -signet -named createwallet \
57+
[online]$ ./build/bin/bitcoin-cli -signet -named restorewallet \
6458
wallet_name="watch_only_wallet" \
65-
disable_private_keys=true \
66-
blank=true
59+
backup_file=/path/to/watch_only_wallet.dat
6760

6861
{
6962
"name": "watch_only_wallet"
7063
}
7164
```
7265

73-
2. Import the `offline_wallet`s public key descriptors to the online `watch_only_wallet` using the `descriptors.json` file created on the offline wallet.
74-
75-
```sh
76-
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" importdescriptors "$(cat /path/to/descriptors.json)"
77-
78-
[
79-
{
80-
"success": true
81-
},
82-
{
83-
"success": true
84-
},
85-
{
86-
"success": true
87-
},
88-
{
89-
"success": true
90-
},
91-
{
92-
"success": true
93-
},
94-
{
95-
"success": true
96-
},
97-
{
98-
"success": true
99-
},
100-
{
101-
"success": true
102-
}
103-
]
104-
```
105-
> [!NOTE]
106-
> Multiple success values indicate that multiple descriptors, for different address types, have been successfully imported. This allows generating different address types on the `watch_only_wallet`.
107-
10866
### Fund the `offline_wallet`
10967

11068
At this point, it's important to understand that both the `offline_wallet` and online `watch_only_wallet` share the same public keys. As a result, they generate the same addresses. Transactions can be created using either wallet, but valid signatures can only be added by the `offline_wallet` as only it has the private keys.

src/script/descriptor.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ struct PubkeyProvider
247247

248248
/** Get the count of keys known by this PubkeyProvider. Usually one, but may be more for key aggregation schemes */
249249
virtual size_t GetKeyCount() const { return 1; }
250+
251+
/** Whether this PubkeyProvider can always provide a public key without cache or private key arguments */
252+
virtual bool CanSelfExpand() const = 0;
250253
};
251254

252255
class OriginPubkeyProvider final : public PubkeyProvider
@@ -317,6 +320,7 @@ class OriginPubkeyProvider final : public PubkeyProvider
317320
{
318321
return std::make_unique<OriginPubkeyProvider>(m_expr_index, m_origin, m_provider->Clone(), m_apostrophe);
319322
}
323+
bool CanSelfExpand() const override { return m_provider->CanSelfExpand(); }
320324
};
321325

322326
/** An object representing a parsed constant public key in a descriptor. */
@@ -381,6 +385,7 @@ class ConstPubkeyProvider final : public PubkeyProvider
381385
{
382386
return std::make_unique<ConstPubkeyProvider>(m_expr_index, m_pubkey, m_xonly);
383387
}
388+
bool CanSelfExpand() const final { return true; }
384389
};
385390

386391
enum class DeriveType {
@@ -607,6 +612,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
607612
{
608613
return std::make_unique<BIP32PubkeyProvider>(m_expr_index, m_root_extkey, m_path, m_derive, m_apostrophe);
609614
}
615+
bool CanSelfExpand() const override { return !IsHardened(); }
610616
};
611617

612618
/** PubkeyProvider for a musig() expression */
@@ -810,6 +816,15 @@ class MuSigPubkeyProvider final : public PubkeyProvider
810816
{
811817
return 1 + m_participants.size();
812818
}
819+
bool CanSelfExpand() const override
820+
{
821+
// Participants must be self expandable for all MuSig expressions to be self expandable; the aggregate pubkey cannot be stored
822+
// in the descriptor cache, so even aggregate-then-derive still requires the self expansion of participants prior to aggregation.
823+
for (const auto& key : m_participants) {
824+
if (!key->CanSelfExpand()) return false;
825+
}
826+
return true;
827+
}
813828
};
814829

815830
/** Base class for all Descriptor implementations. */
@@ -824,7 +839,7 @@ class DescriptorImpl : public Descriptor
824839
std::vector<std::string> m_warnings;
825840

826841
//! The sub-descriptor arguments (empty for everything but SH and WSH).
827-
//! In doc/descriptors.m this is referred to as SCRIPT expressions sh(SCRIPT)
842+
//! In doc/descriptors.md this is referred to as SCRIPT expressions sh(SCRIPT)
828843
//! and wsh(SCRIPT), and distinct from KEY expressions and ADDR expressions.
829844
//! Subdescriptors can only ever generate a single script.
830845
const std::vector<std::unique_ptr<DescriptorImpl>> m_subdescriptor_args;
@@ -1099,6 +1114,18 @@ class DescriptorImpl : public Descriptor
10991114
}
11001115
return count;
11011116
}
1117+
1118+
// NOLINTNEXTLINE(misc-no-recursion)
1119+
bool CanSelfExpand() const override
1120+
{
1121+
for (const auto& key : m_pubkey_args) {
1122+
if (!key->CanSelfExpand()) return false;
1123+
}
1124+
for (const auto& sub : m_subdescriptor_args) {
1125+
if (!sub->CanSelfExpand()) return false;
1126+
}
1127+
return true;
1128+
}
11021129
};
11031130

11041131
/** A parsed addr(A) descriptor. */

src/script/descriptor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ struct Descriptor {
140140
/** Convert the descriptor to a normalized string. Normalized descriptors have the xpub at the last hardened step. This fails if the provided provider does not have the private keys to derive that xpub. */
141141
virtual bool ToNormalizedString(const SigningProvider& provider, std::string& out, const DescriptorCache* cache = nullptr) const = 0;
142142

143+
/** Whether the descriptor can be used to produce its address(es) without needing a cache or private keys. */
144+
virtual bool CanSelfExpand() const = 0;
145+
143146
/** Expand a descriptor at a specified position.
144147
*
145148
* @param[in] pos The position at which to expand the descriptor. If IsRange() is false, this is ignored.

0 commit comments

Comments
 (0)