Skip to content

Commit 4ff4276

Browse files
committed
Merge bitcoin/bitcoin#28336: rpc: parse legacy pubkeys consistently with specific error messages
98570fe test: add coverage for parsing cryptographically invalid pubkeys (Sebastian Falbesoner) c740b15 rpc: use `HexToPubKey` helper for all legacy pubkey-parsing RPCs (Sebastian Falbesoner) 100e8a7 rpc: check and throw specific pubkey parsing errors in `HexToPubKey` (Sebastian Falbesoner) Pull request description: Parsing legacy public keys can fail for three reasons (in this order): - pubkey is not in hex - pubkey has an invalid length (not 33 or 65 bytes for compressed/uncompressed, respectively) - pubkey is crytographically invalid, i.e. is not on curve (`CPubKey.IsFullyValid()` check) Many RPCs currently perform these checks manually with different error messages, even though we already have a `HexToPubKey` helper. This PR puts all three checks in this helper (the length check was done on the call-sites before), adds specific error messages for each case, and consequently uses it for all RPCs that parse legacy pubkeys. This leads to deduplicated code and also to more consistent and detailed error messages for the user. Affected RPC calls are `createmultisig`, `addmultisigaddress`, `importpubkey`, `importmulti`, `fundrawtransaction`, `walletcreatefundedpsbt`, `send` and `sendall`. Note that the error code (-5 a.k.a. `RPC_INVALID_ADDRESS_OR_KEY`) doesn't change in any of the causes, so the changes are not breaking RPC API compatibility. Only the messages are more specific. The last commits adds test coverage for the cryptographically invalid (not-on-curve) pubkey case which wasn't exercised before. ACKs for top commit: stratospher: tested ACK 98570fe. davidgumberg: ACK bitcoin/bitcoin@98570fe Eunovo: Tested ACK bitcoin/bitcoin@98570fe achow101: ACK 98570fe Tree-SHA512: cfa474176e95b5b18f3a9da28fdd9e87195cd58994c1331198f2840925fff322fd323a6371feab74a1b32e4b9ea58a6dc732fa751b4cdd45402c1029af609ece
2 parents 43a66c5 + 98570fe commit 4ff4276

File tree

7 files changed

+19
-38
lines changed

7 files changed

+19
-38
lines changed

src/rpc/output_script.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,7 @@ static RPCHelpMan createmultisig()
124124
const UniValue& keys = request.params[1].get_array();
125125
std::vector<CPubKey> pubkeys;
126126
for (unsigned int i = 0; i < keys.size(); ++i) {
127-
if (IsHex(keys[i].get_str()) && (keys[i].get_str().length() == 66 || keys[i].get_str().length() == 130)) {
128-
pubkeys.push_back(HexToPubKey(keys[i].get_str()));
129-
} else {
130-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid public key: %s\n.", keys[i].get_str()));
131-
}
127+
pubkeys.push_back(HexToPubKey(keys[i].get_str()));
132128
}
133129

134130
// Get the output type

src/rpc/util.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,14 @@ std::string HelpExampleRpcNamed(const std::string& methodname, const RPCArgList&
194194
CPubKey HexToPubKey(const std::string& hex_in)
195195
{
196196
if (!IsHex(hex_in)) {
197-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key: " + hex_in);
197+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + hex_in + "\" must be a hex string");
198+
}
199+
if (hex_in.length() != 66 && hex_in.length() != 130) {
200+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + hex_in + "\" must have a length of either 33 or 65 bytes");
198201
}
199202
CPubKey vchPubKey(ParseHex(hex_in));
200203
if (!vchPubKey.IsFullyValid()) {
201-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key: " + hex_in);
204+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + hex_in + "\" must be cryptographically valid.");
202205
}
203206
return vchPubKey;
204207
}

src/wallet/rpc/backup.cpp

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,7 @@ RPCHelpMan importpubkey()
456456
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
457457
}
458458

459-
if (!IsHex(request.params[0].get_str()))
460-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey must be a hex string");
461-
std::vector<unsigned char> data(ParseHex(request.params[0].get_str()));
462-
CPubKey pubKey(data);
463-
if (!pubKey.IsFullyValid())
464-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey is not a valid public key");
459+
CPubKey pubKey = HexToPubKey(request.params[0].get_str());
465460

466461
{
467462
LOCK(pwallet->cs_wallet);
@@ -983,15 +978,7 @@ static UniValue ProcessImportLegacy(ImportData& import_data, std::map<CKeyID, CP
983978
import_data.witnessscript = std::make_unique<CScript>(parsed_witnessscript.begin(), parsed_witnessscript.end());
984979
}
985980
for (size_t i = 0; i < pubKeys.size(); ++i) {
986-
const auto& str = pubKeys[i].get_str();
987-
if (!IsHex(str)) {
988-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" must be a hex string");
989-
}
990-
auto parsed_pubkey = ParseHex(str);
991-
CPubKey pubkey(parsed_pubkey);
992-
if (!pubkey.IsFullyValid()) {
993-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" is not a valid public key");
994-
}
981+
CPubKey pubkey = HexToPubKey(pubKeys[i].get_str());
995982
pubkey_map.emplace(pubkey.GetID(), pubkey);
996983
ordered_pubkeys.push_back(pubkey.GetID());
997984
}

