Add mempool support to /get_address_txs and /feed (#251)

Co-authored-by: William Swanson <swansontec@gmail.com>
This commit is contained in:
Lee *!* Clagett
2026-05-22 23:45:46 -04:00
committed by Lee *!* Clagett
co-authored by William Swanson
parent d2d03b5c6f
commit a7aea46ebe
32 changed files with 1906 additions and 634 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ set(monero-lws-common_headers config.h error.h fwd.h)
add_library(monero-lws-common ${monero-lws-common_sources} ${monero-lws-common_headers}) add_library(monero-lws-common ${monero-lws-common_sources} ${monero-lws-common_headers})
target_link_libraries(monero-lws-common monero::libraries) target_link_libraries(monero-lws-common monero::libraries)
add_library(monero-lws-daemon-common rest_server.cpp scanner.cpp) add_library(monero-lws-daemon-common rest_server.cpp scanner.cpp mempool.cpp)
target_include_directories(monero-lws-daemon-common PUBLIC ${ZMQ_INCLUDE_PATH}) target_include_directories(monero-lws-daemon-common PUBLIC ${ZMQ_INCLUDE_PATH})
target_link_libraries(monero-lws-daemon-common target_link_libraries(monero-lws-daemon-common
PUBLIC PUBLIC
+16
View File
@@ -541,6 +541,22 @@ namespace db
); );
} }
std::size_t get_hash(account_address const& value) noexcept
{
/* use view_public - it has good distribution and cannot be selected
by user (and has been verified as such at login). */
std::size_t out;
static_assert(sizeof(out) <= sizeof(value.view_public));
static_assert(std::is_trivially_copyable<account_address>::value);
std::memcpy(std::addressof(out), std::addressof(value.view_public), sizeof(out));
return out;
}
bool operator==(account_address const& left, account_address const& right) noexcept
{
static_assert(std::is_trivially_copyable<account_address>::value);
return std::memcmp(std::addressof(left), std::addressof(right), sizeof(left)) == 0;
}
bool operator<(const webhook_dupsort& left, const webhook_dupsort& right) noexcept bool operator<(const webhook_dupsort& left, const webhook_dupsort& right) noexcept
{ {
return left.payment_id == right.payment_id ? return left.payment_id == right.payment_id ?
+17
View File
@@ -433,6 +433,13 @@ namespace db
}; };
void write_bytes(wire::writer&, const webhook_new_account&); void write_bytes(wire::writer&, const webhook_new_account&);
std::size_t get_hash(account_address const& value) noexcept;
bool operator==(account_address const& left, account_address const& right) noexcept;
inline bool operator!=(account_address const& left, account_address const& right) noexcept
{
return !(left == right);
}
inline constexpr bool operator==(address_index const& left, address_index const& right) noexcept inline constexpr bool operator==(address_index const& left, address_index const& right) noexcept
{ {
return left.maj_i == right.maj_i && left.min_i == right.min_i; return left.maj_i == right.maj_i && left.min_i == right.min_i;
@@ -508,3 +515,13 @@ namespace wire
: std::true_type : std::true_type
{}; {};
} }
namespace std
{
template<>
struct hash<lws::db::account_address>
{
std::size_t operator()(lws::db::account_address const& value) const noexcept
{ return lws::db::get_hash(value); }
};
}
+1
View File
@@ -30,6 +30,7 @@
namespace lws namespace lws
{ {
class account; class account;
class mempool;
class rest_server; class rest_server;
class scanner; class scanner;
} }
+215
View File
@@ -0,0 +1,215 @@
// Copyright (c) 2024, 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 "mempool.h"
#include <algorithm>
#include <boost/numeric/conversion/cast.hpp>
#include <boost/thread/lock_guard.hpp>
#include "common/error.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "error.h"
#include "misc_log_ex.h"
#include "rpc/daemon_messages.h" // external/monero/src
#include "rpc/daemon_zmq.h"
#include "rpc/json.h"
#include "util/ownership_test.h"
namespace lws
{
const size_t max_cache_size = 1000;
void mempool::add_txs(epee::span<cryptonote::transaction> txs)
{
const auto now = std::chrono::system_clock::now();
const boost::lock_guard<boost::mutex> lock{mutex_};
snapshot_.reset();
for (auto& tx: txs)
{
auto found = state_.try_emplace(get_transaction_hash(tx), nullptr);
if (!found.first->second)
found.first->second = std::make_shared<pool_entry>(std::move(tx), now);
}
}
void mempool::reset_txs(pool_table&& txs)
{
const boost::lock_guard<boost::mutex> lock{mutex_};
snapshot_.reset();
MDEBUG("Mempool reset from size " << state_.size() << " to " << txs.size());
state_ = std::move(txs);
}
std::unordered_set<crypto::hash> mempool::address_cache::get(const db::account_address& address) const
{
auto it = map_.find(address);
if (it == map_.end())
return {};
return {it->second.txids.begin(), it->second.txids.end()};
}
void mempool::address_cache::set(const db::account_address& address, std::vector<crypto::hash>&& txids)
{
// find or create entry for this address
auto status = map_.emplace(address, entry{});
entry& e = status.first->second;
e.txids = std::move(txids);
if (status.second)
{
// add new address to order list
order_.push_front(address);
e.order = order_.begin();
// evict old address if needed
if (map_.size() >= max_cache_size)
{
auto& evict_address = order_.back();
map_.erase(evict_address);
order_.pop_back();
}
}
else
// move existing address to front of order list
order_.splice(order_.begin(), order_, e.order);
}
std::vector<found_pool_tx> mempool::scan_account(lws::account& user) const
{
std::unordered_set<crypto::hash> skip;
std::shared_ptr<const pool_snapshot> snapshot;
{
const boost::lock_guard<boost::mutex> lock{mutex_};
skip = cache_.get(user.db_address());
if (!snapshot_)
{
// otherwise make a fresh snapshot
pool_snapshot temp;
temp.resize(state_.size());
std::copy(state_.begin(), state_.end(), temp.begin());
snapshot_ = std::make_shared<const pool_snapshot>(std::move(temp));
}
snapshot = snapshot_;
}
std::vector<found_pool_tx> found{};
ownership_test scan_transaction{
[&found](account&, db::spend&& spend)
{
if (found.empty() || found.back().hash != spend.link.tx_hash)
found.push_back({spend.link.tx_hash});
found.back().spends.push_back(std::move(spend));
},
[&found](account&, db::output&& output)
{
if (found.empty() || found.back().hash != output.link.tx_hash)
found.push_back({output.link.tx_hash});
found.back().outputs.push_back(std::move(output));
}
};
// uint64::max is for txpool
static const std::vector<std::uint64_t> fake_outs(
256, std::numeric_limits<std::uint64_t>::max()
);
std::vector<crypto::hash> skipped;
for (const auto& pair: *snapshot) {
if (!skip.count(pair.first))
scan_transaction(
epee::span<lws::account>(&user, 1),
db::block_id::txpool,
boost::numeric_cast<std::uint64_t>(std::chrono::system_clock::to_time_t(pair.second->timestamp)),
std::addressof(pair.first),
pair.second->tx,
fake_outs
);
if (found.empty() || found.back().hash != pair.first)
skipped.push_back(pair.first);
}
{
const boost::lock_guard<boost::mutex> lock{mutex_};
cache_.set(user.db_address(), std::move(skipped));
}
return found;
}
expect<void> pool_update_loop(std::shared_ptr<mempool> pool, rpc::client& client)
{
LWS_VERIFY(pool);
return client.event_loop(
[pool](std::string&& json) -> expect<void>
{
auto msg = rpc::parse_json_response<rpc::get_transaction_pool>(std::move(json));
if (!msg)
{
MERROR("Pool failed to parse block response" << msg.error());
return msg.error();
}
mempool::pool_table txs{};
txs.reserve(msg->transactions.size());
const auto now = std::chrono::system_clock::now();
for (auto& tx: msg->transactions)
{
auto shared_tx = std::make_shared<mempool::pool_entry>(std::move(tx.tx), now);
txs.emplace(tx.tx_hash, std::move(shared_tx));
}
pool->reset_txs(std::move(txs));
return {};
},
[&client](rpc::minimal_chain_pub&&) -> expect<void>
{
cryptonote::rpc::GetTransactionPool::Request req{};
expect<void> status = success();
for (unsigned i = 0; i < 2; ++i)
{
status = client.send(
rpc::client::make_message("get_transaction_pool", req), std::chrono::seconds(0)
);
if (status || status != net::zmq::make_error_code(EFSM))
return status;
MONERO_CHECK(client.daemon_reconnect());
}
return status;
},
[pool](rpc::full_txpool_pub&& msg) -> expect<void>
{
pool->add_txs(epee::to_mut_span(msg.txes));
return {};
}
);
}
} // namespace lws
+116
View File
@@ -0,0 +1,116 @@
// Copyright (c) 2024, 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.
#pragma once
#include <boost/thread/mutex.hpp>
#include <chrono>
#include <memory>
#include <unordered_map>
#include <vector>
#include "common/expect.h" // external/monero/src
#include "cryptonote_basic/cryptonote_basic.h" // external/monero/src
#include "db/data.h"
#include "db/fwd.h"
#include "rpc/client.h"
namespace lws
{
struct found_pool_tx {
crypto::hash hash;
std::vector<db::spend> spends;
std::vector<db::output> outputs;
};
/*!
Thread-safe mempool cache, designed to perform well with a single writer
and multiple concurrent readers.
The primary mutable state is guarded with a mutex, while each reader
receives an immutable snapshot of the latest state. These snapshots
use structural sharing to avoid copying the heavyweight transactions.
The two update methods are `add_tx` and `filter_txs`.
*/
class mempool
{
public:
struct pool_entry
{
pool_entry(cryptonote::transaction&& tx, const std::chrono::system_clock::time_point timestamp)
: tx(std::move(tx)), timestamp(timestamp)
{}
cryptonote::transaction tx;
std::chrono::system_clock::time_point timestamp;
};
using pool_snapshot = std::vector<std::pair<crypto::hash, std::shared_ptr<const pool_entry>>>;
using pool_table = std::unordered_map<crypto::hash, std::shared_ptr<pool_entry>>;
//! Adds transactions to the pool, if they are not already present.
// Thread-safe.
void add_txs(epee::span<cryptonote::transaction>);
//! Replaces all transactions to the pool. Thread-safe.
void reset_txs(pool_table&&);
//! Scans the pool for transactions belonging to an address. Thread-safe.
std::vector<found_pool_tx> scan_account(lws::account& user) const;
private:
//! Remembers which txid's that do *not* belong to an address,
// since there is no need for the scanner to revisit these.
class address_cache
{
public:
std::unordered_set<crypto::hash> get(const db::account_address& address) const;
void set(const db::account_address& address, std::vector<crypto::hash>&& txids);
private:
struct entry
{
std::vector<crypto::hash> txids;
std::list<db::account_address>::iterator order;
};
std::unordered_map<db::account_address, entry> map_;
std::list<db::account_address> order_;
};
pool_table state_;
mutable address_cache cache_;
mutable std::shared_ptr<const pool_snapshot> snapshot_;
mutable boost::mutex mutex_;
};
//! Run this on a thread to keep the pool in sync.
// Loops until an error or shutdown signal occurs.
expect<void> pool_update_loop(std::shared_ptr<mempool> pool, rpc::client& client);
} // namespace lws
+32 -11
View File
@@ -65,12 +65,14 @@
#include "common/expect.h" // monero/src #include "common/expect.h" // monero/src
#include "config.h" #include "config.h"
#include "crypto/crypto.h" // monero/src #include "crypto/crypto.h" // monero/src
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/srcqq
#include "cryptonote_config.h" // monero/src #include "cryptonote_config.h" // monero/src
#include "db/data.h" #include "db/data.h"
#include "db/storage.h" #include "db/storage.h"
#include "db/string.h" #include "db/string.h"
#include "error.h" #include "error.h"
#include "lmdb/util.h" // monero/src #include "lmdb/util.h" // monero/src
#include "mempool.h"
#include "net/http/client.h" #include "net/http/client.h"
#include "net/http/slice_body.h" #include "net/http/slice_body.h"
#include "net/net_parse_helpers.h" // monero/contrib/epee/include #include "net/net_parse_helpers.h" // monero/contrib/epee/include
@@ -86,7 +88,9 @@
#include "rpc/login.h" #include "rpc/login.h"
#include "rpc/rates.h" #include "rpc/rates.h"
#include "rpc/webhook.h" #include "rpc/webhook.h"
#include "string_tools.h" // monero/contrib/epee/include
#include "util/gamma_picker.h" #include "util/gamma_picker.h"
#include "util/ownership_test.h"
#include "util/random_outputs.h" #include "util/random_outputs.h"
#include "util/source_location.h" #include "util/source_location.h"
#include "wire/adapted/crypto.h" #include "wire/adapted/crypto.h"
@@ -118,15 +122,17 @@ namespace lws
boost::asio::io_context io; boost::asio::io_context io;
const db::storage disk; const db::storage disk;
const rpc::client client; const rpc::client client;
const std::shared_ptr<lws::mempool> mempool;
const runtime_options options; const runtime_options options;
std::vector<net::zmq::async_client> clients; std::vector<net::zmq::async_client> clients;
net::http::client webhook_client; net::http::client webhook_client;
boost::mutex sync; boost::mutex sync;
rest_server_data(db::storage disk, rpc::client client, runtime_options options) rest_server_data(db::storage disk, rpc::client client, std::shared_ptr<lws::mempool> mempool, runtime_options options)
: io(), : io(),
disk(std::move(disk)), disk(std::move(disk)),
client(std::move(client)), client(std::move(client)),
mempool(std::move(mempool)),
options(std::move(options)), options(std::move(options)),
webhook_client(options.webhook_verify), webhook_client(options.webhook_verify),
clients(), clients(),
@@ -551,7 +557,7 @@ namespace lws
return user.error(); return user.error();
data.passed_login = true; data.passed_login = true;
return response::load(user->second, user->first, false); return response::load(user->second, user->first, data.global->mempool.get(), false);
} }
}; };
@@ -1433,6 +1439,10 @@ namespace lws
{ {
using transaction_rpc = cryptonote::rpc::SendRawTxHex; using transaction_rpc = cryptonote::rpc::SendRawTxHex;
cryptonote::blobdata tx_blob;
if (!epee::string_tools::parse_hexstr_to_binbuff(req.tx, tx_blob))
return {lws::error::bad_client_tx};
struct frame struct frame
{ {
rest_server_data* parent; rest_server_data* parent;
@@ -1440,7 +1450,7 @@ namespace lws
net::zmq::async_client client; net::zmq::async_client client;
boost::asio::steady_timer timer; boost::asio::steady_timer timer;
boost::asio::io_context::strand strand; boost::asio::io_context::strand strand;
std::deque<std::pair<epee::byte_slice, std::function<async_complete>>> resumers; std::deque<std::tuple<epee::byte_slice, std::function<async_complete>, cryptonote::transaction>> resumers;
std::size_t outstanding; std::size_t outstanding;
frame(rest_server_data& parent, net::zmq::async_client client) frame(rest_server_data& parent, net::zmq::async_client client)
@@ -1480,7 +1490,11 @@ namespace lws
return {error::load}; return {error::load};
active->outstanding += msg.size(); active->outstanding += msg.size();
active->resumers.emplace_back(std::move(msg), std::move(resume)); auto& elem = active->resumers.emplace_back();
std::get<0>(elem) = std::move(msg);
std::get<1>(elem) = std::move(resume);
if (!cryptonote::parse_and_validate_tx_from_blob(tx_blob, std::get<2>(elem)))
return {lws::error::bad_client_tx};
return success(); return success();
} }
@@ -1497,7 +1511,7 @@ namespace lws
assert(self_ != nullptr); assert(self_ != nullptr);
assert(self_->strand.running_in_this_thread()); assert(self_->strand.running_in_this_thread());
std::deque<std::pair<epee::byte_slice, std::function<async_complete>>> resumers; std::deque<std::tuple<epee::byte_slice, std::function<async_complete>, cryptonote::transaction>> resumers;
{ {
const boost::lock_guard<boost::mutex> lock{cache.sync}; const boost::lock_guard<boost::mutex> lock{cache.sync};
if (error) if (error)
@@ -1510,6 +1524,9 @@ namespace lws
} }
else else
{ {
if (self_->parent && self_->parent->mempool)
self_->parent->mempool->add_txs({std::addressof(std::get<2>(self_->resumers.front())), 1});
MDEBUG("Completed ZMQ request in /submit_raw_tx"); MDEBUG("Completed ZMQ request in /submit_raw_tx");
resumers.push_back(std::move(self_->resumers.front())); resumers.push_back(std::move(self_->resumers.front()));
self_->resumers.pop_front(); self_->resumers.pop_front();
@@ -1517,7 +1534,7 @@ namespace lws
} }
for (const auto& r : resumers) for (const auto& r : resumers)
r.second(value); std::get<1>(r)(value);
} }
bool set_timeout(std::chrono::steady_clock::duration timeout, const bool expecting) const bool set_timeout(std::chrono::steady_clock::duration timeout, const bool expecting) const
@@ -1568,7 +1585,7 @@ namespace lws
self_->parent->store_async_client(std::move(self_->client)); self_->parent->store_async_client(std::move(self_->client));
return; return;
} }
next = std::move(self_->resumers.front().first); next = std::move(std::get<0>(self_->resumers.front()));
self_->outstanding -= std::min(self_->outstanding, next.size()); self_->outstanding -= std::min(self_->outstanding, next.size());
} }
@@ -1613,7 +1630,11 @@ namespace lws
cache.status = active; cache.status = active;
active->outstanding += msg.size(); active->outstanding += msg.size();
active->resumers.emplace_back(std::move(msg), std::move(resume)); auto& elem = active->resumers.emplace_back();
std::get<0>(elem) = std::move(msg);
std::get<1>(elem) = std::move(resume);
if (!cryptonote::parse_and_validate_tx_from_blob(tx_blob, std::get<2>(elem)))
return {lws::error::bad_client_tx};
lock.unlock(); lock.unlock();
MDEBUG("Starting new ZMQ request in /submit_raw_tx"); MDEBUG("Starting new ZMQ request in /submit_raw_tx");
@@ -2072,7 +2093,7 @@ namespace lws
if (!feed_disabled && boost::beast::websocket::is_upgrade(self_->parser_->get())) if (!feed_disabled && boost::beast::websocket::is_upgrade(self_->parser_->get()))
{ {
MDEBUG("Attempting websocket '" << feed->name << "' on " << self_.get()); MDEBUG("Attempting websocket '" << feed->name << "' on " << self_.get());
if (!rpc::feed::start(std::move(self_->sock()), from->io, from->client, self_->parser_->get(), from->disk, feed_timeout)) if (!rpc::feed::start(std::move(self_->sock()), from->io, from->client, from->mempool, self_->parser_->get(), from->disk, feed_timeout))
return self_->bad_request(boost::beast::http::status::bad_request, std::forward<F>(resume)); return self_->bad_request(boost::beast::http::status::bad_request, std::forward<F>(resume));
return; // else a websocket has I/O queued on `io`. return; // else a websocket has I/O queued on `io`.
} }
@@ -2240,8 +2261,8 @@ namespace lws
} }
} }
rest_server::rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, configuration config) rest_server::rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, std::shared_ptr<lws::mempool> mempool, configuration config)
: global_(std::make_unique<rest_server_data>(std::move(disk), std::move(client), runtime_options{config})), : global_(std::make_unique<rest_server_data>(std::move(disk), std::move(client), std::move(mempool), runtime_options{config})),
ports_(), ports_(),
workers_() workers_()
{ {
+8 -1
View File
@@ -41,6 +41,7 @@
namespace lws namespace lws
{ {
class mempool;
struct rest_server_data; struct rest_server_data;
class rest_server class rest_server
{ {
@@ -70,7 +71,13 @@ namespace lws
bool auto_accept_import; bool auto_accept_import;
}; };
explicit rest_server(epee::span<const std::string> addresses, std::vector<std::string> admin, db::storage disk, rpc::client client, configuration config); explicit rest_server(
epee::span<const std::string> addresses,
std::vector<std::string> admin,
db::storage disk,
rpc::client client,
std::shared_ptr<lws::mempool> mempool,
configuration config);
rest_server(rest_server&&) = delete; rest_server(rest_server&&) = delete;
rest_server(rest_server const&) = delete; rest_server(rest_server const&) = delete;
+115
View File
@@ -27,6 +27,7 @@
#include "client.h" #include "client.h"
#include <array>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/thread/mutex.hpp> #include <boost/thread/mutex.hpp>
#include <boost/utility/string_ref.hpp> #include <boost/utility/string_ref.hpp>
@@ -433,6 +434,15 @@ namespace rpc
return ctx && (ctx->external_pub || ctx->rmq.is_available()); return ctx && (ctx->external_pub || ctx->rmq.is_available());
} }
expect<void> client::daemon_reconnect() noexcept
{
auto new_daemon = make_daemon(ctx);
if (!new_daemon)
return new_daemon.error();
daemon = std::move(*new_daemon);
return success();
}
expect<void> client::watch_scan_signals() noexcept expect<void> client::watch_scan_signals() noexcept
{ {
MONERO_PRECOND(ctx != nullptr); MONERO_PRECOND(ctx != nullptr);
@@ -502,6 +512,111 @@ namespace rpc
return {lws::error::bad_daemon_response}; return {lws::error::bad_daemon_response};
} }
expect<void> client::event_loop(rpc_handler on_rpc, block_pub_handler on_block, txpool_pub_handler on_txpool)
{
MONERO_PRECOND(ctx != nullptr);
assert(signal_sub != nullptr);
std::array<zmq_pollitem_t, 3> poll_items{};
std::size_t count = 0;
poll_items[count++] = {signal_sub.get(), 0, short(ZMQ_POLLIN | ZMQ_POLLERR), 0};
if (daemon != nullptr)
poll_items[count++] = {daemon.get(), 0, short(ZMQ_POLLIN | ZMQ_POLLERR), 0};
if (daemon_sub != nullptr)
poll_items[count++] = {daemon_sub.get(), 0, short(ZMQ_POLLIN | ZMQ_POLLERR), 0};
while (true)
{
const int ready = zmq_poll(poll_items.data(), count, -1);
if (ready < 0)
{
const int err = zmq_errno();
if (err == EINTR)
continue;
MERROR("Failed polling sockets: " << err);
return net::zmq::make_error_code(err);
}
// check for abort messages:
if (poll_items[0].revents & (ZMQ_POLLIN | ZMQ_POLLERR))
{
char buf[1];
MONERO_ZMQ_CHECK(zmq_recv(signal_sub.get(), buf, 1, 0));
switch (buf[0])
{
case 'P':
return {lws::error::signal_abort_process};
case 'S':
return {lws::error::signal_abort_scan};
default:
return {lws::error::signal_unknown};
}
}
// check for rpc responses:
if (daemon && (poll_items[1].revents & (ZMQ_POLLIN | ZMQ_POLLERR)))
{
auto json = net::zmq::receive(daemon.get(), ZMQ_DONTWAIT);
if (!json)
{
if (json != net::zmq::make_error_code(EAGAIN))
{
MERROR("Failed reading RPC response: " << json.error());
return json.error();
}
} else if (on_rpc)
MONERO_CHECK(on_rpc(std::move(*json)));
}
// check for PUB notificatons:
std::size_t sub_index = daemon ? 2 : 1;
if (daemon_sub && (poll_items[sub_index].revents & (ZMQ_POLLIN | ZMQ_POLLERR)))
{
while (true)
{
auto json = net::zmq::receive(daemon_sub.get(), ZMQ_DONTWAIT);
if (!json)
{
if (json == net::zmq::make_error_code(EAGAIN))
break;
MERROR("Failed reading pub message: " << json.error());
return json.error();
}
if (boost::string_ref{*json}.starts_with(minimal_chain_topic))
{
json->erase(0, sizeof(minimal_chain_topic));
auto parsed = rpc::minimal_chain_pub::from_json(std::move(*json));
if (!parsed)
{
MERROR("Failed parsing chain pub: " << parsed.error().message());
return parsed.error();
}
if (on_block)
MONERO_CHECK(on_block(std::move(*parsed)));
}
else if (boost::string_ref{*json}.starts_with(full_txpool_topic))
{
json->erase(0, sizeof(full_txpool_topic));
auto parsed = rpc::full_txpool_pub::from_json(std::move(*json));
if (!parsed)
{
MERROR("Failed parsing txpool pub: " << parsed.error().message());
return parsed.error();
}
if (on_txpool)
MONERO_CHECK(on_txpool(std::move(*parsed)));
}
else
return {lws::error::bad_daemon_response};
}
}
}
return {};
}
expect<account_sub> client::make_account_sub(boost::asio::io_context& io) const expect<account_sub> client::make_account_sub(boost::asio::io_context& io) const
{ {
MONERO_PRECOND(ctx != nullptr); MONERO_PRECOND(ctx != nullptr);
+15
View File
@@ -29,6 +29,7 @@
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
#include <chrono> #include <chrono>
#include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -158,12 +159,19 @@ namespace rpc
//! \return True if an external pub/sub was setup //! \return True if an external pub/sub was setup
bool has_publish() const noexcept; bool has_publish() const noexcept;
//! Reconnect ZMQ connection to daemon
expect<void> daemon_reconnect() noexcept;
//! `wait`, `send`, and `receive` will watch for `raise_abort_scan()`. //! `wait`, `send`, and `receive` will watch for `raise_abort_scan()`.
expect<void> watch_scan_signals() noexcept; expect<void> watch_scan_signals() noexcept;
//! Wait for new block announce or internal timeout. //! Wait for new block announce or internal timeout.
expect<std::vector<std::pair<topic, std::string>>> wait_for_block(); expect<std::vector<std::pair<topic, std::string>>> wait_for_block();
using rpc_handler = std::function<expect<void>(std::string&&)>;
using block_pub_handler = std::function<expect<void>(minimal_chain_pub&&)>;
using txpool_pub_handler = std::function<expect<void>(full_txpool_pub&&)>;
//! \return A JSON message for RPC request `M`. //! \return A JSON message for RPC request `M`.
template<typename M> template<typename M>
static epee::byte_slice make_message(char const* const name, const M& message) static epee::byte_slice make_message(char const* const name, const M& message)
@@ -171,6 +179,13 @@ namespace rpc
return cryptonote::rpc::FullMessage::getRequest(name, message, 0); return cryptonote::rpc::FullMessage::getRequest(name, message, 0);
} }
//! Loops until an error or shutdown happens, emitting events.
expect<void> event_loop(
rpc_handler on_rpc,
block_pub_handler on_block,
txpool_pub_handler on_txpool
);
//! \return An async ZMQ_SUB socket that is bound to the account update publisher //! \return An async ZMQ_SUB socket that is bound to the account update publisher
expect<account_sub> make_account_sub(boost::asio::io_context& io) const; expect<account_sub> make_account_sub(boost::asio::io_context& io) const;
+3 -2
View File
@@ -34,6 +34,7 @@
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <memory>
#include <system_error> #include <system_error>
#include "db/fwd.h" #include "db/fwd.h"
@@ -72,8 +73,8 @@ namespace lws { namespace rpc { namespace feed
return std::error_code{int(value), error_category()}; return std::error_code{int(value), error_category()};
} }
bool start(boost::asio::ip::tcp::socket&& sock, boost::asio::io_context& io, const lws::rpc::client& client, const request& req, const lws::db::storage& disk, std::chrono::seconds timeout); bool start(boost::asio::ip::tcp::socket&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, std::chrono::seconds timeout);
bool start(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>&& sock, boost::asio::io_context& io, const lws::rpc::client& client, const request& req, const lws::db::storage& disk, std::chrono::seconds idle_timeout); bool start(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<mempool> pool, const request& req, const lws::db::storage& disk, std::chrono::seconds idle_timeout);
}}} // lws // rpc // feed }}} // lws // rpc // feed
namespace std namespace std
+9 -7
View File
@@ -171,12 +171,13 @@ namespace lws { namespace rpc { namespace feed
// defined in `feed_tcp.cpp` // defined in `feed_tcp.cpp`
std::string_view get_prefix(const boost::beast::net::const_buffer buf); std::string_view get_prefix(const boost::beast::net::const_buffer buf);
expect<epee::byte_slice> prep_error(std::error_code error, protocol proto); expect<epee::byte_slice> prep_error(std::error_code error, protocol proto);
expect<epee::byte_slice> prep_login(const db::storage& disk, account_sub* sub, connection_sync* sync, boost::beast::flat_buffer& buffer, protocol proto); expect<epee::byte_slice> prep_login(const db::storage& disk, const mempool* pool, account_sub* sub, connection_sync* sync, boost::beast::flat_buffer& buffer, protocol proto);
expect<epee::byte_slice> prep_update(const db::storage& disk, std::string&& source, connection_sync* sync, protocol proto); expect<epee::byte_slice> prep_update(const db::storage& disk, std::string&& source, connection_sync* sync, protocol proto);
class connection class connection
{ {
lws::db::storage disk_; lws::db::storage disk_;
const std::shared_ptr<const mempool> pool_;
std::string sub_buffer_; std::string sub_buffer_;
boost::beast::flat_buffer ws_buffer_; boost::beast::flat_buffer ws_buffer_;
std::deque<epee::byte_slice> write_queue_; std::deque<epee::byte_slice> write_queue_;
@@ -324,8 +325,9 @@ namespace lws { namespace rpc { namespace feed
} }
public: public:
connection(boost::asio::io_context& io, const lws::rpc::client& client, const lws::db::storage& disk, const protocol proto) connection(boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const lws::db::storage& disk, const protocol proto)
: disk_(disk.clone()), : disk_(disk.clone()),
pool_(std::move(pool)),
sub_buffer_(), sub_buffer_(),
ws_buffer_(), ws_buffer_(),
write_queue_(), write_queue_(),
@@ -347,7 +349,7 @@ namespace lws { namespace rpc { namespace feed
bool login(std::shared_ptr<connection> self) bool login(std::shared_ptr<connection> self)
{ {
const bool rc = do_write(std::move(self), prep_login(disk_, &sub_, &sync_, ws_buffer_, proto_)); const bool rc = do_write(std::move(self), prep_login(disk_, pool_.get(), &sub_, &sync_, ws_buffer_, proto_));
ws_buffer_.consume(ws_buffer_.cdata().size()); ws_buffer_.consume(ws_buffer_.cdata().size());
return rc; return rc;
} }
@@ -465,8 +467,8 @@ namespace lws { namespace rpc { namespace feed
{ do_async_shutdown(sock().next_layer(), std::move(self)); } { do_async_shutdown(sock().next_layer(), std::move(self)); }
public: public:
explicit connection_(T&& sock, boost::asio::io_context& io, const lws::rpc::client& client, const lws::db::storage& disk, const protocol proto) explicit connection_(T&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const lws::db::storage& disk, const protocol proto)
: connection(io, client, disk, proto), sock_(std::move(sock)) : connection(io, client, std::move(pool), disk, proto), sock_(std::move(sock))
{ {
if (is_binary(proto)) if (is_binary(proto))
sock_.binary(true); sock_.binary(true);
@@ -581,14 +583,14 @@ namespace lws { namespace rpc { namespace feed
} }
template<typename T> template<typename T>
inline bool do_start(T&& sock, boost::asio::io_context& io, const lws::rpc::client& client, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout) inline bool do_start(T&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout)
{ {
const protocol proto = get_protocol(req.base()[boost::beast::http::field::sec_websocket_protocol]); const protocol proto = get_protocol(req.base()[boost::beast::http::field::sec_websocket_protocol]);
switch (proto) switch (proto)
{ {
case protocol::v0_msgpack: case protocol::v0_msgpack:
case protocol::v0_json: case protocol::v0_json:
do_start(std::make_shared<connection_<T>>(std::forward<T>(sock), io, client, disk, proto), req, timeout); do_start(std::make_shared<connection_<T>>(std::forward<T>(sock), io, client, std::move(pool), disk, proto), req, timeout);
return true; return true;
default: default:
case protocol::invalid: case protocol::invalid:
+2 -2
View File
@@ -31,11 +31,11 @@
namespace lws { namespace rpc { namespace feed namespace lws { namespace rpc { namespace feed
{ {
bool start(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>&& sock, boost::asio::io_context& io, const lws::rpc::client& client, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout) bool start(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout)
{ {
// moving ssl streams only supported in boost 1.74+ // moving ssl streams only supported in boost 1.74+
#if BOOST_VERSION >= 107400 #if BOOST_VERSION >= 107400
return do_start(std::move(sock), io, client, req, disk, timeout); return do_start(std::move(sock), io, client, std::move(pool), req, disk, timeout);
#else #else
return false; return false;
#endif #endif
+4 -4
View File
@@ -231,7 +231,7 @@ namespace lws { namespace rpc { namespace feed
return prep(feed_error{error}, proto); return prep(feed_error{error}, proto);
} }
expect<epee::byte_slice> prep_login(const db::storage& disk, account_sub* const sub, connection_sync* const sync, boost::beast::flat_buffer& buffer, const protocol proto) expect<epee::byte_slice> prep_login(const db::storage& disk, mempool const* const pool, account_sub* const sub, connection_sync* const sync, boost::beast::flat_buffer& buffer, const protocol proto)
{ {
LWS_VERIFY(sub && sync); LWS_VERIFY(sub && sync);
const std::string_view prefix = get_prefix(buffer.cdata()); const std::string_view prefix = get_prefix(buffer.cdata());
@@ -271,7 +271,7 @@ namespace lws { namespace rpc { namespace feed
return prep(feed_blocks{acct.start_height, acct.scan_height, block->id, acct.lookahead_fail, acct.lookahead}, proto); return prep(feed_blocks{acct.start_height, acct.scan_height, block->id, acct.lookahead_fail, acct.lookahead}, proto);
} }
auto txs = get_address_txs_response::load(account->second, account->first, true); auto txs = get_address_txs_response::load(account->second, account->first, pool, true);
if (!txs) if (!txs)
return txs.error(); return txs.error();
@@ -366,9 +366,9 @@ namespace lws { namespace rpc { namespace feed
return prep(feed_mempool{from_scanner.outputs.at(0)}, proto); return prep(feed_mempool{from_scanner.outputs.at(0)}, proto);
} }
bool start(boost::asio::ip::tcp::socket&& sock, boost::asio::io_context& io, const lws::rpc::client& client, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout) bool start(boost::asio::ip::tcp::socket&& sock, boost::asio::io_context& io, const lws::rpc::client& client, std::shared_ptr<const mempool> pool, const request& req, const lws::db::storage& disk, const std::chrono::seconds timeout)
{ {
return do_start(std::move(sock), io, client, req, disk, timeout); return do_start(std::move(sock), io, client, std::move(pool), req, disk, timeout);
} }
}}} // lws // rpc // feed }}} // lws // rpc // feed
+1
View File
@@ -42,6 +42,7 @@ namespace lws
enum class status : std::uint16_t; enum class status : std::uint16_t;
} }
} }
class mempool;
struct rates; struct rates;
class scan_manager; class scan_manager;
} }
+87 -7
View File
@@ -33,12 +33,14 @@
#include <limits> #include <limits>
#include <stdexcept> #include <stdexcept>
#include <type_traits> #include <type_traits>
#include <unordered_set>
#include "config.h" #include "config.h"
#include "db/storage.h" #include "db/storage.h"
#include "db/string.h" #include "db/string.h"
#include "error.h" #include "error.h"
#include "lws_version.h" #include "lws_version.h"
#include "mempool.h"
#include "time_helper.h" // monero/contrib/epee/include #include "time_helper.h" // monero/contrib/epee/include
#include "ringct/rctOps.h" // monero/src #include "ringct/rctOps.h" // monero/src
#include "rpc/feed.h" #include "rpc/feed.h"
@@ -506,19 +508,22 @@ namespace lws
} }
const bool is_coinbase = (extra.first & db::coinbase_output); const bool is_coinbase = (extra.first & db::coinbase_output);
const auto height = self.value().info.link.height;
const bool mempool = height == db::block_id::txpool;
const iso_timestamp timestamp{self.value().info.timestamp};
wire::object(dest, wire::object(dest,
wire::field("id", std::uint64_t(self.index())), wire::field("id", std::uint64_t(self.index())),
wire::field("hash", std::cref(self.value().info.link.tx_hash)), wire::field("hash", std::cref(self.value().info.link.tx_hash)),
wire::field("timestamp", iso_timestamp(self.value().info.timestamp)), wire::optional_field("timestamp", mempool ? nullptr : &timestamp),
wire::field("total_received", safe_uint64(self.value().info.spend_meta.amount)), wire::field("total_received", safe_uint64(self.value().info.spend_meta.amount)),
wire::field("total_sent", safe_uint64(self.value().spent)), wire::field("total_sent", safe_uint64(self.value().spent)),
wire::field("fee", safe_uint64(self.value().info.fee)), wire::field("fee", safe_uint64(self.value().info.fee)),
wire::field("unlock_time", self.value().info.unlock_time), wire::field("unlock_time", self.value().info.unlock_time),
wire::field("height", self.value().info.link.height), wire::optional_field("height", mempool ? nullptr : &height),
wire::optional_field("payment_id", payment_id), wire::optional_field("payment_id", payment_id),
wire::field("coinbase", is_coinbase), wire::field("coinbase", is_coinbase),
wire::field("mempool", false), wire::field("mempool", mempool),
wire::field("mixin", self.value().info.spend_meta.mixin_count), wire::field("mixin", self.value().info.spend_meta.mixin_count),
wire::field("recipient", self.value().info.recipient), wire::field("recipient", self.value().info.recipient),
wire::field("spent_outputs", std::cref(self.value().spends)) wire::field("spent_outputs", std::cref(self.value().spends))
@@ -583,16 +588,24 @@ namespace lws
vec_lmdb_<T> vec_lmdb(const std::vector<T>& src) { return {src.begin(), src.end()}; } vec_lmdb_<T> vec_lmdb(const std::vector<T>& src) { return {src.begin(), src.end()}; }
template<typename T, typename U> template<typename T, typename U>
std::pair<std::vector<rpc::get_transaction>, std::uint64_t> merge_into_txes(T output, U spend, const std::size_t reserve, const bool all_outputs, const bool skip_spend_meta) std::pair<std::vector<rpc::get_transaction>, std::uint64_t> merge_into_txes(T output, U spend, db::account const* const user, mempool const* const pool, const std::size_t reserve, const bool all_outputs, const bool skip_spend_meta)
{ {
// merge input and output info into a single set of txes. // merge input and output info into a single set of txes.
const bool has_pool = (user && pool);
std::uint64_t total = 0; std::uint64_t total = 0;
std::vector<rpc::get_transaction> out{}; std::vector<rpc::get_transaction> out{};
std::vector<db::output::spend_meta_> metas{}; std::vector<db::output::spend_meta_> metas{};
std::vector<std::pair<db::output_id, db::address_index>> receives{};
std::unordered_set<crypto::hash> txes_processed{};
out.reserve(reserve); out.reserve(reserve);
metas.reserve(reserve); metas.reserve(reserve);
if (has_pool)
{
receives.reserve(reserve);
txes_processed.reserve(reserve);
}
db::transaction_link next_output{}; db::transaction_link next_output{};
db::transaction_link next_spend{}; db::transaction_link next_spend{};
@@ -619,6 +632,8 @@ namespace lws
{ {
out.push_back({*output}); out.push_back({*output});
amount = out.back().info.spend_meta.amount; amount = out.back().info.spend_meta.amount;
if (has_pool)
txes_processed.emplace(next_output.tx_hash);
} }
else else
{ {
@@ -626,6 +641,13 @@ namespace lws
out.back().info.spend_meta.amount += amount; out.back().info.spend_meta.amount += amount;
} }
if (has_pool)
{
auto id = output.template get_value<MONERO_FIELD(db::output, spend_meta.id)>();
auto subaddr = output.template get_value<MONERO_FIELD(db::output, recipient)>();
receives.emplace_back(std::move(id), std::move(subaddr));
}
if (all_outputs) if (all_outputs)
out.back().receives.push_back(*output); out.back().receives.push_back(*output);
@@ -660,6 +682,8 @@ namespace lws
out.back().spends.back().possible_spend.mixin_count; out.back().spends.back().possible_spend.mixin_count;
out.back().info.timestamp = out.back().spends.back().possible_spend.timestamp; out.back().info.timestamp = out.back().spends.back().possible_spend.timestamp;
out.back().info.unlock_time = out.back().spends.back().possible_spend.unlock_time; out.back().info.unlock_time = out.back().spends.back().possible_spend.unlock_time;
if (has_pool)
txes_processed.emplace(out.back().info.link.tx_hash);
} }
else else
out.back().spends.push_back({skip_spend_meta ? meta_type{} : *meta, *spend}); out.back().spends.push_back({skip_spend_meta ? meta_type{} : *meta, *spend});
@@ -672,6 +696,62 @@ namespace lws
next_spend = spend.template get_value<MONERO_FIELD(db::spend, link)>(); next_spend = spend.template get_value<MONERO_FIELD(db::spend, link)>();
} }
} }
if (has_pool)
{
// Add mempool transactions. Order is not important, since
// mempool txs cannot depend on each other.
lws::account full_user{*user, std::move(receives), {}};
const auto pool_txs = pool->scan_account(full_user);
for (const auto& row: pool_txs)
{
// mempool update after block could be delayed
if (txes_processed.count(row.hash))
continue;
rpc::get_transaction& tx = out.emplace_back();
// Ingore spends that use unknown outputs
// (perhaps the scanner thread is behind, and hasn't seen them yet).
bool from_future = false;
for (const auto& spend: row.spends)
{
const auto meta = rpc::get_address_txs_response::find_metadata(metas, spend.source);
if (meta == metas.end() || meta->id != spend.source)
{
from_future = true;
break;
}
tx.spends.push_back({*meta, spend});
tx.spent += meta->amount;
}
uint64_t tx_total = 0;
for (const auto& output: row.outputs)
{
const auto amount = output.spend_meta.amount;
total += amount;
tx_total += amount;
tx.receives.push_back(output);
}
if (!row.outputs.empty())
{
tx.info = row.outputs.front();
tx.info.spend_meta.amount = tx_total;
}
else
{
if (row.spends.empty() || from_future) break;
auto spend = row.spends.front();
tx.info.link.tx_hash = row.hash;
tx.info.link.height = db::block_id::txpool;
tx.info.spend_meta.amount = tx_total;
tx.info.spend_meta.mixin_count = spend.mixin_count;
tx.info.timestamp = spend.timestamp;
tx.info.unlock_time = spend.unlock_time;
}
}
}
return {std::move(out), total}; return {std::move(out), total};
} }
@@ -691,10 +771,10 @@ namespace lws
{ {
std::sort(outputs.begin(), outputs.end(), by_tx_link); std::sort(outputs.begin(), outputs.end(), by_tx_link);
std::sort(spends.begin(), spends.end(), by_tx_link); std::sort(spends.begin(), spends.end(), by_tx_link);
return merge_into_txes(vec_lmdb(outputs), vec_lmdb(spends), outputs.size(), true, true).first; return merge_into_txes(vec_lmdb(outputs), vec_lmdb(spends), nullptr, nullptr, outputs.size(), true, true).first;
} }
expect<rpc::get_address_txs_response> rpc::get_address_txs_response::load(db::storage_reader& reader, const db::account& acct, const bool all_outputs) expect<rpc::get_address_txs_response> rpc::get_address_txs_response::load(db::storage_reader& reader, const db::account& acct, mempool const* const pool, const bool all_outputs)
{ {
auto outputs = reader.get_outputs(acct.id); auto outputs = reader.get_outputs(acct.id);
if (!outputs) if (!outputs)
@@ -717,7 +797,7 @@ namespace lws
resp.lookahead_fail = db::to_uint(acct.lookahead_fail); resp.lookahead_fail = db::to_uint(acct.lookahead_fail);
resp.lookahead = acct.lookahead; resp.lookahead = acct.lookahead;
auto out = merge_into_txes(outputs->make_iterator(), spends->make_iterator(), outputs->count(), all_outputs, false); auto out = merge_into_txes(outputs->make_iterator(), spends->make_iterator(), std::addressof(acct), pool, outputs->count(), all_outputs, false);
resp.total_received = safe_uint64(out.second); resp.total_received = safe_uint64(out.second);
resp.transactions = std::move(out.first); resp.transactions = std::move(out.first);
return resp; return resp;
+10 -2
View File
@@ -29,6 +29,7 @@
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -120,7 +121,14 @@ namespace rpc
struct get_transaction struct get_transaction
{ {
get_transaction() = delete; get_transaction()
: info{}, receives(), spends(), spent(0)
{}
get_transaction(const db::output& in)
: info(in), receives(), spends(), spent(0)
{}
db::output info; db::output info;
std::vector<db::output> receives; std::vector<db::output> receives;
std::vector<transaction_spend> spends; std::vector<transaction_spend> spends;
@@ -135,7 +143,7 @@ namespace rpc
find_metadata(std::vector<db::output::spend_meta_> const& metas, db::output_id id); find_metadata(std::vector<db::output::spend_meta_> const& metas, db::output_id id);
static std::vector<get_transaction> load(std::vector<db::output> outputs, std::vector<db::spend> spends); static std::vector<get_transaction> load(std::vector<db::output> outputs, std::vector<db::spend> spends);
static expect<get_address_txs_response> load(db::storage_reader& reader, const db::account& acct, const bool all_outputs); static expect<get_address_txs_response> load(db::storage_reader& reader, const db::account& acct, const mempool* pool, const bool all_outputs);
safe_uint64 total_received; safe_uint64 total_received;
std::uint64_t scanned_height; std::uint64_t scanned_height;
+77 -394
View File
@@ -53,6 +53,7 @@
#include "db/data.h" #include "db/data.h"
#include "cryptonote_basic/difficulty.h" // monero/src #include "cryptonote_basic/difficulty.h" // monero/src
#include "error.h" #include "error.h"
#include "mempool.h"
#include "hardforks/hardforks.h" // monero/src #include "hardforks/hardforks.h" // monero/src
#include "misc_log_ex.h" // monero/contrib/epee/include #include "misc_log_ex.h" // monero/contrib/epee/include
#include "net/net_parse_helpers.h" #include "net/net_parse_helpers.h"
@@ -67,6 +68,7 @@
#include "rpc/scanner/server.h" #include "rpc/scanner/server.h"
#include "rpc/webhook.h" #include "rpc/webhook.h"
#include "util/blocks.h" #include "util/blocks.h"
#include "util/ownership_test.h"
#include "util/source_location.h" #include "util/source_location.h"
#include "util/transactions.h" #include "util/transactions.h"
@@ -194,18 +196,21 @@ namespace lws
struct add_spend struct add_spend
{ {
void operator()(lws::account& user, const db::spend& spend) const void operator()(lws::account& user, db::spend&& spend) const
{ user.add_spend(spend); } { user.add_spend(spend); }
}; };
struct add_output struct add_output
{ {
bool operator()(expect<db::storage_reader>&, lws::account& user, const db::output& out) const void operator()(lws::account& user, db::output&& out) const
{ return user.add_out(out); } {
if (!user.add_out(out))
MWARNING("Output not added, duplicate public key encountered");
}
}; };
struct null_spend struct null_spend
{ {
void operator()(lws::account&, const db::spend&) const noexcept void operator()(lws::account&, db::spend&&) const noexcept
{} {}
}; };
struct send_webhook struct send_webhook
@@ -213,404 +218,41 @@ namespace lws
db::storage const& disk_; db::storage const& disk_;
rpc::client& client_; rpc::client& client_;
scanner_sync& http_; scanner_sync& http_;
std::unordered_map<crypto::hash, crypto::hash> txpool_;
bool operator()(expect<db::storage_reader>& reader, lws::account& user, const db::output& out) void operator()(lws::account& user, db::output&& out)
{ {
/* Upstream monerod does not send all fields for a transaction, so
mempool notifications cannot compute tx_hash correctly (it is not
sent separately, a further blunder). Instead, if there are matching
outputs with webhooks, fetch mempool to compare tx_prefix_hash and
then use corresponding tx_hash. */
const db::webhook_key key{user.id(), db::webhook_type::tx_confirmation}; const db::webhook_key key{user.id(), db::webhook_type::tx_confirmation};
std::vector<db::webhook_value> hooks{}; std::vector<db::webhook_value> hooks{};
{ {
db::storage_reader* active_reader = reader ? expect<db::storage_reader> reader = disk_.start_read();
std::addressof(*reader) : nullptr; if (!reader)
expect<db::storage_reader> temp_reader{common_error::kInvalidArgument};
if (!active_reader)
{ {
temp_reader = disk_.start_read(); MERROR("Unable to lookup webhook on tx in pool: " << reader.error().message());
if (!temp_reader) return;
{
MERROR("Unable to lookup webhook on tx in pool: " << reader.error().message());
return false;
}
active_reader = std::addressof(*temp_reader);
} }
auto found = active_reader->find_webhook(key, out.payment_id.short_); auto found = reader->find_webhook(key, out.payment_id.short_);
if (!found) if (!found)
{ {
MERROR("Failed db lookup for webhooks: " << found.error().message()); MERROR("Failed db lookup for webhooks: " << found.error().message());
return false; return;
} }
hooks = std::move(*found); hooks = std::move(*found);
} }
if (!hooks.empty() && txpool_.empty())
{
cryptonote::rpc::GetTransactionPool::Request req{};
if (!send(client_, rpc::client::make_message("get_transaction_pool", req)))
{
MERROR("Unable to compute tx hash for webhook, aborting");
return false;
}
auto resp = client_.get_message(std::chrono::seconds{3});
if (!resp)
{
MERROR("Unable to get txpool: " << resp.error().message());
return false;
}
auto txpool = rpc::parse_json_response<rpc::get_transaction_pool>(std::move(*resp));
if (!txpool)
MONERO_THROW(txpool.error(), "Failed fetching transaction pool");
for (auto& tx : txpool->transactions)
txpool_.emplace(get_transaction_prefix_hash(tx.tx), tx.tx_hash);
}
std::vector<db::webhook_tx_confirmation> events{}; std::vector<db::webhook_tx_confirmation> events{};
for (auto& hook : hooks) for (auto& hook : hooks)
{ {
events.push_back(db::webhook_tx_confirmation{key, std::move(hook), out}); events.push_back(db::webhook_tx_confirmation{key, std::move(hook), out});
events.back().value.second.confirmations = 0; events.back().value.second.confirmations = 0;
const auto hash = txpool_.find(out.tx_prefix_hash);
if (hash != txpool_.end())
events.back().tx_info.link.tx_hash = hash->second;
else
events.pop_back(); //cannot compute tx_hash
} }
send_payment_hook(http_.io_, client_, http_.webhooks_, epee::to_span(events)); send_payment_hook(http_.io_, client_, http_.webhooks_, epee::to_span(events));
const expect<void> pushed = client_.push_update(user, mempool_receive{out}); const expect<void> pushed = client_.push_update(user, mempool_receive{out});
if (!pushed) if (!pushed)
MERROR("Failed to send mempool update to " << user.address() << ": " << pushed.error().message()); MERROR("Failed to send mempool update to " << user.address() << ": " << pushed.error().message());
return true;
} }
}; };
struct subaddress_reader
{
expect<db::storage_reader> reader;
std::optional<db::storage> disk;
db::cursor::subaddress_indexes cur;
const std::uint32_t max_subaddresses;
subaddress_reader(std::optional<db::storage> const& disk_in, const std::uint32_t max_subaddresses)
: reader(common_error::kInvalidArgument), disk(), cur(nullptr), max_subaddresses(max_subaddresses)
{
if (disk_in)
disk = disk_in->clone();
if (max_subaddresses)
update_reader();
}
void update_reader()
{
if (disk)
reader = disk->start_read();
if (!reader)
MERROR("Subadress lookup failure: " << reader.error().message());
}
};
void update_lookahead(const account& user, subaddress_reader& reader, const db::address_index& match, db::block_id height)
{
if (match.is_zero())
return; // keep subaddress disabled servers quick
if (!reader.disk)
throw std::runtime_error{"Bad DB handle in scanner"};
if (height == db::block_id::txpool)
height = user.scan_height();
auto upserted = reader.disk->update_lookahead(user.db_address(), height, match, reader.max_subaddresses);
if (upserted)
{
if (0 < *upserted)
reader.update_reader(); // update reader after upsert added new addresses
else if (*upserted < 0)
upserted = {error::max_subaddresses};
}
if (!upserted)
MWARNING("Failed to update lookahead for " << user.address() << ": " << upserted.error());
}
void scan_transaction_base(
epee::span<lws::account> users,
const db::block_id height,
const std::uint64_t timestamp,
crypto::hash const* tx_hash,
cryptonote::transaction const& tx,
std::vector<std::uint64_t> const& out_ids,
subaddress_reader& reader,
std::function<void(lws::account&, const db::spend&)> spend_action,
std::function<bool(expect<db::storage_reader>&, lws::account&, const db::output&)> output_action)
{
if (2 < tx.version)
throw std::runtime_error{"Unsupported tx version"};
crypto::hash tx_hash_lazy;
cryptonote::tx_extra_pub_key key;
boost::optional<crypto::hash> prefix_hash;
boost::optional<cryptonote::tx_extra_nonce> extra_nonce;
std::pair<std::uint8_t, db::output::payment_id_> payment_id;
cryptonote::tx_extra_additional_pub_keys additional_tx_pub_keys;
std::vector<crypto::key_derivation> additional_derivations;
const auto get_tx_hash = [&] () -> const crypto::hash&
{
if (!tx_hash)
{
tx_hash_lazy = get_transaction_hash(tx);
tx_hash = std::addressof(tx_hash_lazy);
}
return *tx_hash;
};
{
std::vector<cryptonote::tx_extra_field> extra;
cryptonote::parse_tx_extra(tx.extra, extra);
// allow partial parsing of tx extra (similar to wallet2.cpp)
if (!cryptonote::find_tx_extra_field_by_type(extra, key))
return;
extra_nonce.emplace();
if (cryptonote::find_tx_extra_field_by_type(extra, *extra_nonce))
{
if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.long_))
payment_id.first = sizeof(crypto::hash);
}
else
extra_nonce = boost::none;
// additional tx pub keys present when there are 3+ outputs in a tx involving subaddresses
if (reader.reader)
cryptonote::find_tx_extra_field_by_type(extra, additional_tx_pub_keys);
} // destruct `extra` vector
for (account& user : users)
{
if (height <= user.scan_height())
continue; // to next user
crypto::key_derivation derived;
if (!crypto::wallet::generate_key_derivation(key.pub_key, user.view_key(), derived))
continue; // to next user
if (reader.reader && additional_tx_pub_keys.data.size() == tx.vout.size())
{
additional_derivations.resize(tx.vout.size());
std::size_t index = -1;
for (auto const& out: tx.vout)
{
++index;
if (!crypto::wallet::generate_key_derivation(additional_tx_pub_keys.data[index], user.view_key(), additional_derivations[index]))
{
additional_derivations.clear();
break; // vout loop
}
}
}
db::extra ext{};
std::uint32_t mixin = 0;
for (auto const& in : tx.vin)
{
cryptonote::txin_to_key const* const in_data =
boost::get<cryptonote::txin_to_key>(std::addressof(in));
if (in_data)
{
mixin = boost::numeric_cast<std::uint32_t>(
std::max(std::size_t(1), in_data->key_offsets.size()) - 1
);
std::uint64_t goffset = 0;
for (std::uint64_t offset : in_data->key_offsets)
{
goffset += offset;
const boost::optional<db::address_index> subaccount =
user.get_spendable(db::output_id{in_data->amount, goffset});
if (!subaccount)
continue; // to next input
spend_action(
user,
db::spend{
db::transaction_link{height, get_tx_hash()},
in_data->k_image,
db::output_id{in_data->amount, goffset},
timestamp,
tx.unlock_time,
mixin,
{0, 0, 0}, // reserved
payment_id.first,
payment_id.second.long_,
*subaccount
}
);
}
}
else if (boost::get<cryptonote::txin_gen>(std::addressof(in)))
ext = db::extra(ext | db::coinbase_output);
}
std::size_t index = -1;
for (auto const& out : tx.vout)
{
++index;
crypto::public_key out_pub_key;
if (!cryptonote::get_output_public_key(out, out_pub_key))
continue; // to next output
boost::optional<crypto::view_tag> view_tag_opt =
cryptonote::get_output_view_tag(out);
const bool found_tag =
(!additional_derivations.empty() && cryptonote::out_can_be_to_acc(view_tag_opt, additional_derivations.at(index), index)) ||
cryptonote::out_can_be_to_acc(view_tag_opt, derived, index);
if (!found_tag)
continue; // to next output
bool found_pub = false;
db::address_index account_index{db::major_index::primary, db::minor_index::primary};
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
crypto::public_key derived_pub;
if (!crypto::wallet::derive_subaddress_public_key(out_pub_key, active_derived, index, derived_pub))
continue; // to next available active_derived
if (user.spend_public() != derived_pub)
{
if (!reader.reader)
continue; // to next available active_derived
const expect<db::address_index> match =
reader.reader->find_subaddress(user.id(), derived_pub, reader.cur);
if (!match)
{
if (match != lmdb::error(MDB_NOTFOUND))
MERROR("Failure when doing subaddress search: " << match.error().message());
continue; // to next available active_derived
}
update_lookahead(user, reader, *match, height);
found_pub = true;
account_index = *match;
break; // additional_derivations loop
}
else
{
found_pub = true;
break; // additional_derivations loop
}
}
if (!found_pub)
continue; // to next output
if (!prefix_hash)
{
prefix_hash.emplace();
cryptonote::get_transaction_prefix_hash(tx, *prefix_hash);
}
std::uint64_t amount = out.amount;
rct::key mask = rct::identity();
if (!amount && !(ext & db::coinbase_output) && 1 < tx.version)
{
const bool bulletproof2 = (rct::RCTTypeBulletproof2 <= tx.rct_signatures.type);
const auto decrypted = lws::decode_amount(
tx.rct_signatures.outPk.at(index).mask, tx.rct_signatures.ecdhInfo.at(index), active_derived, index, bulletproof2
);
if (!decrypted)
{
MWARNING(user.address() << " failed to decrypt amount for tx " << get_tx_hash() << ", skipping output");
continue; // to next output
}
amount = decrypted->first;
mask = decrypted->second;
ext = db::extra(ext | db::ringct_output);
}
else if (1 < tx.version)
ext = db::extra(ext | db::ringct_output);
if (extra_nonce)
{
if (!payment_id.first && cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.short_))
{
payment_id.first = sizeof(crypto::hash8);
lws::decrypt_payment_id(payment_id.second.short_, active_derived);
}
}
const bool added = output_action(
reader.reader,
user,
db::output{
db::transaction_link{height, get_tx_hash()},
db::output::spend_meta_{
db::output_id{tx.version < 2 ? out.amount : 0, out_ids.at(index)},
amount,
mixin,
boost::numeric_cast<std::uint32_t>(index),
active_pub
},
timestamp,
tx.unlock_time,
*prefix_hash,
out_pub_key,
mask,
{0, 0, 0, 0, 0, 0, 0}, // reserved bytes
db::pack(ext, payment_id.first),
payment_id.second,
cryptonote::get_tx_fee(tx),
account_index
}
);
if (!added)
MWARNING("Output not added, duplicate public key encountered");
} // for all tx outs
} // for all users
}
void scan_transaction(
epee::span<lws::account> users,
const db::block_id height,
const std::uint64_t timestamp,
crypto::hash const& tx_hash,
cryptonote::transaction const& tx,
std::vector<std::uint64_t> const& out_ids,
subaddress_reader& reader)
{
scan_transaction_base(users, height, timestamp, std::addressof(tx_hash), tx, out_ids, reader, add_spend{}, add_output{});
}
void scan_transactions(std::string&& txpool_msg, epee::span<lws::account> users, db::storage const& disk, scanner_sync& self, rpc::client& client, const scanner_options& opts) void scan_transactions(std::string&& txpool_msg, epee::span<lws::account> users, db::storage const& disk, scanner_sync& self, rpc::client& client, const scanner_options& opts)
{ {
// uint64::max is for txpool // uint64::max is for txpool
@@ -628,10 +270,11 @@ namespace lws
const auto time = const auto time =
boost::numeric_cast<std::uint64_t>(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); boost::numeric_cast<std::uint64_t>(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
subaddress_reader reader{std::optional<db::storage>{disk.clone()}, opts.max_subaddresses}; ownership_test scan_transaction{null_spend{}, send_webhook{disk, client, self}};
send_webhook sender{disk, client, self}; if (opts.max_subaddresses > 0)
scan_transaction.enable_subaddresses(disk, opts.max_subaddresses);
for (const auto& tx : parsed->txes) for (const auto& tx : parsed->txes)
scan_transaction_base(users, db::block_id::txpool, time, nullptr, tx, fake_outs, reader, null_spend{}, sender); scan_transaction(users, db::block_id::txpool, time, nullptr, tx, fake_outs);
} }
void do_scan_loop(scanner_sync& self, std::shared_ptr<thread_data> data, const size_t thread_n) noexcept void do_scan_loop(scanner_sync& self, std::shared_ptr<thread_data> data, const size_t thread_n) noexcept
@@ -889,7 +532,9 @@ namespace lws
); );
} }
subaddress_reader reader{disk, opts.max_subaddresses}; ownership_test scan_transaction{add_spend{}, add_output{}};
if (disk && opts.max_subaddresses)
scan_transaction.enable_subaddresses(*disk, opts.max_subaddresses);
db::block_difficulty::unsigned_int diff{}; db::block_difficulty::unsigned_int diff{};
const db::block_id initial_height = db::block_id(fetched->start_height); const db::block_id initial_height = db::block_id(fetched->start_height);
for (auto block_data : boost::combine(blocks, indices)) for (auto block_data : boost::combine(blocks, indices))
@@ -906,18 +551,13 @@ namespace lws
if (indices.empty()) if (indices.empty())
throw std::runtime_error{"Bad daemon response - missing /coinbase tx indices"}; throw std::runtime_error{"Bad daemon response - missing /coinbase tx indices"};
crypto::hash miner_tx_hash;
if (!cryptonote::get_transaction_hash(block.miner_tx, miner_tx_hash))
throw std::runtime_error{"Failed to calculate miner tx hash"};
scan_transaction( scan_transaction(
epee::to_mut_span(users), epee::to_mut_span(users),
db::block_id(fetched->start_height), db::block_id(fetched->start_height),
block.timestamp, block.timestamp,
miner_tx_hash, nullptr,
block.miner_tx, block.miner_tx,
*(indices.begin()), *(indices.begin())
reader
); );
if (opts.untrusted_daemon) if (opts.untrusted_daemon)
@@ -966,10 +606,9 @@ namespace lws
epee::to_mut_span(users), epee::to_mut_span(users),
db::block_id(fetched->start_height), db::block_id(fetched->start_height),
block.timestamp, block.timestamp,
boost::get<0>(tx_data), std::addressof(boost::get<0>(tx_data)), // tx_hashes
boost::get<1>(tx_data), boost::get<1>(tx_data), // txes
boost::get<2>(tx_data), boost::get<2>(tx_data) // indices
reader
); );
} }
@@ -988,10 +627,9 @@ namespace lws
blockchain.push_back(cryptonote::get_block_hash(block)); blockchain.push_back(cryptonote::get_block_hash(block));
} // for each block } // for each block
reader.reader = std::error_code{common_error::kInvalidArgument}; // cleanup reader before next write
MINFO("Thread " << thread_n << " processed " << blockchain.size() << " blocks(s) @ height " << fetched->start_height << " against " << users.size() << " account(s)"); MINFO("Thread " << thread_n << " processed " << blockchain.size() << " blocks(s) @ height " << fetched->start_height << " against " << users.size() << " account(s)");
scan_transaction.disable_subaddresses(); // cleanup reader before next write
if (!store(self.io_, client, self.webhooks_, epee::to_span(blockchain), epee::to_mut_span(users), epee::to_span(new_pow))) if (!store(self.io_, client, self.webhooks_, epee::to_span(blockchain), epee::to_mut_span(users), epee::to_span(new_pow)))
return false; return false;
@@ -1015,7 +653,17 @@ namespace lws
Launches `thread_count` threads to run `scan_loop`, and then polls for Launches `thread_count` threads to run `scan_loop`, and then polls for
active account changes in background active account changes in background
*/ */
void check_loop(scanner_sync& self, db::storage disk, rpc::context& ctx, const std::size_t thread_count, const std::string& lws_server_addr, std::string lws_server_pass, std::vector<lws::account> users, std::vector<db::account_id> active, const scanner_options& opts) void check_loop(
scanner_sync& self,
db::storage disk,
rpc::context& ctx,
std::shared_ptr<lws::mempool> pool,
const std::size_t thread_count,
const std::string& lws_server_addr,
std::string lws_server_pass,
std::vector<lws::account> users,
std::vector<db::account_id> active,
const scanner_options& opts)
{ {
assert(users.size() == active.size()); assert(users.size() == active.size());
assert(thread_count || !lws_server_addr.empty()); assert(thread_count || !lws_server_addr.empty());
@@ -1298,6 +946,35 @@ namespace lws
} }
} }
if (pool) {
auto client = std::make_shared<rpc::client>(MONERO_UNWRAP(ctx.connect()));
MONERO_UNWRAP(client->watch_scan_signals());
threads.emplace_back(attrs, [pool, client, &self] ()
{
MINFO("Pool updater thread started");
while (self.is_running())
{
try
{
auto result = pool_update_loop(pool, *client);
if (!result
&& result.error() != make_error_code(lws::error::signal_abort_process)
&& result.error() != make_error_code(lws::error::signal_abort_scan))
MERROR("Pool update loop failed " << result.error());
}
catch (std::exception const& e)
{
MERROR("Pool update threw " << e.what());
}
catch (...)
{
MERROR("Pool update threw unknown exception");
}
}
MINFO("Pool updater thread stopped");
});
}
users.clear(); users.clear();
users.shrink_to_fit(); users.shrink_to_fit();
@@ -1595,7 +1272,13 @@ namespace lws
return sync_quick(sync_, disk_.clone(), std::move(client), regtest); return sync_quick(sync_, disk_.clone(), std::move(client), regtest);
} }
void scanner::run(rpc::context ctx, std::size_t thread_count, const std::string& lws_server_addr, std::string lws_server_pass, const scanner_options& opts) void scanner::run(
rpc::context ctx,
std::shared_ptr<mempool> pool,
std::size_t thread_count,
const std::string& lws_server_addr,
std::string lws_server_pass,
const scanner_options& opts)
{ {
if (has_shutdown()) if (has_shutdown())
MONERO_THROW(common_error::kInvalidArgument, "this has shutdown"); MONERO_THROW(common_error::kInvalidArgument, "this has shutdown");
@@ -1683,7 +1366,7 @@ namespace lws
} }
} }
else else
check_loop(sync_, disk_.clone(), ctx, thread_count, lws_server_addr, lws_server_pass, std::move(users), std::move(active), opts); check_loop(sync_, disk_.clone(), ctx, pool, thread_count, lws_server_addr, lws_server_pass, std::move(users), std::move(active), opts);
if (has_shutdown()) if (has_shutdown())
return; return;
+8 -1
View File
@@ -35,6 +35,7 @@
#include "db/fwd.h" #include "db/fwd.h"
#include "db/storage.h" #include "db/storage.h"
#include "fwd.h"
#include "net/http/client.h" #include "net/http/client.h"
#include "net/net_ssl.h" // monero/contrib/epee/include #include "net/net_ssl.h" // monero/contrib/epee/include
#include "rpc/client.h" #include "rpc/client.h"
@@ -130,7 +131,13 @@ namespace lws
expect<rpc::client> sync(rpc::client client, const bool untrusted_daemon = false, const bool regtest = false); expect<rpc::client> sync(rpc::client client, const bool untrusted_daemon = false, const bool regtest = false);
//! Poll daemon until `shutdown()` is called, using `thread_count` threads. //! Poll daemon until `shutdown()` is called, using `thread_count` threads.
void run(rpc::context ctx, std::size_t thread_count, const std::string& server_addr, std::string server_pass, const scanner_options&); void run(
rpc::context ctx,
std::shared_ptr<mempool> pool,
std::size_t thread_count,
const std::string& server_addr,
std::string server_pass,
const scanner_options&);
//! \return True iff `stop()` and `shutdown()` has never been called //! \return True iff `stop()` and `shutdown()` has never been called
bool is_running() const noexcept { return sync_.is_running(); } bool is_running() const noexcept { return sync_.is_running(); }
+12 -1
View File
@@ -45,6 +45,7 @@
#include "db/storage.h" #include "db/storage.h"
#include "error.h" #include "error.h"
#include "lws_version.h" #include "lws_version.h"
#include "mempool.h"
#include "options.h" #include "options.h"
#include "rest_server.h" #include "rest_server.h"
#include "scanner.h" #include "scanner.h"
@@ -373,14 +374,23 @@ namespace
//! SIGINT handle registered by `scanner` constructor //! SIGINT handle registered by `scanner` constructor
lws::scanner scanner{disk.clone(), prog.rest_config.webhook_verify}; lws::scanner scanner{disk.clone(), prog.rest_config.webhook_verify};
std::shared_ptr<lws::mempool> mempool;
MINFO("Using monerod ZMQ RPC at " << ctx.daemon_address()); MINFO("Using monerod ZMQ RPC at " << ctx.daemon_address());
if (!sub_address.empty()) if (!sub_address.empty())
{
MINFO("Using monerod ZMQ sub at " << sub_address); MINFO("Using monerod ZMQ sub at " << sub_address);
mempool = std::make_shared<lws::mempool>();
}
auto client = scanner.sync(ctx.connect().value(), prog.untrusted_daemon).value(); auto client = scanner.sync(ctx.connect().value(), prog.untrusted_daemon).value();
lws::rest_server server{ lws::rest_server server{
epee::to_span(prog.rest_servers), prog.admin_rest_servers, std::move(disk), std::move(client), std::move(prog.rest_config) epee::to_span(prog.rest_servers),
prog.admin_rest_servers,
std::move(disk),
std::move(client),
mempool,
std::move(prog.rest_config)
}; };
for (const std::string& address : prog.rest_servers) for (const std::string& address : prog.rest_servers)
MINFO("Listening for REST clients at " << address); MINFO("Listening for REST clients at " << address);
@@ -390,6 +400,7 @@ namespace
// blocks until SIGINT // blocks until SIGINT
scanner.run( scanner.run(
std::move(ctx), std::move(ctx),
mempool,
prog.scan_threads, prog.scan_threads,
std::move(prog.lws_server_addr), std::move(prog.lws_server_addr),
std::move(prog.lws_server_pass), std::move(prog.lws_server_pass),
+2 -2
View File
@@ -26,8 +26,8 @@
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # 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. # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
set(monero-lws-util_sources blocks.cpp gamma_picker.cpp random_outputs.cpp source_location.cpp transactions.cpp) set(monero-lws-util_sources blocks.cpp gamma_picker.cpp ownership_test.cpp random_outputs.cpp source_location.cpp transactions.cpp)
set(monero-lws-util_headers blocks.h fwd.h gamma_picker.h random_outputs.h source_location.h transactions.h) set(monero-lws-util_headers blocks.h fwd.h gamma_picker.h ownership_test.h random_outputs.h source_location.h transactions.h)
add_library(monero-lws-util ${monero-lws-util_sources} ${monero-lws-util_headers}) add_library(monero-lws-util ${monero-lws-util_sources} ${monero-lws-util_headers})
target_link_libraries(monero-lws-util monero::libraries monero-lws-db) target_link_libraries(monero-lws-util monero::libraries monero-lws-db)
+338
View File
@@ -0,0 +1,338 @@
// Copyright (c) 2018-2025, 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 "ownership_test.h"
#include <boost/optional/optional.hpp>
#include <boost/range/combine.hpp>
#include "common/error.h"
#include "crypto/wallet/crypto.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "cryptonote_basic/tx_extra.h"
#include "db/account.h"
#include "db/data.h"
#include "db/storage.h"
#include "error.h"
#include "misc_log_ex.h"
#include "rpc/daemon_messages.h"
#include "util/transactions.h"
namespace lws
{
ownership_test::ownership_test(spend_action on_spend, output_action on_output)
: on_spend(std::move(on_spend))
, on_output(std::move(on_output))
{}
void ownership_test::enable_subaddresses(const db::storage& disk, const std::uint32_t max_subaddresses)
{
subaddress.emplace(disk, max_subaddresses);
}
void ownership_test::disable_subaddresses()
{
subaddress.reset();
}
void ownership_test::operator()(
epee::span<account> users,
db::block_id height,
std::uint64_t timestamp,
crypto::hash const* tx_hash,
const cryptonote::transaction& tx,
const std::vector<std::uint64_t>& out_ids)
{
if (2 < tx.version)
throw std::runtime_error{"Unsupported tx version"};
crypto::hash tx_hash_lazy;
cryptonote::tx_extra_pub_key key;
boost::optional<crypto::hash> prefix_hash;
boost::optional<cryptonote::tx_extra_nonce> extra_nonce;
std::pair<std::uint8_t, db::output::payment_id_> payment_id;
cryptonote::tx_extra_additional_pub_keys additional_tx_pub_keys;
std::vector<crypto::key_derivation> additional_derivations;
const auto get_tx_hash = [&] () -> const crypto::hash&
{
if (!tx_hash)
{
tx_hash_lazy = get_transaction_hash(tx);
tx_hash = std::addressof(tx_hash_lazy);
}
return *tx_hash;
};
{
std::vector<cryptonote::tx_extra_field> extra;
cryptonote::parse_tx_extra(tx.extra, extra);
if (!cryptonote::find_tx_extra_field_by_type(extra, key))
return;
extra_nonce.emplace();
if (cryptonote::find_tx_extra_field_by_type(extra, *extra_nonce))
{
if (cryptonote::get_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.long_))
payment_id.first = sizeof(crypto::hash);
}
else
extra_nonce = boost::none;
if (subaddress)
cryptonote::find_tx_extra_field_by_type(extra, additional_tx_pub_keys);
}
for (account& user : users)
{
if (height <= user.scan_height())
continue;
crypto::key_derivation derived;
if (!crypto::wallet::generate_key_derivation(key.pub_key, user.view_key(), derived))
continue;
if (subaddress && additional_tx_pub_keys.data.size() == tx.vout.size())
{
additional_derivations.resize(tx.vout.size());
for (std::size_t index = 0; index < tx.vout.size(); ++index)
{
if (!crypto::wallet::generate_key_derivation(additional_tx_pub_keys.data[index], user.view_key(), additional_derivations[index]))
{
additional_derivations.clear();
break;
}
}
}
db::extra ext{};
std::uint32_t mixin = 0;
for (auto const& in : tx.vin)
{
if (const auto in_data = boost::get<cryptonote::txin_to_key>(std::addressof(in)))
{
mixin = boost::numeric_cast<std::uint32_t>(std::max<std::size_t>(1, in_data->key_offsets.size()) - 1);
std::uint64_t goffset = 0;
for (std::uint64_t offset : in_data->key_offsets)
{
goffset += offset;
const auto address_index = user.get_spendable(db::output_id{in_data->amount, goffset});
if (!address_index)
continue;
on_spend(
user,
db::spend{
db::transaction_link{height, get_tx_hash()},
in_data->k_image,
db::output_id{in_data->amount, goffset},
timestamp,
tx.unlock_time,
mixin,
{0, 0, 0},
payment_id.first,
payment_id.second.long_,
*address_index
}
);
}
}
else if (boost::get<cryptonote::txin_gen>(std::addressof(in)))
ext = db::extra(ext | db::coinbase_output);
}
for (std::size_t index = 0; index < tx.vout.size(); ++index)
{
crypto::public_key out_pub_key;
if (!cryptonote::get_output_public_key(tx.vout[index], out_pub_key))
continue;
const auto view_tag = cryptonote::get_output_view_tag(tx.vout[index]);
const bool matched =
(!additional_derivations.empty() && cryptonote::out_can_be_to_acc(view_tag, additional_derivations.at(index), index)) ||
cryptonote::out_can_be_to_acc(view_tag, derived, index);
if (!matched)
continue;
bool found_pub = false;
db::address_index account_index{db::major_index::primary, db::minor_index::primary};
crypto::key_derivation active_derivation{};
crypto::public_key active_pub{};
for (std::size_t attempt = 0; attempt < 2; ++attempt)
{
if (attempt == 0)
{
active_derivation = derived;
active_pub = key.pub_key;
}
else if (!additional_derivations.empty())
{
active_derivation = additional_derivations.at(index);
active_pub = additional_tx_pub_keys.data.at(index);
}
else
break;
crypto::public_key derived_pub;
if (!crypto::wallet::derive_subaddress_public_key(out_pub_key, active_derivation, index, derived_pub))
continue;
if (user.spend_public() != derived_pub)
{
if (!subaddress)
continue;
const expect<db::address_index> match = subaddress->find_subaddress(user, derived_pub);
if (!match)
{
if (match != lmdb::error(MDB_NOTFOUND))
MERROR("Failure when doing subaddress search: " << match.error().message());
continue;
}
auto result = subaddress->update_lookahead(user, *match, height);
if (!result)
MWARNING("Failed to update lookahead for " << user.address() << ": " << result.error());
found_pub = true;
account_index = *match;
break;
}
else
{
found_pub = true;
break;
}
}
if (!found_pub)
continue;
if (!prefix_hash)
{
prefix_hash.emplace();
cryptonote::get_transaction_prefix_hash(tx, *prefix_hash);
}
std::uint64_t amount = tx.vout[index].amount;
rct::key mask = rct::identity();
if (!amount && !(ext & db::coinbase_output) && 1 < tx.version)
{
const bool bulletproof2 = (rct::RCTTypeBulletproof2 <= tx.rct_signatures.type);
const auto decrypted = lws::decode_amount(
tx.rct_signatures.outPk.at(index).mask,
tx.rct_signatures.ecdhInfo.at(index),
active_derivation,
index,
bulletproof2
);
if (!decrypted)
{
MWARNING(user.address() << " failed to decrypt amount for tx " << get_tx_hash() << ", skipping output");
continue;
}
amount = decrypted->first;
mask = decrypted->second;
ext = db::extra(ext | db::ringct_output);
}
else if (1 < tx.version)
ext = db::extra(ext | db::ringct_output);
if (extra_nonce && !payment_id.first && cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce->nonce, payment_id.second.short_))
{
payment_id.first = sizeof(crypto::hash8);
lws::decrypt_payment_id(payment_id.second.short_, active_derivation);
}
on_output(
user,
db::output{
db::transaction_link{height, get_tx_hash()},
db::output::spend_meta_{
db::output_id{tx.version < 2 ? tx.vout[index].amount : 0, out_ids.at(index)},
amount,
mixin,
boost::numeric_cast<std::uint32_t>(index),
active_pub
},
timestamp,
tx.unlock_time,
*prefix_hash,
out_pub_key,
mask,
{0, 0, 0, 0, 0, 0, 0},
db::pack(ext, payment_id.first),
payment_id.second,
cryptonote::get_tx_fee(tx),
account_index
}
);
}
}
}
subaddress_reader::subaddress_reader(db::storage const& disk_in, const std::uint32_t max_subaddresses)
: reader(common_error::kInvalidArgument), disk(disk_in.clone()), cur(nullptr), max_subaddresses(max_subaddresses)
{
update_reader();
}
void subaddress_reader::update_reader()
{
reader = disk.start_read();
if (!reader)
MERROR("Subadress lookup failure: " << reader.error().message());
}
expect<db::address_index> subaddress_reader::find_subaddress(const account& user, crypto::public_key const& pubkey)
{
if (!reader)
return {lmdb::error(MDB_NOTFOUND)};
return reader->find_subaddress(user.id(), pubkey, cur);
}
expect<void> subaddress_reader::update_lookahead(const account& user, const db::address_index& match, db::block_id height)
{
if (match.is_zero())
return {}; // keep subaddress disabled servers quick
if (height == db::block_id::txpool)
height = user.scan_height();
auto upserted = disk.update_lookahead(user.db_address(), height, match, max_subaddresses);
if (!upserted)
return upserted.error();
if (0 < *upserted)
update_reader(); // update reader after upsert added new addresses
else if (*upserted < 0)
upserted = {error::max_subaddresses};
return upserted.error();
}
} // namespace lws
+95
View File
@@ -0,0 +1,95 @@
// Copyright (c) 2018-2025, 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.
#pragma once
#include <boost/optional/optional.hpp>
#include <functional>
#include "cryptonote_basic/cryptonote_basic.h"
#include "db/account.h"
#include "db/storage.h"
#include "span.h"
namespace lws
{
class subaddress_reader
{
public:
subaddress_reader(db::storage const& disk_in, const std::uint32_t max_subaddresses);
expect<db::address_index> find_subaddress(const account& user, crypto::public_key const& pubkey);
expect<void> update_lookahead(const account& user, const db::address_index& match, db::block_id height);
private:
expect<db::storage_reader> reader;
db::storage disk;
db::cursor::subaddress_indexes cur;
const std::uint32_t max_subaddresses;
void update_reader();
};
class ownership_test {
public:
using spend_action = std::function<void(account&, db::spend&&)>;
using output_action = std::function<void(account&, db::output&&)>;
ownership_test(spend_action, output_action);
ownership_test(const ownership_test&) = delete;
ownership_test(ownership_test&&) = default;
ownership_test& operator=(const ownership_test&) = delete;
ownership_test& operator=(ownership_test&&) = default;
/*! Tests the transaction against our accounts,
and invokes callbacks for matching inputs or outputs.
@param height mined height
@param timestamp mined block timestamp
@param out_ids maps vout indices to global utxo indexes
*/
void operator()(
epee::span<account> users,
db::block_id height,
std::uint64_t timestamp,
crypto::hash const* tx_hash,
const cryptonote::transaction& tx,
const std::vector<std::uint64_t>& out_ids
);
void enable_subaddresses(const db::storage& disk, const std::uint32_t max_subaddresses);
void disable_subaddresses();
private:
spend_action on_spend;
output_action on_output;
boost::optional<subaddress_reader> subaddress;
};
} // namespace lws
+3 -1
View File
@@ -33,9 +33,10 @@ target_link_libraries(monero-lws-unit-framework)
add_subdirectory(db) add_subdirectory(db)
add_subdirectory(net) add_subdirectory(net)
add_subdirectory(rpc) add_subdirectory(rpc)
add_subdirectory(util)
add_subdirectory(wire) add_subdirectory(wire)
add_executable(monero-lws-unit main.cpp rest.test.cpp scanner.test.cpp) add_executable(monero-lws-unit main.cpp mempool.test.cpp rest.test.cpp scanner.test.cpp)
target_link_libraries(monero-lws-unit target_link_libraries(monero-lws-unit
monero::libraries monero::libraries
monero-lws-daemon-common monero-lws-daemon-common
@@ -44,6 +45,7 @@ target_link_libraries(monero-lws-unit
monero-lws-unit-net monero-lws-unit-net
monero-lws-unit-net-http monero-lws-unit-net-http
monero-lws-unit-rpc monero-lws-unit-rpc
monero-lws-unit-util
monero-lws-unit-wire monero-lws-unit-wire
monero-lws-unit-wire-json monero-lws-unit-wire-json
monero-lws-unit-wire-msgpack monero-lws-unit-wire-msgpack
+137
View File
@@ -0,0 +1,137 @@
// Copyright (c) 2024, 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 <cstring>
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src
#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src
#include "db/account.h"
#include "db/data.h"
#include "mempool.h"
#include "util/account.test.h"
#include "util/transaction.test.h"
namespace
{
lws::db::account make_account(const cryptonote::account_keys& src)
{
lws::db::account out{};
std::memcpy(&out.address.spend_public, &src.m_account_address.m_spend_public_key, sizeof(out.address.spend_public));
std::memcpy(&out.address.view_public, &src.m_account_address.m_view_public_key, sizeof(out.address.view_public));
std::memcpy(&out.key, &unwrap(unwrap(src.m_view_secret_key)), sizeof(out.key));
return out;
}
}
LWS_CASE("mempool")
{
const auto base = lws_test::make_account();
SETUP("mempool")
{
lws::mempool pool{};
lws::account account{make_account(base), {}, {}};
SECTION("empty")
{
EXPECT(pool.scan_account(account).empty());
}
SECTION("add_txs")
{
const auto other = lws_test::make_account();
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
const auto tx1 = lws_test::make_tx(lest_env, base, destinations, 20, true);
destinations.back().amount = 9000;
destinations.back().addr = other.m_account_address;
const auto tx2 = lws_test::make_tx(lest_env, other, destinations, 20, true);
std::array<cryptonote::transaction, 2> txes{{tx1.tx, tx2.tx}};
pool.add_txs(epee::to_mut_span(txes));
auto results = pool.scan_account(account);
EXPECT(results.size() == 1);
EXPECT(results.at(0).hash == get_transaction_hash(tx1.tx));
EXPECT(results.at(0).spends.size() == 0);
EXPECT(results.at(0).outputs.size() == 1);
EXPECT(results.at(0).outputs.at(0).link.height == lws::db::block_id::txpool);
EXPECT(results.at(0).outputs.at(0).link.tx_hash == get_transaction_hash(tx1.tx));
results = pool.scan_account(account);
EXPECT(results.size() == 1);
EXPECT(results.at(0).hash == get_transaction_hash(tx1.tx));
EXPECT(results.at(0).spends.size() == 0);
EXPECT(results.at(0).outputs.size() == 1);
EXPECT(results.at(0).outputs.at(0).link.height == lws::db::block_id::txpool);
EXPECT(results.at(0).outputs.at(0).link.tx_hash == get_transaction_hash(tx1.tx));
pool.reset_txs(lws::mempool::pool_table{});
results = pool.scan_account(account);
EXPECT(results.empty());
}
SECTION("rest_txs")
{
const auto now = std::chrono::system_clock::now();
const auto other = lws_test::make_account();
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
const auto tx1 = lws_test::make_tx(lest_env, base, destinations, 20, true);
destinations.back().amount = 9000;
destinations.back().addr = other.m_account_address;
const auto tx2 = lws_test::make_tx(lest_env, other, destinations, 20, true);
pool.reset_txs(
lws::mempool::pool_table{
{get_transaction_hash(tx1.tx), std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx1.tx}, now)},
{get_transaction_hash(tx2.tx), std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx2.tx}, now)}
}
);
auto results = pool.scan_account(account);
EXPECT(results.size() == 1);
EXPECT(results.at(0).hash == get_transaction_hash(tx1.tx));
EXPECT(results.at(0).spends.size() == 0);
EXPECT(results.at(0).outputs.size() == 1);
EXPECT(results.at(0).outputs.at(0).link.height == lws::db::block_id::txpool);
EXPECT(results.at(0).outputs.at(0).link.tx_hash == get_transaction_hash(tx1.tx));
}
}
}
+180 -45
View File
@@ -31,22 +31,26 @@
#include <boost/beast/websocket/error.hpp> #include <boost/beast/websocket/error.hpp>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <time.h>
#include "cryptonote_basic/account.h" // monero/src
#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src
#include "db/data.h" #include "db/data.h"
#include "db/print.test.h" #include "db/print.test.h"
#include "db/storage.test.h" #include "db/storage.test.h"
#include "db/string.h" #include "db/string.h"
#include "error.h" #include "error.h"
#include "hex.h" // monero/epee/contrib/include #include "hex.h" // monero/epee/contrib/include
#include "mempool.h"
#include "net/http_client.h" #include "net/http_client.h"
#include "rapidjson/document.h" // monero/external/rapidjson/include #include "rapidjson/document.h" // monero/external/rapidjson/include
#include "rapidjson/stringbuffer.h" // monero/external/rapidjson/incldue #include "rapidjson/stringbuffer.h" // monero/external/rapidjson/incldue
#include "rapidjson/prettywriter.h" // monero/external/rapidjson/incldue #include "rapidjson/prettywriter.h" // monero/external/rapidjson/incldue
#include "rest_server.h" #include "rest_server.h"
#include "scanner.test.h"
#include "rpc/pull.test.h" #include "rpc/pull.test.h"
#include "scanner.test.h"
#include "misc_log_ex.h" #include "util/account.test.h"
#include "util/transaction.test.h"
namespace rapidjson namespace rapidjson
{ {
@@ -160,10 +164,13 @@ namespace
LWS_CASE("rest_server") LWS_CASE("rest_server")
{ {
lws::db::account_address account_address{}; const auto now = std::chrono::system_clock::now();
crypto::secret_key view{}; const auto base = lws_test::make_account();
crypto::generate_keys(account_address.spend_public, view); lws::db::account_address account_address{
crypto::generate_keys(account_address.view_public, view); base.m_account_address.m_view_public_key,
base.m_account_address.m_spend_public_key
};
const crypto::secret_key view = base.m_view_secret_key;
const std::string address = lws::db::address_string(account_address); const std::string address = lws::db::address_string(account_address);
const std::string viewkey = epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(view)))); const std::string viewkey = epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(view))));
@@ -175,6 +182,7 @@ LWS_CASE("rest_server")
auto context = auto context =
lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true); lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true);
const auto rpc = MONERO_UNWRAP(context.connect()); const auto rpc = MONERO_UNWRAP(context.connect());
const auto init_server = [&] (std::shared_ptr<lws::mempool> pool)
{ {
const lws::rest_server::configuration config{ const lws::rest_server::configuration config{
{}, {}, std::chrono::seconds{10}, 1, 20, {}, false, true, true, false {}, {}, std::chrono::seconds{10}, 1, 20, {}, false, true, true, false
@@ -185,9 +193,12 @@ LWS_CASE("rest_server")
std::vector<std::string>{admin_server}, std::vector<std::string>{admin_server},
db.clone(), db.clone(),
MONERO_UNWRAP(rpc.clone()), MONERO_UNWRAP(rpc.clone()),
std::move(pool),
config config
); );
} };
init_server(nullptr);
const lws::db::block_info last_block = const lws::db::block_info last_block =
MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_last_block()); MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_last_block());
@@ -592,8 +603,30 @@ LWS_CASE("rest_server")
); );
} }
SECTION("One Receive, One Spend") SECTION("One Receive, One Spend, and one mempool")
{ {
const auto pool = std::make_shared<lws::mempool>();
EXPECT(client.disconnect());
init_server(pool);
EXPECT(client.connect(std::chrono::milliseconds{500}));
lws_test::transaction tx;
{
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
tx = lws_test::make_tx(lest_env, base, destinations, 20, true);
const auto hash = get_transaction_hash(tx.tx);
pool->reset_txs(
lws::mempool::pool_table{
{hash, std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx.tx}, now)}
}
);
}
const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5); const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5);
const std::string start_height = std::to_string(std::uint64_t(account.start_height)); const std::string start_height = std::to_string(std::uint64_t(account.start_height));
message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}";
@@ -621,7 +654,7 @@ LWS_CASE("rest_server")
lws::db::output{ lws::db::output{
link, link,
lws::db::output::spend_meta_{ lws::db::output::spend_meta_{
lws::db::output_id{500, 30}, lws::db::output_id{0, 35},
std::uint64_t(40000), std::uint64_t(40000),
std::uint32_t(16), std::uint32_t(16),
std::uint32_t(2), std::uint32_t(2),
@@ -643,7 +676,7 @@ LWS_CASE("rest_server")
lws::db::spend{ lws::db::spend{
link, link,
image, image,
lws::db::output_id{500, 30}, lws::db::output_id{0, 35},
std::uint64_t(66), std::uint64_t(66),
std::uint64_t(1500), std::uint64_t(1500),
std::uint32_t(16), std::uint32_t(16),
@@ -688,37 +721,61 @@ LWS_CASE("rest_server")
); );
response = invoke(client, "/get_address_txs", message); response = invoke(client, "/get_address_txs", message);
EXPECT(response == const std::string expected =
"{\"total_received\":\"40000\"," R"({
"\"scanned_height\":" + scan_height + "," + "total_received":"48000",
"\"scanned_block_height\":" + scan_height + "," "scanned_height":)" + scan_height + R"(,
"\"start_height\":" + start_height + "," "scanned_block_height":)" + scan_height + R"(,
"\"transaction_height\":" + scan_height + "," "start_height":)" + start_height + R"(,
"\"blockchain_height\":" + scan_height + "," "transaction_height":)" + scan_height + R"(,
"\"transactions\":[" "blockchain_height":)" + scan_height + R"(,
"{\"id\":0," "transactions":[
"\"hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\"," {
"\"timestamp\":\"1970-01-01T01:56:40Z\"," "id":0,
"\"total_received\":\"40000\"," "hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(",
"\"total_sent\":\"40000\"," "timestamp":"1970-01-01T01:56:40Z",
"\"fee\":\"100\"," "total_received":"40000",
"\"unlock_time\":4670," "total_sent":"40000",
"\"height\":4000," "fee":"100",
"\"payment_id\":\"" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + "\"," "unlock_time":4670,
"\"coinbase\":true," "height":4000,
"\"mempool\":false," "payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(",
"\"mixin\":16," "coinbase":true,
"\"recipient\":{\"maj_i\":2,\"min_i\":66}," "mempool":false,
"\"spent_outputs\":[{" "mixin":16,
"\"amount\":\"40000\"," "recipient":{"maj_i":2,"min_i":66},
"\"key_image\":\"" + epee::to_hex::string(epee::as_byte_span(image)) + "\"," "spent_outputs":[{
"\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\"," "amount":"40000",
"\"out_index\":2," "key_image":")" + epee::to_hex::string(epee::as_byte_span(image)) + R"(",
"\"mixin\":16," "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"\"sender\":{\"maj_i\":4,\"min_i\":55}" "out_index":2,
"}]}" "mixin":16,
"]}" "sender":{"maj_i":4,"min_i":55}
); }]
},{
"id":1,
"hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_hash(tx.tx))) + R"(",
"total_received":"8000",
"total_sent":"40000",
"fee":"12000",
"unlock_time":0,
"payment_id":"0000000000000000",
"coinbase":false,
"mempool":true,
"mixin":15,
"recipient":{"maj_i":0,"min_i":0},
"spent_outputs":[{
"amount":"40000",
"key_image":")" + epee::to_hex::string(epee::as_byte_span(tx.images.at(0))) + R"(",
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(",
"out_index":2,
"mixin":15,
"sender":{"maj_i":2, "min_i": 66}
}]
}
]
})";
verify_json(lest_env, "/get_address_txs", response, expected);
std::vector<epee::byte_slice> messages; std::vector<epee::byte_slice> messages;
messages.emplace_back(get_fee_response()); messages.emplace_back(get_fee_response());
@@ -736,8 +793,8 @@ LWS_CASE("rest_server")
"{\"amount\":\"40000\"," "{\"amount\":\"40000\","
"\"public_key\":\"" + epee::to_hex::string(epee::as_byte_span(pub)) + "\"," "\"public_key\":\"" + epee::to_hex::string(epee::as_byte_span(pub)) + "\","
"\"index\":2," "\"index\":2,"
"\"global_index\":30," "\"global_index\":35,"
"\"tx_id\":30," "\"tx_id\":35,"
"\"tx_hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\"," "\"tx_hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\","
"\"tx_prefix_hash\":\"" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + "\"," "\"tx_prefix_hash\":\"" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + "\","
"\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\"," "\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\","
@@ -813,6 +870,7 @@ LWS_CASE("rest_server")
SECTION("feed subscription") SECTION("feed subscription")
{ {
const auto pool = std::make_shared<lws::mempool>();
const std::string scan_height = std::to_string(std::uint64_t(account.scan_height)); const std::string scan_height = std::to_string(std::uint64_t(account.scan_height));
namespace pull = lws_test::rpc::pull; namespace pull = lws_test::rpc::pull;
@@ -861,7 +919,7 @@ LWS_CASE("rest_server")
io.run_one(); io.run_one();
} }
std::string expected = std::string expected=
R"({"scanned_block_height":)" + scan_height + R"(, R"({"scanned_block_height":)" + scan_height + R"(,
"start_height":)" + scan_height + R"(, "start_height":)" + scan_height + R"(,
"blockchain_height":)" + scan_height + "}"; "blockchain_height":)" + scan_height + "}";
@@ -872,6 +930,83 @@ LWS_CASE("rest_server")
response.erase(0, std::strlen("tx_sync:")); response.erase(0, std::strlen("tx_sync:"));
verify_json(lest_env, "tx_sync", response, expected); verify_json(lest_env, "tx_sync", response, expected);
pull::async_close(conn, handler);
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(response == "");
// mempool tx in sync//
init_server(pool);
conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json");
pull::async_handshake(conn, message, handler);
lws_test::transaction tx;
{
std::vector<cryptonote::tx_destination_entry> destinations;
destinations.emplace_back();
destinations.back().amount = 8000;
destinations.back().addr = base.m_account_address;
tx = lws_test::make_tx(lest_env, base, destinations, 20, true);
const auto hash = get_transaction_hash(tx.tx);
pool->reset_txs(
lws::mempool::pool_table{
{hash, std::make_shared<lws::mempool::pool_entry>(cryptonote::transaction{tx.tx}, now)}
}
);
}
ran = false;
error = {};
response = {};
while (!ran && !io.stopped())
{
io.restart();
io.run_one();
}
expected =
R"({"scanned_block_height":)" + scan_height + R"(,
"start_height":)" + scan_height + R"(,
"blockchain_height":)" + scan_height + R"(,
"transactions":[{
"hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_hash(tx.tx))) + R"(",
"prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_prefix_hash(tx.tx))) + R"(",
"timestamp":)" + std::to_string(std::chrono::system_clock::to_time_t(now)) + R"(,
"fee":12000,
"unlock_time":0,
"payment_id": "0000000000000000",
"mempool":true,
"mixin":15,
"receives":[
{
"amount":8000,
"public_key":")" + epee::to_hex::string(epee::as_byte_span(tx.spend_publics.at(0))) + R"(",
"index":0,
"tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx.pub_keys.at(0))) + R"(",
"rct":")" + epee::to_hex::string(epee::as_byte_span(tx.ringct.at(0))) + R"("
}
]
}]
})";
EXPECT(!io.stopped());
EXPECT(error == boost::system::error_code{});
EXPECT(get_prefix(response) == "tx_sync:");
response.erase(0, std::strlen("tx_sync:"));
verify_json(lest_env, "tx_sync non empty", response, expected);
const lws::db::transaction_link link{ const lws::db::transaction_link link{
lws::db::block_id::txpool, crypto::rand<crypto::hash>() lws::db::block_id::txpool, crypto::rand<crypto::hash>()
}; };
+14 -153
View File
@@ -38,11 +38,11 @@
#include "db/print.test.h" #include "db/print.test.h"
#include "db/storage.test.h" #include "db/storage.test.h"
#include "device/device_default.hpp" // monero/src #include "device/device_default.hpp" // monero/src
#include "hardforks/hardforks.h" // monero/src
#include "net/zmq.h" // monero/src #include "net/zmq.h" // monero/src
#include "rpc/client.h" #include "rpc/client.h"
#include "rpc/daemon_messages.h" // monero/src #include "rpc/daemon_messages.h" // monero/src
#include "scanner.h" #include "scanner.h"
#include "util/transaction.test.h"
#include "wire/error.h" #include "wire/error.h"
#include "wire/json/write.h" #include "wire/json/write.h"
@@ -87,147 +87,7 @@ namespace
boost::thread& thread; boost::thread& thread;
~join() { thread.join(); } ~join() { thread.join(); }
}; };
struct transaction
{
cryptonote::transaction tx;
std::vector<crypto::secret_key> additional_keys;
std::vector<crypto::public_key> pub_keys;
std::vector<crypto::public_key> 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<crypto::public_key>& pub_keys;
template<typename T>
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<cryptonote::tx_destination_entry> 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<std::uint32_t>::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<crypto::public_key, cryptonote::subaddress_index> subaddresses;
for (const auto& destination : destinations)
{
++index;
subaddresses[destination.addr.m_spend_public_key] = {0, index};
}
if (2 < destinations.size())
destinations.erase(destinations.begin() + 1, destinations.end() - 1);
std::vector<cryptonote::tx_source_entry> 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::RangeProofPaddedBulletproof, 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<cryptonote::tx_extra_field> 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;
}
void scanner_thread(lws::scanner& scanner, void* ctx, const std::vector<epee::byte_slice>& reply) void scanner_thread(lws::scanner& scanner, void* ctx, const std::vector<epee::byte_slice>& reply)
{ {
struct stop_ struct stop_
@@ -331,6 +191,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
SETUP("lws::rpc::context, ZMQ_REP Server, and lws::db::storage") SETUP("lws::rpc::context, ZMQ_REP Server, and lws::db::storage")
{ {
std::shared_ptr<lws::mempool> pool{};
auto rpc = auto rpc =
lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true); lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true);
@@ -448,15 +309,15 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys.m_account_address; destinations.back().addr = keys.m_account_address;
std::vector<epee::byte_slice> messages{}; std::vector<epee::byte_slice> messages{};
transaction tx = make_miner_tx(lest_env, last_block.id, account, false); lws_test::transaction tx = lws_test::make_miner_tx(lest_env, last_block.id, account, false);
EXPECT(tx.pub_keys.size() == 1); EXPECT(tx.pub_keys.size() == 1);
EXPECT(tx.spend_publics.size() == 1); EXPECT(tx.spend_publics.size() == 1);
transaction tx2 = make_tx(lest_env, keys, destinations, 20, true); lws_test::transaction tx2 = lws_test::make_tx(lest_env, keys, destinations, 20, true);
EXPECT(tx2.pub_keys.size() == 1); EXPECT(tx2.pub_keys.size() == 1);
EXPECT(tx2.spend_publics.size() == 1); EXPECT(tx2.spend_publics.size() == 1);
transaction tx3 = make_tx(lest_env, keys, destinations, 86, false); lws_test::transaction tx3 = lws_test::make_tx(lest_env, keys, destinations, 86, false);
EXPECT(tx3.pub_keys.size() == 1); EXPECT(tx3.pub_keys.size() == 1);
EXPECT(tx3.spend_publics.size() == 1); EXPECT(tx3.spend_publics.size() == 1);
@@ -465,7 +326,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys_subaddr1.m_account_address; destinations.back().addr = keys_subaddr1.m_account_address;
destinations.back().is_subaddress = true; destinations.back().is_subaddress = true;
transaction tx4 = make_tx(lest_env, keys, destinations, 50, false); lws_test::transaction tx4 = lws_test::make_tx(lest_env, keys, destinations, 50, false);
EXPECT(tx4.pub_keys.size() == 1); EXPECT(tx4.pub_keys.size() == 1);
EXPECT(tx4.spend_publics.size() == 2); EXPECT(tx4.spend_publics.size() == 2);
@@ -474,7 +335,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
//destinations.back().addr = keys_subaddr2.m_account_address; //destinations.back().addr = keys_subaddr2.m_account_address;
//destinations.back().is_subaddress = true; //destinations.back().is_subaddress = true;
//transaction tx5 = make_tx(lest_env, keys, destinations, 100, true); //transaction tx5 = lws_test::make_tx(lest_env, keys, destinations, 100, true);
//EXPECT(tx5.pub_keys.size() == 3); //EXPECT(tx5.pub_keys.size() == 3);
//EXPECT(tx5.spend_publics.size() == 3); //EXPECT(tx5.spend_publics.size() == 3);
@@ -542,7 +403,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
lws::scanner scanner{db.clone(), epee::net_utils::ssl_verification_t::none}; lws::scanner scanner{db.clone(), epee::net_utils::ssl_verification_t::none};
boost::thread server_thread(&scanner_thread, std::ref(scanner), rpc.zmq_context(), std::cref(messages)); boost::thread server_thread(&scanner_thread, std::ref(scanner), rpc.zmq_context(), std::cref(messages));
const join on_scope_exit{server_thread}; const join on_scope_exit{server_thread};
scanner.run(std::move(rpc), 1, {}, {}, opts); scanner.run(std::move(rpc), pool, 1, {}, {}, opts);
} }
hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block)); hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block));
@@ -749,15 +610,15 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys.m_account_address; destinations.back().addr = keys.m_account_address;
std::vector<epee::byte_slice> messages{}; std::vector<epee::byte_slice> messages{};
transaction tx = make_miner_tx(lest_env, last_block.id, account, false); lws_test::transaction tx = lws_test::make_miner_tx(lest_env, last_block.id, account, false);
EXPECT(tx.pub_keys.size() == 1); EXPECT(tx.pub_keys.size() == 1);
EXPECT(tx.spend_publics.size() == 1); EXPECT(tx.spend_publics.size() == 1);
transaction tx2 = make_tx(lest_env, keys, destinations, 20, true); lws_test::transaction tx2 = lws_test::make_tx(lest_env, keys, destinations, 20, true);
EXPECT(tx2.pub_keys.size() == 1); EXPECT(tx2.pub_keys.size() == 1);
EXPECT(tx2.spend_publics.size() == 1); EXPECT(tx2.spend_publics.size() == 1);
transaction tx3 = make_tx(lest_env, keys, destinations, 86, false); lws_test::transaction tx3 = lws_test::make_tx(lest_env, keys, destinations, 86, false);
EXPECT(tx3.pub_keys.size() == 1); EXPECT(tx3.pub_keys.size() == 1);
EXPECT(tx3.spend_publics.size() == 1); EXPECT(tx3.spend_publics.size() == 1);
@@ -766,7 +627,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys_subaddr1.m_account_address; destinations.back().addr = keys_subaddr1.m_account_address;
destinations.back().is_subaddress = true; destinations.back().is_subaddress = true;
transaction tx4 = make_tx(lest_env, keys, destinations, 50, false); lws_test::transaction tx4 = lws_test::make_tx(lest_env, keys, destinations, 50, false);
EXPECT(tx4.pub_keys.size() == 1); EXPECT(tx4.pub_keys.size() == 1);
EXPECT(tx4.spend_publics.size() == 2); EXPECT(tx4.spend_publics.size() == 2);
@@ -775,7 +636,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
destinations.back().addr = keys_subaddr2.m_account_address; destinations.back().addr = keys_subaddr2.m_account_address;
destinations.back().is_subaddress = true; destinations.back().is_subaddress = true;
transaction tx5 = make_tx(lest_env, keys, destinations, 146, true); lws_test::transaction tx5 = lws_test::make_tx(lest_env, keys, destinations, 146, true);
EXPECT(tx5.pub_keys.size() == 1); EXPECT(tx5.pub_keys.size() == 1);
EXPECT(tx5.spend_publics.size() == 2); EXPECT(tx5.spend_publics.size() == 2);
@@ -868,7 +729,7 @@ LWS_CASE("lws::scanner::sync and lws::scanner::run")
lws::scanner scanner{db.clone(), epee::net_utils::ssl_verification_t::none}; lws::scanner scanner{db.clone(), epee::net_utils::ssl_verification_t::none};
boost::thread server_thread(&scanner_thread, std::ref(scanner), rpc.zmq_context(), std::cref(messages)); boost::thread server_thread(&scanner_thread, std::ref(scanner), rpc.zmq_context(), std::cref(messages));
const join on_scope_exit{server_thread}; const join on_scope_exit{server_thread};
scanner.run(std::move(rpc), 1, {}, {}, opts); scanner.run(std::move(rpc), pool, 1, {}, {}, opts);
} }
hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block)); hashes.push_back(cryptonote::get_block_hash(bmessage.blocks.back().block));
+30
View File
@@ -0,0 +1,30 @@
# Copyright (c) 2024, 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.
add_library(monero-lws-unit-util account.test.cpp account.test.h transaction.test.cpp transaction.test.h)
target_link_libraries(monero-lws-unit-util monero-lws-unit-framework monero-lws-db monero::libraries)
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) 2026, 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 "account.test.h"
#include "crypto/crypto.h"
#include "cryptonote_basic/account.h"
namespace lws_test
{
cryptonote::account_keys make_account()
{
cryptonote::account_keys out{};
crypto::generate_keys(out.m_account_address.m_spend_public_key, out.m_spend_secret_key);
crypto::generate_keys(out.m_account_address.m_view_public_key, out.m_view_secret_key);
return out;
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (c) 2026, 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.
#pragma once
namespace cryptonote { class account_keys; }
namespace lws_test
{
cryptonote::account_keys make_account();
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright (c) 2026, 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 "transaction.test.h"
#include "crypto/crypto.h" // monero/src
#include "cryptonote_basic/account.h" // monero/src
#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src
#include "db/data.h"
#include "hardforks/hardforks.h" // monero/src
#include "util/transactions.h"
namespace lws_test
{
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_key_image
{
std::vector<crypto::key_image>& images;
template<typename T>
void operator()(const T&) const noexcept
{}
void operator()(const cryptonote::txin_to_key& val) const
{
images.push_back(val.k_image);
}
};
struct get_spend_public
{
std::vector<crypto::public_key>& pub_keys;
template<typename T>
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<cryptonote::tx_destination_entry> 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<std::uint32_t>::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<crypto::public_key, cryptonote::subaddress_index> subaddresses;
for (const auto& destination : destinations)
{
++index;
subaddresses[destination.addr.m_spend_public_key] = {0, index};
}
if (2 < destinations.size())
destinations.erase(destinations.begin() + 1, destinations.end() - 1);
std::vector<cryptonote::tx_source_entry> 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::RangeProofPaddedBulletproof, 2}, use_view_tag
)
);
for (const auto& vin : out.tx.vin)
boost::apply_visitor(get_key_image{out.images}, vin);
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<cryptonote::tx_extra_field> 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;
crypto::key_derivation derivation{};
EXPECT(crypto::generate_key_derivation(key.pub_key, keys.m_view_secret_key, derivation));
for (index = 0; index < out.tx.vout.size(); ++index)
{
const auto decrypted = lws::decode_amount(
out.tx.rct_signatures.outPk.at(index).mask,
out.tx.rct_signatures.ecdhInfo.at(index),
derivation,
index,
true
);
EXPECT(bool(decrypted));
out.ringct.push_back(decrypted->second);
}
}
else
{
index = -1;
for (const auto& this_key : out.additional_keys)
{
++index;
out.pub_keys.emplace_back();
EXPECT(crypto::secret_key_to_public_key(this_key, out.pub_keys.back()));
crypto::key_derivation derivation{};
EXPECT(crypto::generate_key_derivation(out.pub_keys.back(), keys.m_view_secret_key, derivation));
const auto decrypted = lws::decode_amount(
out.tx.rct_signatures.outPk.at(index).mask,
out.tx.rct_signatures.ecdhInfo.at(index),
derivation,
index,
true
);
EXPECT(bool(decrypted));
out.ringct.push_back(decrypted->second);
}
}
for (const auto& rct : out.tx.rct_signatures.ecdhInfo)
out.ringct.push_back(rct.mask);
return out;
}
} // lws_test
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2026, 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.
#pragma once
#include <cstdint>
#include <vector>
#include "cryptonote_basic/cryptonote_basic.h" // monero/src
#include "db/fwd.h"
#include "lest.hpp"
namespace cryptonote
{
class account_keys;
struct public_key;
class secret_key;
class tx_destination_entry;
}
namespace rct { struct key; }
namespace lws_test
{
struct transaction
{
cryptonote::transaction tx;
std::vector<crypto::secret_key> additional_keys;
std::vector<crypto::public_key> pub_keys;
std::vector<crypto::public_key> spend_publics;
std::vector<crypto::key_image> images;
std::vector<rct::key> ringct;
};
transaction make_miner_tx(lest::env& lest_env, lws::db::block_id height, const lws::db::account_address& miner_address, bool use_view_tags);
transaction make_tx(lest::env& lest_env, const cryptonote::account_keys& keys, std::vector<cryptonote::tx_destination_entry> destinations, const std::uint32_t ring_base, const bool use_view_tag);
}