Skip to content

Commit

Permalink
Merge #28336: rpc: parse legacy pubkeys consistently with specific er…
Browse files Browse the repository at this point in the history
…ror 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 98570fe
  Eunovo:
    Tested ACK 98570fe
  achow101:
    ACK 98570fe

Tree-SHA512: cfa474176e95b5b18f3a9da28fdd9e87195cd58994c1331198f2840925fff322fd323a6371feab74a1b32e4b9ea58a6dc732fa751b4cdd45402c1029af609ece
  • Loading branch information
achow101 committed May 8, 2024
2 parents 43a66c5 + 98570fe commit 4ff4276
Show file tree
Hide file tree
Showing 7 changed files with 19 additions and 38 deletions.
6 changes: 1 addition & 5 deletions src/rpc/output_script.cpp
Expand Up @@ -124,11 +124,7 @@ static RPCHelpMan createmultisig()
const UniValue& keys = request.params[1].get_array();
std::vector<CPubKey> pubkeys;
for (unsigned int i = 0; i < keys.size(); ++i) {
if (IsHex(keys[i].get_str()) && (keys[i].get_str().length() == 66 || keys[i].get_str().length() == 130)) {
pubkeys.push_back(HexToPubKey(keys[i].get_str()));
} else {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid public key: %s\n.", keys[i].get_str()));
}
pubkeys.push_back(HexToPubKey(keys[i].get_str()));
}

// Get the output type
Expand Down
7 changes: 5 additions & 2 deletions src/rpc/util.cpp
Expand Up @@ -194,11 +194,14 @@ std::string HelpExampleRpcNamed(const std::string& methodname, const RPCArgList&
CPubKey HexToPubKey(const std::string& hex_in)
{
if (!IsHex(hex_in)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key: " + hex_in);
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + hex_in + "\" must be a hex string");
}
if (hex_in.length() != 66 && hex_in.length() != 130) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + hex_in + "\" must have a length of either 33 or 65 bytes");
}
CPubKey vchPubKey(ParseHex(hex_in));
if (!vchPubKey.IsFullyValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid public key: " + hex_in);
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + hex_in + "\" must be cryptographically valid.");
}
return vchPubKey;
}
Expand Down
17 changes: 2 additions & 15 deletions src/wallet/rpc/backup.cpp
Expand Up @@ -456,12 +456,7 @@ RPCHelpMan importpubkey()
throw JSONRPCError(RPC_WALLET_ERROR, "Wallet is currently rescanning. Abort existing rescan or wait.");
}

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

{
LOCK(pwallet->cs_wallet);
Expand Down Expand Up @@ -983,15 +978,7 @@ static UniValue ProcessImportLegacy(ImportData& import_data, std::map<CKeyID, CP
import_data.witnessscript = std::make_unique<CScript>(parsed_witnessscript.begin(), parsed_witnessscript.end());
}
for (size_t i = 0; i < pubKeys.size(); ++i) {
const auto& str = pubKeys[i].get_str();
if (!IsHex(str)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" must be a hex string");
}
auto parsed_pubkey = ParseHex(str);
CPubKey pubkey(parsed_pubkey);
if (!pubkey.IsFullyValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pubkey \"" + str + "\" is not a valid public key");
}
CPubKey pubkey = HexToPubKey(pubKeys[i].get_str());
pubkey_map.emplace(pubkey.GetID(), pubkey);
ordered_pubkeys.push_back(pubkey.GetID());
}
Expand Down
10 changes: 1 addition & 9 deletions src/wallet/rpc/spend.cpp
Expand Up @@ -627,15 +627,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
const UniValue solving_data = options["solving_data"].get_obj();
if (solving_data.exists("pubkeys")) {
for (const UniValue& pk_univ : solving_data["pubkeys"].get_array().getValues()) {
const std::string& pk_str = pk_univ.get_str();
if (!IsHex(pk_str)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not hex", pk_str));
}
const std::vector<unsigned char> data(ParseHex(pk_str));
const CPubKey pubkey(data.begin(), data.end());
if (!pubkey.IsFullyValid()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("'%s' is not a valid public key", pk_str));
}
const CPubKey pubkey = HexToPubKey(pk_univ.get_str());
coinControl.m_external_provider.pubkeys.emplace(pubkey.GetID(), pubkey);
// Add witness script for pubkeys
const CScript wit_script = GetScriptForDestination(WitnessV0KeyHash(pubkey));
Expand Down
4 changes: 2 additions & 2 deletions test/functional/rpc_rawtransaction.py
Expand Up @@ -490,11 +490,11 @@ def raw_multisig_transaction_legacy_tests(self):
addr2Obj = self.nodes[2].getaddressinfo(addr2)

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

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

Expand Down
9 changes: 6 additions & 3 deletions test/functional/wallet_basic.py
Expand Up @@ -461,10 +461,13 @@ def run_test(self):
assert_raises_rpc_error(-5, "Invalid Bitcoin address or script", self.nodes[0].importaddress, "invalid")

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

# This will raise an exception for importing an invalid pubkey
assert_raises_rpc_error(-5, "Pubkey is not a valid public key", self.nodes[0].importpubkey, "5361746f736869204e616b616d6f746f")
# This will raise exceptions for importing a pubkeys with invalid length / invalid coordinates
too_short_pubkey = "5361746f736869204e616b616d6f746f"
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)
not_on_curve_pubkey = bytes([4] + [0]*64).hex() # pubkey with coordinates (0,0) is not on curve
assert_raises_rpc_error(-5, f'Pubkey "{not_on_curve_pubkey}" must be cryptographically valid', self.nodes[0].importpubkey, not_on_curve_pubkey)

# Bech32m addresses cannot be imported into a legacy wallet
assert_raises_rpc_error(-5, "Bech32m addresses cannot be imported into legacy wallets", self.nodes[0].importaddress, "bcrt1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6")
Expand Down
4 changes: 2 additions & 2 deletions test/functional/wallet_fundrawtransaction.py
Expand Up @@ -1054,8 +1054,8 @@ def test_external_inputs(self):
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)

# Error conditions
assert_raises_rpc_error(-5, "'not a pubkey' is not hex", wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["not a pubkey"]})
assert_raises_rpc_error(-5, "'01234567890a0b0c0d0e0f' is not a valid public key", wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["01234567890a0b0c0d0e0f"]})
assert_raises_rpc_error(-5, 'Pubkey "not a pubkey" must be a hex string', wallet.fundrawtransaction, raw_tx, solving_data={"pubkeys":["not a pubkey"]})
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"]})
assert_raises_rpc_error(-5, "'not a script' is not hex", wallet.fundrawtransaction, raw_tx, solving_data={"scripts":["not a script"]})
assert_raises_rpc_error(-8, "Unable to parse descriptor 'not a descriptor'", wallet.fundrawtransaction, raw_tx, solving_data={"descriptors":["not a descriptor"]})
assert_raises_rpc_error(-8, "Invalid parameter, missing vout key", wallet.fundrawtransaction, raw_tx, input_weights=[{"txid": ext_utxo["txid"]}])
Expand Down

0 comments on commit 4ff4276

Please sign in to comment.