mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-19 14:53:43 +01:00
a0abcbd382doc: Mention multipath specifier (Ava Chow)0019f61fc5tests: Test importing of multipath descriptors (Ava Chow)f97d5c137dwallet, rpc: Allow importdescriptors to import multipath descriptors (Ava Chow)32dcbca3fbrpc: Allow importmulti to import multipath descriptors correctly (Ava Chow)64dfe3ce4bwallet: Move internal to be per key when importing (Ava Chow)1692245525tests: Multipath descriptors for scantxoutset and deriveaddresses (Ava Chow)cddc0ba9a9rpc: Have deriveaddresses derive receiving and change (Ava Chow)360456cd22tests: Multipath descriptors for getdescriptorinfo (Ava Chow)a90eee444ctests: Add unit tests for multipath descriptors (Ava Chow)1bbf46e2dadescriptors: Change Parse to return vector of descriptors (Ava Chow)0d640c6f02descriptors: Have ParseKeypath handle multipath specifiers (Ava Chow)a5f39b1034descriptors: Change ParseScript to return vector of descriptors (Ava Chow)0d55deae15descriptors: Add DescriptorImpl::Clone (Ava Chow)7e86541f72descriptors: Add PubkeyProvider::Clone (Ava Chow) Pull request description: It is convenient to have a descriptor which specifies both receiving and change addresses in a single string. However, as discussed in https://github.com/bitcoin/bitcoin/issues/17190#issuecomment-895515768, it is not feasible to use a generic multipath specification like BIP 88 due to combinatorial blow up and that it would result in unexpected descriptors. To resolve that problem, this PR proposes a targeted solution which allows only a single pair of 2 derivation indexes to be inserted in the place of a single derivation index. So instead of two descriptor `wpkh(xpub.../0/0/*)` and `wpkh(xpub.../0/1/*)` to represent receive and change addresses, this could be written as `wpkh(xpub.../0/<0;1>/*)`. The multipath specifier is of the form `<NUM;NUM>`. Each `NUM` can have its own hardened specifier, e.g. `<0;1h>` is valid. The multipath specifier can also only appear in one path index in the derivation path. This results in the parser returning two descriptors. The first descriptor uses the first `NUM` in all pairs present, and the second uses the second `NUM`. In our implementation, if a multipath descriptor is not provided, a pair is still returned, but the second element is just `nullptr`. The wallet will not output the multipath descriptors (yet). Furthermore, when a multipath descriptor is imported, it is expanded to the two descriptors and each imported on its own, with the second descriptor being implicitly for internal (change) addresses. There is no change to how the wallet stores or outputs descriptors (yet). Note that the path specifier is different from what was proposed. It uses angle brackets and the semicolon because these are unused characters available in the character set and I wanted to avoid conflicts with characters already in use in descriptors. Closes #17190 ACKs for top commit: darosior: re-ACKa0abcbd382mjdietzx: reACKa0abcbd382pythcoiner: reACKa0abcbdfurszy: Code review ACKa0abcbdglozow: light code review ACKa0abcbd382Tree-SHA512: 84ea40b3fd1b762194acd021cae018c2f09b98e595f5e87de5c832c265cfe8a6d0bc4dae25785392fa90db0f6301ddf9aea787980a29c74f81d04b711ac446c2
127 lines
4.9 KiB
C++
127 lines
4.9 KiB
C++
// Copyright (c) 2009-2021 The Bitcoin Core developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#include <chainparams.h>
|
|
#include <key_io.h>
|
|
#include <pubkey.h>
|
|
#include <script/descriptor.h>
|
|
#include <test/fuzz/fuzz.h>
|
|
#include <test/fuzz/util/descriptor.h>
|
|
#include <util/chaintype.h>
|
|
#include <util/strencodings.h>
|
|
|
|
//! The converter of mocked descriptors, needs to be initialized when the target is.
|
|
MockedDescriptorConverter MOCKED_DESC_CONVERTER;
|
|
|
|
/** Test a successfully parsed descriptor. */
|
|
static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy, std::optional<bool>& is_ranged, std::optional<bool>& is_solvable)
|
|
{
|
|
// Trivial helpers.
|
|
(void)desc.IsRange();
|
|
(void)desc.IsSingleType();
|
|
(void)desc.GetOutputType();
|
|
|
|
if (is_ranged.has_value()) {
|
|
assert(desc.IsRange() == *is_ranged);
|
|
} else {
|
|
is_ranged = desc.IsRange();
|
|
}
|
|
if (is_solvable.has_value()) {
|
|
assert(desc.IsSolvable() == *is_solvable);
|
|
} else {
|
|
is_solvable = desc.IsSolvable();
|
|
}
|
|
|
|
// Serialization to string representation.
|
|
(void)desc.ToString();
|
|
(void)desc.ToPrivateString(sig_provider, dummy);
|
|
(void)desc.ToNormalizedString(sig_provider, dummy);
|
|
|
|
// Serialization to Script.
|
|
DescriptorCache cache;
|
|
std::vector<CScript> out_scripts;
|
|
(void)desc.Expand(0, sig_provider, out_scripts, sig_provider, &cache);
|
|
(void)desc.ExpandPrivate(0, sig_provider, sig_provider);
|
|
(void)desc.ExpandFromCache(0, cache, out_scripts, sig_provider);
|
|
|
|
// If we could serialize to script we must be able to infer using the same provider.
|
|
if (!out_scripts.empty()) {
|
|
assert(InferDescriptor(out_scripts.back(), sig_provider));
|
|
|
|
// The ScriptSize() must match the size of the serialized Script. (ScriptSize() is set for all descs but 'combo()'.)
|
|
const bool is_combo{!desc.IsSingleType()};
|
|
assert(is_combo || desc.ScriptSize() == out_scripts.back().size());
|
|
}
|
|
|
|
const auto max_sat_maxsig{desc.MaxSatisfactionWeight(true)};
|
|
const auto max_sat_nonmaxsig{desc.MaxSatisfactionWeight(true)};
|
|
const auto max_elems{desc.MaxSatisfactionElems()};
|
|
// We must be able to estimate the max satisfaction size for any solvable descriptor (but combo).
|
|
const bool is_nontop_or_nonsolvable{!*is_solvable || !desc.GetOutputType()};
|
|
const bool is_input_size_info_set{max_sat_maxsig && max_sat_nonmaxsig && max_elems};
|
|
assert(is_input_size_info_set || is_nontop_or_nonsolvable);
|
|
}
|
|
|
|
void initialize_descriptor_parse()
|
|
{
|
|
static ECC_Context ecc_context{};
|
|
SelectParams(ChainType::MAIN);
|
|
}
|
|
|
|
void initialize_mocked_descriptor_parse()
|
|
{
|
|
initialize_descriptor_parse();
|
|
MOCKED_DESC_CONVERTER.Init();
|
|
}
|
|
|
|
FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
|
|
{
|
|
// Key derivation is expensive. Deriving deep derivation paths take a lot of compute and we'd
|
|
// rather spend time elsewhere in this target, like on the actual descriptor syntax. So rule
|
|
// out strings which could correspond to a descriptor containing a too large derivation path.
|
|
if (HasDeepDerivPath(buffer)) return;
|
|
|
|
// Some fragments can take a virtually unlimited number of sub-fragments (thresh, multi_a) but
|
|
// may perform quadratic operations on them. Limit the number of sub-fragments per fragment.
|
|
if (HasTooManySubFrag(buffer)) return;
|
|
|
|
// The script building logic performs quadratic copies in the number of nested wrappers. Limit
|
|
// the number of nested wrappers per fragment.
|
|
if (HasTooManyWrappers(buffer)) return;
|
|
|
|
const std::string mocked_descriptor{buffer.begin(), buffer.end()};
|
|
if (const auto descriptor = MOCKED_DESC_CONVERTER.GetDescriptor(mocked_descriptor)) {
|
|
FlatSigningProvider signing_provider;
|
|
std::string error;
|
|
const auto desc = Parse(*descriptor, signing_provider, error);
|
|
std::optional<bool> is_ranged;
|
|
std::optional<bool> is_solvable;
|
|
for (const auto& d : desc) {
|
|
assert(d);
|
|
TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable);
|
|
}
|
|
}
|
|
}
|
|
|
|
FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
|
|
{
|
|
// See comments above for rationales.
|
|
if (HasDeepDerivPath(buffer)) return;
|
|
if (HasTooManySubFrag(buffer)) return;
|
|
if (HasTooManyWrappers(buffer)) return;
|
|
|
|
const std::string descriptor(buffer.begin(), buffer.end());
|
|
FlatSigningProvider signing_provider;
|
|
std::string error;
|
|
for (const bool require_checksum : {true, false}) {
|
|
const auto desc = Parse(descriptor, signing_provider, error, require_checksum);
|
|
std::optional<bool> is_ranged;
|
|
std::optional<bool> is_solvable;
|
|
for (const auto& d : desc) {
|
|
assert(d);
|
|
TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable);
|
|
}
|
|
}
|
|
}
|