src/wallet/rpc/spend.cpp

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -627,15 +627,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
627627
const UniValue solving_data = options["solving_data"].get_obj();
628628
if (solving_data.exists("pubkeys")) {
629629
for (const UniValue& pk_univ : solving_data["pubkeys"].get_array().getValues()) {
630-
const std::string& pk_str = pk_univ.get_str();
631-
if (!IsHex(pk_str)) {
632-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not hex", pk_str));
633-
}
634-
const std::vector<unsigned char> data(ParseHex(pk_str));
635-
const CPubKey pubkey(data.begin(), data.end());
636-
if (!pubkey.IsFullyValid()) {
637-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not a valid public key", pk_str));
638-
}
630+
const CPubKey pubkey = HexToPubKey(pk_univ.get_str());
639631
coinControl.m_external_provider.pubkeys.emplace(pubkey.GetID(), pubkey);
640632
// Add witness script for pubkeys
641633
const CScript wit_script = GetScriptForDestination(WitnessV0KeyHash(pubkey));

test/functional/rpc_rawtransaction.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,11 +490,11 @@ def raw_multisig_transaction_legacy_tests(self):
490490
addr2Obj = self.nodes[2].getaddressinfo(addr2)
491491

492492
# Tests for createmultisig and addmultisigaddress
493-
assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 1, ["01020304"])
493+
assert_raises_rpc_error(-5, 'Pubkey "01020304" must have a length of either 33 or 65 bytes', self.nodes[0].createmultisig, 1, ["01020304"])
494494
# createmultisig can only take public keys
495495
self.nodes[0].createmultisig(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])
496496
# addmultisigaddress can take both pubkeys and addresses so long as they are in the wallet, which is tested here
497-
assert_raises_rpc_error(-5, "Invalid public key", self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1])
497+
assert_raises_rpc_error(-5, f'Pubkey "{addr1}" must be a hex string', self.nodes[0].createmultisig, 2, [addr1Obj['pubkey'], addr1])
498498

499499
mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr1])['address']
500500

test/functional/wallet_basic.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -461,10 +461,13 @@ def run_test(self):
461461
assert_raises_rpc_error(-5, "Invalid Bitcoin address or script", self.nodes[0].importaddress, "invalid")
462462

463463
# This will raise an exception for attempting to import a pubkey that isn't in hex
464-
assert_raises_rpc_error(-5, "Pubkey must be a hex string", self.nodes[0].importpubkey, "not hex")
464+
assert_raises_rpc_error(-5, 'Pubkey "not hex" must be a hex string', self.nodes[0].importpubkey, "not hex")
465465

466-
# This will raise an exception for importing an invalid pubkey
467-
assert_raises_rpc_error(-5, "Pubkey is not a valid public key", self.nodes[0].importpubkey, "5361746f736869204e616b616d6f746f")
466+
# This will raise exceptions for importing a pubkeys with invalid length / invalid coordinates
467+
too_short_pubkey = "5361746f736869204e616b616d6f746f"
468+
assert_raises_rpc_error(-5, f'Pubkey "{too_short_pubkey}" must have a length of either 33 or 65 bytes', self.nodes[0].importpubkey, too_short_pubkey)
469+
not_on_curve_pubkey = bytes([4] + [0]*64).hex() # pubkey with coordinates (0,0) is not on curve
470+
assert_raises_rpc_error(-5, f'Pubkey "{not_on_curve_pubkey}" must be cryptographically valid', self.nodes[0].importpubkey, not_on_curve_pubkey)
468471

469472
# Bech32m addresses cannot be imported into a legacy wallet
470473
assert_raises_rpc_error(-5, "Bech32m addresses cannot be imported into legacy wallets", self.nodes[0].importaddress, "bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6")

test/functional/wallet_fundrawtransaction.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,8 +1054,8 @@ def test_external_inputs(self):
10541054
assert_raises_rpc_error(-4, "Not solvable pre-selected input COutPoint(%s, %s)" % (ext_utxo["txid"][0:10], ext_utxo["vout"]), wallet.fundrawtransaction, raw_tx)
10551055

10561056
# Error conditions
1057-
assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["not a pubkey"]})
1058-
assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["01234567890a0b0c0d0e0f"]})
1057+
assert_raises_rpc_error(-5, 'Pubkey "not a pubkey" must be a hex string', wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["not a pubkey"]})
1058+
assert_raises_rpc_error(-5, 'Pubkey "01234567890a0b0c0d0e0f" must have a length of either 33 or 65 bytes', wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["01234567890a0b0c0d0e0f"]})
10591059
assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, solving_data={"scripts":["not a script"]})
10601060
assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, solving_data={"descriptors":["not a descriptor"]})
10611061
assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"]}])

0 commit comments

Comments
 (0)