diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index cde6762da9e7ea..f3d4d9ef1ae35a 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -830,11 +830,20 @@ class DescriptorImpl : public Descriptor for (const auto& p : m_pubkey_args) { try { auto sppubkeyprovider = dynamic_cast(*p); - SpKey key; - sppubkeyprovider.GetSpKey(provider, key); - out.spkeys.emplace(key.Neuter().GetID(), key); - out.keys.emplace(key.spendKey.GetPubKey().GetID(), key.spendKey); - out.keys.emplace(key.scanKey.GetPubKey().GetID(), key.scanKey); + { + SpKey key; + if (sppubkeyprovider.GetSpKey(provider, key)) { + out.spkeys.emplace(key.Neuter().GetID(), key); + out.keys.emplace(key.spendKey.GetPubKey().GetID(), key.spendKey); + out.keys.emplace(key.scanKey.GetPubKey().GetID(), key.scanKey); + } + } + { + SpPubKey key; + if (sppubkeyprovider.GetSpPubKey(key)) { + out.keys.emplace(key.scanKey.GetPubKey().GetID(), key.scanKey); + } + } continue; } catch (const std::bad_cast&) {} @@ -1428,7 +1437,9 @@ enum class ParseScriptContext { P2WPKH, //!< Inside wpkh() (no script, pubkey only) P2WSH, //!< Inside wsh() (script becomes v0 witness script) P2TR, //!< Inside tr() (either internal key, or BIP342 script leaf) - SP, //!< Inside sp() (spkeys are only valid under sp()) + SP_ONLY, //!< Argument inside sp() variant (spkeys are only valid under sp()) + SP_SCAN, //!< First argument inside sp() + SP_SPEND, //!< Second argument inside sp() }; /** @@ -1484,6 +1495,10 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S error = "Hybrid public keys are not allowed"; return nullptr; } + if (pubkey.IsValid() && ctx == ParseScriptContext::SP_SCAN) { + error = "Scan key must be a private key or extended private key"; + return nullptr; + } if (pubkey.IsFullyValid()) { if (permit_uncompressed || pubkey.IsCompressed()) { return std::make_unique(key_exp_index, pubkey, false); @@ -1520,12 +1535,7 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S CExtKey extkey = DecodeExtKey(str); CExtPubKey extpubkey = DecodeExtPubKey(str); - if (ctx == ParseScriptContext::SP) { - if (extkey.key.IsValid() || extpubkey.pubkey.IsValid()) { - error = "extended keys are not allowed"; - return nullptr; - } - + if (ctx == ParseScriptContext::SP_ONLY) { if (spkey.IsValid()) { out.keys.emplace(spkey.scanKey.GetPubKey().GetID(), spkey.scanKey); out.keys.emplace(spkey.spendKey.GetPubKey().GetID(), spkey.spendKey); @@ -1537,7 +1547,7 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S return std::make_unique(key_exp_index, sppubkey); } - error = "provided key is not a valid silent payment key"; + error = strprintf("key '%s' is not a valid sp key", str); return nullptr; } @@ -1556,10 +1566,58 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S type = DeriveType::HARDENED; } if (!ParseKeyPath(split, path, apostrophe, error)) return nullptr; + + if (ctx == ParseScriptContext::SP_SCAN || ctx == ParseScriptContext::SP_SPEND) { + CExtKey derivedKey; + CExtPubKey derivedPubKey; + for (const auto& p : path) { + derivedKey = extkey; + derivedPubKey = extpubkey; + + if (derivedKey.key.IsValid()) { + if (!derivedKey.Derive(derivedKey, p)) { + error = "Failed to derive key"; + return nullptr; + } + } + if (derivedPubKey.pubkey.IsValid()) { + if (!derivedPubKey.Derive(derivedPubKey, p)) { + error = "Failed to derive key"; + return nullptr; + } + } + } + + if (ctx == ParseScriptContext::SP_SCAN) { + if (!extkey.key.IsValid()) { + error = "Scan key must be a private key or extended private key"; + return nullptr; + } + + // Derive sp scan key from extkey + CKey scan_key = derivedKey.key.IsValid() ? derivedKey.key : DeriveScanKey(extkey); + out.keys.emplace(scan_key.GetPubKey().GetID(), scan_key); + return std::make_unique(key_exp_index, scan_key.GetPubKey(), false); + } + + if (extkey.key.IsValid()) { + // Derive sp spend key from extkey + CKey spend_key = derivedKey.key.IsValid() ? derivedKey.key : DeriveSpendKey(extkey); + out.keys.emplace(spend_key.GetPubKey().GetID(), spend_key); + return std::make_unique(key_exp_index, spend_key.GetPubKey(), false); + } + if (extpubkey.pubkey.IsValid()) { + // Derive sp spend key from extpubkey + CPubKey spend_key = derivedPubKey.pubkey.IsValid() ? derivedPubKey.pubkey : DeriveSpendPubKey(extpubkey); + return std::make_unique(key_exp_index, spend_key, false); + } + } + if (extkey.key.IsValid()) { extpubkey = extkey.Neuter(); out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key); } + return std::make_unique(key_exp_index, extpubkey, std::move(path), type, apostrophe); } @@ -1744,22 +1802,30 @@ std::unique_ptr ParseScript(uint32_t& key_exp_index, Span(std::move(sppKey)); + } + auto arg2 = Expr(expr); + + auto scanKey = ParsePubkey(key_exp_index, arg1, ParseScriptContext::SP_SCAN, out, error); + if (!scanKey) { error = strprintf("sp(): %s", error); return nullptr; } ++key_exp_index; - if (!Const(",", expr)) { - return std::make_unique(std::move(firstKey)); - } - auto arg2 = Expr(expr); - auto spendKey = ParsePubkey(key_exp_index, arg2, ParseScriptContext::SP, out, error); + + auto spendKey = ParsePubkey(key_exp_index, arg2, ParseScriptContext::SP_SPEND, out, error); if (!spendKey) { error = strprintf("sp(): %s", error); return nullptr; } - auto scanPubKey = firstKey->GetRootPubKey(); + auto scanPubKey = scanKey->GetRootPubKey(); auto spendPubKey = spendKey->GetRootPubKey(); if (!scanPubKey.has_value()) { error = "sp(): could not get scan pubkey"; diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index e6821dd321884f..18f973cc35b982 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -434,6 +434,27 @@ void CheckInferDescriptor(const std::string& script_hex, const std::string& expe BOOST_CHECK_EQUAL(desc->ToString(), expected_desc + "#" + checksum); } +void CheckSilentPayments(const std::string& desc, const std::string& expected_private_string, const std::string& expected_public_string, const std::string& expected_norm_string, int flags = 0) +{ + FlatSigningProvider keys; + std::string error; + auto parsed_desc = Parse(desc, keys, error, false); + BOOST_CHECK_MESSAGE(parsed_desc, error); + + if (~flags & MISSING_PRIVKEYS) { + std::string private_string; + BOOST_CHECK(parsed_desc->ToPrivateString(keys, private_string)); + BOOST_CHECK_MESSAGE(EqualDescriptor(private_string, expected_private_string), "Private: " + private_string + " Expected: " + expected_private_string); + } + + std::string public_string = parsed_desc->ToString(); + std::string norm_string; + BOOST_CHECK(parsed_desc->ToNormalizedString(keys, norm_string)); + + BOOST_CHECK_MESSAGE(EqualDescriptor(public_string, expected_public_string), "Public: " + public_string + " Expected: " + expected_public_string); + BOOST_CHECK_MESSAGE(EqualDescriptor(norm_string, expected_norm_string), "Normalized: " + norm_string + " Expected: " + expected_norm_string); +} + } BOOST_FIXTURE_TEST_SUITE(descriptor_tests, BasicTestingSetup) @@ -657,6 +678,53 @@ BOOST_AUTO_TEST_CASE(descriptor_test) CheckInferDescriptor("76a914a31725c74421fadc50d35520ab8751ed120af80588ac", "pkh(04c56fe4a92d401bcbf1b3dfbe4ac3dac5602ca155a3681497f02c1b9a733b92d704e2da6ec4162e4846af9236ef4171069ac8b7f8234a8405b6cadd96f34f5a31)", {}, {{"04c56fe4a92d401bcbf1b3dfbe4ac3dac5602ca155a3681497f02c1b9a733b92d704e2da6ec4162e4846af9236ef4171069ac8b7f8234a8405b6cadd96f34f5a31", ""}}); // Infer pk() from p2pk with uncompressed key CheckInferDescriptor("4104032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220ac", "pk(04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220)", {}, {{"04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220", ""}}); + + // Silent Payments + // Check that /* uses default derivation path for SP + CheckSilentPayments("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/*,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/*)", + "sp(spprv1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvqryr9c920g3tru2cs3nazwqjdapvhx249ph95zkp3scsg957vzqag8ml0eq)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2cklv80y)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2cklv80y)"); + + // Check that no path uses default derivation path for SP + CheckSilentPayments("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)", + "sp(spprv1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvqryr9c920g3tru2cs3nazwqjdapvhx249ph95zkp3scsg957vzqag8ml0eq)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2cklv80y)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2cklv80y)"); + + // Check that provided path is used instead of the default derivation path for SP + CheckSilentPayments("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/0h)", + "sp(spprv1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvqwmvhpf70wwlfxmkfmfm8dargka4qgec2fkmxcpvr3tgkezxs2l6syyh62p)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvp457zxv2j2yzn9ha42hxhf3fkqdz5pc5hykqevp765qrrsdn7vc4smuzha9)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvp457zxv2j2yzn9ha42hxhf3fkqdz5pc5hykqevp765qrrsdn7vc4smuzha9)"); + + CheckSilentPayments("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/0h,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)", + "sp(spprv1qqqqqqqqqqqqqq8dkts5l8h805ndmya5ank735tw6syvu9ymdnvqkpc45tv3rg90agqryr9c920g3tru2cs3nazwqjdapvhx249ph95zkp3scsg957vzqaggc43p3)", + "sp(sppub1qqqqqqqqqqqqqq8dkts5l8h805ndmya5ank735tw6syvu9ymdnvqkpc45tv3rg90agper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2ceuxeh4)", + "sp(sppub1qqqqqqqqqqqqqq8dkts5l8h805ndmya5ank735tw6syvu9ymdnvqkpc45tv3rg90agper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2ceuxeh4)"); + + // Check that xpubs are accepted for spend key + CheckSilentPayments("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvpw0e9upmdssjnn4tk5law6fv0u08v99kjn6yt2ut80ngpj8dq7s8grpfrr5)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvpw0e9upmdssjnn4tk5law6fv0u08v99kjn6yt2ut80ngpj8dq7s8grpfrr5)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvpw0e9upmdssjnn4tk5law6fv0u08v99kjn6yt2ut80ngpj8dq7s8grpfrr5)", + MISSING_PRIVKEYS); + + CheckSilentPayments("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/0)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvp8cjcfl7ucts5c4ln7tqfjvm9ledmcpdyq4s55kz6rmssl903az0qwte4qy)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvp8cjcfl7ucts5c4ln7tqfjvm9ledmcpdyq4s55kz6rmssl903az0qwte4qy)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvp8cjcfl7ucts5c4ln7tqfjvm9ledmcpdyq4s55kz6rmssl903az0qwte4qy)", + MISSING_PRIVKEYS); + + CheckUnparsable("sp(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)", + "sp(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)", + "sp(): key 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8' is not a valid sp key"); + CheckUnparsable("sp(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)", + "sp(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)", + "sp(): Scan key must be a private key or extended private key"); + CheckUnparsable("sp(spprv1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvqryr9c920g3tru2cs3nazwqjdapvhx249ph95zkp3scsg957vzqag8ml0eq,xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi)", + "sp(sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2cklv80y,xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)", + "sp(): key 'sppub1qqqqqqqqqqqqqqqcw78khf9nvvgng9a0vsnzgz9hc29vqta5gwhwls59y60grpjpnvper07qpyg2fg4r32cfll4wytqcf9km7zzjaqd0aw69y2tdw0g2y2cklv80y' is not valid"); } BOOST_AUTO_TEST_SUITE_END()