Skip to content

Commit 90fe3c7

Browse files
committed
add -xpub option to encryptbackup
To include derivation path in backup header.
1 parent 5cdd12a commit 90fe3c7

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

src/bitcoin-wallet.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ static void SetupWalletToolArgs(ArgsManager& argsman)
4040
argsman.AddArg("-debug=<category>", "Output debugging information (default: 0).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
4141
argsman.AddArg("-printtoconsole", "Send trace/debug info to console (default: 1 when no -debug is true, 0 otherwise).", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
4242
argsman.AddArg("-pubkey=<key>", "Extended public key (xpub/tpub) for decrypting a backup", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
43+
argsman.AddArg("-xpub=<key>", "Extended public key (xpub/tpub) whose derivation path to include in backup header", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
4344

4445
argsman.AddCommand("info", "Get wallet info");
4546
argsman.AddCommand("create", "Create a new descriptor wallet file");

src/wallet/wallettool.cpp

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,24 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
195195

196196
LOCK(wallet_instance->cs_wallet);
197197

198+
// If -xpub is provided, validate it
199+
std::optional<std::string> target_xpub;
200+
if (args.IsArgSet("-xpub")) {
201+
std::string xpub_str = args.GetArg("-xpub", "");
202+
CExtPubKey ext_pubkey = DecodeExtPubKey(xpub_str);
203+
if (!ext_pubkey.pubkey.IsValid()) {
204+
tfm::format(std::cerr, "Invalid extended public key: %s\n", xpub_str);
205+
wallet_instance->Close();
206+
return false;
207+
}
208+
target_xpub = xpub_str;
209+
}
210+
198211
// Collect all descriptors from the wallet in listdescriptors format
199212
// This format preserves origin info and can be used with importdescriptors
200213
std::vector<std::pair<std::string, WalletDescriptorInfo>> all_descriptors;
214+
std::vector<DerivationPath> derivation_paths;
215+
bool found_target_xpub = false;
201216

202217
const auto active_spk_mans = wallet_instance->GetActiveScriptPubKeyMans();
203218

@@ -226,6 +241,34 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
226241
wallet_desc.next_index
227242
};
228243
all_descriptors.emplace_back(desc_str, std::move(info));
244+
245+
// Check if this descriptor contains the target xpub (if specified)
246+
// and extract its derivation path from the origin info
247+
if (target_xpub) {
248+
// Look for the xpub in the descriptor string and check for origin info
249+
// Format: [fingerprint/path]xpub...
250+
size_t xpub_pos = desc_str.find(*target_xpub);
251+
if (xpub_pos != std::string::npos) {
252+
found_target_xpub = true;
253+
// Look for origin info before the xpub: [fingerprint/path]
254+
if (xpub_pos > 0 && desc_str[xpub_pos - 1] == ']') {
255+
size_t bracket_start = desc_str.rfind('[', xpub_pos - 1);
256+
if (bracket_start != std::string::npos) {
257+
std::string origin = desc_str.substr(bracket_start + 1, xpub_pos - bracket_start - 2);
258+
// origin is "fingerprint/path" - find the first /
259+
size_t slash_pos = origin.find('/');
260+
if (slash_pos != std::string::npos) {
261+
std::string path_str = "m" + origin.substr(slash_pos);
262+
263+
auto parsed_path = ParseDerivationPath(path_str);
264+
if (parsed_path && derivation_paths.empty()) {
265+
derivation_paths.push_back(*parsed_path);
266+
}
267+
}
268+
}
269+
}
270+
}
271+
}
229272
}
230273
}
231274

@@ -235,6 +278,18 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
235278
return false;
236279
}
237280

281+
if (target_xpub && !found_target_xpub) {
282+
tfm::format(std::cerr, "Specified xpub not found in any wallet descriptor: %s\n", *target_xpub);
283+
wallet_instance->Close();
284+
return false;
285+
}
286+
287+
if (target_xpub && derivation_paths.empty()) {
288+
tfm::format(std::cerr, "Specified xpub has no origin info (derivation path) in descriptor.\n");
289+
wallet_instance->Close();
290+
return false;
291+
}
292+
238293
// Sort by descriptor string for deterministic ordering
239294
std::sort(all_descriptors.begin(), all_descriptors.end(),
240295
[](const auto& a, const auto& b) { return a.first < b.first; });
@@ -256,7 +311,7 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
256311
content.bip_number = BIP_DESCRIPTORS;
257312

258313
// Create the encrypted backup
259-
auto backup_result = CreateEncryptedBackup(primary_descriptor, plaintext, content, {});
314+
auto backup_result = CreateEncryptedBackup(primary_descriptor, plaintext, content, derivation_paths);
260315
if (!backup_result) {
261316
tfm::format(std::cerr, "Failed to create encrypted backup: %s\n",
262317
util::ErrorString(backup_result).original);

test/functional/tool_wallet.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,9 +482,35 @@ def test_encrypted_backup(self):
482482
assert_equal(metadata["version"], 1)
483483
assert_equal(metadata["encryption"], "ChaCha20-Poly1305")
484484
assert metadata["recipients"] >= 1
485-
# Content type is encrypted, not visible in header
485+
# Without -xpub, there should be no derivation paths
486+
assert_equal(metadata["derivation_paths"], [])
486487
self.log.debug(f"Backup metadata: {json.dumps(metadata, indent=2)}")
487488

489+
# Test that -xpub with an xpub not in the wallet gets rejected
490+
self.log.info("Testing that unknown xpub is rejected...")
491+
# Use a valid but unrelated testnet xpub
492+
unknown_xpub = "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp"
493+
p = self.bitcoin_wallet_process(f"-wallet={wallet_name}", f"-xpub={unknown_xpub}", "encryptbackup")
494+
backup_output, stderr = p.communicate()
495+
assert_equal(p.poll(), 1)
496+
assert "not found in any wallet descriptor" in stderr
497+
498+
# Test encryptbackup with -xpub to include derivation path
499+
self.log.info("Creating encrypted backup with -xpub for derivation path...")
500+
p = self.bitcoin_wallet_process(f"-wallet={wallet_name}", f"-xpub={xpub_for_decrypt}", "encryptbackup")
501+
backup_with_path_output, stderr = p.communicate()
502+
assert_equal(p.poll(), 0)
503+
backup_with_path_base64 = backup_with_path_output.strip()
504+
505+
# Inspect the backup with derivation path
506+
p = self.bitcoin_wallet_process("inspectbackup")
507+
inspect_output, stderr = p.communicate(input=backup_with_path_base64)
508+
assert_equal(p.poll(), 0)
509+
metadata_with_path = json.loads(inspect_output)
510+
# BIP44 testnet path: m/44'/1'/0'
511+
assert_equal(metadata_with_path["derivation_paths"], ["m/44'/1'/0'"])
512+
self.log.info(f"Derivation paths in backup: {metadata_with_path['derivation_paths']}")
513+
488514
# Decrypt the backup using just the xpub (no wallet needed)
489515
self.log.info("Decrypting backup using xpub...")
490516
p = self.bitcoin_wallet_process(f"-pubkey={xpub_for_decrypt}", "decryptbackup")

0 commit comments

Comments
 (0)