From e477c174e2797005be6c472b11cfe862da301f1d Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Mon, 22 Jan 2024 14:17:20 -0500 Subject: [PATCH] Add unit tests for chain syncing (#87) --- src/CMakeLists.txt | 23 +- src/rpc/CMakeLists.txt | 2 +- src/rpc/client.cpp | 7 + src/rpc/client.h | 3 + src/scanner.cpp | 12 +- src/scanner.h | 3 + tests/unit/CMakeLists.txt | 11 +- tests/unit/db/CMakeLists.txt | 8 +- tests/unit/db/chain.test.cpp | 117 ++++++ tests/unit/db/chain.test.h | 38 ++ tests/unit/scanner.test.cpp | 731 +++++++++++++++++++++++++++++++++++ 11 files changed, 939 insertions(+), 16 deletions(-) create mode 100644 tests/unit/db/chain.test.cpp create mode 100644 tests/unit/db/chain.test.h create mode 100644 tests/unit/scanner.test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6030128..81436eb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,28 +40,35 @@ set(monero-lws-common_headers config.h error.h fwd.h) add_library(monero-lws-common ${monero-lws-common_sources} ${monero-lws-common_headers}) target_link_libraries(monero-lws-common monero::libraries) - -add_executable(monero-lws-daemon server_main.cpp rest_server.cpp scanner.cpp) -target_include_directories(monero-lws-daemon PUBLIC ${ZMQ_INCLUDE_PATH}) -target_link_libraries(monero-lws-daemon - PRIVATE +add_library(monero-lws-daemon-common rest_server.cpp scanner.cpp) +target_include_directories(monero-lws-daemon-common PUBLIC ${ZMQ_INCLUDE_PATH}) +target_link_libraries(monero-lws-daemon-common + PUBLIC monero::libraries ${MONERO_lmdb} monero-lws-common monero-lws-db monero-lws-rpc - monero-lws-util monero-lws-wire-json + monero-lws-util ${Boost_CHRONO_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} - ${Boost_FILESYSTEM_LIBRARY} ${Boost_THREAD_LIBRARY} - ${CMAKE_THREAD_LIBS_INIT} + ${Boost_THREAD_LIBS_INIT} ${EXTRA_LIBRARIES} ${ZMQ_LIB} Threads::Threads ) +add_executable(monero-lws-daemon server_main.cpp) +target_link_libraries(monero-lws-daemon + PRIVATE + monero::libraries + monero-lws-daemon-common + ${Boost_PROGRAM_OPTIONS_LIBRARY} + ${Boost_FILESYSTEM_LIBRARY} +) + add_executable(monero-lws-admin admin_main.cpp) target_link_libraries(monero-lws-admin PRIVATE diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt index 538363a..6b28d11 100644 --- a/src/rpc/CMakeLists.txt +++ b/src/rpc/CMakeLists.txt @@ -31,4 +31,4 @@ set(monero-lws-rpc_headers admin.h client.h daemon_pub.h daemon_zmq.h fwd.h json add_library(monero-lws-rpc ${monero-lws-rpc_sources} ${monero-lws-rpc_headers}) target_include_directories(monero-lws-rpc PRIVATE ${RMQ_INCLUDE_DIR}) -target_link_libraries(monero-lws-rpc monero::libraries monero-lws-wire-json monero-lws-wire-wrapper ${RMQ_LIBRARY}) +target_link_libraries(monero-lws-rpc monero::libraries monero-lws-util monero-lws-wire-json monero-lws-wire-wrapper ${RMQ_LIBRARY}) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index db438a9..761317e 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -513,6 +513,13 @@ namespace rpc raise_abort_process(); } + void* context::zmq_context() const + { + if (ctx == nullptr) + return nullptr; + return ctx->comm.get(); + } + std::string const& context::daemon_address() const { if (ctx == nullptr) diff --git a/src/rpc/client.h b/src/rpc/client.h index 8574e21..8735c62 100644 --- a/src/rpc/client.h +++ b/src/rpc/client.h @@ -218,6 +218,9 @@ namespace rpc // Do not create clone method, only one of these should exist right now. + // \return zmq context pointer (for testing). + void* zmq_context() const; + //! \return The full address of the monerod ZMQ daemon. std::string const& daemon_address() const; diff --git a/src/scanner.cpp b/src/scanner.cpp index 0d7a88c..8aa7fe8 100644 --- a/src/scanner.cpp +++ b/src/scanner.cpp @@ -416,15 +416,22 @@ namespace lws bool found_pub = false; db::address_index account_index{db::major_index::primary, db::minor_index::primary}; - crypto::key_derivation active_derived; + crypto::key_derivation active_derived{}; + crypto::public_key active_pub{}; // inspect the additional and traditional keys for (std::size_t attempt = 0; attempt < 2; ++attempt) { if (attempt == 0) + { active_derived = derived; + active_pub = key.pub_key; + } else if (!additional_derivations.empty()) + { active_derived = additional_derivations.at(index); + active_pub = additional_tx_pub_keys.data.at(index); + } else break; // inspection loop @@ -491,7 +498,6 @@ namespace lws lws::decrypt_payment_id(payment_id.second.short_, active_derived); } } - const bool added = output_action( user, db::output{ @@ -501,7 +507,7 @@ namespace lws amount, mixin, boost::numeric_cast(index), - key.pub_key + active_pub }, timestamp, tx.unlock_time, diff --git a/src/scanner.h b/src/scanner.h index bccfeb0..d7d8db3 100644 --- a/src/scanner.h +++ b/src/scanner.h @@ -56,5 +56,8 @@ namespace lws //! Stops all scanner instances globally. static void stop() noexcept { running = false; } + + //! For testing, \post is_running() == true + static void reset() noexcept { running = true; } }; } // lws diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 6b901fb..e1463d0 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -34,15 +34,20 @@ add_subdirectory(db) add_subdirectory(rpc) add_subdirectory(wire) -add_executable(monero-lws-unit main.cpp) -target_link_libraries( - monero-lws-unit +add_executable(monero-lws-unit main.cpp scanner.test.cpp) +target_link_libraries(monero-lws-unit + monero::libraries + monero-lws-daemon-common monero-lws-unit-db monero-lws-unit-framework monero-lws-unit-rpc monero-lws-unit-wire monero-lws-unit-wire-json monero-lws-unit-wire-msgpack + ${Boost_FILESYSTEM_LIBRARY} + ${Boost_PROGRAM_OPTIONS_LIBRARY} + ${Boost_THREAD_LIBRARY} + ${Boost_THREAD_LIBS_INIT} Threads::Threads ) add_test(NAME monero-lws-unit COMMAND monero-lws-unit -v) diff --git a/tests/unit/db/CMakeLists.txt b/tests/unit/db/CMakeLists.txt index 1d42eca..f082f55 100644 --- a/tests/unit/db/CMakeLists.txt +++ b/tests/unit/db/CMakeLists.txt @@ -26,7 +26,13 @@ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -add_library(monero-lws-unit-db OBJECT data.test.cpp storage.test.cpp subaddress.test.cpp webhook.test.cpp) +add_library(monero-lws-unit-db OBJECT + chain.test.cpp + data.test.cpp + storage.test.cpp + subaddress.test.cpp + webhook.test.cpp +) target_link_libraries( monero-lws-unit-db monero-lws-unit-framework diff --git a/tests/unit/db/chain.test.cpp b/tests/unit/db/chain.test.cpp new file mode 100644 index 0000000..54e0cbc --- /dev/null +++ b/tests/unit/db/chain.test.cpp @@ -0,0 +1,117 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "chain.test.h" + +#include +#include "db/storage.test.h" +#include "error.h" + +namespace lws_test +{ + void test_chain(lest::env& lest_env, lws::db::storage_reader reader, lws::db::block_id id, epee::span snapshot) + { + EXPECT(1 <= snapshot.size()); + + std::uint64_t d = std::uint64_t(id); + for (const auto& hash : snapshot) + { + SETUP("Testing Block #: " + std::to_string(d)) + { + EXPECT(MONERO_UNWRAP(reader.get_block_hash(lws::db::block_id(d))) == hash); + ++d; + } + } + + const lws::db::block_info last_block = + MONERO_UNWRAP(reader.get_last_block()); + EXPECT(last_block.id == lws::db::block_id(d - 1)); + EXPECT(last_block.hash == snapshot[snapshot.size() - 1]); + } +} + +LWS_CASE("db::storage::sync_chain") +{ + lws::db::account_address account{}; + crypto::secret_key view{}; + crypto::generate_keys(account.spend_public, view); + crypto::generate_keys(account.view_public, view); + + SETUP("Appended Chain") + { + lws::db::test::cleanup_db on_scope_exit{}; + lws::db::storage db = lws::db::test::get_fresh_db(); + const lws::db::block_info last_block = + MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_last_block()); + + const auto get_account = [&db, &account] () -> lws::db::account + { + return MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_account(account)).second; + }; + + const crypto::hash chain[5] = { + last_block.hash, + crypto::rand(), + crypto::rand(), + crypto::rand(), + crypto::rand() + }; + + EXPECT(db.add_account(account, view)); + EXPECT(db.sync_chain(lws::db::block_id(0), chain) == lws::error::bad_blockchain); + EXPECT(db.sync_chain(last_block.id, {chain + 1, 4}) == lws::error::bad_blockchain); + EXPECT(db.sync_chain(last_block.id, chain)); + + { + const lws::account accounts[1] = {lws::account{get_account(), {}, {}}}; + EXPECT(accounts[0].scan_height() == last_block.id); + EXPECT(db.update(last_block.id, chain, accounts)); + EXPECT(get_account().scan_height == lws::db::block_id(std::uint64_t(last_block.id) + 4)); + } + + SECTION("Verify Append") + { + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, chain); + } + + SECTION("Fork Chain") + { + const crypto::hash fchain[5] = { + chain[0], + chain[1], + crypto::rand(), + crypto::rand(), + crypto::rand() + }; + + EXPECT(db.sync_chain(last_block.id, fchain)); + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, fchain); + EXPECT(get_account().scan_height == lws::db::block_id(std::uint64_t(last_block.id) + 1)); + } + } +} + diff --git a/tests/unit/db/chain.test.h b/tests/unit/db/chain.test.h new file mode 100644 index 0000000..0bac5c0 --- /dev/null +++ b/tests/unit/db/chain.test.h @@ -0,0 +1,38 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "framework.test.h" + +#include "crypto/crypto.h" // monero/src +#include "db/data.h" +#include "db/storage.h" +#include "span.h" // monero/contrib/epee/include + +namespace lws_test +{ + void test_chain(lest::env& lest_env, lws::db::storage_reader reader, lws::db::block_id id, epee::span snapshot); +} diff --git a/tests/unit/scanner.test.cpp b/tests/unit/scanner.test.cpp new file mode 100644 index 0000000..c2d239a --- /dev/null +++ b/tests/unit/scanner.test.cpp @@ -0,0 +1,731 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "framework.test.h" + +#include +#include + +#include "cryptonote_basic/account.h" // monero/src +#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src +#include "cryptonote_config.h" // monero/src +#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src +#include "db/chain.test.h" +#include "db/storage.test.h" +#include "device/device_default.hpp" // monero/src +#include "hardforks/hardforks.h" // monero/src +#include "net/zmq.h" // monero/src +#include "rpc/client.h" +#include "rpc/daemon_messages.h" // monero/src +#include "scanner.h" +#include "wire/error.h" +#include "wire/json/write.h" + +namespace +{ + constexpr const char rendevous[] = "inproc://fake_daemon"; + constexpr const std::chrono::seconds message_timeout{3}; + + template + struct json_rpc_response + { + T result; + std::uint32_t id = 0; + }; + + template + void write_bytes(wire::json_writer& dest, const json_rpc_response& self) + { + wire::object(dest, WIRE_FIELD(id), WIRE_FIELD(result)); + } + + template + epee::byte_slice to_json_rpc(T message) + { + epee::byte_slice out{}; + const std::error_code err = + wire::json::to_bytes(out, json_rpc_response{std::move(message)}); + if (err) + MONERO_THROW(err, "Failed to serialize json_rpc_response"); + return out; + } + + template + epee::byte_slice daemon_response(const T& message) + { + rapidjson::Value id; + id.SetInt(0); + return cryptonote::rpc::FullMessage::getResponse(message, id); + } + + void rpc_thread(void* ctx, const std::vector& reply) + { + struct stop_ + { + ~stop_() noexcept { lws::scanner::stop(); }; + } stop{}; + + try + { + net::zmq::socket server{}; + server.reset(zmq_socket(ctx, ZMQ_REP)); + if (!server || zmq_bind(server.get(), rendevous)) + { + std::cout << "Failed to create ZMQ server" << std::endl; + return; + } + + for (const epee::byte_slice& message : reply) + { + const auto start = std::chrono::steady_clock::now(); + for (;;) + { + const auto request = net::zmq::receive(server.get(), ZMQ_DONTWAIT); + if (request) + break; + + if (request != net::zmq::make_error_code(EAGAIN)) + { + std::cout << "Failed to retrieve message in fake ZMQ server: " << request.error().message() << std::endl;; + return; + } + + if (message_timeout <= std::chrono::steady_clock::now() - start) + { + std::cout << "Timeout in dummy RPC server" << std::endl; + return; + } + boost::this_thread::sleep_for(boost::chrono::milliseconds{10}); + } // until error or received message + + const auto sent = net::zmq::send(message.clone(), server.get()); + if (!sent) + { + std::cout << "Failed to send dummy RPC message: " << sent.error().message() << std::endl; + return; + } + } // foreach message + } + catch (const std::exception& e) + { + std::cout << "Unexpected exception in dummy RPC server: " << e.what() << std::endl; + } + } + + struct join + { + boost::thread& thread; + ~join() { thread.join(); } + }; + + struct transaction + { + cryptonote::transaction tx; + std::vector additional_keys; + std::vector pub_keys; + std::vector spend_publics; + }; + + transaction make_miner_tx(lest::env& lest_env, lws::db::block_id height, const lws::db::account_address& miner_address, bool use_view_tags) + { + static constexpr std::uint64_t fee = 0; + + transaction tx{}; + tx.pub_keys.emplace_back(); + tx.spend_publics.emplace_back(); + + crypto::secret_key key; + crypto::generate_keys(tx.pub_keys.back(), key); + EXPECT(add_tx_pub_key_to_extra(tx.tx, tx.pub_keys.back())); + + cryptonote::txin_gen in; + in.height = std::uint64_t(height); + tx.tx.vin.push_back(in); + + // This will work, until size of constructed block is less then CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE + uint64_t block_reward; + EXPECT(cryptonote::get_block_reward(0, 0, 1000000, block_reward, num_testnet_hard_forks)); + block_reward += fee; + + crypto::key_derivation derivation; + EXPECT(crypto::generate_key_derivation(miner_address.view_public, key, derivation)); + EXPECT(crypto::derive_public_key(derivation, 0, miner_address.spend_public, tx.spend_publics.back())); + + crypto::view_tag view_tag; + if (use_view_tags) + crypto::derive_view_tag(derivation, 0, view_tag); + + cryptonote::tx_out out; + cryptonote::set_tx_out(block_reward, tx.spend_publics.back(), use_view_tags, view_tag, out); + + tx.tx.vout.push_back(out); + tx.tx.version = 2; + tx.tx.unlock_time = std::uint64_t(height) + CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW; + + return tx; + } + + struct get_spend_public + { + std::vector& pub_keys; + + template + void operator()(const T&) const noexcept + {} + + void operator()(const cryptonote::txout_to_key& val) const + { pub_keys.push_back(val.key); } + + void operator()(const cryptonote::txout_to_tagged_key& val) const + { pub_keys.push_back(val.key); } + }; + + transaction make_tx(lest::env& lest_env, const cryptonote::account_keys& keys, std::vector& destinations, const std::uint32_t ring_base, const bool use_view_tag) + { + static constexpr std::uint64_t input_amount = 20000; + static constexpr std::uint64_t output_amount = 8000; + + EXPECT(15 < std::numeric_limits::max() - ring_base); + + crypto::secret_key unused_key{}; + crypto::secret_key og_tx_key{}; + crypto::public_key og_tx_public{}; + crypto::generate_keys(og_tx_public, og_tx_key); + + crypto::key_derivation derivation{}; + crypto::public_key spend_public{}; + EXPECT(crypto::generate_key_derivation(keys.m_account_address.m_view_public_key, og_tx_key, derivation)); + EXPECT(crypto::derive_public_key(derivation, 0, keys.m_account_address.m_spend_public_key, spend_public)); + + std::uint32_t index = -1; + std::unordered_map subaddresses; + for (const auto& destination : destinations) + { + ++index; + subaddresses[destination.addr.m_spend_public_key] = {0, index}; + } + + std::vector sources; + sources.emplace_back(); + sources.back().amount = input_amount; + sources.back().rct = true; + sources.back().real_output = 15; + sources.back().real_output_in_tx_index = 0; + sources.back().real_out_tx_key = og_tx_public; + for (std::uint32_t i = ring_base; i < 15 + ring_base; ++i) + { + crypto::public_key next{}; + crypto::generate_keys(next, unused_key); + sources.back().push_output(i, next, 10000); + } + sources.back().outputs.emplace_back(); + sources.back().outputs.back().first = 15 + ring_base; + sources.back().outputs.back().second.dest = rct::pk2rct(spend_public); + + transaction out{}; + EXPECT( + cryptonote::construct_tx_and_get_tx_key( + keys, subaddresses, sources, destinations, keys.m_account_address, {}, out.tx, 0, unused_key, + out.additional_keys, true, {rct::RangeProofType::RangeProofBulletproof, 2}, use_view_tag + ) + ); + + for (const auto& vout : out.tx.vout) + boost::apply_visitor(get_spend_public{out.spend_publics}, vout.target); + + if (out.additional_keys.empty()) + { + std::vector extra; + EXPECT(cryptonote::parse_tx_extra(out.tx.extra, extra)); + + cryptonote::tx_extra_pub_key key; + EXPECT(cryptonote::find_tx_extra_field_by_type(extra, key)); + + out.pub_keys.emplace_back(); + out.pub_keys.back() = key.pub_key; + } + else + { + for (const auto& this_key : out.additional_keys) + { + out.pub_keys.emplace_back(); + EXPECT(crypto::secret_key_to_public_key(this_key, out.pub_keys.back())); + } + } + return out; + } +} + +LWS_CASE("lws::scanner::sync and lws::scanner::run") +{ + mlog_set_log_level(4); + + cryptonote::account_keys keys{}; + crypto::generate_keys(keys.m_account_address.m_spend_public_key, keys.m_spend_secret_key); + crypto::generate_keys(keys.m_account_address.m_view_public_key, keys.m_view_secret_key); + + const lws::db::account_address account{ + keys.m_account_address.m_view_public_key, + keys.m_account_address.m_spend_public_key + }; + + cryptonote::account_keys keys_subaddr1{}; + cryptonote::account_keys keys_subaddr2{}; + { + hw::core::device_default hw{}; + keys_subaddr1.m_account_address = hw.get_subaddress(keys, cryptonote::subaddress_index{0, 1}); + keys_subaddr2.m_account_address = hw.get_subaddress(keys, cryptonote::subaddress_index{0, 2}); + + const auto sub1_secret = hw.get_subaddress_secret_key(keys.m_view_secret_key, cryptonote::subaddress_index{0, 1}); + const auto sub2_secret = hw.get_subaddress_secret_key(keys.m_view_secret_key, cryptonote::subaddress_index{0, 1}); + + sc_add(to_bytes(keys_subaddr1.m_spend_secret_key), to_bytes(sub1_secret), to_bytes(keys.m_spend_secret_key)); + sc_add(to_bytes(keys_subaddr1.m_view_secret_key), to_bytes(keys_subaddr1.m_spend_secret_key), to_bytes(keys.m_view_secret_key)); + + sc_add(to_bytes(keys_subaddr2.m_spend_secret_key), to_bytes(sub2_secret), to_bytes(keys.m_spend_secret_key)); + sc_add(to_bytes(keys_subaddr2.m_view_secret_key), to_bytes(keys_subaddr2.m_spend_secret_key), to_bytes(keys.m_view_secret_key)); + } + + cryptonote::account_keys keys2{}; + crypto::generate_keys(keys2.m_account_address.m_spend_public_key, keys2.m_spend_secret_key); + crypto::generate_keys(keys2.m_account_address.m_view_public_key, keys2.m_view_secret_key); + + const lws::db::account_address account2{ + keys2.m_account_address.m_view_public_key, + keys2.m_account_address.m_spend_public_key + }; + + SETUP("lws::rpc::context, ZMQ_REP Server, and lws::db::storage") + { + lws::scanner::reset(); + auto rpc = + lws::rpc::context::make(rendevous, {}, {}, {}, std::chrono::minutes{0}); + + + lws::db::test::cleanup_db on_scope_exit{}; + lws::db::storage db = lws::db::test::get_fresh_db(); + const lws::db::block_info last_block = + MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_last_block()); + + const auto get_account = [&db, &account] () -> lws::db::account + { + return MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_account(account)).second; + }; + + SECTION("lws::scanner::sync Invalid Response") + { + const crypto::hash hashes[1] = { + last_block.hash + }; + + std::vector messages{}; + messages.push_back(to_json_rpc(1)); + + boost::thread server_thread(&rpc_thread, rpc.zmq_context(), std::cref(messages)); + const join on_scope_exit{server_thread}; + EXPECT(!lws::scanner::sync(db.clone(), MONERO_UNWRAP(rpc.connect()))); + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, hashes); + } + + SECTION("lws::scanner::sync Update") + { + std::vector messages{}; + std::vector hashes{ + last_block.hash, + crypto::rand(), + crypto::rand(), + crypto::rand(), + crypto::rand(), + crypto::rand() + }; + + cryptonote::rpc::GetHashesFast::Response message{}; + + message.start_height = std::uint64_t(last_block.id); + message.hashes = hashes; + message.current_height = message.start_height + hashes.size() - 1; + messages.push_back(daemon_response(message)); + + message.start_height = message.current_height; + message.hashes.front() = message.hashes.back(); + message.hashes.resize(1); + messages.push_back(daemon_response(message)); + + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, {hashes.data(), 1}); + { + boost::thread server_thread(&rpc_thread, rpc.zmq_context(), std::cref(messages)); + const join on_scope_exit{server_thread}; + EXPECT(lws::scanner::sync(db.clone(), MONERO_UNWRAP(rpc.connect()))); + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, epee::to_span(hashes)); + } + + SECTION("Fork Chain") + { + messages.clear(); + hashes[2] = crypto::rand(); + hashes[3] = crypto::rand(); + hashes[4] = crypto::rand(); + hashes[5] = crypto::rand(); + + message.start_height = std::uint64_t(last_block.id); + message.hashes = hashes; + messages.push_back(daemon_response(message)); + + message.start_height = message.current_height; + message.hashes.front() = message.hashes.back(); + message.hashes.resize(1); + messages.push_back(daemon_response(message)); + + boost::thread server_thread(&rpc_thread, rpc.zmq_context(), std::cref(messages)); + const join on_scope_exit{server_thread}; + EXPECT(lws::scanner::sync(db.clone(), MONERO_UNWRAP(rpc.connect()))); + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, epee::to_span(hashes)); + } + } + + SECTION("lws::scanner::run") + { + { + const std::vector indexes{ + lws::db::subaddress_dict{ + lws::db::major_index::primary, + lws::db::index_ranges{ + lws::db::index_range{lws::db::minor_index(1), lws::db::minor_index(2)} + } + } + }; + const auto result = + db.upsert_subaddresses(lws::db::account_id(1), account, keys.m_view_secret_key, indexes, 2); + EXPECT(result); + EXPECT(result->size() == 1); + EXPECT(result->at(0).first == lws::db::major_index::primary); + EXPECT(result->at(0).second.size() == 1); + EXPECT(result->at(0).second.at(0).size() == 2); + EXPECT(result->at(0).second.at(0).at(0) == lws::db::minor_index(1)); + EXPECT(result->at(0).second.at(0).at(1) == lws::db::minor_index(2)); + } + + std::vector destinations; + destinations.emplace_back(); + destinations.back().amount = 8000; + destinations.back().addr = keys.m_account_address; + + std::vector messages{}; + transaction tx = make_miner_tx(lest_env, last_block.id, account, false); + EXPECT(tx.pub_keys.size() == 1); + EXPECT(tx.spend_publics.size() == 1); + + transaction tx2 = make_tx(lest_env, keys, destinations, 20, true); + EXPECT(tx2.pub_keys.size() == 1); + EXPECT(tx2.spend_publics.size() == 1); + + transaction tx3 = make_tx(lest_env, keys, destinations, 86, false); + EXPECT(tx3.pub_keys.size() == 1); + EXPECT(tx3.spend_publics.size() == 1); + + destinations.emplace_back(); + destinations.back().amount = 2000; + destinations.back().addr = keys_subaddr1.m_account_address; + destinations.back().is_subaddress = true; + + transaction tx4 = make_tx(lest_env, keys, destinations, 50, false); + EXPECT(tx4.pub_keys.size() == 1); + EXPECT(tx4.spend_publics.size() == 2); + + //destinations.emplace_back(); + //destinations.back().amount = 1000; + //destinations.back().addr = keys_subaddr2.m_account_address; + //destinations.back().is_subaddress = true; + + //transaction tx5 = make_tx(lest_env, keys, destinations, 100, true); + //EXPECT(tx5.pub_keys.size() == 3); + //EXPECT(tx5.spend_publics.size() == 3); + + cryptonote::rpc::GetBlocksFast::Response bmessage{}; + bmessage.start_height = std::uint64_t(last_block.id) + 1; + bmessage.current_height = bmessage.start_height + 1; + bmessage.blocks.emplace_back(); + bmessage.blocks.back().block.miner_tx = tx.tx; + bmessage.blocks.back().block.tx_hashes.push_back(cryptonote::get_transaction_hash(tx2.tx)); + bmessage.blocks.back().block.tx_hashes.push_back(cryptonote::get_transaction_hash(tx3.tx)); + bmessage.blocks.back().block.tx_hashes.push_back(cryptonote::get_transaction_hash(tx4.tx)); + bmessage.blocks.back().transactions.push_back(tx2.tx); + bmessage.blocks.back().transactions.push_back(tx3.tx); + bmessage.blocks.back().transactions.push_back(tx4.tx); + bmessage.output_indices.emplace_back(); + bmessage.output_indices.back().emplace_back(); + bmessage.output_indices.back().back().push_back(100); + bmessage.output_indices.back().emplace_back(); + bmessage.output_indices.back().back().push_back(101); + bmessage.output_indices.back().emplace_back(); + bmessage.output_indices.back().back().push_back(102); + bmessage.output_indices.back().emplace_back(); + bmessage.output_indices.back().back().push_back(200); + bmessage.output_indices.back().back().push_back(201); + bmessage.blocks.push_back(bmessage.blocks.back()); + bmessage.output_indices.push_back(bmessage.output_indices.back()); + + std::vector hashes{ + last_block.hash, + cryptonote::get_block_hash(bmessage.blocks.back().block), + }; + { + cryptonote::rpc::GetHashesFast::Response hmessage{}; + + hmessage.start_height = std::uint64_t(last_block.id); + hmessage.hashes = hashes; + hmessage.current_height = hmessage.start_height + hashes.size() - 1; + messages.push_back(daemon_response(hmessage)); + + hmessage.start_height = hmessage.current_height; + hmessage.hashes.front() = hmessage.hashes.back(); + hmessage.hashes.resize(1); + messages.push_back(daemon_response(hmessage)); + + { + boost::thread server_thread(&rpc_thread, rpc.zmq_context(), std::cref(messages)); + const join on_scope_exit{server_thread}; + EXPECT(lws::scanner::sync(db.clone(), MONERO_UNWRAP(rpc.connect()))); + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, epee::to_span(hashes)); + } + } + + lws::scanner::reset(); + + EXPECT(db.add_account(account, keys.m_view_secret_key)); + EXPECT(db.add_account(account2, keys2.m_view_secret_key)); + + messages.clear(); + messages.push_back(daemon_response(bmessage)); + bmessage.start_height = bmessage.current_height; + bmessage.blocks.resize(1); + bmessage.output_indices.resize(1); + messages.push_back(daemon_response(bmessage)); + { + boost::thread server_thread(&rpc_thread, rpc.zmq_context(), std::cref(messages)); + const join on_scope_exit{server_thread}; + lws::scanner::run(db.clone(), std::move(rpc), 1, epee::net_utils::ssl_verification_t::none, true); + } + + hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block)); + lws_test::test_chain(lest_env, MONERO_UNWRAP(db.start_read()), last_block.id, epee::to_span(hashes)); + + const lws::db::block_id new_last_block_id = lws::db::block_id(std::uint64_t(last_block.id) + 2); + EXPECT(get_account().scan_height == new_last_block_id); + { + const std::map, lws::db::output> expected{ + { + {lws::db::output_id{0, 100}, 35184372088830}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 100}, 35184372088830, 0, 0, tx.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx.tx), + tx.spend_publics.at(0), + rct::commit(35184372088830, rct::identity()), + {}, + lws::db::pack(lws::db::extra::coinbase_output, 0), + {}, + 0, // fee + lws::db::address_index{} + }, + }, + { + {lws::db::output_id{0, 101}, 8000}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx2.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 101}, 8000, 15, 0, tx2.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx2.tx), + tx2.spend_publics.at(0), + tx2.tx.rct_signatures.outPk.at(0).mask, + {}, + lws::db::pack(lws::db::extra::ringct_output, 8), + {}, + 12000, // fee + lws::db::address_index{} + }, + }, + { + {lws::db::output_id{0, 102}, 8000}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx3.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 102}, 8000, 15, 0, tx3.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx3.tx), + tx3.spend_publics.at(0), + tx3.tx.rct_signatures.outPk.at(0).mask, + {}, + lws::db::pack(lws::db::extra::ringct_output, 8), + {}, + 12000, // fee + lws::db::address_index{} + }, + }, + { + {lws::db::output_id{0, 200}, 8000}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx4.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 200}, 8000, 15, 0, tx4.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx4.tx), + tx4.spend_publics.at(0), + tx4.tx.rct_signatures.outPk.at(0).mask, + {}, + lws::db::pack(lws::db::extra::ringct_output, 8), + {}, + 10000, // fee + lws::db::address_index{} + } + }, + { + {lws::db::output_id{0, 201}, 8000}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx4.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 201}, 8000, 15, 1, tx4.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx4.tx), + tx4.spend_publics.at(1), + tx4.tx.rct_signatures.outPk.at(1).mask, + {}, + lws::db::pack(lws::db::extra::ringct_output, 8), + {}, + 10000, // fee + lws::db::address_index{} + } + }, + { + {lws::db::output_id{0, 200}, 2000}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx4.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 200}, 2000, 15, 0, tx4.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx4.tx), + tx4.spend_publics.at(0), + tx4.tx.rct_signatures.outPk.at(0).mask, + {}, + lws::db::pack(lws::db::extra::ringct_output, 8), + {}, + 10000, // fee + lws::db::address_index{lws::db::major_index::primary, lws::db::minor_index(1)} + } + }, + { + {lws::db::output_id{0, 201}, 2000}, lws::db::output{ + lws::db::transaction_link{new_last_block_id, cryptonote::get_transaction_hash(tx4.tx)}, + lws::db::output::spend_meta_{ + lws::db::output_id{0, 201}, 2000, 15, 1, tx4.pub_keys.at(0) + }, + 0, + 0, + cryptonote::get_transaction_prefix_hash(tx4.tx), + tx4.spend_publics.at(1), + tx4.tx.rct_signatures.outPk.at(1).mask, + {}, + lws::db::pack(lws::db::extra::ringct_output, 8), + {}, + 10000, // fee + lws::db::address_index{lws::db::major_index::primary, lws::db::minor_index(1)} + } + } + }; + + auto reader = MONERO_UNWRAP(db.start_read()); + auto outputs = MONERO_UNWRAP(reader.get_outputs(lws::db::account_id(1))); + EXPECT(outputs.count() == 5); + auto output_it = outputs.make_iterator(); + for (auto output_it = outputs.make_iterator(); !output_it.is_end(); ++output_it) + { + auto real_output = *output_it; + const auto expected_output = + expected.find(std::make_pair(real_output.spend_meta.id, real_output.spend_meta.amount)); + EXPECT(expected_output != expected.end()); + + EXPECT(real_output.link.height == expected_output->second.link.height); + EXPECT(real_output.link.tx_hash == expected_output->second.link.tx_hash); + EXPECT(real_output.spend_meta.id == expected_output->second.spend_meta.id); + EXPECT(real_output.spend_meta.amount == expected_output->second.spend_meta.amount); + EXPECT(real_output.spend_meta.mixin_count == expected_output->second.spend_meta.mixin_count); + EXPECT(real_output.spend_meta.index == expected_output->second.spend_meta.index); + EXPECT(real_output.tx_prefix_hash == expected_output->second.tx_prefix_hash); + EXPECT(real_output.spend_meta.tx_public == expected_output->second.spend_meta.tx_public); + EXPECT(real_output.pub == expected_output->second.pub); + EXPECT(rct::commit(real_output.spend_meta.amount, real_output.ringct_mask) == expected_output->second.ringct_mask); + EXPECT(real_output.extra == expected_output->second.extra); + if (unpack(expected_output->second.extra).second == 8) + EXPECT(real_output.payment_id.short_ == expected_output->second.payment_id.short_); + EXPECT(real_output.fee == expected_output->second.fee); + EXPECT(real_output.recipient == expected_output->second.recipient); + } + + auto spends = MONERO_UNWRAP(reader.get_spends(lws::db::account_id(1))); + EXPECT(spends.count() == 2); + auto spend_it = spends.make_iterator(); + EXPECT(!spend_it.is_end()); + + auto real_spend = *spend_it; + EXPECT(real_spend.link.height == new_last_block_id); + EXPECT(real_spend.link.tx_hash == cryptonote::get_transaction_hash(tx3.tx)); + lws::db::output_id expected_out{0, 100}; + EXPECT(real_spend.source == expected_out); + EXPECT(real_spend.mixin_count == 15); + EXPECT(real_spend.length == 0); + EXPECT(real_spend.payment_id == crypto::hash{}); + EXPECT(real_spend.sender == lws::db::address_index{}); + + ++spend_it; + EXPECT(!spend_it.is_end()); + + real_spend = *spend_it; + EXPECT(real_spend.link.height == new_last_block_id); + EXPECT(real_spend.link.tx_hash == cryptonote::get_transaction_hash(tx3.tx)); + expected_out = lws::db::output_id{0, 101}; + EXPECT(real_spend.source == expected_out); + EXPECT(real_spend.mixin_count == 15); + EXPECT(real_spend.length == 0); + EXPECT(real_spend.payment_id == crypto::hash{}); + EXPECT(real_spend.sender == lws::db::address_index{}); + + EXPECT(MONERO_UNWRAP(reader.get_outputs(lws::db::account_id(2))).count() == 0); + EXPECT(MONERO_UNWRAP(reader.get_spends(lws::db::account_id(2))).count() == 0); + } + } //SECTION (lws::scanner::run) + } // SETUP +} // LWS_CASE +