Return an error from descriptor Parse that gives more information about what failed
This commit is contained in:
parent
7a960ba775
commit
c325f619dd
7 changed files with 110 additions and 51 deletions
|
@ -150,9 +150,10 @@ UniValue getdescriptorinfo(const JSONRPCRequest& request)
|
|||
RPCTypeCheck(request.params, {UniValue::VSTR});
|
||||
|
||||
FlatSigningProvider provider;
|
||||
auto desc = Parse(request.params[0].get_str(), provider);
|
||||
std::string error;
|
||||
auto desc = Parse(request.params[0].get_str(), provider, error);
|
||||
if (!desc) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor"));
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor, %s", error));
|
||||
}
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
|
@ -199,9 +200,10 @@ UniValue deriveaddresses(const JSONRPCRequest& request)
|
|||
}
|
||||
|
||||
FlatSigningProvider key_provider;
|
||||
auto desc = Parse(desc_str, key_provider, /* require_checksum = */ true);
|
||||
std::string error;
|
||||
auto desc = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
|
||||
if (!desc) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor"));
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor, %s", error));
|
||||
}
|
||||
|
||||
if (!desc->IsRange() && request.params.size() > 1) {
|
||||
|
|
|
@ -717,9 +717,10 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
|
|||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Scan object needs to be either a string or an object");
|
||||
}
|
||||
|
||||
auto desc = Parse(desc_str, provider);
|
||||
std::string error;
|
||||
auto desc = Parse(desc_str, provider, error);
|
||||
if (!desc) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s'", desc_str));
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid descriptor '%s', %s", desc_str, error));
|
||||
}
|
||||
if (!desc->IsRange()) {
|
||||
range.first = 0;
|
||||
|
|
|
@ -690,7 +690,7 @@ std::vector<Span<const char>> Split(const Span<const char>& sp, char sep)
|
|||
}
|
||||
|
||||
/** Parse a key path, being passed a split list of elements (the first element is ignored). */
|
||||
NODISCARD bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out)
|
||||
NODISCARD bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath& out, std::string& error)
|
||||
{
|
||||
for (size_t i = 1; i < split.size(); ++i) {
|
||||
Span<const char> elem = split[i];
|
||||
|
@ -700,14 +700,17 @@ NODISCARD bool ParseKeyPath(const std::vector<Span<const char>>& split, KeyPath&
|
|||
hardened = true;
|
||||
}
|
||||
uint32_t p;
|
||||
if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p) || p > 0x7FFFFFFFUL) return false;
|
||||
if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p) || p > 0x7FFFFFFFUL) {
|
||||
error = strprintf("Key path value %u is out of range", p);
|
||||
return false;
|
||||
}
|
||||
out.push_back(p | (((uint32_t)hardened) << 31));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Parse a public key that excludes origin information. */
|
||||
std::unique_ptr<PubkeyProvider> ParsePubkeyInner(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
|
||||
std::unique_ptr<PubkeyProvider> ParsePubkeyInner(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out, std::string& error)
|
||||
{
|
||||
auto split = Split(sp, '/');
|
||||
std::string str(split[0].begin(), split[0].end());
|
||||
|
@ -726,7 +729,10 @@ std::unique_ptr<PubkeyProvider> ParsePubkeyInner(const Span<const char>& sp, boo
|
|||
}
|
||||
CExtKey extkey = DecodeExtKey(str);
|
||||
CExtPubKey extpubkey = DecodeExtPubKey(str);
|
||||
if (!extkey.key.IsValid() && !extpubkey.pubkey.IsValid()) return nullptr;
|
||||
if (!extkey.key.IsValid() && !extpubkey.pubkey.IsValid()) {
|
||||
error = strprintf("key '%s' is not valid", str);
|
||||
return nullptr;
|
||||
}
|
||||
KeyPath path;
|
||||
DeriveType type = DeriveType::NO;
|
||||
if (split.back() == MakeSpan("*").first(1)) {
|
||||
|
@ -736,7 +742,7 @@ std::unique_ptr<PubkeyProvider> ParsePubkeyInner(const Span<const char>& sp, boo
|
|||
split.pop_back();
|
||||
type = DeriveType::HARDENED;
|
||||
}
|
||||
if (!ParseKeyPath(split, path)) return nullptr;
|
||||
if (!ParseKeyPath(split, path, error)) return nullptr;
|
||||
if (extkey.key.IsValid()) {
|
||||
extpubkey = extkey.Neuter();
|
||||
out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key);
|
||||
|
@ -745,43 +751,55 @@ std::unique_ptr<PubkeyProvider> ParsePubkeyInner(const Span<const char>& sp, boo
|
|||
}
|
||||
|
||||
/** Parse a public key including origin information (if enabled). */
|
||||
std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out)
|
||||
std::unique_ptr<PubkeyProvider> ParsePubkey(const Span<const char>& sp, bool permit_uncompressed, FlatSigningProvider& out, std::string& error)
|
||||
{
|
||||
auto origin_split = Split(sp, ']');
|
||||
if (origin_split.size() > 2) return nullptr;
|
||||
if (origin_split.size() == 1) return ParsePubkeyInner(origin_split[0], permit_uncompressed, out);
|
||||
if (origin_split[0].size() < 1 || origin_split[0][0] != '[') return nullptr;
|
||||
if (origin_split.size() > 2) {
|
||||
error = "Multiple ']' characters found for a single pubkey";
|
||||
return nullptr;
|
||||
}
|
||||
if (origin_split.size() == 1) return ParsePubkeyInner(origin_split[0], permit_uncompressed, out, error);
|
||||
if (origin_split[0].size() < 1 || origin_split[0][0] != '[') {
|
||||
error = strprintf("Key origin expected but not found, got '%s' instead", std::string(origin_split[0].begin(), origin_split[0].end()));
|
||||
return nullptr;
|
||||
}
|
||||
auto slash_split = Split(origin_split[0].subspan(1), '/');
|
||||
if (slash_split[0].size() != 8) return nullptr;
|
||||
if (slash_split[0].size() != 8) {
|
||||
error = strprintf("Fingerprint is not 4 bytes (%u characters instead of 8 characters)", slash_split[0].size());
|
||||
return nullptr;
|
||||
}
|
||||
std::string fpr_hex = std::string(slash_split[0].begin(), slash_split[0].end());
|
||||
if (!IsHex(fpr_hex)) return nullptr;
|
||||
if (!IsHex(fpr_hex)) {
|
||||
error = strprintf("Fingerprint '%s' is not hex", fpr_hex);
|
||||
return nullptr;
|
||||
}
|
||||
auto fpr_bytes = ParseHex(fpr_hex);
|
||||
KeyOriginInfo info;
|
||||
static_assert(sizeof(info.fingerprint) == 4, "Fingerprint must be 4 bytes");
|
||||
assert(fpr_bytes.size() == 4);
|
||||
std::copy(fpr_bytes.begin(), fpr_bytes.end(), info.fingerprint);
|
||||
if (!ParseKeyPath(slash_split, info.path)) return nullptr;
|
||||
auto provider = ParsePubkeyInner(origin_split[1], permit_uncompressed, out);
|
||||
if (!ParseKeyPath(slash_split, info.path, error)) return nullptr;
|
||||
auto provider = ParsePubkeyInner(origin_split[1], permit_uncompressed, out, error);
|
||||
if (!provider) return nullptr;
|
||||
return MakeUnique<OriginPubkeyProvider>(std::move(info), std::move(provider));
|
||||
}
|
||||
|
||||
/** Parse a script in a particular context. */
|
||||
std::unique_ptr<DescriptorImpl> ParseScript(Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out)
|
||||
std::unique_ptr<DescriptorImpl> ParseScript(Span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error)
|
||||
{
|
||||
auto expr = Expr(sp);
|
||||
if (Func("pk", expr)) {
|
||||
auto pubkey = ParsePubkey(expr, ctx != ParseScriptContext::P2WSH, out);
|
||||
auto pubkey = ParsePubkey(expr, ctx != ParseScriptContext::P2WSH, out, error);
|
||||
if (!pubkey) return nullptr;
|
||||
return MakeUnique<PKDescriptor>(std::move(pubkey));
|
||||
}
|
||||
if (Func("pkh", expr)) {
|
||||
auto pubkey = ParsePubkey(expr, ctx != ParseScriptContext::P2WSH, out);
|
||||
auto pubkey = ParsePubkey(expr, ctx != ParseScriptContext::P2WSH, out, error);
|
||||
if (!pubkey) return nullptr;
|
||||
return MakeUnique<PKHDescriptor>(std::move(pubkey));
|
||||
}
|
||||
if (ctx == ParseScriptContext::TOP && Func("combo", expr)) {
|
||||
auto pubkey = ParsePubkey(expr, true, out);
|
||||
auto pubkey = ParsePubkey(expr, true, out, error);
|
||||
if (!pubkey) return nullptr;
|
||||
return MakeUnique<ComboDescriptor>(std::move(pubkey));
|
||||
}
|
||||
|
@ -789,51 +807,70 @@ std::unique_ptr<DescriptorImpl> ParseScript(Span<const char>& sp, ParseScriptCon
|
|||
auto threshold = Expr(expr);
|
||||
uint32_t thres;
|
||||
std::vector<std::unique_ptr<PubkeyProvider>> providers;
|
||||
if (!ParseUInt32(std::string(threshold.begin(), threshold.end()), &thres)) return nullptr;
|
||||
if (!ParseUInt32(std::string(threshold.begin(), threshold.end()), &thres)) {
|
||||
error = strprintf("multi threshold %u out of range", thres);
|
||||
return nullptr;
|
||||
}
|
||||
size_t script_size = 0;
|
||||
while (expr.size()) {
|
||||
if (!Const(",", expr)) return nullptr;
|
||||
if (!Const(",", expr)) {
|
||||
error = strprintf("multi: expected ',', got '%c'", expr[0]);
|
||||
return nullptr;
|
||||
}
|
||||
auto arg = Expr(expr);
|
||||
auto pk = ParsePubkey(arg, ctx != ParseScriptContext::P2WSH, out);
|
||||
auto pk = ParsePubkey(arg, ctx != ParseScriptContext::P2WSH, out, error);
|
||||
if (!pk) return nullptr;
|
||||
script_size += pk->GetSize() + 1;
|
||||
providers.emplace_back(std::move(pk));
|
||||
}
|
||||
if (providers.size() < 1 || providers.size() > 16 || thres < 1 || thres > providers.size()) return nullptr;
|
||||
if (ctx == ParseScriptContext::TOP) {
|
||||
if (providers.size() > 3) return nullptr; // Not more than 3 pubkeys for raw multisig
|
||||
if (providers.size() > 3) {
|
||||
error = strprintf("Cannot %u pubkeys in bare multisig; only at most 3 pubkeys", providers.size());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
if (ctx == ParseScriptContext::P2SH) {
|
||||
if (script_size + 3 > 520) return nullptr; // Enforce P2SH script size limit
|
||||
if (script_size + 3 > 520) {
|
||||
error = strprintf("P2SH script is too large, %d bytes is larger than 520 bytes", script_size + 3);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return MakeUnique<MultisigDescriptor>(thres, std::move(providers));
|
||||
}
|
||||
if (ctx != ParseScriptContext::P2WSH && Func("wpkh", expr)) {
|
||||
auto pubkey = ParsePubkey(expr, false, out);
|
||||
auto pubkey = ParsePubkey(expr, false, out, error);
|
||||
if (!pubkey) return nullptr;
|
||||
return MakeUnique<WPKHDescriptor>(std::move(pubkey));
|
||||
}
|
||||
if (ctx == ParseScriptContext::TOP && Func("sh", expr)) {
|
||||
auto desc = ParseScript(expr, ParseScriptContext::P2SH, out);
|
||||
auto desc = ParseScript(expr, ParseScriptContext::P2SH, out, error);
|
||||
if (!desc || expr.size()) return nullptr;
|
||||
return MakeUnique<SHDescriptor>(std::move(desc));
|
||||
}
|
||||
if (ctx != ParseScriptContext::P2WSH && Func("wsh", expr)) {
|
||||
auto desc = ParseScript(expr, ParseScriptContext::P2WSH, out);
|
||||
auto desc = ParseScript(expr, ParseScriptContext::P2WSH, out, error);
|
||||
if (!desc || expr.size()) return nullptr;
|
||||
return MakeUnique<WSHDescriptor>(std::move(desc));
|
||||
}
|
||||
if (ctx == ParseScriptContext::TOP && Func("addr", expr)) {
|
||||
CTxDestination dest = DecodeDestination(std::string(expr.begin(), expr.end()));
|
||||
if (!IsValidDestination(dest)) return nullptr;
|
||||
if (!IsValidDestination(dest)) {
|
||||
error = "Address is not valid";
|
||||
return nullptr;
|
||||
}
|
||||
return MakeUnique<AddressDescriptor>(std::move(dest));
|
||||
}
|
||||
if (ctx == ParseScriptContext::TOP && Func("raw", expr)) {
|
||||
std::string str(expr.begin(), expr.end());
|
||||
if (!IsHex(str)) return nullptr;
|
||||
if (!IsHex(str)) {
|
||||
error = "Raw script is not hex";
|
||||
return nullptr;
|
||||
}
|
||||
auto bytes = ParseHex(str);
|
||||
return MakeUnique<RawDescriptor>(CScript(bytes.begin(), bytes.end()));
|
||||
}
|
||||
error = strprintf("%s is not a valid descriptor function", std::string(expr.begin(), expr.end()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -915,29 +952,44 @@ std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptCo
|
|||
} // namespace
|
||||
|
||||
/** Check a descriptor checksum, and update desc to be the checksum-less part. */
|
||||
bool CheckChecksum(Span<const char>& sp, bool require_checksum, std::string* out_checksum = nullptr)
|
||||
bool CheckChecksum(Span<const char>& sp, bool require_checksum, std::string& error, std::string* out_checksum = nullptr)
|
||||
{
|
||||
auto check_split = Split(sp, '#');
|
||||
if (check_split.size() > 2) return false; // Multiple '#' symbols
|
||||
if (check_split.size() == 1 && require_checksum) return false; // Missing checksum
|
||||
if (check_split.size() > 2) {
|
||||
error = "Multiple '#' symbols";
|
||||
return false;
|
||||
}
|
||||
if (check_split.size() == 1 && require_checksum){
|
||||
error = "Missing checksum";
|
||||
return false;
|
||||
}
|
||||
if (check_split.size() == 2) {
|
||||
if (check_split[1].size() != 8) return false; // Unexpected length for checksum
|
||||
if (check_split[1].size() != 8) {
|
||||
error = strprintf("Expected 8 character checksum, not %u characters", check_split[1].size());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto checksum = DescriptorChecksum(check_split[0]);
|
||||
if (checksum.empty()) return false; // Invalid characters in payload
|
||||
if (checksum.empty()) {
|
||||
error = "Invalid characters in payload";
|
||||
return false;
|
||||
}
|
||||
if (check_split.size() == 2) {
|
||||
if (!std::equal(checksum.begin(), checksum.end(), check_split[1].begin())) return false; // Checksum mismatch
|
||||
if (!std::equal(checksum.begin(), checksum.end(), check_split[1].begin())) {
|
||||
error = strprintf("Provided checksum '%s' does not match computed checksum '%s'", std::string(check_split[1].begin(), check_split[1].end()), checksum);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (out_checksum) *out_checksum = std::move(checksum);
|
||||
sp = check_split[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, bool require_checksum)
|
||||
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum)
|
||||
{
|
||||
Span<const char> sp(descriptor.data(), descriptor.size());
|
||||
if (!CheckChecksum(sp, require_checksum)) return nullptr;
|
||||
auto ret = ParseScript(sp, ParseScriptContext::TOP, out);
|
||||
if (!CheckChecksum(sp, require_checksum, error)) return nullptr;
|
||||
auto ret = ParseScript(sp, ParseScriptContext::TOP, out, error);
|
||||
if (sp.size() == 0 && ret) return std::unique_ptr<Descriptor>(std::move(ret));
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -945,8 +997,9 @@ std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProv
|
|||
std::string GetDescriptorChecksum(const std::string& descriptor)
|
||||
{
|
||||
std::string ret;
|
||||
std::string error;
|
||||
Span<const char> sp(descriptor.data(), descriptor.size());
|
||||
if (!CheckChecksum(sp, false, &ret)) return "";
|
||||
if (!CheckChecksum(sp, false, error, &ret)) return "";
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ struct Descriptor {
|
|||
* If a parse error occurs, or the checksum is missing/invalid, or anything
|
||||
* else is wrong, nullptr is returned.
|
||||
*/
|
||||
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, bool require_checksum = false);
|
||||
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false);
|
||||
|
||||
/** Get the checksum for a descriptor.
|
||||
*
|
||||
|
|
|
@ -16,8 +16,9 @@ namespace {
|
|||
void CheckUnparsable(const std::string& prv, const std::string& pub)
|
||||
{
|
||||
FlatSigningProvider keys_priv, keys_pub;
|
||||
auto parse_priv = Parse(prv, keys_priv);
|
||||
auto parse_pub = Parse(pub, keys_pub);
|
||||
std::string error;
|
||||
auto parse_priv = Parse(prv, keys_priv, error);
|
||||
auto parse_pub = Parse(pub, keys_pub, error);
|
||||
BOOST_CHECK_MESSAGE(!parse_priv, prv);
|
||||
BOOST_CHECK_MESSAGE(!parse_pub, pub);
|
||||
}
|
||||
|
@ -62,10 +63,11 @@ void Check(const std::string& prv, const std::string& pub, int flags, const std:
|
|||
{
|
||||
FlatSigningProvider keys_priv, keys_pub;
|
||||
std::set<std::vector<uint32_t>> left_paths = paths;
|
||||
std::string error;
|
||||
|
||||
// Check that parsing succeeds.
|
||||
auto parse_priv = Parse(MaybeUseHInsteadOfApostrophy(prv), keys_priv);
|
||||
auto parse_pub = Parse(MaybeUseHInsteadOfApostrophy(pub), keys_pub);
|
||||
auto parse_priv = Parse(MaybeUseHInsteadOfApostrophy(prv), keys_priv, error);
|
||||
auto parse_pub = Parse(MaybeUseHInsteadOfApostrophy(pub), keys_pub, error);
|
||||
BOOST_CHECK(parse_priv);
|
||||
BOOST_CHECK(parse_pub);
|
||||
|
||||
|
|
|
@ -1098,9 +1098,10 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID
|
|||
|
||||
const std::string& descriptor = data["desc"].get_str();
|
||||
FlatSigningProvider keys;
|
||||
auto parsed_desc = Parse(descriptor, keys, /* require_checksum = */ true);
|
||||
std::string error;
|
||||
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
|
||||
if (!parsed_desc) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor is invalid");
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Descriptor is invalid, %s", error));
|
||||
}
|
||||
|
||||
have_solving_data = parsed_desc->IsSolvable();
|
||||
|
|
|
@ -552,7 +552,7 @@ class ImportMultiTest(BitcoinTestFramework):
|
|||
"keys": [key.privkey]},
|
||||
success=False,
|
||||
error_code=-5,
|
||||
error_message="Descriptor is invalid")
|
||||
error_message="Descriptor is invalid, Missing checksum")
|
||||
|
||||
# Test importing of a P2SH-P2WPKH address via descriptor + private key
|
||||
key = get_key(self.nodes[0])
|
||||
|
|
Loading…
Add table
Reference in a new issue