diff --git a/doc/bips.md b/doc/bips.md index 34a4ba4357..db8a5ec81d 100644 --- a/doc/bips.md +++ b/doc/bips.md @@ -21,6 +21,7 @@ BIPs that are implemented by Bitcoin Core (up-to-date up to **v25.0**): * [`BIP 61`](https://github.com/bitcoin/bips/blob/master/bip-0061.mediawiki): The 'reject' protocol message (and the protocol version bump to 70002) was added in **v0.9.0** ([PR #3185](https://github.com/bitcoin/bitcoin/pull/3185)). Starting **v0.17.0**, whether to send reject messages can be configured with the `-enablebip61` option, and support is deprecated (disabled by default) as of **v0.18.0**. Support was removed in **v0.20.0** ([PR #15437](https://github.com/bitcoin/bitcoin/pull/15437)). * [`BIP 65`](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki): The CHECKLOCKTIMEVERIFY softfork was merged in **v0.12.0** ([PR #6351](https://github.com/bitcoin/bitcoin/pull/6351)), and backported to **v0.11.2** and **v0.10.4**. Mempool-only CLTV was added in [PR #6124](https://github.com/bitcoin/bitcoin/pull/6124). * [`BIP 66`](https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki): The strict DER rules and associated version 3 blocks have been implemented since **v0.10.0** ([PR #5713](https://github.com/bitcoin/bitcoin/pull/5713)). +* [`BIP 67`](https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki): Sorting multisig keys according to BIP 67 was merged in **v0.15.1** ([PR #8751](https://github.com/bitcoin/bitcoin/pull/8751)). * [`BIP 68`](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki): Sequence locks have been implemented as of **v0.12.1** ([PR #7184](https://github.com/bitcoin/bitcoin/pull/7184)), and have been *buried* since **v0.19.0** ([PR #16060](https://github.com/bitcoin/bitcoin/pull/16060)). * [`BIP 70`](https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki) [`71`](https://github.com/bitcoin/bips/blob/master/bip-0071.mediawiki) [`72`](https://github.com/bitcoin/bips/blob/master/bip-0072.mediawiki): Payment Protocol support has been available in Bitcoin Core GUI since **v0.9.0** ([PR #5216](https://github.com/bitcoin/bitcoin/pull/5216)). diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index eaf7aeccbb..94747b211d 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -99,8 +99,10 @@ static const CRPCConvertParam vRPCConvertParams[] = { "dumptxoutset", 2, "show_header" }, { "addmultisigaddress", 0, "nrequired" }, { "addmultisigaddress", 1, "keys" }, + { "addmultisigaddress", 2, "options" }, { "createmultisig", 0, "nrequired" }, { "createmultisig", 1, "keys" }, + { "createmultisig", 2, "options" }, { "listunspent", 0, "minconf" }, { "listunspent", 1, "maxconf" }, { "listunspent", 2, "addresses" }, diff --git a/src/rpc/output_script.cpp b/src/rpc/output_script.cpp index 90c8ffc3f0..adde686eda 100644 --- a/src/rpc/output_script.cpp +++ b/src/rpc/output_script.cpp @@ -90,14 +90,20 @@ static RPCHelpMan createmultisig() { return RPCHelpMan{"createmultisig", "\nCreates a multi-signature address with n signature of m keys required.\n" - "It returns a json object with the address and redeemScript.\n", + "It returns a json object with the address and redeemScript.\n" + "Public keys can be sorted according to BIP67 during the request if required.\n", { {"nrequired", RPCArg::Type::NUM, RPCArg::Optional::NO, "The number of required signatures out of the n keys."}, {"keys", RPCArg::Type::ARR, RPCArg::Optional::NO, "The hex-encoded public keys.", { {"key", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "The hex-encoded public key"}, }}, - {"address_type", RPCArg::Type::STR, RPCArg::Default{"legacy"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, + {"options|address_type", {RPCArg::Type::OBJ, RPCArg::Type::STR}, RPCArg::Optional::OMITTED, "", + { + {"address_type", RPCArg::Type::STR, RPCArg::Default{"legacy"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, + {"sort", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to sort public keys according to BIP67."}, + }, + RPCArgOptions{.oneline_description="options"}}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -121,33 +127,59 @@ static RPCHelpMan createmultisig() { int required = request.params[0].getInt(); + bool sort = false; + OutputType output_type = OutputType::LEGACY; + + if (request.params[2].isStr()) { + // backward compatibility + std::optional parsed = ParseOutputType(request.params[2].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[2].get_str())); + } + output_type = parsed.value(); + } else if (!request.params[2].isNull()) { + const UniValue& options = request.params[2].get_obj(); + RPCTypeCheckObj(options, + { + {"address_type", UniValueType(UniValue::VSTR)}, + {"sort", UniValueType(UniValue::VBOOL)}, + }, + true, true); + + if (options.exists("address_type")) { + std::optional parsed = ParseOutputType(options["address_type"].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", options["address_type"].get_str())); + } + output_type = parsed.value(); + } + + if (options.exists("sort")) { + sort = options["sort"].get_bool(); + } + } + if (output_type == OutputType::BECH32M) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "createmultisig cannot create bech32m multisig addresses"); + } + // Get the public keys const UniValue& keys = request.params[1].get_array(); std::vector 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())); + if (sort && !pubkeys.back().IsCompressed()) { + throw std::runtime_error(strprintf("Compressed key required for BIP67: %s", keys[i].get_str())); + } } else { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Invalid public key: %s\n.", keys[i].get_str())); } } - // Get the output type - OutputType output_type = OutputType::LEGACY; - if (!request.params[2].isNull()) { - std::optional parsed = ParseOutputType(request.params[2].get_str()); - if (!parsed) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[2].get_str())); - } else if (parsed.value() == OutputType::BECH32M) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "createmultisig cannot create bech32m multisig addresses"); - } - output_type = parsed.value(); - } - // Construct using pay-to-script-hash: FillableSigningProvider keystore; CScript inner; - const CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, keystore, inner); + const CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, keystore, inner, sort); // Make the descriptor std::unique_ptr descriptor = InferDescriptor(GetScriptForDestination(dest), keystore); diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 0c60aa01ef..32fd838342 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -206,7 +206,7 @@ CPubKey AddrToPubKey(const FillableSigningProvider& keystore, const std::string& } // Creates a multisig address from a given list of public keys, number of signatures required, and the address type -CTxDestination AddAndGetMultisigDestination(const int required, const std::vector& pubkeys, OutputType type, FillableSigningProvider& keystore, CScript& script_out) +CTxDestination AddAndGetMultisigDestination(const int required, const std::vector& pubkeys, OutputType type, FillableSigningProvider& keystore, CScript& script_out, bool sort) { // Gather public keys if (required < 1) { @@ -219,7 +219,7 @@ CTxDestination AddAndGetMultisigDestination(const int required, const std::vecto throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Number of keys involved in the multisignature address creation > %d\nReduce the number", MAX_PUBKEYS_PER_MULTISIG)); } - script_out = GetScriptForMultisig(required, pubkeys); + script_out = GetScriptForMultisig(required, pubkeys, sort); // Check if any keys are uncompressed. If so, the type is legacy for (const CPubKey& pk : pubkeys) { diff --git a/src/rpc/util.h b/src/rpc/util.h index 99b6b0bb9a..7c22df6f59 100644 --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -97,7 +97,7 @@ std::string HelpExampleRpcNamed(const std::string& methodname, const RPCArgList& CPubKey HexToPubKey(const std::string& hex_in); CPubKey AddrToPubKey(const FillableSigningProvider& keystore, const std::string& addr_in); -CTxDestination AddAndGetMultisigDestination(const int required, const std::vector& pubkeys, OutputType type, FillableSigningProvider& keystore, CScript& script_out); +CTxDestination AddAndGetMultisigDestination(const int required, const std::vector& pubkeys, OutputType type, FillableSigningProvider& keystore, CScript& script_out, bool sort); UniValue DescribeAddress(const CTxDestination& dest); diff --git a/src/script/standard.cpp b/src/script/standard.cpp index 7c4a05b6e6..1e370d6464 100644 --- a/src/script/standard.cpp +++ b/src/script/standard.cpp @@ -341,13 +341,23 @@ CScript GetScriptForRawPubKey(const CPubKey& pubKey) return CScript() << std::vector(pubKey.begin(), pubKey.end()) << OP_CHECKSIG; } -CScript GetScriptForMultisig(int nRequired, const std::vector& keys) +CScript GetScriptForMultisig(int nRequired, const std::vector& keys, bool fSorted) { - CScript script; + std::vector> vEncoded; + vEncoded.reserve(keys.size()); + for (const CPubKey& key : keys) { + vEncoded.emplace_back(ToByteVector(key)); + } + if (fSorted) { + std::sort(vEncoded.begin(), vEncoded.end()); + } + + CScript script; script << nRequired; - for (const CPubKey& key : keys) - script << ToByteVector(key); + for (const std::vector& bytes : vEncoded) { + script << bytes; + } script << keys.size() << OP_CHECKMULTISIG; return script; diff --git a/src/script/standard.h b/src/script/standard.h index 18cf5c8c88..6ec067bf20 100644 --- a/src/script/standard.h +++ b/src/script/standard.h @@ -193,7 +193,7 @@ CScript GetScriptForRawPubKey(const CPubKey& pubkey); std::optional>>> MatchMultiA(const CScript& script LIFETIMEBOUND); /** Generate a multisig script. */ -CScript GetScriptForMultisig(int nRequired, const std::vector& keys); +CScript GetScriptForMultisig(int nRequired, const std::vector& keys, bool fSorted=false); struct ShortestVectorFirstComparator { diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index e9e975f338..e4075c9a9e 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -219,6 +219,7 @@ RPCHelpMan addmultisigaddress() "This functionality is only intended for use with non-watchonly addresses.\n" "See `importaddress` for watchonly p2sh address support.\n" "If 'label' is specified, assign address to that label.\n" + "Public keys can be sorted according to BIP67 during the request if required.\n" "Note: This command is only compatible with legacy wallets.\n", { {"nrequired", RPCArg::Type::NUM, RPCArg::Optional::NO, "The number of required signatures out of the n keys or addresses."}, @@ -227,8 +228,14 @@ RPCHelpMan addmultisigaddress() {"key", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "bitcoin address or hex-encoded public key"}, }, }, - {"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A label to assign the addresses to."}, - {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -addresstype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, + {"options|label", {RPCArg::Type::OBJ, RPCArg::Type::STR}, RPCArg::Optional::OMITTED, "", + { + {"address_type", RPCArg::Type::STR, RPCArg::DefaultHint{"set by -addresstype"}, "The address type to use. Options are \"legacy\", \"p2sh-segwit\", and \"bech32\"."}, + {"label", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "A label to assign the address to."}, + {"sort", RPCArg::Type::BOOL, RPCArg::Default{false}, "Whether to sort public keys according to BIP67."}, + }, + RPCArgOptions{.oneline_description="\"options\""}}, + {"address_type", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "", RPCArgOptions{.hidden=true}}, }, RPCResult{ RPCResult::Type::OBJ, "", "", @@ -257,10 +264,47 @@ RPCHelpMan addmultisigaddress() LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore); - const std::string label{LabelFromValue(request.params[2])}; - int required = request.params[0].getInt(); + std::string label; + OutputType output_type = pwallet->m_default_address_type; + bool sort = false; + + if (!request.params[2].isNull()) { + if (request.params[2].type() == UniValue::VSTR) { + // Backward compatibility + label = LabelFromValue(request.params[2]); + } else { + const UniValue& options = request.params[2]; + RPCTypeCheckObj(options, + { + {"address_type", UniValueType(UniValue::VSTR)}, + {"label", UniValueType(UniValue::VSTR)}, + {"sort", UniValueType(UniValue::VBOOL)}, + }, + true, true); + + if (options.exists("address_type")) { + if (!request.params[3].isNull()) { + throw JSONRPCError(RPC_MISC_ERROR, "address_type provided in both options and 4th parameter"); + } + std::optional parsed = ParseOutputType(options["address_type"].get_str()); + if (!parsed) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", options["address_type"].get_str())); + } + output_type = parsed.value(); + } + + if (options.exists("label")) { + label = LabelFromValue(options["label"]); + } + + if (options.exists("sort")) { + sort = options["sort"].get_bool(); + } + } + } + // Get the public keys const UniValue& keys_or_addrs = request.params[1].get_array(); std::vector pubkeys; @@ -270,9 +314,11 @@ RPCHelpMan addmultisigaddress() } else { pubkeys.push_back(AddrToPubKey(spk_man, keys_or_addrs[i].get_str())); } + if (sort && !pubkeys.back().IsCompressed()) { + throw std::runtime_error(strprintf("Compressed key required for BIP67: %s", keys_or_addrs[i].get_str())); + } } - OutputType output_type = pwallet->m_default_address_type; if (!request.params[3].isNull()) { std::optional parsed = ParseOutputType(request.params[3].get_str()); if (!parsed) { @@ -285,7 +331,7 @@ RPCHelpMan addmultisigaddress() // Construct using pay-to-script-hash: CScript inner; - CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, spk_man, inner); + CTxDestination dest = AddAndGetMultisigDestination(required, pubkeys, output_type, spk_man, inner, sort); pwallet->SetAddressBook(dest, label, AddressPurpose::SEND); // Make the descriptor diff --git a/test/functional/rpc_sort_multisig.py b/test/functional/rpc_sort_multisig.py new file mode 100755 index 0000000000..e6e030c030 --- /dev/null +++ b/test/functional/rpc_sort_multisig.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2016 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# Exercise the createmultisig API + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +class SortMultisigTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [[]] + self.setup_clean_chain = True + + def run_simple_test(self): + pub1 = "022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da" + pub2 = "03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9" + pub3 = "021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18" + + pubs = [pub1,pub2,pub3] + + default = self.nodes[0].createmultisig(2, pubs) + unsorted = self.nodes[0].createmultisig(2, pubs, {"sort": False}) + + assert_equal("2N2BchzwfyuqJep7sKmFfBucfopHZQuPnpt", unsorted["address"]) + assert_equal("5221022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da2103e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e921021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc1853ae", unsorted["redeemScript"]) + assert_equal(default["address"], unsorted["address"]) + assert_equal(default["redeemScript"], unsorted["redeemScript"]) + + sorted = self.nodes[0].createmultisig(2, pubs, {"sort": True}) + assert_equal("2NFd5JqpwmQNz3gevZJ3rz9ofuHvqaP9Cye", sorted["address"]) + assert_equal("5221021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc1821022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da2103e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e953ae", sorted["redeemScript"]) + + def run_demonstrate_sorting(self): + pub1 = "022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da" + pub2 = "03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9" + pub3 = "021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18" + + sorted = self.nodes[0].createmultisig(2, [pub3,pub1,pub2,]) + + self.test_if_result_matches(2, [pub1,pub2,pub3], True, sorted["address"]) + self.test_if_result_matches(2, [pub1,pub3,pub2], True, sorted["address"]) + self.test_if_result_matches(2, [pub2,pub3,pub1], True, sorted["address"]) + self.test_if_result_matches(2, [pub2,pub1,pub3], True, sorted["address"]) + self.test_if_result_matches(2, [pub3,pub1,pub2], True, sorted["address"]) + self.test_if_result_matches(2, [pub3,pub2,pub1], True, sorted["address"]) + + self.test_if_result_matches(2, [pub1,pub2,pub3], False, sorted["address"]) + self.test_if_result_matches(2, [pub1,pub3,pub2], False, sorted["address"]) + self.test_if_result_matches(2, [pub2,pub3,pub1], False, sorted["address"]) + self.test_if_result_matches(2, [pub2,pub1,pub3], False, sorted["address"]) + self.test_if_result_matches(2, [pub3,pub2,pub1], False, sorted["address"]) + + def test_if_result_matches(self, m, keys, sort, against): + result = self.nodes[0].createmultisig(m, keys, {"sort": sort}) + assert_equal(sort, result["address"] == against) + + def test_compressed_keys_forbidden(self): + pub1 = "02fdf7e1b65a477a7815effde996a03a7d94cbc46f7d14c05ef38425156fc92e22" + pub2 = "04823336da95f0b4cf745839dff26992cef239ad2f08f494e5b57c209e4f3602d5526bc251d480e3284d129f736441560e17f3a7eb7ed665fdf0158f44550b926c" + rs = "522102fdf7e1b65a477a7815effde996a03a7d94cbc46f7d14c05ef38425156fc92e224104823336da95f0b4cf745839dff26992cef239ad2f08f494e5b57c209e4f3602d5526bc251d480e3284d129f736441560e17f3a7eb7ed665fdf0158f44550b926c52ae" + pubs = [pub1,pub2] + + default = self.nodes[0].createmultisig(2, pubs) + assert_equal(rs, default["redeemScript"]) + + unsorted = self.nodes[0].createmultisig(2, pubs, {"sort": False}) + assert_equal(rs, unsorted["redeemScript"]) + assert_equal(default["address"], unsorted["address"]) + assert_equal(default["redeemScript"], unsorted["redeemScript"]) + + assert_raises_rpc_error(-1, "Compressed key required for BIP67: 04823336da95f0b4cf745839dff26992cef239ad2f08f494e5b57c209e4f3602d5526bc251d480e3284d129f736441560e17f3a7eb7ed665fdf0158f44550b926c", self.nodes[0].createmultisig, 2, pubs, {"sort": True}) + + def run_test(self): + self.run_simple_test() + self.run_demonstrate_sorting() + self.test_compressed_keys_forbidden() + +if __name__ == '__main__': + SortMultisigTest().main() + diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 97593401f2..d44fc4b09f 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -803,6 +803,12 @@ class RPCOverloadWrapper(): wallet_info = self.getwalletinfo() if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): return self.__getattr__('addmultisigaddress')(nrequired, keys, label, address_type) + if isinstance(label, dict): + options = dict(label) # copy, so we can pop and check for emptiness + assert address_type is None + address_type = options.pop('address_type', None) + label = options.pop('label', None) + assert not options cms = self.createmultisig(nrequired, keys, address_type) req = [{ 'desc': cms['descriptor'], diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 723868d35a..0d0a9a1a08 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -276,6 +276,7 @@ BASE_SCRIPTS = [ 'mempool_accept.py', 'mempool_fee_histogram.py', 'mempool_expiry.py', + 'rpc_sort_multisig.py', 'wallet_import_with_label.py --legacy-wallet', 'wallet_importdescriptors.py --descriptors', 'wallet_importseed.py --descriptors', diff --git a/test/functional/wallet_labels.py b/test/functional/wallet_labels.py index 1cc7786c94..57767057bb 100755 --- a/test/functional/wallet_labels.py +++ b/test/functional/wallet_labels.py @@ -66,6 +66,52 @@ class WalletLabelsTest(BitcoinTestFramework): for rpc_call in rpc_calls: assert_raises_rpc_error(-11, "Invalid label name", *rpc_call, "*") + def test_sort_multisig(self, node): + node.importprivkey("cSJUMwramrFYHKPfY77FH94bv4Q5rwUCyfD6zX3kLro4ZcWsXFEM") + node.importprivkey("cSpQbSsdKRmxaSWJ3TckCFTrksXNPbh8tfeZESGNQekkVxMbQ77H") + node.importprivkey("cRNbfcJgnvk2QJEVbMsxzoprotm1cy3kVA2HoyjSs3ss5NY5mQqr") + + addresses = [ + "muRmfCwue81ZT9oc3NaepefPscUHtP5kyC", + "n12RzKwqWPPA4cWGzkiebiM7Gu6NXUnDW8", + "n2yWMtx8jVbo8wv9BK2eN1LdbaakgKL3Mt", + ] + + sorted_default = node.addmultisigaddress(2, addresses, None, 'legacy') + sorted_false = node.addmultisigaddress(2, addresses, {"sort": False}, 'legacy') + sorted_true = node.addmultisigaddress(2, addresses, {"sort": True}, 'legacy') + + assert_equal(sorted_default, sorted_false) + assert_equal("2N6dne8yzh13wsRJxCcMgCYNeN9fxKWNHt8", sorted_default['address']) + assert_equal("2MsJ2YhGewgDPGEQk4vahGs4wRikJXpRRtU", sorted_true['address']) + + sorted_default = node.addmultisigaddress(2, addresses, {'address_type': 'legacy'}) + sorted_false = node.addmultisigaddress(2, addresses, {'address_type': 'legacy', "sort": False}) + sorted_true = node.addmultisigaddress(2, addresses, {'address_type': 'legacy', "sort": True}) + + assert_equal(sorted_default, sorted_false) + assert_equal("2N6dne8yzh13wsRJxCcMgCYNeN9fxKWNHt8", sorted_default['address']) + assert_equal("2MsJ2YhGewgDPGEQk4vahGs4wRikJXpRRtU", sorted_true['address']) + + assert_raises_rpc_error(-1, "address_type provided in both options and 4th parameter", node.addmultisigaddress, 2, addresses, {"address_type": 'legacy'}, 'bech32') + + def test_sort_multisig_with_uncompressed_hash160(self, node): + node.importpubkey("02632b12f4ac5b1d1b72b2a3b508c19172de44f6f46bcee50ba33f3f9291e47ed0") + node.importpubkey("04dd4fe618a8ad14732f8172fe7c9c5e76dd18c2cc501ef7f86e0f4e285ca8b8b32d93df2f4323ebb02640fa6b975b2e63ab3c9d6979bc291193841332442cc6ad") + address = "2MxvEpFdXeEDbnz8MbRwS23kDZC8tzQ9NjK" + + addresses = [ + "msDoRfEfZQFaQNfAEWyqf69H99yntZoBbG", + "myrfasv56W7579LpepuRy7KFhVhaWsJYS8", + ] + default = self.nodes[0].addmultisigaddress(2, addresses, {'address_type': 'legacy'}) + assert_equal(address, default['address']) + + unsorted = self.nodes[0].addmultisigaddress(2, addresses, {'address_type': 'legacy', "sort": False}) + assert_equal(address, unsorted['address']) + + assert_raises_rpc_error(-1, "Compressed key required for BIP67: myrfasv56W7579LpepuRy7KFhVhaWsJYS8", node.addmultisigaddress, 2, addresses, {"sort": True}) + def run_test(self): # Check that there's no UTXO on the node node = self.nodes[0] @@ -188,6 +234,10 @@ class WalletLabelsTest(BitcoinTestFramework): self.invalid_label_name_test() + if not self.options.descriptors: + self.test_sort_multisig(node) + self.test_sort_multisig_with_uncompressed_hash160(node) + if self.options.descriptors: # This is a descriptor wallet test because of segwit v1+ addresses self.log.info('Check watchonly labels')