diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -392,17 +392,30 @@ // Derive keys or fetch them from cache CExtPubKey final_extkey = m_root_extkey; + CExtPubKey parent_extkey = m_root_extkey; bool der = true; if (read_cache) { if (!read_cache->GetCachedDerivedExtPubKey(m_expr_index, pos, final_extkey)) { - return false; + if (m_derive == DeriveType::HARDENED) { + return false; + } + // Try to get the derivation parent + if (!read_cache->GetCachedParentExtPubKey(m_expr_index, + parent_extkey)) { + return false; + } + final_extkey = parent_extkey; + if (m_derive == DeriveType::UNHARDENED) { + der = parent_extkey.Derive(final_extkey, pos); + } } } else if (IsHardened()) { CExtKey xprv; if (!GetDerivedExtKey(arg, xprv)) { return false; } + parent_extkey = xprv.Neuter(); if (m_derive == DeriveType::UNHARDENED) { der = xprv.Derive(xprv, pos); } @@ -412,11 +425,12 @@ final_extkey = xprv.Neuter(); } else { for (auto entry : m_path) { - der = final_extkey.Derive(final_extkey, entry); + der = parent_extkey.Derive(parent_extkey, entry); assert(der); } + final_extkey = parent_extkey; if (m_derive == DeriveType::UNHARDENED) { - der = final_extkey.Derive(final_extkey, pos); + der = parent_extkey.Derive(final_extkey, pos); } assert(m_derive != DeriveType::HARDENED); } @@ -427,6 +441,10 @@ if (write_cache) { write_cache->CacheDerivedExtPubKey(m_expr_index, pos, final_extkey); + // Only cache parent if there is any unhardened derivation + if (m_derive != DeriveType::HARDENED) { + write_cache->CacheParentExtPubKey(m_expr_index, parent_extkey); + } } return true; diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -37,6 +37,8 @@ // We can sign with this descriptor (this is not true when actual BIP32 // derivation is used, as that's not integrated in our signing code) constexpr int SIGNABLE = 8; +// The final derivation is hardened, i.e. ends with *' or *h +constexpr int DERIVE_HARDENED = 16; /** * Compare two descriptors. If only one of them has a checksum, the checksum is @@ -179,6 +181,31 @@ BOOST_CHECK(script_provider.origins == script_provider_cached.origins); + // Make sure we can expand using cached xpubs for unhardened + // derivation + if (!(flags & DERIVE_HARDENED)) { + // Evaluate the descriptor at i + 1 + FlatSigningProvider script_provider1, script_provider_cached1; + std::vector spks1, spk1_from_cache; + BOOST_CHECK((t ? parse_priv : parse_pub) + ->Expand(i + 1, key_provider, spks1, + script_provider1, nullptr)); + + // Try again but use the cache from expanding i. That cache + // won't have the pubkeys for i + 1, but will have the parent + // xpub for derivation. + BOOST_CHECK(parse_pub->ExpandFromCache( + i + 1, desc_cache, spk1_from_cache, + script_provider_cached1)); + BOOST_CHECK(spks1 == spk1_from_cache); + BOOST_CHECK(script_provider1.pubkeys == + script_provider_cached1.pubkeys); + BOOST_CHECK(script_provider1.scripts == + script_provider_cached1.scripts); + BOOST_CHECK(script_provider1.origins == + script_provider_cached1.origins); + } + // For each of the produced scripts, verify solvability, and when // possible, try to sign a transaction spending it. for (size_t n = 0; n < spks.size(); ++n) { @@ -396,7 +423,7 @@ "sh(pkh(" "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjq" "JoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", - RANGE | HARDENED, + RANGE | HARDENED | DERIVE_HARDENED, {{"a9149976cc014a7c012bbc43954a38cdee554865bd1987"}, {"a914ad29a49cb0420b53d9d6bb8944bcd819c5ff716e87"}, {"a914b2d9d290b193fd23b720e738184e3eedadc7d87d87"}}, @@ -533,7 +560,7 @@ "*," "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjq" "JoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*'))", - HARDENED | RANGE, + HARDENED | RANGE | DERIVE_HARDENED, {{"a914261bbb58cd9714f92e91668d3eed7c4c860f4cdc87"}, {"a914ff07ad97dc4458ed5236c52bb94ccb7339dedff887"}, {"a91451f96613fdd66e75a026811ba4fdb9efec2c83a987"}},