From 80c29bc6f14f184cdff28e6bbda2da3821832606 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Fri, 22 Dec 2023 15:25:13 -0500 Subject: [PATCH] descriptor: Add unused(KEY) descriptor unused() descriptors do not have scriptPubKeys. Instead, the wallet uses them to store keys without having any scripts to watch for. --- src/script/descriptor.cpp | 40 ++++++++++++++++++ src/script/descriptor.h | 5 ++- src/test/descriptor_tests.cpp | 61 ++++++++++++++++++++++++++++ src/wallet/test/walletload_tests.cpp | 1 + src/wallet/wallet.cpp | 4 +- 5 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 19315dd6a30..67770ada78e 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -1038,6 +1038,8 @@ public: virtual std::unique_ptr Clone() const = 0; + bool HasScripts() const override { return true; } + // NOLINTNEXTLINE(misc-no-recursion) std::vector Warnings() const override { std::vector all = m_warnings; @@ -1738,6 +1740,23 @@ public: } }; +/** A parsed unused(KEY) descriptor */ +class UnusedDescriptor final : public DescriptorImpl +{ +protected: + std::vector MakeScripts(const std::vector& keys, std::span scripts, FlatSigningProvider& out) const override { return {}; } +public: + UnusedDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), "unused") {} + bool IsSingleType() const final { return true; } + bool HasScripts() const override { return false; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_pubkey_args.at(0)->Clone()); + } +}; + + //////////////////////////////////////////////////////////////////////////// // Parser // //////////////////////////////////////////////////////////////////////////// @@ -2575,6 +2594,27 @@ std::vector> ParseScript(uint32_t& key_exp_index error = "Can only have rawtr at top level"; return {}; } + if (ctx == ParseScriptContext::TOP && Func("unused", expr)) { + // Check for only one expression, should not find commas, brackets, or parentheses + auto arg = Expr(expr); + if (expr.size()) { + error = strprintf("unused(): only one key expected"); + return {}; + } + auto keys = ParsePubkey(key_exp_index, arg, ctx, out, error); + if (keys.empty()) return {}; + for (auto& pubkey : keys) { + if (pubkey->IsRange()) { + error = "unused(): key cannot be ranged"; + return {}; + } + ret.emplace_back(std::make_unique(std::move(pubkey))); + } + return ret; + } else if (Func("unused", expr)) { + error = "Can only have unused at top level"; + return {}; + } if (ctx == ParseScriptContext::TOP && Func("raw", expr)) { std::string str(expr.begin(), expr.end()); if (!IsHex(str)) { diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 3ac748a1746..0f1e799e8f1 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -108,7 +108,7 @@ struct Descriptor { /** Convert the descriptor back to a string, undoing parsing. */ virtual std::string ToString(bool compat_format=false) const = 0; - /** Whether this descriptor will return one scriptPubKey or multiple (aka is or is not combo) */ + /** Whether this descriptor will return at most one scriptPubKey or multiple (aka is or is not combo) */ virtual bool IsSingleType() const = 0; /** Whether the given provider has all private keys required by this descriptor. @@ -179,6 +179,9 @@ struct Descriptor { */ virtual void GetPubKeys(std::set& pubkeys, std::set& ext_pubs) const = 0; + /** Whether this descriptor produces any scripts with the Expand functions */ + virtual bool HasScripts() const = 0; + /** Semantic/safety warnings (includes subdescriptors). */ virtual std::vector Warnings() const = 0; diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 0ede4dc9dfe..347efd77449 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1334,4 +1334,65 @@ BOOST_AUTO_TEST_CASE(descriptor_older_warnings) } } +void CheckSingleUnparsable(const std::string& desc, const std::string& expected_error) +{ + FlatSigningProvider keys; + std::string error; + auto parsed = Parse(desc, keys, error); + BOOST_CHECK_MESSAGE(parsed.empty(), desc); + BOOST_CHECK_EQUAL(error, expected_error); +} + +void CheckUnused(const std::string& prv, const std::string& pub) +{ + FlatSigningProvider keys_priv, keys_pub; + std::string error; + + std::unique_ptr parse_priv; + std::unique_ptr parse_pub; + parse_priv = std::move(Parse(prv, keys_priv, error).at(0)); + parse_pub = std::move(Parse(pub, keys_pub, error).at(0)); + BOOST_CHECK_MESSAGE(parse_priv, error); + BOOST_CHECK_MESSAGE(parse_pub, error); + + BOOST_CHECK(parse_priv->GetOutputType() == std::nullopt); + BOOST_CHECK(parse_pub->GetOutputType() == std::nullopt); + + // Check private keys are extracted from the private version but not the public one. + BOOST_CHECK(keys_priv.keys.size()); + BOOST_CHECK(!keys_pub.keys.size()); + + // Check that both versions serialize back to the public version. + std::string pub1 = parse_priv->ToString(); + std::string pub2 = parse_pub->ToString(); + BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub1), "Private ser: " + pub1 + " Public desc: " + pub); + BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub2), "Public ser: " + pub2 + " Public desc: " + pub); + + // Check both only have one pubkey + std::set prv_pubkeys; + std::set prv_extpubs; + parse_pub->GetPubKeys(prv_pubkeys, prv_extpubs); + BOOST_CHECK_EQUAL(prv_pubkeys.size() + prv_extpubs.size(), 1); + std::set pub_pubkeys; + std::set pub_extpubs; + parse_pub->GetPubKeys(pub_pubkeys, pub_extpubs); + BOOST_CHECK_EQUAL(pub_pubkeys.size() + pub_extpubs.size(), 1); +} + +// unused() descriptors don't produce scripts, so these need to be tested separately +BOOST_AUTO_TEST_CASE(unused_descriptor_test) +{ + CheckUnparsable("unused(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "unused(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "unused(): only one key expected"); + CheckUnparsable("wsh(unused(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1))", "wsh(unused(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))", "Can only have unused at top level"); + CheckUnparsable("unused(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/*)", "unused(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/*)", "unused(): key cannot be ranged"); + CheckUnparsable("unused()", "unused()", "No key provided"); + + // x-only keys cannot be used in unused() + CheckSingleUnparsable("unused(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)", "Pubkey 'a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd' is invalid"); + + CheckUnused("unused(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc)", "unused(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL)"); + CheckUnused("unused(L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1)", "unused(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)"); + CheckUnused("unused(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/0h/0h/1)", "unused(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0h/0h/1)"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/test/walletload_tests.cpp b/src/wallet/test/walletload_tests.cpp index 46b0ab16c6d..8648af6b6c2 100644 --- a/src/wallet/test/walletload_tests.cpp +++ b/src/wallet/test/walletload_tests.cpp @@ -37,6 +37,7 @@ public: std::optional MaxSatisfactionWeight(bool) const override { return {}; } std::optional MaxSatisfactionElems() const override { return {}; } void GetPubKeys(std::set& pubkeys, std::set& ext_pubs) const override {} + bool HasScripts() const override { return true; } std::vector Warnings() const override { return {}; } uint32_t GetMaxKeyExpr() const override { return 0; } size_t GetKeyCount() const override { return 0; } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 823e1b48211..f3f6e8f477a 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3813,8 +3813,8 @@ util::Result> CWallet::AddWall } // Apply the label if necessary - // Note: we disable labels for ranged descriptors - if (!desc.descriptor->IsRange()) { + // Note: we disable labels for descriptors that are ranged or that don't produce output scripts (i.e. unused()) + if (!desc.descriptor->IsRange() && desc.descriptor->HasScripts()) { auto script_pub_keys = spk_man->GetScriptPubKeys(); if (script_pub_keys.empty()) { return util::Error{_("Could not generate scriptPubKeys (cache is empty)")};