From 24a39aa0d7031eb67f7c0c54a4a8e68af6c0ae5d Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 28 Aug 2023 10:05:22 -0400 Subject: [PATCH] add monero_tests.cpp with multisig stress test --- CMakeLists.txt | 52 +++++ src/utils/gen_utils.h | 25 +++ test/monero_tests.cpp | 448 ++++++++++++++++++++++++++++++++++++++++++ test/scratchpad.cpp | 2 +- 4 files changed, 526 insertions(+), 1 deletion(-) create mode 100644 test/monero_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5355ac2..7270f47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ project(MoneroCppLibrary) set(BUILD_LIBRARY ON) set(BUILD_SAMPLE ON) set(BUILD_SCRATCHPAD ON) +set(BUILD_TESTS ON) ################### # monero-project @@ -457,3 +458,54 @@ if (BUILD_SCRATCHPAD) target_link_libraries(scratchpad dl) endif() endif() + +######################## +# Build C++ tests +######################## + +if (BUILD_TESTS) + set(TEST_SRC_FILES test/monero_tests.cpp) + + add_executable(monero_tests ${LIBRARY_SRC_FILES} ${TEST_SRC_FILES}) + + target_link_libraries(monero_tests + wallet + rpc_base + net + lmdb + easylogging + cryptonote_core + cryptonote_protocol + cryptonote_basic + cryptonote_format_utils_basic + mnemonics + ringct + ringct_basic + common + cncrypto + blockchain_db + blocks + checkpoints + device + device_trezor + multisig + version + randomx + epee + hardforks + cncrypto + ${wallet_crypto} + + ${UNBOUND_LIBRARIES} + ${Boost_LIBRARIES} + ${Protobuf_LIBRARY} + ${LibUSB_LIBRARIES} + ${OPENSSL_LIBRARIES} + ${SODIUM_LIBRARY} + ${HIDAPI_LIBRARIES} + ${EXTRA_LIBRARIES} + ) + if (NOT WIN32) + target_link_libraries(monero_tests dl) + endif() +endif() diff --git a/src/utils/gen_utils.h b/src/utils/gen_utils.h index 96bc817..226e35a 100644 --- a/src/utils/gen_utils.h +++ b/src/utils/gen_utils.h @@ -55,6 +55,10 @@ #define gen_utils_h #include +#include +#include +#include +#include #include "include_base_utils.h" #include "common/util.h" @@ -63,6 +67,27 @@ */ namespace gen_utils { + + /** + * Return a unique identifier. + * + * @return a unique id + */ + static std::string get_uuid() { + boost::uuids::random_generator generator; + boost::uuids::uuid uuid = generator(); + return boost::uuids::to_string(uuid); + } + + /** + * Wait for the given duration. + * + * @param duration_ms the duration to wait in milliseconds + */ + static void wait_for(uint64_t duration_ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(duration_ms)); + } + // ------------------------- VALUE RECONCILATION ---------------------------- // TODO: refactor common template code diff --git a/test/monero_tests.cpp b/test/monero_tests.cpp new file mode 100644 index 0000000..407cf1a --- /dev/null +++ b/test/monero_tests.cpp @@ -0,0 +1,448 @@ +#include +#include +#include "wallet2.h" +#include "wallet/monero_wallet_full.h" +#include "utils/gen_utils.h" +#include "utils/monero_utils.h" +//#include "monero_wallet_model.h" + +using namespace std; + +// ----------------------------- TEST CONFIG ---------------------------------- + +// constants +const int LOG_LEVEL = 2; +const string DAEMON_URI = "http://localhost:28081"; +const monero_network_type NETWORK_TYPE = monero_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++) { + monero_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]); + monero_wallet* wallet = wallets[i]; + wallet->sync(); // TODO monero-project: creating multisig tx fails if wallet not explicitly synced before import_multisig_hex: https://github.com/monero-project/monero/issues/6850 + wallet->import_multisig_hex(peer_multisig_hexes); + } +} + +void init_multisig_wallets(monero_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++) { + monero_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++) { + monero_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 (monero-project): improve error message https://github.com/monero-project/monero/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++) { + monero_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 + monero_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 + monero_wallet* participant = participants[0]; + monero_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; + monero_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); + monero_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 + monero_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()) { + monero_utils::free(outputs); + break; + } + } + monero_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"); + } + monero_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?"; + monero_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(monero_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++) { + monero_wallet_config wallet_config; + wallet_config.m_path = wallet_path + string("_") + std::to_string(i); + wallet_config.m_server_uri = DAEMON_URI; + participants.push_back(monero_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++) { + monero_wallet_config wallet_config; + wallet_config.m_path = wallet_path + string("_") + std::to_string(i); + wallet_config.m_network_type = monero_network_type::TESTNET; + wallet_config.m_server_uri = DAEMON_URI; + participants.push_back(monero_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); + } + monero_wallet* participant = participants[0]; + + // start mining + funding_wallet->start_mining(1, false, false); + + // receive notifications when the wallet receives funds + struct : monero_wallet_listener { + void on_output_received(const monero_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 + monero_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 + monero_tx_set tx_set = participant->describe_tx_set(*txs[0]->m_tx_set.get()); + monero_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(); + monero_utils::free(txs); + std::cout << "Signing" << std::endl; + for (int j = 1; j < M; j++) { + monero_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 + monero_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; + monero_utils::free(multisig_txs); + monero_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 + monero_wallet* wallet = monero_wallet_full::open_wallet("../../test_wallets/test_wallet_1", "supersecretpassword123", monero_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()); + monero_utils::free(txs); + + // get transfers + vector> transfers = wallet->get_transfers(monero_transfer_query()); + MINFO("Wallet has " << transfers.size() << " transfers"); + monero_utils::free(transfers); + + // get outputs + vector> outputs = wallet->get_outputs(monero_output_query()); + MINFO("Wallet has " << outputs.size() << " outputs"); + monero_utils::free(outputs); + + // multisig stress test + test_multisig_stress(wallet); + + // close wallet and free pointer + wallet->close(true); + delete wallet; +} \ No newline at end of file diff --git a/test/scratchpad.cpp b/test/scratchpad.cpp index d172c46..c005ae3 100644 --- a/test/scratchpad.cpp +++ b/test/scratchpad.cpp @@ -7,7 +7,7 @@ using namespace std; /** - * Scratchpad main entry point. + * Main entry point for scratchpad. */ int main(int argc, const char* argv[]) {