diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index ea69280c41d..a246383a09d 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -203,6 +203,7 @@ public: enum class StringType { PUBLIC, + CANONICAL, // string calculation that always use h COMPAT // string calculation that mustn't change over time to stay compatible with previous software versions }; @@ -259,7 +260,7 @@ class OriginPubkeyProvider final : public PubkeyProvider std::string OriginString(StringType type, bool normalized=false) const { // If StringType==COMPAT, always use the apostrophe to stay compatible with previous versions - bool use_apostrophe = (!normalized && m_apostrophe) || type == StringType::COMPAT; + bool use_apostrophe = (type != StringType::CANONICAL && !normalized && m_apostrophe) || type == StringType::COMPAT; return HexStr(m_origin.fingerprint) + FormatHDKeypath(m_origin.path, use_apostrophe); } @@ -509,7 +510,7 @@ public: std::string ToString(StringType type, bool normalized) const { // If StringType==COMPAT, always use the apostrophe to stay compatible with previous versions - const bool use_apostrophe = (!normalized && m_apostrophe) || type == StringType::COMPAT; + const bool use_apostrophe = (type != StringType::CANONICAL && !normalized && m_apostrophe) || type == StringType::COMPAT; std::string ret = EncodeExtPubKey(m_root_extkey) + FormatHDKeypath(m_path, /*apostrophe=*/use_apostrophe); if (IsRange()) { ret += "/*"; @@ -874,6 +875,7 @@ public: PUBLIC, PRIVATE, NORMALIZED, + CANONICAL, COMPAT, // string calculation that mustn't change over time to stay compatible with previous software versions }; @@ -960,6 +962,9 @@ public: case StringType::COMPAT: tmp = pubkey->ToString(PubkeyProvider::StringType::COMPAT); break; + case StringType::CANONICAL: + tmp = pubkey->ToString(PubkeyProvider::StringType::CANONICAL); + break; } ret += tmp; } @@ -979,6 +984,13 @@ public: return AddChecksum(ret); } + std::string ToCanonicalString() const final + { + std::string ret; + ToStringHelper(nullptr, ret, StringType::CANONICAL); + return AddChecksum(ret); + } + bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { bool has_priv_key{ToStringHelper(&arg, out, StringType::PRIVATE)}; @@ -1667,6 +1679,9 @@ public: // must be preserved for wallets with Miniscript descriptors to be loaded ret = m_pubkeys[key]->ToString(PubkeyProvider::StringType::PUBLIC); break; + case DescriptorImpl::StringType::CANONICAL: + ret = m_pubkeys[key]->ToString(PubkeyProvider::StringType::CANONICAL); + break; } return ret; } diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 26781629cbf..8d9f8a4e717 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -118,6 +118,11 @@ struct Descriptor { /** Convert the descriptor back to a string, undoing parsing. */ virtual std::string ToString(bool compat_format=false) const = 0; + /** Convert the descriptor to the canonical string. + * The canonical string is the same as the public string but always uses h as the hardened indicator + */ + virtual std::string ToCanonicalString() const = 0; + /** Whether this descriptor will return at most one scriptPubKey or multiple (aka is or is not combo) */ virtual bool IsSingleType() const = 0; diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index ed410dfbb14..5b5b10aaa01 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -226,14 +226,21 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int // Otherwise check that they serialize back to the public version. std::string pub1 = parse_priv->ToString(); std::string pub2 = parse_pub->ToString(); + std::string canonical = UseHInsteadOfApostrophe(pub); if (expected_pub) { BOOST_CHECK_MESSAGE(EqualDescriptor(*expected_pub, pub1), "Private ser: " + pub1 + " Public desc: " + *expected_pub); BOOST_CHECK_MESSAGE(EqualDescriptor(*expected_pub, pub2), "Public ser: " + pub2 + " Public desc: " + *expected_pub); + canonical = UseHInsteadOfApostrophe(*expected_pub); } else { BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub1), "Private ser: " + pub1 + " Public desc: " + pub); BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub2), "Public ser: " + pub2 + " Public desc: " + pub); } + std::string priv_canonical = parse_priv->ToCanonicalString(); + std::string pub_canonical = parse_pub->ToCanonicalString(); + BOOST_CHECK_MESSAGE(EqualDescriptor(priv_canonical, canonical), "Private ser: " + priv_canonical + " Expected desc: " + canonical); + BOOST_CHECK_MESSAGE(EqualDescriptor(pub_canonical, canonical), "Public ser: " + pub_canonical + " Expected desc: " + canonical); + // Check that the COMPAT identifier did not change if (op_desc_id) { BOOST_CHECK_MESSAGE(DescriptorID(*parse_priv) == *op_desc_id, "DescriptorID() " + DescriptorID(*parse_priv).ToString() + " does not match for priv " + prv); diff --git a/src/wallet/test/walletload_tests.cpp b/src/wallet/test/walletload_tests.cpp index 66a34deed72..abda2bf4cb1 100644 --- a/src/wallet/test/walletload_tests.cpp +++ b/src/wallet/test/walletload_tests.cpp @@ -22,6 +22,7 @@ public: ~DummyDescriptor() = default; std::string ToString(bool compat_format) const override { return desc; } + std::string ToCanonicalString() const override { return desc; } std::optional GetOutputType() const override { return OutputType::UNKNOWN; } bool IsRange() const override { return false; }