From 78348bcddd18fa7a25541595576bef1df4fc4022 Mon Sep 17 00:00:00 2001 From: jeffro256 Date: Thu, 22 Jun 2023 09:15:12 +0200 Subject: [PATCH] wallet-rpc: restore from multisig seed --- contrib/epee/include/span.h | 10 ++ src/wallet/wallet2.cpp | 47 +++--- src/wallet/wallet2.h | 2 +- src/wallet/wallet_rpc_server.cpp | 33 +++- src/wallet/wallet_rpc_server_commands_defs.h | 2 + tests/functional_tests/multisig.py | 155 +++++++++++++++++-- utils/python-rpc/framework/wallet.py | 3 +- 7 files changed, 217 insertions(+), 35 deletions(-) diff --git a/contrib/epee/include/span.h b/contrib/epee/include/span.h index 99c2ebb4f..23bd51f8c 100644 --- a/contrib/epee/include/span.h +++ b/contrib/epee/include/span.h @@ -147,6 +147,16 @@ namespace epee return {reinterpret_cast(src.data()), src.size_bytes()}; } + //! \return `span` from a STL compatible `src`. + template + constexpr span to_mut_byte_span(T& src) + { + using value_type = typename T::value_type; + static_assert(!std::is_empty(), "empty value types will not work -> sizeof == 1"); + static_assert(!has_padding(), "source value type may have padding"); + return {reinterpret_cast(src.data()), src.size() * sizeof(value_type)}; + } + //! \return `span` which represents the bytes at `&src`. template span as_byte_span(const T& src) noexcept diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 727ab32f9..28c785f23 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -988,6 +988,21 @@ bool get_pruned_tx(const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry &entry, return false; } +// Given M (threshold) and N (total), calculate the number of private multisig keys each +// signer should have. This value is equal to (N - 1) choose (N - M) +// Prereq: M >= 1 && N >= M && N <= 16 +uint64_t num_priv_multisig_keys_post_setup(uint64_t threshold, uint64_t total) +{ + THROW_WALLET_EXCEPTION_IF(threshold < 1 || total < threshold || threshold > 16, + tools::error::wallet_internal_error, "Invalid arguments to num_priv_multisig_keys_post_setup"); + + uint64_t n_multisig_keys = 1; + for (uint64_t i = 2; i <= total - 1; ++i) n_multisig_keys *= i; // multiply by (N - 1)! + for (uint64_t i = 2; i <= total - threshold; ++i) n_multisig_keys /= i; // divide by (N - M)! + for (uint64_t i = 2; i <= threshold - 1; ++i) n_multisig_keys /= i; // divide by ((N - 1) - (N - M))! + return n_multisig_keys; +} + //----------------------------------------------------------------- } //namespace @@ -1410,7 +1425,7 @@ bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeab return true; } //---------------------------------------------------------------------------------------------------- -bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase, bool raw) const +bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase) const { bool ready; uint32_t threshold, total; @@ -1424,15 +1439,14 @@ bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeabl std::cout << "This multisig wallet is not yet finalized" << std::endl; return false; } - if (!raw && seed_language.empty()) - { - std::cout << "seed_language not set" << std::endl; - return false; - } + + const uint64_t num_expected_ms_keys = num_priv_multisig_keys_post_setup(threshold, total); crypto::secret_key skey; crypto::public_key pkey; const account_keys &keys = get_account().get_keys(); + THROW_WALLET_EXCEPTION_IF(num_expected_ms_keys != keys.m_multisig_keys.size(), + error::wallet_internal_error, "Unexpected number of private multisig keys") epee::wipeable_string data; data.append((const char*)&threshold, sizeof(uint32_t)); data.append((const char*)&total, sizeof(uint32_t)); @@ -1457,18 +1471,7 @@ bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeabl data = encrypt(data, key, true); } - if (raw) - { - seed = epee::to_hex::wipeable_string({(const unsigned char*)data.data(), data.size()}); - } - else - { - if (!crypto::ElectrumWords::bytes_to_words(data.data(), data.size(), seed, seed_language)) - { - std::cout << "Failed to encode seed"; - return false; - } - } + seed = epee::to_hex::wipeable_string({(const unsigned char*)data.data(), data.size()}); return true; } @@ -5078,9 +5081,11 @@ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& offset += sizeof(uint32_t); uint32_t total = *(uint32_t*)(multisig_data.data() + offset); offset += sizeof(uint32_t); - THROW_WALLET_EXCEPTION_IF(threshold < 2, error::invalid_multisig_seed); - THROW_WALLET_EXCEPTION_IF(total != threshold && total != threshold + 1, error::invalid_multisig_seed); - const size_t n_multisig_keys = total == threshold ? 1 : threshold; + + THROW_WALLET_EXCEPTION_IF(threshold < 1, error::invalid_multisig_seed); + THROW_WALLET_EXCEPTION_IF(total < threshold, error::invalid_multisig_seed); + THROW_WALLET_EXCEPTION_IF(threshold > 16, error::invalid_multisig_seed); // doing N choose (N - M + 1) might overflow + const uint64_t n_multisig_keys = num_priv_multisig_keys_post_setup(threshold, total); THROW_WALLET_EXCEPTION_IF(multisig_data.size() != 8 + 32 * (4 + n_multisig_keys + total), error::invalid_multisig_seed); std::vector multisig_keys; diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index cb857ee43..11e4621ac 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1059,7 +1059,7 @@ private: bool multisig(bool *ready = NULL, uint32_t *threshold = NULL, uint32_t *total = NULL) const; bool has_multisig_partial_key_images() const; bool has_unknown_key_images() const; - bool get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase = std::string(), bool raw = true) const; + bool get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase = std::string()) const; bool key_on_device() const { return get_device_type() != hw::device::device_type::SOFTWARE; } hw::device::device_type get_device_type() const { return m_key_device_type; } bool reconnect_device(); diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 32628d65b..078535928 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -3806,7 +3806,7 @@ namespace tools std::string old_language; // check the given seed - { + if (!req.enable_multisig_experimental) { if (!crypto::ElectrumWords::words_to_bytes(req.seed, recovery_key, old_language)) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; @@ -3829,6 +3829,13 @@ namespace tools // process seed_offset if given { + if (req.enable_multisig_experimental && !req.seed_offset.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Multisig seeds are not compatible with seed offsets"; + return false; + } + if (!req.seed_offset.empty()) { recovery_key = cryptonote::decrypt_key(recovery_key, req.seed_offset); @@ -3892,7 +3899,27 @@ namespace tools crypto::secret_key recovery_val; try { - recovery_val = wal->generate(wallet_file, std::move(rc.second).password(), recovery_key, true, false, false); + if (req.enable_multisig_experimental) + { + // Parse multisig seed into raw multisig data + epee::wipeable_string multisig_data; + multisig_data.resize(req.seed.size() / 2); + if (!epee::from_hex::to_buffer(epee::to_mut_byte_span(multisig_data), req.seed)) + { + er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; + er.message = "Multisig seed not represented as hexadecimal string"; + return false; + } + + // Generate multisig wallet + wal->generate(wallet_file, std::move(rc.second).password(), multisig_data, false); + wal->enable_multisig(true); + } + else + { + // Generate normal wallet + recovery_val = wal->generate(wallet_file, std::move(rc.second).password(), recovery_key, true, false, false); + } MINFO("Wallet has been restored.\n"); } catch (const std::exception &e) @@ -3903,7 +3930,7 @@ namespace tools // // Convert the secret key back to seed epee::wipeable_string electrum_words; - if (!crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language)) + if (!req.enable_multisig_experimental && !crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language)) { er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR; er.message = "Failed to encode seed"; diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index 74c3862cb..d5c6bfd6b 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -2262,6 +2262,7 @@ namespace wallet_rpc std::string password; std::string language; bool autosave_current; + bool enable_multisig_experimental; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_OPT(restore_height, (uint64_t)0) @@ -2271,6 +2272,7 @@ namespace wallet_rpc KV_SERIALIZE(password) KV_SERIALIZE(language) KV_SERIALIZE_OPT(autosave_current, true) + KV_SERIALIZE_OPT(enable_multisig_experimental, false) END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init request; diff --git a/tests/functional_tests/multisig.py b/tests/functional_tests/multisig.py index 59f124e00..980adc2df 100755 --- a/tests/functional_tests/multisig.py +++ b/tests/functional_tests/multisig.py @@ -29,6 +29,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import print_function +import random """Test multisig transfers """ @@ -36,42 +37,74 @@ from __future__ import print_function from framework.daemon import Daemon from framework.wallet import Wallet +MULTISIG_PUB_ADDRS = [ + '45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG', # 2/2 + '44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i', # 2/3 + '41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP', # 3/3 + '44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff', # 3/4 + '47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U', # 2/4 + '4A8RnBQixry4VXkqeWhmg8L7vWJVDJj4FN9PV4E7Mgad5ZZ6LKQdn8dYJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ4S8RSB' # 1/2 +] + class MultisigTest(): def run_test(self): self.reset() - self.mine('45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG', 5) - self.mine('44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i', 5) - self.mine('41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP', 5) - self.mine('44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff', 5) - self.mine('47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U', 5) + for pub_addr in MULTISIG_PUB_ADDRS: + self.mine(pub_addr, 4) self.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 80) self.test_states() + self.fund_addrs_with_normal_wallet(MULTISIG_PUB_ADDRS) + self.create_multisig_wallets(2, 2, '45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG') self.import_multisig_info([1, 0], 5) txid = self.transfer([1, 0]) self.import_multisig_info([0, 1], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(2) + self.import_multisig_info([0, 1], 6) # six outputs, same as before + txid = self.transfer([0, 1]) + self.import_multisig_info([0, 1], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(2, 3, '44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i') self.import_multisig_info([0, 2], 5) txid = self.transfer([0, 2]) self.import_multisig_info([0, 1, 2], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(2) + self.import_multisig_info([0, 2], 6) # six outputs, same as before + txid = self.transfer([0, 2]) + self.import_multisig_info([0, 1, 2], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(3, 3, '41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP') self.import_multisig_info([2, 0, 1], 5) txid = self.transfer([2, 1, 0]) self.import_multisig_info([0, 2, 1], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(3) + self.import_multisig_info([2, 0, 1], 6) # six outputs, same as before + txid = self.transfer([2, 1, 0]) + self.import_multisig_info([0, 2, 1], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(3, 4, '44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff') self.import_multisig_info([0, 2, 3], 5) txid = self.transfer([0, 2, 3]) self.import_multisig_info([0, 1, 2, 3], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(3) + self.import_multisig_info([0, 2, 3], 6) # six outputs, same as before + txid = self.transfer([0, 2, 3]) + self.import_multisig_info([0, 1, 2, 3], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(2, 4, '47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U') self.import_multisig_info([1, 2], 5) txid = self.transfer([1, 2]) @@ -81,6 +114,24 @@ class MultisigTest(): self.import_multisig_info([0, 1, 2, 3], 7) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(2) + self.import_multisig_info([0, 1, 2, 3], 6) # six outputs, same as before + txid = self.transfer([2, 3]) + self.import_multisig_info([0, 1, 2, 3], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + + self.create_multisig_wallets(1, 2, '4A8RnBQixry4VXkqeWhmg8L7vWJVDJj4FN9PV4E7Mgad5ZZ6LKQdn8dYJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ4S8RSB') + self.import_multisig_info([0, 1], 5) + txid = self.transfer([0]) + self.import_multisig_info([0, 1], 6) + self.check_transaction(txid) + + self.remake_some_multisig_wallets_by_multsig_seed(1) + self.import_multisig_info([0, 1], 6) # six outputs, same as before + txid = self.transfer([1]) + self.import_multisig_info([0, 1], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + def reset(self): print('Resetting blockchain') daemon = Daemon() @@ -93,6 +144,11 @@ class MultisigTest(): daemon = Daemon() daemon.generateblocks(address, blocks) + # This method sets up N_total wallets with a threshold of M_threshold doing the following steps: + # * restore_deterministic_wallet(w/ hardcoded seeds) + # * prepare_multisig(enable_multisig_experimental = True) + # * make_multisig() + # * exchange_multisig_keys() def create_multisig_wallets(self, M_threshold, N_total, expected_address): print('Creating ' + str(M_threshold) + '/' + str(N_total) + ' multisig wallet') seeds = [ @@ -103,6 +159,8 @@ class MultisigTest(): ] assert M_threshold <= N_total assert N_total <= len(seeds) + + # restore_deterministic_wallet() & prepare_multisig() self.wallet = [None] * N_total info = [] for i in range(N_total): @@ -114,10 +172,12 @@ class MultisigTest(): assert len(res.multisig_info) > 0 info.append(res.multisig_info) + # Assert that all wallets are multisig for i in range(N_total): res = self.wallet[i].is_multisig() assert res.multisig == False + # make_multisig() with each other's info addresses = [] next_stage = [] for i in range(N_total): @@ -125,6 +185,7 @@ class MultisigTest(): addresses.append(res.address) next_stage.append(res.multisig_info) + # Assert multisig paramaters M/N for each wallet for i in range(N_total): res = self.wallet[i].is_multisig() assert res.multisig == True @@ -132,13 +193,15 @@ class MultisigTest(): assert res.threshold == M_threshold assert res.total == N_total - while True: + # exchange_multisig_keys() + num_exchange_multisig_keys_stages = 0 + while True: # while not all wallets are ready n_ready = 0 for i in range(N_total): res = self.wallet[i].is_multisig() if res.ready == True: n_ready += 1 - assert n_ready == 0 or n_ready == N_total + assert n_ready == 0 or n_ready == N_total # No partial readiness if n_ready == N_total: break info = next_stage @@ -148,10 +211,17 @@ class MultisigTest(): res = self.wallet[i].exchange_multisig_keys(info) next_stage.append(res.multisig_info) addresses.append(res.address) + num_exchange_multisig_keys_stages += 1 + + # We should only need N - M + 1 key exchange rounds + assert num_exchange_multisig_keys_stages == N_total - M_threshold + 1 + + # Assert that the all wallets have expected public address for i in range(N_total): - assert addresses[i] == expected_address + assert addresses[i] == expected_address, addresses[i] self.wallet_address = expected_address + # Assert multisig paramaters M/N and "ready" for each wallet for i in range(N_total): res = self.wallet[i].is_multisig() assert res.multisig == True @@ -159,6 +229,73 @@ class MultisigTest(): assert res.threshold == M_threshold assert res.total == N_total + # We want to test if multisig wallets can receive normal transfers as well and mining transfers + def fund_addrs_with_normal_wallet(self, addrs): + print("Funding multisig wallets with normal wallet-to-wallet transfers") + + # Generate normal deterministic wallet + normal_seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted' + assert not hasattr(self, 'wallet') or not self.wallet + self.wallet = [Wallet(idx = 0)] + res = self.wallet[0].restore_deterministic_wallet(seed = normal_seed) + assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm' + + self.wallet[0].refresh() + + # Check that we own enough spendable enotes + res = self.wallet[0].incoming_transfers(transfer_type = 'available') + assert 'transfers' in res + num_outs_spendable = 0 + min_out_amount = None + for t in res.transfers: + if not t.spent: + num_outs_spendable += 1 + min_out_amount = min(min_out_amount, t.amount) if min_out_amount is not None else t.amount + assert num_outs_spendable >= 2 * len(addrs) + + # Transfer to addrs and mine to confirm tx + dsts = [{'address': addr, 'amount': int(min_out_amount * 0.95)} for addr in addrs] + res = self.wallet[0].transfer(dsts, get_tx_metadata = True) + tx_hex = res.tx_metadata + res = self.wallet[0].relay_tx(tx_hex) + self.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 10) + + def remake_some_multisig_wallets_by_multsig_seed(self, threshold): + N = len(self.wallet) + signers_to_remake = set() + num_signers_to_remake = random.randint(1, N) # Do at least one + while len(signers_to_remake) < num_signers_to_remake: + signers_to_remake.add(random.randint(0, N - 1)) + + for i in signers_to_remake: + print("Remaking {}/{} multsig wallet from multisig seed: #{}".format(threshold, N, i+1)) + + otherwise_unused_seed = \ + 'factual wiggle awakened maul sash biscuit pause reinvest fonts sleepless knowledge tossed jewels request gusts dagger gumball onward dotted amended powder cynical strained topic request' + + # Get information about wallet, will compare against later + old_viewkey = self.wallet[i].query_key('view_key').key + old_spendkey = self.wallet[i].query_key('spend_key').key + old_multisig_seed = self.wallet[i].query_key('mnemonic').key + + # Close old wallet and restore w/ random seed so we know that restoring actually did something + self.wallet[i].close_wallet() + self.wallet[i].restore_deterministic_wallet(seed=otherwise_unused_seed) + mid_viewkey = self.wallet[i].query_key('view_key').key + assert mid_viewkey != old_viewkey + + # Now restore w/ old multisig seed and check against original + self.wallet[i].close_wallet() + self.wallet[i].restore_deterministic_wallet(seed=old_multisig_seed, enable_multisig_experimental=True) + new_viewkey = self.wallet[i].query_key('view_key').key + new_spendkey = self.wallet[i].query_key('spend_key').key + new_multisig_seed = self.wallet[i].query_key('mnemonic').key + assert new_viewkey == old_viewkey + assert new_spendkey == old_spendkey + assert new_multisig_seed == old_multisig_seed + + self.wallet[i].refresh() + def test_states(self): print('Testing multisig states') seeds = [ @@ -251,7 +388,7 @@ class MultisigTest(): assert res.n_outputs == expected_outputs def transfer(self, signers): - assert len(signers) >= 2 + assert len(signers) >= 1 daemon = Daemon() diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py index 0f6db76bd..37e91034b 100644 --- a/utils/python-rpc/framework/wallet.py +++ b/utils/python-rpc/framework/wallet.py @@ -297,7 +297,7 @@ class Wallet(object): } return self.rpc.send_json_rpc_request(query_key) - def restore_deterministic_wallet(self, seed = '', seed_offset = '', filename = '', restore_height = 0, password = '', language = '', autosave_current = True): + def restore_deterministic_wallet(self, seed = '', seed_offset = '', filename = '', restore_height = 0, password = '', language = '', autosave_current = True, enable_multisig_experimental = False): restore_deterministic_wallet = { 'method': 'restore_deterministic_wallet', 'params' : { @@ -308,6 +308,7 @@ class Wallet(object): 'password': password, 'language': language, 'autosave_current': autosave_current, + 'enable_multisig_experimental': enable_multisig_experimental }, 'jsonrpc': '2.0', 'id': '0'