Merge bitcoin/bitcoin#35445: wallet, descriptor: Revert `StringType::COMPAT` for Miniscript expressions and drop the concept of a Descriptor ID that can be validated
What changed, and why it matters
This Bitcoin Core update fixes a wallet bug where certain newer-style descriptors (called Miniscript) could not be loaded after being created in older versions. The root cause was an internal ID that was computed differently for Miniscript than for other descriptors. Rather than keep trying to make the ID match perfectly, the developers changed the wallet to treat the stored ID as an opaque label and compare descriptors by their text instead. This prevents 'wallet corrupted' errors and allows older wallets to load safely. It is a backward-compatibility and robustness fix, not a remote-exploitable vulnerability.
Users running descriptor wallets with Miniscript should upgrade to the version containing this fix to avoid 'wallet corrupted' load failures. Wallet developers should avoid relying on DescriptorID/CompatDescriptorHash for validation or equality checks and use canonical descriptor strings instead. No immediate emergency action is required; this is a compatibility/robustness fix rather than a remote-exploitable security flaw.
Security signals we found
Fixes wallet load failure (DBErrors::CORRUPT) for Miniscript descriptors created in prior versions
Removes validation of stored descriptor ID against recomputed hash
Switches descriptor equality checks from hash comparison to canonical string comparison
Adds backward-compatibility test coverage for v30.2 and v31.0 with Miniscript descriptors
Includes descriptor cache in export data to preserve non-self-expanding descriptors
Evidence from the diff
The commit reverts use of StringType::COMPAT inside Miniscript expressions, renames DescriptorID to CompatDescriptorHash, and removes the database validation that compared the stored descriptor ID against a recomputed ID. Descriptor equality is now determined by canonical string comparison (ToCanonicalString) instead of hash equality. The SPKM ID is read from the database as an opaque blob and stored in m_id. This addresses issue #35432, where Miniscript descriptors using ‘h’ vs apostrophe hardened indicators produced mismatched IDs, causing wallets to fail to load with DBErrors::CORRUPT. The change also includes descriptor cache export fixes and backward-compatibility tests for v30.2 and v31.0.
Changed components
src/script/descriptor.cppsrc/script/descriptor.hsrc/wallet/scriptpubkeyman.cppsrc/wallet/scriptpubkeyman.hsrc/wallet/wallet.cppsrc/wallet/walletdb.cppsrc/wallet/walletutil.cppsrc/wallet/walletutil.hsrc/wallet/export.cppsrc/wallet/export.hsrc/wallet/external_signer_scriptpubkeyman.cppsrc/wallet/external_signer_scriptpubkeyman.hsrc/wallet/rpc/wallet.cppInspect captured patch +293 / −195
### src/script/descriptor.cpp
@@ -203,11 +203,12 @@ struct PubkeyProvider
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
};
/** Get the descriptor string form. */
- virtual std::string ToString(StringType type=StringType::PUBLIC) const = 0;
+ virtual std::string ToString(StringType type) const = 0;
/** Get the descriptor string form including private data (if available in arg).
* If the private data is not available, the output string in the "out" parameter
@@ -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,15 +510,15 @@ class BIP32PubkeyProvider final : public PubkeyProvider
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 += "/*";
if (m_derive == DeriveType::HARDENED_RANGED) ret += use_apostrophe ? '\'' : 'h';
}
return ret;
}
- std::string ToString(StringType type=StringType::PUBLIC) const override
+ std::string ToString(StringType type) const override
{
return ToString(type, /*normalized=*/false);
}
@@ -551,7 +552,7 @@ class BIP32PubkeyProvider final : public PubkeyProvider
}
// Either no derivation or all unhardened derivation
if (i == -1) {
- out = ToString();
+ out = ToString(StringType::PUBLIC);
return true;
}
// Get the path to the last hardened stup
@@ -719,7 +720,7 @@ class MuSigPubkeyProvider final : public PubkeyProvider
// musig() expressions can only be used in tr() contexts which have 32 byte xonly pubkeys
size_t GetSize() const override { return 32; }
- std::string ToString(StringType type=StringType::PUBLIC) const override
+ std::string ToString(StringType type) const override
{
std::string out = "musig(";
for (size_t i = 0; i < m_participants.size(); ++i) {
@@ -874,6 +875,7 @@ class DescriptorImpl : public Descriptor
PUBLIC,
PRIVATE,
NORMALIZED,
+ CANONICAL,
COMPAT, // string calculation that mustn't change over time to stay compatible with previous software versions
};
@@ -955,11 +957,14 @@ class DescriptorImpl : public Descriptor
any_success = pubkey->ToPrivateString(*arg, tmp) || any_success;
break;
case StringType::PUBLIC:
- tmp = pubkey->ToString();
+ tmp = pubkey->ToString(PubkeyProvider::StringType::PUBLIC);
break;
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 @@ class DescriptorImpl : public Descriptor
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)};
@@ -1652,7 +1664,7 @@ class StringMaker {
has_priv_key = false;
switch (m_type) {
case DescriptorImpl::StringType::PUBLIC:
- ret = m_pubkeys[key]->ToString();
+ ret = m_pubkeys[key]->ToString(PubkeyProvider::StringType::PUBLIC);
break;
case DescriptorImpl::StringType::PRIVATE:
has_priv_key = m_pubkeys[key]->ToPrivateString(*m_arg, ret);
@@ -1661,7 +1673,14 @@ class StringMaker {
if (!m_pubkeys[key]->ToNormalizedString(*m_arg, ret, m_cache)) return {};
break;
case DescriptorImpl::StringType::COMPAT:
- ret = m_pubkeys[key]->ToString(PubkeyProvider::StringType::COMPAT);
+ // For backwards compatibility, we do not pass StringType::COMPAT.
+ // Prior to 31.0, COMPAT was not provided, so PUBLIC was in use. From this string,
+ // DescriptorSPKM IDs were computed from this string, so the incorrect behavior
+ // 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;
@@ -2260,7 +2279,7 @@ struct KeyParser {
// Keys that cannot be derived sort before the ones that can, and are compared by their
// expression so that two different keys are not taken for duplicates.
if (pub_a.has_value() != pub_b.has_value()) return !pub_a.has_value();
- return key_a.ToString() < key_b.ToString();
+ return key_a.ToString(PubkeyProvider::StringType::PUBLIC) < key_b.ToString(PubkeyProvider::StringType::PUBLIC);
}
ParseScriptContext ParseContext() const {
@@ -2283,7 +2302,7 @@ struct KeyParser {
std::optional<std::string> ToString(const Key& key, bool&) const
{
- return m_keys.at(key).at(0)->ToString();
+ return m_keys.at(key).at(0)->ToString(PubkeyProvider::StringType::PUBLIC);
}
template<typename I> std::optional<Key> FromPKBytes(I begin, I end) const
@@ -2985,7 +3004,7 @@ std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const Signing
return InferScript(script, ParseScriptContext::TOP, provider);
}
-uint256 DescriptorID(const Descriptor& desc)
+uint256 CompatDescriptorHash(const Descriptor& desc)
{
std::string desc_str = desc.ToString(/*compat_format=*/true);
uint256 id;
### 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;
@@ -239,9 +244,12 @@ std::string GetDescriptorChecksum(const std::string& descriptor);
*/
std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const SigningProvider& provider);
-/** Unique identifier that may not change over time, unless explicitly marked as not backwards compatible.
-* This is not part of BIP 380, not guaranteed to be interoperable and should not be exposed to the user.
+/** Hash of the COMPAT string representation of the descriptor that is not supposed to change over time.
+ * Due to the hash's usage in previous versions, the COMPAT string is computed with some quirks.
+ *
+ * The hash is the sha256 of the public descriptor using apostrophes as the hardened indicator, except inside of
+ * Miniscript expressions, where "h" is the hardened indicator.
*/
-uint256 DescriptorID(const Descriptor& desc);
+uint256 CompatDescriptorHash(const Descriptor& desc);
#endif // BITCOIN_SCRIPT_DESCRIPTOR_H
### src/test/descriptor_tests.cpp
[binary or diff unavailable]
### src/wallet/export.cpp
@@ -37,7 +37,8 @@ util::Expected<std::vector<WalletDescInfo>, std::string> ExportDescriptors(const
wallet.IsActiveScriptPubKeyMan(*desc_spk_man),
wallet.IsInternalScriptPubKeyMan(desc_spk_man),
is_range ? std::optional(std::make_pair(wallet_descriptor.GetStart(), wallet_descriptor.GetEnd())) : std::nullopt,
- wallet_descriptor.GetNext()
+ wallet_descriptor.GetNext(),
+ wallet_descriptor.cache
);
}
return wallet_descriptors;
@@ -105,15 +106,14 @@ util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs:
WalletDescriptor w_desc(std::move(descs.at(0)), desc_info.creation_time, range_start, range_end, desc_info.next_index);
- // For descriptors that cannot self expand (i.e. needs private keys or cache), retrieve the cache
- uint256 desc_id = w_desc.id;
+ // For descriptors that cannot self expand (i.e. needs private keys or cache), set the cache
if (!w_desc.descriptor->CanSelfExpand()) {
- DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(wallet.GetScriptPubKeyMan(desc_id));
- w_desc.cache = WITH_LOCK(desc_spkm->cs_desc_man, return desc_spkm->GetWalletDescriptor().cache);
+ w_desc.cache = desc_info.cache;
}
// Add to the watchonly wallet
- if (auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, dummy_keys, /*label=*/"", /*internal=*/false); !spkm_res) {
+ auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, dummy_keys, /*label=*/"", /*internal=*/false);
+ if (!spkm_res) {
return util::Error{util::ErrorString(spkm_res)};
}
@@ -125,7 +125,7 @@ util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs:
if (desc_info.internal) {
internal = *desc_info.internal;
}
- watchonly_wallet->AddActiveScriptPubKeyMan(desc_id, *Assert(w_desc.descriptor->GetOutputType()), internal);
+ watchonly_wallet->AddActiveScriptPubKeyMan(spkm_res->get().GetID(), *Assert(w_desc.descriptor->GetOutputType()), internal);
}
}
### src/wallet/export.h
@@ -14,7 +14,7 @@
namespace wallet {
// Struct containing all of the info from WalletDescriptor, except with the descriptor as a string,
-// and without its ID or cache.
+// and without its ID.
// Used when exporting descriptors from the wallet.
struct WalletDescInfo {
std::string descriptor;
@@ -23,6 +23,7 @@ struct WalletDescInfo {
std::optional<bool> internal;
std::optional<std::pair<int64_t,int64_t>> range;
int64_t next_index;
+ DescriptorCache cache;
};
//! Export the descriptors from a wallet so that they can be imported elsewhere
### src/wallet/external_signer_scriptpubkeyman.cpp
@@ -21,24 +21,23 @@
using common::PSBTError;
namespace wallet {
-std::unique_ptr<ExternalSignerScriptPubKeyMan> ExternalSignerScriptPubKeyMan::LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+std::unique_ptr<ExternalSignerScriptPubKeyMan> ExternalSignerScriptPubKeyMan::LoadFromStorage(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
{
- return std::unique_ptr<ExternalSignerScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(storage, descriptor, keypool_size, keys, ckeys));
+ return std::unique_ptr<ExternalSignerScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(storage, id, descriptor, keypool_size, keys, ckeys));
}
std::unique_ptr<ExternalSignerScriptPubKeyMan> ExternalSignerScriptPubKeyMan::CreateNew(WalletStorage& storage, WalletBatch& batch, int64_t keypool_size, std::unique_ptr<Descriptor> desc)
{
- auto spkm = std::unique_ptr<ExternalSignerScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(storage, keypool_size));
-
- LOCK(spkm->cs_desc_man);
- assert(storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
- assert(storage.IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
-
int64_t creation_time = GetTime();
// Make the descriptor
WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0);
- spkm->m_wallet_descriptor = w_desc;
+
+ auto spkm = std::unique_ptr<ExternalSignerScriptPubKeyMan>(new ExternalSignerScriptPubKeyMan(storage, w_desc, keypool_size));
+
+ LOCK(spkm->cs_desc_man);
+ assert(storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
+ assert(storage.IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER));
// Store the descriptor
if (!batch.WriteDescriptor(spkm->GetID(), spkm->m_wallet_descriptor)) {
### src/wallet/external_signer_scriptpubkeyman.h
@@ -16,17 +16,10 @@ namespace wallet {
class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
{
private:
- //! Create an ExternalSPKM from existing wallet data
- ExternalSignerScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
- : DescriptorScriptPubKeyMan(storage, descriptor, keypool_size, keys, ckeys)
- {}
-
- ExternalSignerScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
- : DescriptorScriptPubKeyMan(storage, keypool_size)
- {}
+ using DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan;
public:
- static std::unique_ptr<ExternalSignerScriptPubKeyMan> LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
+ static std::unique_ptr<ExternalSignerScriptPubKeyMan> LoadFromStorage(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
static std::unique_ptr<ExternalSignerScriptPubKeyMan> CreateNew(WalletStorage& storage, WalletBatch& batch, int64_t keypool_size, std::unique_ptr<Descriptor> desc);
static util::Result<ExternalSigner> GetExternalSigner();
### src/wallet/rpc/wallet.cpp
@@ -812,8 +812,7 @@ static RPCMethod createwalletdescriptor()
WalletBatch batch{pwallet->GetDatabase()};
for (bool internal : internals) {
WalletDescriptor w_desc = GenerateWalletDescriptor(xpub, *output_type, internal);
- uint256 w_id = DescriptorID(*w_desc.descriptor);
- if (!pwallet->GetScriptPubKeyMan(w_id)) {
+ if (!pwallet->GetDescriptorScriptPubKeyMan(w_desc)) {
spkms.emplace_back(pwallet->SetupDescriptorScriptPubKeyMan(batch, active_hdkey, *output_type, internal));
}
}
### src/wallet/scriptpubkeyman.cpp
@@ -846,11 +846,12 @@ std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::CreateFrom
return spkm;
}
-DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
: ScriptPubKeyMan(storage),
m_map_keys(keys),
m_map_crypted_keys(ckeys),
m_keypool_size(keypool_size),
+ m_id(id),
m_wallet_descriptor(descriptor)
{
if (!keys.empty() && !ckeys.empty()) {
@@ -859,15 +860,37 @@ DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, Wal
Load();
}
-std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
+std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::LoadFromStorage(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
{
- return std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, descriptor, keypool_size, keys, ckeys));
+ return std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, id, descriptor, keypool_size, keys, ckeys));
}
std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::GenerateNewSingleSig(WalletStorage& storage, WalletBatch& batch, int64_t keypool_size, const CExtKey& master_key, OutputType addr_type, bool internal)
{
- auto spkm = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, keypool_size));
- spkm->SetupDescriptorGeneration(batch, master_key, addr_type, internal);
+ WalletDescriptor desc = GenerateWalletDescriptor(master_key.Neuter(), addr_type, internal);
+
+ auto spkm = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, desc, keypool_size));
+
+ LOCK(spkm->cs_desc_man);
+ Assert(spkm->m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
+
+ // Store the master private key, and descriptor
+ if (!spkm->AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey())) {
+ throw std::runtime_error(std::string(__func__) + ": writing descriptor master private key failed");
+ }
+ if (!batch.WriteDescriptor(spkm->GetID(), spkm->m_wallet_descriptor)) {
+ throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
+ }
+
+ // Set m_decryption_thoroughly_checked for encrypted wallets
+ if (spkm->m_storage.HasEncryptionKeys()) {
+ spkm->m_decryption_thoroughly_checked = true;
+ }
+
+ // TopUp
+ spkm->TopUpWithDB(batch);
+
+ spkm->m_storage.UnsetBlankWalletFlag(batch);
return spkm;
}
@@ -1205,33 +1228,6 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const
}
}
-void DescriptorScriptPubKeyMan::SetupDescriptorGeneration(WalletBatch& batch, const CExtKey& master_key, OutputType addr_type, bool internal)
-{
- LOCK(cs_desc_man);
- Assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
- Assert(!m_wallet_descriptor.descriptor);
-
- m_wallet_descriptor = GenerateWalletDescriptor(master_key.Neuter(), addr_type, internal);
-
- // Store the master private key, and descriptor
- if (!AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey())) {
- throw std::runtime_error(std::string(__func__) + ": writing descriptor master private key failed");
- }
- if (!batch.WriteDescriptor(GetID(), m_wallet_descriptor)) {
- throw std::runtime_error(std::string(__func__) + ": writing descriptor failed");
- }
-
- // Set m_decryption_thoroughly_checked for encrypted wallets
- if (m_storage.HasEncryptionKeys()) {
- m_decryption_thoroughly_checked = true;
- }
-
- // TopUp
- TopUpWithDB(batch);
-
- m_storage.UnsetBlankWalletFlag(batch);
-}
-
bool DescriptorScriptPubKeyMan::IsHDEnabled() const
{
LOCK(cs_desc_man);
@@ -1494,8 +1490,7 @@ std::unique_ptr<CKeyMetadata> DescriptorScriptPubKeyMan::GetMetadata(const CTxDe
uint256 DescriptorScriptPubKeyMan::GetID() const
{
- LOCK(cs_desc_man);
- return m_wallet_descriptor.id;
+ return m_id;
}
void DescriptorScriptPubKeyMan::Load()
@@ -1534,7 +1529,8 @@ void DescriptorScriptPubKeyMan::Load()
bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc) const
{
LOCK(cs_desc_man);
- return !m_wallet_descriptor.id.IsNull() && !desc.id.IsNull() && m_wallet_descriptor.id == desc.id;
+ // Compare by using the canonical string to make the hardened indicators consistent for comparison
+ return m_wallet_descriptor.descriptor->ToCanonicalString() == desc.descriptor->ToCanonicalString();
}
void DescriptorScriptPubKeyMan::WriteDescriptor()
@@ -1630,7 +1626,7 @@ util::Result<void> DescriptorScriptPubKeyMan::UpdateWalletDescriptor(WalletDescr
m_map_pubkeys.clear();
m_map_script_pub_keys.clear();
m_max_cached_index = -1;
- m_wallet_descriptor = descriptor;
+ m_wallet_descriptor.UpdateFrom(descriptor);
WalletBatch batch(m_storage.GetDatabase());
UpdateWithSigningProvider(batch, provider);
### src/wallet/scriptpubkeyman.h
@@ -301,12 +301,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
*/
mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
- //! Create a new DescriptorScriptPubKeyMan from an existing descriptor (i.e. from an import)
- DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size)
- : ScriptPubKeyMan(storage),
- m_keypool_size(keypool_size),
- m_wallet_descriptor(descriptor)
- {}
+ const uint256 m_id;
bool AddDescriptorKeyWithDB(WalletBatch& batch, const CKey& key, const CPubKey &pubkey) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
@@ -323,16 +318,16 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
void UpdateWithSigningProvider(WalletBatch& batch, const FlatSigningProvider& signing_provider) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
- //! Setup descriptors based on the given CExtKey
- void SetupDescriptorGeneration(WalletBatch& batch, const CExtKey& master_key, OutputType addr_type, bool internal);
-
protected:
//! Create a DescriptorScriptPubKeyMan from existing data (i.e. during loading)
- DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
+ DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
- DescriptorScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
+ //! Create a new DescriptorScriptPubKeyMan from a descriptor (e.g. from an import, newly generated)
+ DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size)
: ScriptPubKeyMan(storage),
- m_keypool_size(keypool_size)
+ m_keypool_size(keypool_size),
+ m_id(CompatDescriptorHash(*descriptor.descriptor)),
+ m_wallet_descriptor(descriptor)
{}
WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
@@ -344,7 +339,7 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
bool TopUpWithDB(WalletBatch& batch, unsigned int size = 0);
public:
- static std::unique_ptr<DescriptorScriptPubKeyMan> LoadFromStorage(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
+ static std::unique_ptr<DescriptorScriptPubKeyMan> LoadFromStorage(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
static std::unique_ptr<DescriptorScriptPubKeyMan> CreateFromImport(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
static std::unique_ptr<DescriptorScriptPubKeyMan> CreateFromMigration(WalletStorage& storage, WalletBatch& batch, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
static std::unique_ptr<DescriptorScriptPubKeyMan> GenerateNewSingleSig(WalletStorage& storage, WalletBatch& batch, int64_t keypool_size, const CExtKey& master_key, OutputType addr_type, bool internal);
### src/wallet/test/walletload_tests.cpp
@@ -22,9 +22,10 @@ class DummyDescriptor final : public Descriptor {
~DummyDescriptor() = default;
std::string ToString(bool compat_format) const override { return desc; }
+ std::string ToCanonicalString() const override { return desc; }
std::optional<OutputType> GetOutputType() const override { return OutputType::UNKNOWN; }
- bool IsRange() const override { return false; }
+ bool IsRange() const override { return true; }
bool IsSolvable() const override { return false; }
bool IsSingleType() const override { return true; }
bool HavePrivateKeys(const SigningProvider&) const override { return false; }
@@ -65,30 +66,21 @@ BOOST_FIXTURE_TEST_CASE(wallet_load_descriptors, TestingSetup)
}
// Test 2
- // Now write a valid descriptor with an invalid ID.
- // As the software produces another ID for the descriptor, the loading process must be aborted.
+ // Now write a valid descriptor with a different ID which must be accepted
database = CreateMockableWalletDatabase();
- // Verify the error
- bool found = false;
- DebugLogHelper logHelper("The descriptor ID calculated by the wallet differs from the one in DB", [&](const std::string* s) {
- found = true;
- return false;
- });
-
{
- // Write valid descriptor with invalid ID
+ // Write valid descriptor with arbitrary ID
WalletBatch batch(*database);
std::string desc = "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu";
WalletDescriptor wallet_descriptor(std::make_shared<DummyDescriptor>(desc), 0, 0, 0, 0);
BOOST_CHECK(batch.WriteDescriptor(uint256::ONE, wallet_descriptor));
}
{
- // Now try to load the wallet and verify the error.
+ // Now try to load the wallet and verify the result.
const std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", std::move(database)));
- BOOST_CHECK_EQUAL(wallet->PopulateWalletFromDB(_error, _warnings), DBErrors::CORRUPT);
- BOOST_CHECK(found); // The error must be logged
+ BOOST_CHECK_EQUAL(wallet->PopulateWalletFromDB(_error, _warnings), DBErrors::LOAD_OK);
}
}
### src/wallet/wallet.cpp
@@ -3603,9 +3603,9 @@ void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc,
{
std::unique_ptr<DescriptorScriptPubKeyMan> spk_manager;
if (IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
- spk_manager = ExternalSignerScriptPubKeyMan::LoadFromStorage(*this, desc, m_keypool_size, keys, ckeys);
+ spk_manager = ExternalSignerScriptPubKeyMan::LoadFromStorage(*this, id, desc, m_keypool_size, keys, ckeys);
} else {
- spk_manager = DescriptorScriptPubKeyMan::LoadFromStorage(*this, desc, m_keypool_size, keys, ckeys);
+ spk_manager = DescriptorScriptPubKeyMan::LoadFromStorage(*this, id, desc, m_keypool_size, keys, ckeys);
}
AddScriptPubKeyMan(id, std::move(spk_manager));
}
@@ -3765,14 +3765,13 @@ void CWallet::DeactivateScriptPubKeyMan(uint256 id, OutputType type, bool intern
DescriptorScriptPubKeyMan* CWallet::GetDescriptorScriptPubKeyMan(const WalletDescriptor& desc) const
{
- auto spk_man_pair = m_spk_managers.find(desc.id);
+ auto spk_man_pair = std::find_if(m_spk_managers.begin(), m_spk_managers.end(), [&desc](const auto& item) {
+ DescriptorScriptPubKeyMan* spk_manager = dynamic_cast<DescriptorScriptPubKeyMan*>(item.second.get());
+ return spk_manager != nullptr && spk_manager->HasWalletDescriptor(desc);
+ });
if (spk_man_pair != m_spk_managers.end()) {
- // Try to downcast to DescriptorScriptPubKeyMan then check if the descriptors match
- DescriptorScriptPubKeyMan* spk_manager = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man_pair->second.get());
- if (spk_manager != nullptr && spk_manager->HasWalletDescriptor(desc)) {
- return spk_manager;
- }
+ return dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man_pair->second.get());
}
return nullptr;
### src/wallet/walletdb.cpp
@@ -777,11 +777,6 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
return DBErrors::UNKNOWN_DESCRIPTOR;
}
- if (id != desc.id) {
- strErr = "The descriptor ID calculated by the wallet differs from the one in DB";
- return DBErrors::CORRUPT;
- }
-
DescriptorCache cache;
// Get key cache for this descriptor
### src/wallet/walletutil.cpp
@@ -85,4 +85,16 @@ WalletDescriptor GenerateWalletDescriptor(const CExtPubKey& master_key, const Ou
return w_desc;
}
+void WalletDescriptor::UpdateFrom(const WalletDescriptor& other)
+{
+ if (descriptor->ToCanonicalString() != other.descriptor->ToCanonicalString()) {
+ return;
+ }
+ range_start = other.range_start;
+ next_index = other.next_index;
+ range_end = other.range_end;
+ creation_time = other.creation_time;
+ cache = other.cache;
+}
+
} // namespace wallet
### src/wallet/walletutil.h
@@ -68,7 +68,6 @@ class WalletDescriptor
int32_t range_end = 0; // Item after the last; end of range, exclusive, i.e. [range_start, range_end). This will increment with each TopUp()
public:
std::shared_ptr<Descriptor> descriptor;
- uint256 id; // Descriptor ID (calculated once at descriptor initialization/deserialization)
uint64_t creation_time = 0;
DescriptorCache cache;
@@ -109,7 +108,6 @@ class WalletDescriptor
throw std::ios_base::failure("Can't load a multipath descriptor from databases");
}
descriptor = std::move(descs.at(0));
- id = DescriptorID(*descriptor);
}
SERIALIZE_METHODS(WalletDescriptor, obj)
@@ -126,8 +124,9 @@ class WalletDescriptor
next_index(next_index),
range_end(descriptor->IsRange() ? range_end : 1),
descriptor(descriptor),
- id(DescriptorID(*descriptor)),
creation_time(creation_time) {}
+
+ void UpdateFrom(const WalletDescriptor& other);
};
WalletDescriptor GenerateWalletDescriptor(const CExtPubKey& master_key, const OutputType& output_type, bool internal);
### test/functional/wallet_backwards_compatibility.py
@@ -36,11 +36,13 @@
class BackwardsCompatibilityTest(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
- self.num_nodes = 8
+ self.num_nodes = 10
# Add new version after each release:
self.extra_args = [
["-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # Pre-release: use to mine blocks. noban for immediate tx relay
["-nowallet", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # Pre-release: use to receive coins, swap wallets, etc
+ ["-nowallet", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v31.0
+ ["-nowallet", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v30.2
["-nowallet", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v25.0
["-nowallet", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v24.0.1
["-nowallet", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v23.0
@@ -58,6 +60,8 @@ def setup_nodes(self):
self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[
None,
None,
+ 310000,
+ 300200,
250000,
240001,
230000,
@@ -269,7 +273,8 @@ def run_test(self):
node_v21 = self.nodes[self.num_nodes - 2]
node_v20 = self.nodes[self.num_nodes - 1] # bdb only
- legacy_nodes = self.nodes[2:] # Nodes that support legacy wallets
+ previous_nodes = self.nodes[2:] # All previous version nodes
+ legacy_nodes = self.nodes[-6:] # Nodes that support legacy wallets
descriptors_nodes = self.nodes[2:-1] # Nodes that support descriptor wallets
self.generatetoaddress(node_miner, COINBASE_MATURITY + 1, node_miner.getnewaddress())
@@ -323,13 +328,23 @@ def run_test(self):
assert info['private_keys_enabled']
assert_equal(info['keypoolsize'], 0)
+ node_master.createwallet(wallet_name="miniscript")
+ wallet = node_master.get_wallet_rpc("miniscript")
+ miniscript_desc = "wsh(or_b(pk([deadbeef/0h/1h/2h]tprv8ZgxMBicQKsPerQj6m35no46amfKQdjY7AhLnmatHYXs8S4MTgeZYkWAn4edSGwwL3vkSiiGqSZQrmy5D3P5gBoqgvYP2fCUpBwbKTMTAkL/3h/*),s:pk([beefdead/4h/5h]tpubD6NzVbkrYhZ4YU9vM1s53UhD75UyJatx8EMzMZ3VUjR2FciNfLLkAw6a4pWACChzobTseNqdWk4G7ZdBqRDLtLSACKykTScmqibb1ZrCvJu/6/7/*)))"
+ miniscript_apos = miniscript_desc.replace("[beefdead/4h/5h]", "[beefdead/4'/5']")
+ assert miniscript_apos != miniscript_desc
+ for desc in [miniscript_desc, miniscript_apos]:
+ res = wallet.importdescriptors([{"desc": descsum_create(desc), "timestamp":"now"}])
+ assert_equal(res[0]["success"], True)
+
# Unload wallets and copy to older nodes:
node_master_wallets_dir = node_master.wallets_path
node_master.unloadwallet("w1")
node_master.unloadwallet("w2")
node_master.unloadwallet("w3")
+ node_master.unloadwallet("miniscript")
- for node in legacy_nodes:
+ for node in previous_nodes:
# Copy wallets to previous version
for wallet in os.listdir(node_master_wallets_dir):
dest = node.wallets_path / wallet
@@ -341,12 +356,23 @@ def run_test(self):
# since we can no longer create legacy wallets.
for node in descriptors_nodes:
self.log.info(f"- {node.version}")
- for wallet_name in ["w1", "w2", "w3"]:
+ for wallet_name in ["w1", "w2", "w3", "miniscript"]:
if self.major_version_less_than(node, 22) and wallet_name == "w1":
# Descriptor wallets created after 0.21 have taproot descriptors which 0.21 does not support, tested below
continue
+ if self.major_version_less_than(node, 24) and wallet_name == "miniscript":
+ # Miniscript was introduced in 24.0
+ continue
# Also try to reopen on master after opening on old
for n in [node, node_master]:
+ # 31.0 has a descriptor id calculation incompatibility.
+ # Miniscript descriptors imported into node versions other than 31.0 will
+ # result in wallets that cannot be loaded into 31.0.
+ # These wallets will emit a "Wallet corrupted" error.
+ if wallet_name == "miniscript" and n.version == 310000:
+ assert_raises_rpc_error(-4, "Wallet corrupted", n.loadwallet, wallet_name)
+ continue
+
n.loadwallet(wallet_name)
wallet = n.get_wallet_rpc(wallet_name)
info = wallet.getwalletinfo()
@@ -368,9 +394,15 @@ def run_test(self):
elif wallet_name == "w2":
assert_equal(info['private_keys_enabled'], False)
assert_equal(info['keypoolsize'], 0)
- else:
+ elif wallet_name == "w3":
assert_equal(info['private_keys_enabled'], True)
assert_equal(info['keypoolsize'], 0)
+ elif wallet_name == "miniscript":
+ for desc in wallet.listdescriptors()["descriptors"]:
+ if desc["desc"].startswith("wsh(or_b(pk"):
+ break
+ else:
+ assert False, "Did not find miniscript descriptor"
# Copy back to master
wallet.unloadwallet()
@@ -405,6 +437,10 @@ def run_test(self):
hdkeypath = addr_info["hdkeypath"].replace("'", "h")
pubkey = addr_info["pubkey"]
+ if self.major_version_at_least(node, 24):
+ res = wallet_prev.importdescriptors([{"desc": descsum_create(miniscript_desc), "timestamp":"now"}])
+ assert_equal(res[0]["success"], True)
+
# Make a backup of the wallet file
backup_path = os.path.join(self.options.tmpdir, f"{wallet_name}.dat")
wallet_prev.backupwallet(backup_path)
@@ -429,6 +465,13 @@ def get_flags(conn):
descriptor = f"wpkh([{info['hdmasterfingerprint']}{hdkeypath[1:]}]{pubkey})"
assert_equal(info["desc"], descsum_create(descriptor))
+ if self.major_version_at_least(node, 24):
+ for desc in wallet.listdescriptors()["descriptors"]:
+ if desc["desc"].startswith("wsh(or_b(pk"):
+ break
+ else:
+ assert False, "Did not find miniscript descriptor"
+
# Make backup so the wallet can be copied back to old node
down_wallet_name = f"re_down_{node.version}"
down_backup_path = os.path.join(self.options.tmpdir, f"{down_wallet_name}.dat")
### test/functional/wallet_importdescriptors.py
@@ -345,11 +345,14 @@ def run_test(self):
assert_equal(w1.getwalletinfo()['keypoolsize'], 0)
self.log.info("Test can import same descriptor with public key twice")
+ list_descs = w1.listdescriptors()
self.test_importdesc(import_request, success=True)
+ assert_equal(list_descs, w1.listdescriptors())
self.log.info("Test can update descriptor label")
self.test_importdesc({**import_request, "label": "Updated label"}, success=True)
test_address(w1, key.p2pkh_addr, solvable=True, ismine=True, labels=["Updated label"])
+ assert_equal(list_descs, w1.listdescriptors())
self.log.info("Internal addresses cannot have labels")
self.test_importdesc({**import_request, "internal": True},
@@ -615,7 +618,9 @@ def run_test(self):
self.log.info("Check we can change next_index")
# go back and forth with next_index
for i in [4, 0, 2, 1, 3]:
- self.test_importdesc({'desc': descsum_create('wpkh([80002067/0h/0h]' + xpub + '/*)'),
+ # Sometimes use h, sometimes use ' for hardened indicator
+ hard = "'" if i & 2 == 0 else "h"
+ self.test_importdesc({'desc': descsum_create(f'wpkh([80002067/0{hard}/0{hard}]' + xpub + '/*)'),
'active': True,
'range': [0, 9],
'next_index': i,
@@ -624,6 +629,24 @@ def run_test(self):
success=True)
assert_equal(w1.getnewaddress('', 'bech32'), addresses[i])
+ self.log.info("Equivalent Miniscript descriptors should not be duplicated")
+ self.nodes[1].createwallet(wallet_name="wminiscript", disable_private_keys=True, blank=True)
+ wminiscript = self.nodes[1].get_wallet_rpc("wminiscript")
+ miniscript_request = {
+ 'active': True,
+ 'range': [0, 9],
+ 'timestamp': 'now',
+ }
+ self.test_importdesc({
+ **miniscript_request,
+ 'desc': descsum_create(f"wsh(and_v(v:pk([80002067/0h/0h]{xpub}/*),older(1)))"),
+ }, success=True, wallet=wminiscript)
+ self.test_importdesc({
+ **miniscript_request,
+ 'desc': descsum_create(f"wsh(and_v(v:pk([80002067/0'/0']{xpub}/*),older(1)))"),
+ }, success=True, wallet=wminiscript)
+ assert_equal(len(wminiscript.listdescriptors()["descriptors"]), 1)
+
# Check active=False default
self.log.info('Check imported descriptors are not active by default')
self.test_importdesc({'desc': descsum_create('pkh([12345678/1h]' + xpub + '/*)'),
### test/get_previous_releases.py
@@ -91,6 +91,24 @@
"866a4b703a2095301151c17dcc753e19e4dba61ec68d19709ec4f81ff4320103": {"tag": "v28.2", "archive": "bitcoin-28.2-x86_64-apple-darwin.tar.gz"},
"98add5f220c01b387343b70edeb6273403fe081e22cd85fda132704cdcaa98aa": {"tag": "v28.2", "archive": "bitcoin-28.2-x86_64-linux-gnu.tar.gz"},
"da0869639c323bbf6f264f1829083b9514e10179b90c34b09d8cbcab8a1897e3": {"tag": "v28.2", "archive": "bitcoin-28.2-win64.zip"},
+
+ "73e76c14edc79808a0511c744d102ffbb494807ee90cbcba176568243254b532": {"tag": "v30.2", "archive": "bitcoin-30.2-aarch64-linux-gnu.tar.gz"},
+ "d510542842318ea34d87cb2c93d6a7fe091dcac2e8684460be2b3c44843fb502": {"tag": "v30.2", "archive": "bitcoin-30.2-arm-linux-gnueabihf.tar.gz"},
+ "c2ecab62891de22228043815cb6211549a32272be3d5d052ff19847d3420bd10": {"tag": "v30.2", "archive": "bitcoin-30.2-arm64-apple-darwin.tar.gz"},
+ "db8803f11f8259794864b8b0d2ef8a1a27d01a5943ff4f525bc26a325031fa87": {"tag": "v30.2", "archive": "bitcoin-30.2-powerpc64-linux-gnu.tar.gz"},
+ "b0302e4d9579d19a9a501f1278e5d2c56d33fd9583040f34802d8567a1f81ace": {"tag": "v30.2", "archive": "bitcoin-30.2-riscv64-linux-gnu.tar.gz"},
+ "99d5cee9b9c37be506396c30837a4b98e320bfea71c474d6120a7e8eb6075c7b": {"tag": "v30.2", "archive": "bitcoin-30.2-x86_64-apple-darwin.tar.gz"},
+ "6aa7bb4feb699c4c6262dd23e4004191f6df7f373b5d5978b5bcdd4bb72f75d8": {"tag": "v30.2", "archive": "bitcoin-30.2-x86_64-linux-gnu.tar.gz"},
+ "0d7e1f16f8823aa26d29b44855ff6dbac11c03d75631a6c1d2ea5fab3a84fdf8": {"tag": "v30.2", "archive": "bitcoin-30.2-win64.zip"},
+
+ "4de1d568dedd48604f75132421bc0abeca432639589b49a3909c81db3a813112": {"tag": "v31.0", "archive": "bitcoin-31.0-aarch64-linux-gnu.tar.gz"},
+ "8c19d007bfc73502625095ea4073af3a98ceb722d500556ab173bac5bcadd0d6": {"tag": "v31.0", "archive": "bitcoin-31.0-arm-linux-gnueabihf.tar.gz"},
+ "a2d7a13b4da53d4a3e4c517f3a0269e2429813417bb320d3b268993cfdc545d0": {"tag": "v31.0", "archive": "bitcoin-31.0-arm64-apple-darwin.tar.gz"},
+ "1d9c865aa0ccf675fc068e79d9fa57a5a70b59132fca38bb322a7d44ce2f0ff2": {"tag": "v31.0", "archive": "bitcoin-31.0-powerpc64-linux-gnu.tar.gz"},
+ "7ece4ea365bba9b2008b27f0717ef6a518598a572edaa2815e775faadc53c136": {"tag": "v31.0", "archive": "bitcoin-31.0-riscv64-linux-gnu.tar.gz"},
+ "56824dd705bc2a3b22d42e8aa02ed53498d491ff7c2c8aa96831333871887ead": {"tag": "v31.0", "archive": "bitcoin-31.0-x86_64-apple-darwin.tar.gz"},
+ "d3e4c58a35b1d0a97a457462c94f55501ad167c660c245cb1ffa565641c65074": {"tag": "v31.0", "archive": "bitcoin-31.0-x86_64-linux-gnu.tar.gz"},
+ "82fd2c504a0f20a31d4d13bd407783d6fc7bf17622d0ce85228a9b92694e03f0": {"tag": "v31.0", "archive": "bitcoin-31.0-win64.zip"},
}
Why this scored 57/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.