#include #include #include "wallet2.h" #include "wallet/wownero_wallet_full.h" #include "utils/gen_utils.h" #include "utils/wownero_utils.h" //#include "wownero_wallet_model.h" using namespace std; // ----------------------------- TEST CONFIG ---------------------------------- // constants const int LOG_LEVEL = 2; const string DAEMON_URI = "http://localhost:28081"; const wownero_network_type NETWORK_TYPE = wownero_network_type::TESTNET; // ------------------------ MULTISIG STRESS TEST ------------------------------ void synchronize_multisig_participants(vector wallets) { // collect multisig hex of all participants to synchronize vector multisig_hexes; for (int i = 0; i < wallets.size(); i++) { wownero_wallet* wallet = wallets[i]; wallet->sync(); multisig_hexes.push_back(wallet->export_multisig_hex()); // TODO: does wallet need saved? } // import each wallet's peer multisig hex for (int i = 0; i < wallets.size(); i++) { vector peer_multisig_hexes; for (int j = 0; j < wallets.size(); j++) if (j != i) peer_multisig_hexes.push_back(multisig_hexes[j]); wownero_wallet* wallet = wallets[i]; wallet->sync(); // TODO wownero-project: creating multisig tx fails if wallet not explicitly synced before import_multisig_hex: https://github.com/wownero-project/wownero/issues/6850 wallet->import_multisig_hex(peer_multisig_hexes); } } void init_multisig_wallets(wownero_wallet* funding_wallet, vector participants, int M, int N, int account_idx, int num_addresses_to_fund) { std::cout << "init_multisig_wallets(" << M << ", " << N << ")" << std::endl; if (participants.size() != N) throw std::runtime_error("participants != N"); // prepare multisig hexes vector prepared_multisig_hexes; for (int i = 0; i < N; i++) { wownero_wallet* participant = participants[i]; prepared_multisig_hexes.push_back(participant->prepare_multisig()); } // make wallets multisig vector made_multisig_hexes; for (int i = 0; i < participants.size(); i++) { wownero_wallet* participant = participants[i]; // // test bad input // try { // participant.make_multisig(Arrays.asList("asd", "dsa"), M, TestUtils.WALLET_PASSWORD); // throw new RuntimeException("Should have thrown error making wallet multisig with incorrect values"); // } catch (MoneroError e) { // assertEquals("basic_string", e.getMessage()); // TODO (wownero-project): improve error message https://github.com/wownero-project/wownero/issues/8493 // } // collect prepared multisig hexes from wallet's peers vector peer_multisig_hexes; for (int j = 0; j < participants.size(); j++) if (j != i) peer_multisig_hexes.push_back(prepared_multisig_hexes[j]); // make the wallet multisig string multisig_hex = participant->make_multisig(peer_multisig_hexes, M, ""); made_multisig_hexes.push_back(multisig_hex); } // try to get seed before wallet initialized try { participants[0]->get_seed(); throw std::runtime_error("Expected error getting seed before multisig wallet initialized"); } catch (exception& e) { string msg = string(e.what()); if (msg != "This wallet is multisig, but not yet finalized") throw std::runtime_error("unexpected error: " + msg); } // exchange keys N - M + 1 times string address; if (N != made_multisig_hexes.size()) throw std::runtime_error("N != made_multisig_hexes.size()"); vector prev_multisig_hexes = made_multisig_hexes; for (int i = 0; i < N - M + 1; i++) { //System.out.println("Exchanging multisig keys round " + (i + 1) + " / " + (N - M + 1)); // exchange multisig keys with each wallet and collect results vector exchange_multisig_hexes; for (int j = 0; j < participants.size(); j++) { wownero_wallet* participant = participants[j]; // // test bad input // try { // participant->exchange_multisig_keys(Arrays.asList("asd", "dsa"), TestUtils.WALLET_PASSWORD); // throw new RuntimeException("Should have thrown error exchanging multisig keys with bad input"); // } catch (MoneroError e) { // assertTrue(e.getMessage().length() > 0); // } // collect the multisig hexes of the wallet's peers from last round vector peer_multisig_hexes; for (int k = 0; k < participants.size(); k++) if (k != j) peer_multisig_hexes.push_back(prev_multisig_hexes[k]); // import the multisig hexes of the wallet's peers wownero_multisig_init_result result = participant->exchange_multisig_keys(peer_multisig_hexes, ""); // test result if (result.m_multisig_hex->empty()) throw runtime_error("m_multisig_hex is empty"); if (i == N - M) { // result on last round has address if (result.m_address->empty()) throw runtime_error("address is empty"); if (address.empty()) address = result.m_address.get(); else if (address != result.m_address.get()) throw runtime_error("address != result address"); } else { if (result.m_address) throw runtime_error("address should be empty"); exchange_multisig_hexes.push_back(result.m_multisig_hex.get()); } } // use results for next round of exchange prev_multisig_hexes = exchange_multisig_hexes; } // validate final multisig wownero_wallet* participant = participants[0]; wownero_utils::validate_address(participant->get_primary_address(), NETWORK_TYPE); //test_multisig_info(participant->get_multisig_info(), M, N); // TODO string seed = participant->get_seed(); if (seed.empty()) throw std::runtime_error("seed should not be empty"); // restore participant from multisig seed // participant->close(); // delete participant; // participant = createWallet(new MoneroWalletConfig().setSeed(seed).setIsMultisig(true)); // MoneroUtils.validateAddress(participant.getPrimaryAddress(), TestUtils.NETWORK_TYPE); // assertEquals(address, participant.getPrimaryAddress()); // testMultisigInfo(participant.getMultisigInfo(), M, N); // assertEquals(seed, participant.getSeed()); // participants.set(0, participant); // test sending a multisig transaction if configured if (num_addresses_to_fund == 0) return; // create accounts in the first multisig wallet to receive funds for (int i = 0; i < account_idx; i++) participant->create_account(); // get destinations to subaddresses within the account of the multisig wallet vector> destinations; for (int i = 0; i < num_addresses_to_fund; i++) { shared_ptr destination = make_shared(); destination->m_address = participant->get_address(account_idx, i); destination->m_amount = 200000000000; // TODO: add a 0 for 2 XMR destinations.push_back(destination); if (i + 1 < num_addresses_to_fund) participant->create_subaddress(account_idx); } // // wait for txs to confirm and for sufficient unlocked balance // TestUtils.WALLET_TX_TRACKER.waitForWalletTxsToClearPool(wallet); // TestUtils.WALLET_TX_TRACKER.waitForUnlockedBalance(wallet, 0, null, TestUtils.MAX_FEE.multiply(new BigInteger("20"))); // send funds from the main test wallet to destinations in the first multisig wallet std::cout << "Sending funds from main wallet" << std::endl; wownero_tx_config tx_config; tx_config.m_account_index = 0; tx_config.m_destinations = destinations; tx_config.m_relay = true; shared_ptr tx = funding_wallet->create_tx(tx_config); wownero_utils::free(tx); // attempt to start mining std::cout << "Starting mining" << std::endl; try { funding_wallet->start_mining(1, false, false); } catch (const exception& e) { if (string("BUSY") == string(e.what())) throw e; } uint64_t last_num_confirmations = 0; while (true) { // wait a moment gen_utils::wait_for(5000); // fetch and test outputs wownero_output_query query; vector> outputs = participant->get_outputs(query); if (outputs.size() == 0) std::cout << "No output reported yet" << endl; else { // print num confirmations uint64_t height = funding_wallet->get_height(); uint64_t num_confirmations = height - *outputs[0]->m_tx->get_height(); if (last_num_confirmations != num_confirmations) std::cout << "Output has " << (height - *outputs[0]->m_tx->get_height()) << " confirmations" << endl; // outputs are not spent // break if output is unlocked shared_ptr tx = std::static_pointer_cast(outputs[0]->m_tx); if (!tx->m_is_locked.get()) { wownero_utils::free(outputs); break; } } wownero_utils::free(outputs); } // stop mining funding_wallet->stop_mining(); // multisig wallet should have unlocked balance in subaddresses for (int i = 0; i < num_addresses_to_fund; i++) { if (participant->get_unlocked_balance(account_idx, i) == 0) throw std::runtime_error("balance expected to be > 0"); } wownero_output_query output_query; output_query.m_account_index = account_idx; vector> outputs = participant->get_outputs(output_query); if (outputs.size() == 0) throw runtime_error("no outputs returned"); if (outputs.size() < 3) std::cout << "WARNING: not one output per subaddress?"; wownero_utils::free(outputs); // wallet requires importing multisig to be reliable if (!participant->is_multisig_import_needed()) throw std::runtime_error("multisig import should be needed"); // // attempt creating and relaying transaction without synchronizing with participants // String returnAddress = wallet.getPrimaryAddress(); // funds will be returned to this address from the multisig wallet // try { // participant.createTxs(new MoneroTxConfig().setAccountIndex(accountIdx).setAddress(returnAddress).setAmount(TestUtils.MAX_FEE.multiply(BigInteger.valueOf(3)))); // throw new RuntimeException("Should have failed sending funds without synchronizing with peers"); // } catch (MoneroError e) { // assertEquals("No transaction created", e.getMessage()); // } // synchronize the multisig participants since receiving outputs std::cout << "Synchronizing participants" << std::endl; synchronize_multisig_participants(participants); // // expect error exporting key images // try { // participant.exportKeyImages(true); // } catch (Exception e) { // assertTrue(e.getMessage().contains("key_image generated not matched with cached key image"), "Unexpected error: " + e.getMessage()); // } // // attempt relaying created transactions without co-signing // try { // participant.createTx(new MoneroTxConfig().setAddress(returnAddress).setAmount(TestUtils.MAX_FEE).setAccountIndex(accountIdx).setSubaddressIndex(0).setRelay(true)); // throw new RuntimeException("Should have failed"); // } catch (Exception e) { // assertTrue(e instanceof MoneroError); // assertEquals("Cannot relay multisig transaction until co-signed", e.getMessage()); // } } void test_multisig_stress(wownero_wallet* funding_wallet, string wallet_name = "") { // test config int M = 2; int N = 2; int account_idx = 0; int num_addresses_to_fund = 5; int max_txs = 200; //wallet_name = "multisig_stress_0d7c76d1-69b9-46b7-b0e0-5128bd5c725a"; if (wallet_name.empty()) wallet_name = "multisig_stress_" + gen_utils::get_uuid(); std::string wallet_path = wallet_name; std::cout << "Stress testing multisig wallets: " << wallet_path << std::endl; // open or create multisig wallets vector participants; try { for (int i = 0; i < N; i++) { participants.push_back(wownero_wallet_full::open_wallet(wallet_path + string("_") + std::to_string(i), "", NETWORK_TYPE)); } for (int i = 0; i < participants.size(); i++) { participants[i]->set_daemon_connection(DAEMON_URI); participants[i]->sync(); participants[i]->start_syncing(5000); } } catch (const exception& e) { for (int i = 0; i < N; i++) { wownero_wallet_config wallet_config; wallet_config.m_path = wallet_path + string("_") + std::to_string(i); wallet_config.m_network_type = wownero_network_type::TESTNET; wallet_config.m_server = wownero_rpc_connection(DAEMON_URI); participants.push_back(wownero_wallet_full::create_wallet(wallet_config)); } for (int i = 0; i < participants.size(); i++) { participants[i]->sync(); participants[i]->start_syncing(5000); } init_multisig_wallets(funding_wallet, participants, M, N, account_idx, num_addresses_to_fund); } wownero_wallet* participant = participants[0]; // start mining funding_wallet->start_mining(1, false, false); // receive notifications when the wallet receives funds struct : wownero_wallet_listener { void on_output_received(const wownero_output_wallet& output) { cout << "Wallet received funds!" << endl; uint64_t amount = output.m_amount.get(); string tx_hash = output.m_tx->m_hash.get(); bool is_confirmed = output.m_tx->m_is_confirmed.get(); bool is_locked = static_pointer_cast(output.m_tx)->m_is_locked.get(); int account_index = output.m_account_index.get(); int subaddress_index = output.m_subaddress_index.get(); } } my_listener; participant->add_listener(my_listener); // cycle funds int num_txs = 0; while (num_txs < max_txs) { try { uint64_t unlocked_balance = participant->get_unlocked_balance(0); while (unlocked_balance > 0) { // synchronize the multisig participants since spending outputs std::cout << "Synchronizing participants" << std::endl; synchronize_multisig_participants(participants); // create txs to cycle funds wownero_tx_config tx_config; tx_config.m_account_index = account_idx; for (int i = 0; i < num_addresses_to_fund; i++) { shared_ptr destination = make_shared(); destination->m_address = participant->get_address(0, i); destination->m_amount = unlocked_balance / (num_addresses_to_fund * 3); tx_config.m_destinations.push_back(destination); } vector> txs = participant->create_txs(tx_config); if (txs.empty()) throw runtime_error("txs is empty"); if ((*txs[0]->m_tx_set.get()->m_multisig_tx_hex).empty()) throw runtime_error("multisig_tx_hex is empty"); //assertNull(txSet.getSignedTxHex()); //assertNull(txSet.getUnsignedTxHex()); // describe multisig tx hex and test wownero_tx_set tx_set = participant->describe_tx_set(*txs[0]->m_tx_set.get()); wownero_utils::free(tx_set.m_txs); // sign the tx with participants 1 through m - 1 to meet threshold string multisig_tx_hex = txs[0]->m_tx_set.get()->m_multisig_tx_hex.get(); wownero_utils::free(txs); std::cout << "Signing" << std::endl; for (int j = 1; j < M; j++) { wownero_multisig_sign_result result = participants[j]->sign_multisig_tx_hex(multisig_tx_hex); multisig_tx_hex = result.m_signed_multisig_tx_hex.get(); } // submit the signed multisig tx hex to the network std::cout << "Submitting: " << multisig_tx_hex.length() << std::endl; vector tx_hashes = participant->submit_multisig_tx_hex(multisig_tx_hex); if (tx_hashes.empty()) throw runtime_error("tx hashes is empty"); std::cout << "Tx submitted successfully!" << std::endl; // fetch the wallet's multisig txs wownero_tx_query tx_query; tx_query.m_hashes = tx_hashes; vector> multisig_txs = participant->get_txs(tx_query); if (multisig_txs.size() != tx_hashes.size()) throw runtime_error("multisigTxs.size() != txHashes.size()"); vector> all_txs = participant->get_txs(); num_txs = all_txs.size(); std::cout << "All txs size: " << all_txs.size() << std::endl; wownero_utils::free(multisig_txs); wownero_utils::free(all_txs); if (num_txs >= max_txs) break; } } catch (const exception& e) { std::cout << "There was an error: " << string(e.what()) << std::endl; } // save wallets for (int i = 0; i < participants.size(); i++) { participants[i]->save(); } // loop std::cout << "Starting another loop" << std::endl; gen_utils::wait_for(5000); } // save and close wallets for (int i = 0; i < participants.size(); i++) { participants[i]->close(true); delete participants[i]; } participant = 0; // stop mining funding_wallet->stop_mining(); } // ----------------------------------- MAIN ----------------------------------- /** * Main entry point for tests. */ int main(int argc, const char* argv[]) { // configure logging //mlog_configure("log_cpp_scratchpad.txt", true); //mlog_set_log_level(LOG_LEVEL); //MINFO("logging info!!!"); //MWARNING("logging a warning!!!"); //MERROR("logging an error!!!"); // print header MINFO("===== Tests ====="); for (int i = 0; i < argc; i++) { MINFO("Argument" << i << ": " << argv[i]); } string path = "test_wallet_1"; string password = "supersecretpassword123"; string language = "English"; int network_type = 2; // load test wallet wownero_wallet* wallet = wownero_wallet_full::open_wallet("../../test_wallets/test_wallet_1", "supersecretpassword123", wownero_network_type::TESTNET); wallet->set_daemon_connection(DAEMON_URI); wallet->sync(); wallet->start_syncing(5000); // get txs vector> txs = wallet->get_txs(); MINFO("Wallet has " << txs.size() << " txs"); for (int i = 0; i < txs.size() && i < 10; i++) MINFO(txs[i]->serialize()); wownero_utils::free(txs); // get transfers vector> transfers = wallet->get_transfers(wownero_transfer_query()); MINFO("Wallet has " << transfers.size() << " transfers"); wownero_utils::free(transfers); // get outputs vector> outputs = wallet->get_outputs(wownero_output_query()); MINFO("Wallet has " << outputs.size() << " outputs"); wownero_utils::free(outputs); // multisig stress test test_multisig_stress(wallet); // close wallet and free pointer wallet->close(true); delete wallet; }