Compare commits
10
Commits
16371c5879
...
6d4ed96f6d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d4ed96f6d | ||
|
|
b49f85bcbe | ||
|
|
0bdea68337 | ||
|
|
c045e56d4e | ||
|
|
be69c9bafe | ||
|
|
d9dae45c39 | ||
|
|
e2210cf5d3 | ||
|
|
25ad23aee4 | ||
|
|
6a6ac8cf99 | ||
|
|
998788d351 |
+7
-5
@@ -2163,10 +2163,10 @@ void App::render()
|
|||||||
// long confirmed (the other leg holds the real count). So: a txid with ANY confirmed leg is
|
// long confirmed (the other leg holds the real count). So: a txid with ANY confirmed leg is
|
||||||
// confirmed, and we count UNIQUE unconfirmed txids — otherwise the badge sticks on stale 0-conf
|
// confirmed, and we count UNIQUE unconfirmed txids — otherwise the badge sticks on stale 0-conf
|
||||||
// legs of already-confirmed transactions and double-counts multi-leg ones.
|
// legs of already-confirmed transactions and double-counts multi-leg ones.
|
||||||
// Recompute only when the tx list actually changed (last_tx_update is bumped on every tx refresh;
|
// Recompute only when the tx list actually changed. tx_revision moves on EVERY content change (incl. a
|
||||||
// size() catches same-second content changes). Otherwise this built two unordered_sets over the whole
|
// confirmation flip at equal tx count) and, unlike last_tx_update, lite stamps it. Otherwise this built two unordered_sets over the whole
|
||||||
// wallet tx list every frame just to size a badge.
|
// wallet tx list every frame just to size a badge.
|
||||||
if (state_.last_tx_update != sb_unconf_key_ts_ || state_.transactions.size() != sb_unconf_key_n_) {
|
if (state_.tx_revision != sb_unconf_key_rev_) {
|
||||||
std::unordered_set<std::string> confirmedTxids;
|
std::unordered_set<std::string> confirmedTxids;
|
||||||
for (const auto& tx : state_.transactions) {
|
for (const auto& tx : state_.transactions) {
|
||||||
if (tx.confirmations >= 1) confirmedTxids.insert(tx.txid);
|
if (tx.confirmations >= 1) confirmedTxids.insert(tx.txid);
|
||||||
@@ -2178,8 +2178,7 @@ void App::render()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb_unconf_count_ = static_cast<int>(unconfirmedTxids.size());
|
sb_unconf_count_ = static_cast<int>(unconfirmedTxids.size());
|
||||||
sb_unconf_key_ts_ = state_.last_tx_update;
|
sb_unconf_key_rev_ = state_.tx_revision;
|
||||||
sb_unconf_key_n_ = state_.transactions.size();
|
|
||||||
}
|
}
|
||||||
sbStatus.unconfirmedTxCount = sb_unconf_count_;
|
sbStatus.unconfirmedTxCount = sb_unconf_count_;
|
||||||
|
|
||||||
@@ -7743,6 +7742,9 @@ void App::shutdown()
|
|||||||
// Lite: let an in-flight wallet op (e.g. Encrypt's save) finish. Bounded: the window is already hidden and
|
// Lite: let an in-flight wallet op (e.g. Encrypt's save) finish. Bounded: the window is already hidden and
|
||||||
// main.cpp has a 15 s watchdog. The backend writes tmp + rename, so being cut off never corrupts the wallet.
|
// main.cpp has a 15 s watchdog. The backend writes tmp + rename, so being cut off never corrupts the wallet.
|
||||||
if (lite_wallet_) lite_wallet_->waitWalletOpsIdle(std::chrono::milliseconds(2500));
|
if (lite_wallet_) lite_wallet_->waitWalletOpsIdle(std::chrono::milliseconds(2500));
|
||||||
|
// Persist refresh-scanned state: ~LiteWalletController never runs on the _Exit path. Bounded (4 s) so the
|
||||||
|
// 15 s watchdog budget holds; the save is tmp + rename, so an abandoned/cut-off save never corrupts.
|
||||||
|
if (lite_wallet_) lite_wallet_->flushOnExit(std::chrono::milliseconds(4000));
|
||||||
|
|
||||||
// Full node: persist whatever a refresh changed since beginShutdown() (everything, on the direct-exit path),
|
// Full node: persist whatever a refresh changed since beginShutdown() (everything, on the direct-exit path),
|
||||||
// then drain + join the history writer. Bounded like the settings flush: on a wedged disk the in-flight write
|
// then drain + join the history writer. Bounded like the settings flush: on a wedged disk the in-flight write
|
||||||
|
|||||||
@@ -1006,10 +1006,9 @@ private:
|
|||||||
mutable std::uint64_t chat_unread_rev_ = ~0ull; // ~0 forces the first compute
|
mutable std::uint64_t chat_unread_rev_ = ~0ull; // ~0 forces the first compute
|
||||||
mutable int chat_unread_cached_ = 0;
|
mutable int chat_unread_cached_ = 0;
|
||||||
mutable double chat_unread_computed_at_ = 0.0;
|
mutable double chat_unread_computed_at_ = 0.0;
|
||||||
// Sidebar unconfirmed-tx badge — recomputed only when the tx list changes (keyed on last_tx_update +
|
// Sidebar unconfirmed-tx badge — recomputed only when the tx list changes (keyed on tx_revision),
|
||||||
// size), not every frame. See App::render().
|
// not every frame. See App::render().
|
||||||
std::int64_t sb_unconf_key_ts_ = -1;
|
std::uint64_t sb_unconf_key_rev_ = 0; // sentinel 0 never matches: tx_revision starts at 1
|
||||||
std::size_t sb_unconf_key_n_ = 0;
|
|
||||||
int sb_unconf_count_ = 0;
|
int sb_unconf_count_ = 0;
|
||||||
// Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS): it runs on this sampler's own
|
// Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS): it runs on this sampler's own
|
||||||
// thread, at most once per 3 s and only while a reader asks (Mining tab memory bar, Peers tab Memory cell).
|
// thread, at most once per 3 s and only while a reader asks (Mining tab memory bar, Peers tab Memory cell).
|
||||||
|
|||||||
@@ -451,8 +451,8 @@ json RPCClient::parseRpcResult(long httpCode, const std::string& body, bool scru
|
|||||||
throw RpcError(errCode, "RPC error: " + err_msg, httpCode);
|
throw RpcError(errCode, "RPC error: " + err_msg, httpCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
json result = response["result"]; // a COPY of the subobject (operator[] yields an lvalue ref)
|
json result = std::move(response["result"]); // move the subtree out: the secret now lives only here
|
||||||
if (scrubSource) scrubJsonSecrets(response); // zero the discarded tree's secret before it frees (B7)
|
if (scrubSource) scrubJsonSecrets(response); // response["result"] is now moved-from (null); harmless (B7)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,7 +684,7 @@ void RPCClient::z_getNewAddress(Callback cb, ErrorCallback err)
|
|||||||
|
|
||||||
void RPCClient::listTransactions(int count, Callback cb, ErrorCallback err)
|
void RPCClient::listTransactions(int count, Callback cb, ErrorCallback err)
|
||||||
{
|
{
|
||||||
doRPC("listtransactions", {"", count}, cb, err);
|
doRPC("listtransactions", {"*", count}, cb, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_viewTransaction(const std::string& txid, Callback cb, ErrorCallback err)
|
void RPCClient::z_viewTransaction(const std::string& txid, Callback cb, ErrorCallback err)
|
||||||
|
|||||||
@@ -285,9 +285,9 @@ private:
|
|||||||
// hold curl_mutex_ and have verified impl_->curl.
|
// hold curl_mutex_ and have verified impl_->curl.
|
||||||
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
||||||
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
||||||
// response["result"] on success. When scrubSource is true (secret-bearing calls), the intermediate
|
// response["result"] on success. The result subtree is moved out of the parse tree, so a secret lives
|
||||||
// parse tree's string values are zeroed before it is discarded — parseRpcResult returns a COPY of
|
// only in the returned node (which callSecret/callSecretString callers wipe). When scrubSource is true
|
||||||
// the result node, so its own tree would otherwise free the secret un-wiped (B7).
|
// (secret-bearing calls), any remaining parse-tree string values are zeroed before it is discarded (B7).
|
||||||
static json parseRpcResult(long httpCode, const std::string& body, bool scrubSource = false);
|
static json parseRpcResult(long httpCode, const std::string& body, bool scrubSource = false);
|
||||||
// A non-200 reply: RpcError when the body carries a JSON-RPC error, else RpcTransportError classified
|
// A non-200 reply: RpcError when the body carries a JSON-RPC error, else RpcTransportError classified
|
||||||
// from the status and plain-text body. hideBody keeps the body out of the message (secret calls).
|
// from the status and plain-text body. hideBody keeps the body out of the message (secret calls).
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
@@ -932,6 +933,9 @@ void NetworkRefreshService::appendTransparentTransactions(std::vector<Transactio
|
|||||||
for (const auto& transactionJson : result) {
|
for (const auto& transactionJson : result) {
|
||||||
TransactionInfo info;
|
TransactionInfo info;
|
||||||
if (auto value = readOptional<std::string>(transactionJson, "txid")) info.txid = *value;
|
if (auto value = readOptional<std::string>(transactionJson, "txid")) info.txid = *value;
|
||||||
|
// C266: listtransactions("*") also returns "move" accounting rows (no txid/address/vout) for
|
||||||
|
// every account. They are not wallet transactions - skip so they never become blank History rows.
|
||||||
|
if (info.txid.empty()) continue;
|
||||||
if (auto value = readOptional<std::string>(transactionJson, "category")) info.type = *value;
|
if (auto value = readOptional<std::string>(transactionJson, "category")) info.type = *value;
|
||||||
if (auto value = readOptional<double>(transactionJson, "amount")) info.amount = *value;
|
if (auto value = readOptional<double>(transactionJson, "amount")) info.amount = *value;
|
||||||
if (auto value = readOptional<std::int64_t>(transactionJson, "time")) {
|
if (auto value = readOptional<std::int64_t>(transactionJson, "time")) {
|
||||||
@@ -1169,11 +1173,33 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
// The first page holds last time's count plus headroom, so most wallets get every row in one
|
// The first page holds last time's count plus headroom, so most wallets get every row in one
|
||||||
// call (re-paging 1000 rows at a time formats ~rows^2/2000 rows); skip-paging stays the fallback.
|
// call (re-paging 1000 rows at a time formats ~rows^2/2000 rows); skip-paging stays the fallback.
|
||||||
const int pageSize = transparentPageSize(snapshot.transparentRowHint);
|
const int pageSize = transparentPageSize(snapshot.transparentRowHint);
|
||||||
|
std::unordered_set<std::string> seenPagedRows;
|
||||||
for (int skip = 0;; skip += pageSize) {
|
for (int skip = 0;; skip += pageSize) {
|
||||||
json page = rpc.call("listtransactions", json::array({"", pageSize, skip}));
|
json page = rpc.call("listtransactions", json::array({"*", pageSize, skip}));
|
||||||
appendTransparentTransactions(result.transactions, knownTxids, page, snapshot.miningAddresses);
|
|
||||||
const std::size_t got = page.is_array() ? page.size() : 0;
|
const std::size_t got = page.is_array() ? page.size() : 0;
|
||||||
result.transparentRowCount += got;
|
// C267: skip-paging walks a list that can shift between RPCs (a new tx pushes the
|
||||||
|
// previous page's tail into this page's head). De-dup on the daemon's stable per-output
|
||||||
|
// identity (txid, send/non-send class, vout). Key on the send/non-send CLASS, not the
|
||||||
|
// exact category, so a coinbase maturing (immature->generate) between the two fetches is
|
||||||
|
// still recognised as the same row; keep vout so two equal-value outputs of one tx stay
|
||||||
|
// distinct. Rows without txid or vout ("move") are never keyed and pass through
|
||||||
|
// (appendTransparentTransactions drops them anyway).
|
||||||
|
json uniq = json::array();
|
||||||
|
if (page.is_array()) {
|
||||||
|
for (const auto& row : page) {
|
||||||
|
auto txid = readOptional<std::string>(row, "txid");
|
||||||
|
auto vout = readOptional<int>(row, "vout");
|
||||||
|
if (txid && !txid->empty() && vout) {
|
||||||
|
const bool isSend =
|
||||||
|
readOptional<std::string>(row, "category").value_or("") == "send";
|
||||||
|
std::string key = *txid + '|' + (isSend ? 's' : 'r') + '|' + std::to_string(*vout);
|
||||||
|
if (!seenPagedRows.insert(std::move(key)).second) continue; // true duplicate
|
||||||
|
}
|
||||||
|
uniq.push_back(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendTransparentTransactions(result.transactions, knownTxids, uniq, snapshot.miningAddresses);
|
||||||
|
result.transparentRowCount += got; // RAW page size: drives the stop test + next row hint
|
||||||
if (got < static_cast<std::size_t>(pageSize)) break;
|
if (got < static_cast<std::size_t>(pageSize)) break;
|
||||||
}
|
}
|
||||||
result.transparentChainOk = true;
|
result.transparentChainOk = true;
|
||||||
@@ -1555,7 +1581,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
|||||||
try {
|
try {
|
||||||
std::set<std::string> recentTxids;
|
std::set<std::string> recentTxids;
|
||||||
std::vector<TransactionInfo> recentTransactions;
|
std::vector<TransactionInfo> recentTransactions;
|
||||||
json transactions = rpc.call("listtransactions", json::array({"", pageSize, 0}));
|
json transactions = rpc.call("listtransactions", json::array({"*", pageSize, 0}));
|
||||||
appendTransparentTransactions(recentTransactions, recentTxids, transactions, snapshot.miningAddresses);
|
appendTransparentTransactions(recentTransactions, recentTxids, transactions, snapshot.miningAddresses);
|
||||||
|
|
||||||
// A coinbase changes category in place (immature -> generate at maturity, or -> orphan on a reorg): drop
|
// A coinbase changes category in place (immature -> generate at maturity, or -> orphan on a reorg): drop
|
||||||
|
|||||||
@@ -11,7 +11,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -499,6 +503,27 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
|||||||
status_ = lifecycle_.status();
|
status_ = lifecycle_.status();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LiteWalletController::flushOnExit(std::chrono::milliseconds budget)
|
||||||
|
{
|
||||||
|
// Same guards as the destructor, plus walletOpInProgress(): App::shutdown() already ran
|
||||||
|
// waitWalletOpsIdle(); if an op is still pending it holds sync_lock and issues its own save,
|
||||||
|
// so skip rather than block the budget on a save that cannot acquire the lock.
|
||||||
|
if (retired_.load() || !walletOpen_.load() || !syncComplete() ||
|
||||||
|
broadcastInProgress() || walletOpInProgress() || !bridge_) return;
|
||||||
|
struct Done { std::mutex m; std::condition_variable cv; bool done = false; };
|
||||||
|
auto st = std::make_shared<Done>();
|
||||||
|
auto bridge = bridge_; // shared_ptr copy; never captures `this`
|
||||||
|
try {
|
||||||
|
std::thread([bridge, st]() {
|
||||||
|
bridge->execute("save", ""); // may block on sync_lock; abandoned on timeout
|
||||||
|
{ std::lock_guard<std::mutex> l(st->m); st->done = true; }
|
||||||
|
st->cv.notify_all();
|
||||||
|
}).detach();
|
||||||
|
} catch (...) { return; } // no thread: skip (pre-fix behaviour, no crash)
|
||||||
|
std::unique_lock<std::mutex> l(st->m);
|
||||||
|
st->cv.wait_for(l, budget, [&] { return st->done; }); // timeout: abandon the thread; _Exit follows
|
||||||
|
}
|
||||||
|
|
||||||
LiteWalletController::~LiteWalletController()
|
LiteWalletController::~LiteWalletController()
|
||||||
{
|
{
|
||||||
scan_->cancel.store(true); // the detached sync thread stops retrying (it may be in a back-off sleep)
|
scan_->cancel.store(true); // the detached sync thread stops retrying (it may be in a back-off sleep)
|
||||||
@@ -515,7 +540,8 @@ LiteWalletController::~LiteWalletController()
|
|||||||
}
|
}
|
||||||
// The sync thread may be blocked in an uninterruptible full scan; detach it. It holds
|
// The sync thread may be blocked in an uninterruptible full scan; detach it. It holds
|
||||||
// shared refs (bridge_ + syncDone_), so it stays safe and the bridge survives until it
|
// shared refs (bridge_ + syncDone_), so it stays safe and the bridge survives until it
|
||||||
// finishes — the process is exiting, so a late litelib_shutdown is harmless.
|
// finishes — on the quit path the process is exiting, so a late litelib_shutdown is harmless
|
||||||
|
// (a server switch already shut the bridge down in retire()).
|
||||||
if (syncThread_.joinable()) syncThread_.detach();
|
if (syncThread_.joinable()) syncThread_.detach();
|
||||||
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
||||||
// running flag + result slot), never `this`, so detaching is safe.
|
// running flag + result slot), never `this`, so detaching is safe.
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ public:
|
|||||||
|
|
||||||
~LiteWalletController(); // stops + joins the background refresh worker
|
~LiteWalletController(); // stops + joins the background refresh worker
|
||||||
|
|
||||||
|
// Best-effort persist on the quit path (the destructor's save is skipped because main() _Exit()s).
|
||||||
|
// Bounded: abandons the save thread on timeout; the process is about to _Exit. Same guards as ~dtor.
|
||||||
|
void flushOnExit(std::chrono::milliseconds budget);
|
||||||
|
|
||||||
LiteWalletController(const LiteWalletController&) = delete;
|
LiteWalletController(const LiteWalletController&) = delete;
|
||||||
LiteWalletController& operator=(const LiteWalletController&) = delete;
|
LiteWalletController& operator=(const LiteWalletController&) = delete;
|
||||||
|
|
||||||
|
|||||||
+106
-4
@@ -1684,6 +1684,20 @@ void testNetworkRefreshSnapshotHelpers()
|
|||||||
EXPECT_EQ(transactions[0].timestamp, static_cast<int64_t>(100));
|
EXPECT_EQ(transactions[0].timestamp, static_cast<int64_t>(100));
|
||||||
EXPECT_EQ(transactions[1].timestamp, static_cast<int64_t>(300));
|
EXPECT_EQ(transactions[1].timestamp, static_cast<int64_t>(300));
|
||||||
|
|
||||||
|
{
|
||||||
|
// C266: "move" accounting rows (no txid) are skipped; labelled-address receive rows are kept.
|
||||||
|
std::vector<dragonx::TransactionInfo> moveTx;
|
||||||
|
std::set<std::string> moveKnown;
|
||||||
|
Refresh::appendTransparentTransactions(moveTx, moveKnown, json::array({
|
||||||
|
json{{"category", "move"}, {"account", "x"}, {"amount", 0.5}, {"time", 50}},
|
||||||
|
json{{"txid", "labelled-recv"}, {"category", "receive"}, {"account", "savings"},
|
||||||
|
{"amount", 2.0}, {"time", 60}, {"confirmations", 3}, {"address", "R-labelled"}}
|
||||||
|
}));
|
||||||
|
EXPECT_EQ(moveTx.size(), static_cast<size_t>(1));
|
||||||
|
EXPECT_EQ(moveTx[0].txid, std::string("labelled-recv"));
|
||||||
|
EXPECT_EQ(moveKnown.count(""), static_cast<size_t>(0));
|
||||||
|
}
|
||||||
|
|
||||||
Refresh::appendShieldedReceivedTransactions(transactions, knownTxids, "zs-viewonly", json::array({
|
Refresh::appendShieldedReceivedTransactions(transactions, knownTxids, "zs-viewonly", json::array({
|
||||||
json{{"txid", "transparent-a"}, {"amount", 9.0}, {"confirmations", 1}},
|
json{{"txid", "transparent-a"}, {"amount", 9.0}, {"confirmations", 1}},
|
||||||
json{{"txid", "shielded-change"}, {"change", true}, {"amount", 2.0}},
|
json{{"txid", "shielded-change"}, {"change", true}, {"amount", 2.0}},
|
||||||
@@ -2411,11 +2425,41 @@ void testNetworkRefreshRpcCollectors()
|
|||||||
}));
|
}));
|
||||||
auto pagedResult = Refresh::collectTransactionRefreshResult(pagedRpc, pagedSnapshot, 327, 0);
|
auto pagedResult = Refresh::collectTransactionRefreshResult(pagedRpc, pagedSnapshot, 327, 0);
|
||||||
EXPECT_TRUE(pagedRpc.methodNames() == std::vector<std::string>({"listtransactions", "listtransactions"}));
|
EXPECT_TRUE(pagedRpc.methodNames() == std::vector<std::string>({"listtransactions", "listtransactions"}));
|
||||||
EXPECT_EQ(pagedRpc.calls[0].params, json::array({"", 1000, 0}));
|
EXPECT_EQ(pagedRpc.calls[0].params, json::array({"*", 1000, 0}));
|
||||||
EXPECT_EQ(pagedRpc.calls[1].params, json::array({"", 1000, 1000}));
|
EXPECT_EQ(pagedRpc.calls[1].params, json::array({"*", 1000, 1000}));
|
||||||
EXPECT_EQ(pagedResult.transactions.size(), static_cast<size_t>(1001));
|
EXPECT_EQ(pagedResult.transactions.size(), static_cast<size_t>(1001));
|
||||||
EXPECT_EQ(pagedResult.transactions[0].txid, std::string("paged-1000"));
|
EXPECT_EQ(pagedResult.transactions[0].txid, std::string("paged-1000"));
|
||||||
|
|
||||||
|
{
|
||||||
|
// C267: a tx added between page RPCs shifts the list, so the previous page's last row reappears at
|
||||||
|
// the head of the next page. Rows (txid, send/non-send, vout) already taken are dropped; a sibling
|
||||||
|
// output of the same tx (different vout) and an un-keyable row (no vout) are kept (fail-open).
|
||||||
|
Refresh::TransactionRefreshSnapshot dedupSnapshot;
|
||||||
|
MockRefreshRpc dedupRpc;
|
||||||
|
json page1 = json::array();
|
||||||
|
for (int i = 0; i < 1000; ++i) {
|
||||||
|
page1.push_back(json{{"txid", "dd-" + std::to_string(i)}, {"vout", 0}, {"category", "receive"},
|
||||||
|
{"amount", 0.01}, {"time", i}, {"confirmations", 10},
|
||||||
|
{"address", "R-dd"}});
|
||||||
|
}
|
||||||
|
dedupRpc.addResponse("listtransactions", page1);
|
||||||
|
dedupRpc.addResponse("listtransactions", json::array({
|
||||||
|
json{{"txid", "dd-999"}, {"vout", 0}, {"category", "receive"}, {"amount", 0.01},
|
||||||
|
{"time", 999}, {"confirmations", 10}, {"address", "R-dd"}}, // repeat: dropped
|
||||||
|
json{{"txid", "dd-999"}, {"vout", 1}, {"category", "receive"}, {"amount", 0.01},
|
||||||
|
{"time", 999}, {"confirmations", 10}, {"address", "R-dd"}}, // sibling vout: kept
|
||||||
|
json{{"txid", "dd-novout"}, {"category", "receive"}, {"amount", 0.02},
|
||||||
|
{"time", 1001}, {"confirmations", 9}, {"address", "R-dd"}} // no vout: kept
|
||||||
|
}));
|
||||||
|
auto dedupResult = Refresh::collectTransactionRefreshResult(dedupRpc, dedupSnapshot, 328, 0);
|
||||||
|
EXPECT_EQ(dedupRpc.calls.size(), static_cast<size_t>(2));
|
||||||
|
EXPECT_EQ(dedupResult.transactions.size(), static_cast<size_t>(1002));
|
||||||
|
EXPECT_EQ(dedupResult.transparentRowCount, static_cast<size_t>(1003)); // RAW count, not deduped
|
||||||
|
size_t dd999 = 0;
|
||||||
|
for (const auto& t : dedupResult.transactions) if (t.txid == "dd-999") ++dd999;
|
||||||
|
EXPECT_EQ(dd999, static_cast<size_t>(2)); // vout 0 once + vout 1
|
||||||
|
}
|
||||||
|
|
||||||
// The pass floor: an address scanned below it is scanned again; without a floor (no pass running)
|
// The pass floor: an address scanned below it is scanned again; without a floor (no pass running)
|
||||||
// only never-scanned addresses are. A pass's floor is its start height, so it completes however far
|
// only never-scanned addresses are. A pass's floor is its start height, so it completes however far
|
||||||
// the tip moves meanwhile (this is what unsticks the perpetual "refreshing history" state).
|
// the tip moves meanwhile (this is what unsticks the perpetual "refreshing history" state).
|
||||||
@@ -2469,7 +2513,7 @@ void testNetworkRefreshRpcCollectors()
|
|||||||
}));
|
}));
|
||||||
auto recent = Refresh::collectRecentTransactionRefreshResult(recentRpc, recentSnapshot, 123);
|
auto recent = Refresh::collectRecentTransactionRefreshResult(recentRpc, recentSnapshot, 123);
|
||||||
EXPECT_TRUE(recentRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
EXPECT_TRUE(recentRpc.methodNames() == std::vector<std::string>({"listtransactions"}));
|
||||||
EXPECT_EQ(recentRpc.calls[0].params, json::array({"", 100, 0}));
|
EXPECT_EQ(recentRpc.calls[0].params, json::array({"*", 100, 0}));
|
||||||
EXPECT_EQ(recent.blockHeight, 123);
|
EXPECT_EQ(recent.blockHeight, 123);
|
||||||
EXPECT_EQ(recent.transactions.size(), static_cast<size_t>(3));
|
EXPECT_EQ(recent.transactions.size(), static_cast<size_t>(3));
|
||||||
EXPECT_EQ(recent.transactions[0].txid, std::string("recent-two"));
|
EXPECT_EQ(recent.transactions[0].txid, std::string("recent-two"));
|
||||||
@@ -10383,6 +10427,63 @@ void testLiteWalletControllerM5Persistence()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// C30: flushOnExit() persists once on the quit path, is bounded when the save blocks, and obeys the guards.
|
||||||
|
void testLiteWalletControllerFlushOnExit()
|
||||||
|
{
|
||||||
|
using namespace dragonx::wallet;
|
||||||
|
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||||
|
const LiteConnectionSettings conn = defaultLiteConnectionSettings();
|
||||||
|
auto openSynced = [&]() {
|
||||||
|
dragonx::test::g_liteFakeSyncBlock = false;
|
||||||
|
auto c = std::make_unique<LiteWalletController>(
|
||||||
|
liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
|
LiteWalletCreateRequest req;
|
||||||
|
(void)c->createWallet(req);
|
||||||
|
for (int i = 0; i < 400 && !c->syncComplete(); ++i)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
|
||||||
|
// (a) open + synced + idle -> exactly one save
|
||||||
|
{
|
||||||
|
auto c = openSynced();
|
||||||
|
EXPECT_TRUE(c->syncComplete());
|
||||||
|
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||||
|
c->flushOnExit(std::chrono::milliseconds(500));
|
||||||
|
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (b) a blocked save never hangs the caller past the budget
|
||||||
|
{
|
||||||
|
auto c = openSynced();
|
||||||
|
dragonx::test::g_liteFakeBlockSave = true;
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
c->flushOnExit(std::chrono::milliseconds(200));
|
||||||
|
const auto dt = std::chrono::steady_clock::now() - t0;
|
||||||
|
EXPECT_TRUE(dt >= std::chrono::milliseconds(150));
|
||||||
|
EXPECT_TRUE(dt < std::chrono::milliseconds(1500));
|
||||||
|
dragonx::test::g_liteFakeBlockSave = false; // let the abandoned save thread finish
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(150));
|
||||||
|
}
|
||||||
|
|
||||||
|
// (c) retired -> no save
|
||||||
|
{
|
||||||
|
auto c = openSynced();
|
||||||
|
c->retire("switch");
|
||||||
|
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||||
|
c->flushOnExit(std::chrono::milliseconds(300));
|
||||||
|
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (d) wallet never opened -> no save
|
||||||
|
{
|
||||||
|
LiteWalletController c(liteCaps, conn, LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
|
const long before = dragonx::test::g_liteFakeSaveCount.load();
|
||||||
|
c.flushOnExit(std::chrono::milliseconds(300));
|
||||||
|
EXPECT_EQ(dragonx::test::g_liteFakeSaveCount.load(), before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Encryption: the controller drives encrypt/unlock/lock/decrypt + encryptionstatus through the
|
// Encryption: the controller drives encrypt/unlock/lock/decrypt + encryptionstatus through the
|
||||||
// (injected fake) backend, wipes passphrases, and folds status into WalletState (encrypted/locked).
|
// (injected fake) backend, wipes passphrases, and folds status into WalletState (encrypted/locked).
|
||||||
void testLiteWalletControllerEncryption()
|
void testLiteWalletControllerEncryption()
|
||||||
@@ -16194,7 +16295,7 @@ void testHistoryCollectorSweepAndEnrichment()
|
|||||||
MockRefreshRpc rpc;
|
MockRefreshRpc rpc;
|
||||||
rpc.addResponse("listtransactions", json::array());
|
rpc.addResponse("listtransactions", json::array());
|
||||||
auto result = Refresh::collectTransactionRefreshResult(rpc, snap, 110, 4);
|
auto result = Refresh::collectTransactionRefreshResult(rpc, snap, 110, 4);
|
||||||
EXPECT_EQ(rpc.calls[0].params, json::array({"", 1700, 0}));
|
EXPECT_EQ(rpc.calls[0].params, json::array({"*", 1700, 0}));
|
||||||
EXPECT_TRUE(result.transparentChainOk);
|
EXPECT_TRUE(result.transparentChainOk);
|
||||||
}
|
}
|
||||||
{ // Same tip, nothing changed: the transparent rows are reused, no listtransactions at all; a tracked
|
{ // Same tip, nothing changed: the transparent rows are reused, no listtransactions at all; a tracked
|
||||||
@@ -18868,6 +18969,7 @@ int main()
|
|||||||
testLiteRolloutPolicyStagedRollout();
|
testLiteRolloutPolicyStagedRollout();
|
||||||
testLiteRolloutManifestLoader();
|
testLiteRolloutManifestLoader();
|
||||||
testLiteWalletControllerRolloutGate();
|
testLiteWalletControllerRolloutGate();
|
||||||
|
testLiteWalletControllerFlushOnExit();
|
||||||
testXmrigReleaseParsing();
|
testXmrigReleaseParsing();
|
||||||
testXmrigAssetSelection();
|
testXmrigAssetSelection();
|
||||||
testXmrigChecksumParsing();
|
testXmrigChecksumParsing();
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ use tokio_rustls::{rustls::ClientConfig};
|
|||||||
use tonic::{Request};
|
use tonic::{Request};
|
||||||
|
|
||||||
use threadpool::ThreadPool;
|
use threadpool::ThreadPool;
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::{channel, sync_channel};
|
||||||
|
|
||||||
use crate::PubCertificate;
|
use crate::PubCertificate;
|
||||||
use crate::grpc_client::compact_tx_streamer_client::CompactTxStreamerClient;
|
use crate::grpc_client::compact_tx_streamer_client::CompactTxStreamerClient;
|
||||||
@@ -113,7 +113,7 @@ where F : Fn(&[u8], u64) {
|
|||||||
|
|
||||||
let request = Request::new(BlockRange { start: Some(bs), end: Some(be) });
|
let request = Request::new(BlockRange { start: Some(bs), end: Some(be) });
|
||||||
|
|
||||||
let (tx, rx) = channel::<Option<CompactBlock>>();
|
let (tx, rx) = sync_channel::<Option<CompactBlock>>(2048);
|
||||||
let (ftx, frx) = channel();
|
let (ftx, frx) = channel();
|
||||||
|
|
||||||
pool.execute(move || {
|
pool.execute(move || {
|
||||||
|
|||||||
@@ -908,7 +908,7 @@ impl LightClient {
|
|||||||
|
|
||||||
// Write to a temporary file first (atomic save). C254/s1-D9: a fresh owner-only file (0600 on unix); a stale
|
// Write to a temporary file first (atomic save). C254/s1-D9: a fresh owner-only file (0600 on unix); a stale
|
||||||
// .dat.tmp is unlinked first, never reused. The rename carries the mode to the wallet.
|
// .dat.tmp is unlinked first, never reused. The rename carries the mode to the wallet.
|
||||||
let wallet = self.wallet.write().unwrap();
|
let wallet = self.wallet.read().unwrap();
|
||||||
let mut file_buffer = BufWriter::with_capacity(
|
let mut file_buffer = BufWriter::with_capacity(
|
||||||
1_000_000, // 1 MB write buffer
|
1_000_000, // 1 MB write buffer
|
||||||
create_private_file_new(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?);
|
create_private_file_new(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?);
|
||||||
@@ -1660,8 +1660,9 @@ pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
|||||||
// get_block_range streams an ENTIRE range over ONE gRPC connection, so the batch size is
|
// get_block_range streams an ENTIRE range over ONE gRPC connection, so the batch size is
|
||||||
// the dominant factor in how many TCP connections a sync opens. The old 1000 meant ~1300
|
// the dominant factor in how many TCP connections a sync opens. The old 1000 meant ~1300
|
||||||
// connections for a full sync, which on Windows exhausted socket buffers (WSAENOBUFS / os
|
// connections for a full sync, which on Windows exhausted socket buffers (WSAENOBUFS / os
|
||||||
// error 10055). A large batch (25k) cuts that ~50x (~52 connections for a full sync) with
|
// error 10055). A large batch (25k) cuts that ~50x (~52 connections for a full sync). Blocks
|
||||||
// no extra memory — blocks are still streamed + scanned incrementally, not buffered.
|
// are now buffered up to 2048 deep (bounded backpressure, C240), still one connection per
|
||||||
|
// 25k batch; scan_batch_size must NOT shrink or Windows WSAENOBUFS returns.
|
||||||
let scan_batch_size = 25000;
|
let scan_batch_size = 25000;
|
||||||
let mut end_height = std::cmp::min(last_scanned_height + scan_batch_size, latest_block);
|
let mut end_height = std::cmp::min(last_scanned_height + scan_batch_size, latest_block);
|
||||||
|
|
||||||
@@ -1805,7 +1806,7 @@ pub fn start_mempool_monitor(lc: Arc<LightClient>) -> Result<(), String> {
|
|||||||
self.taddr_note_unscanned(start_height, &block_times);
|
self.taddr_note_unscanned(start_height, &block_times);
|
||||||
// Reset the scanning heights
|
// Reset the scanning heights
|
||||||
last_scanned_height = (invalid_height - 1) as u64;
|
last_scanned_height = (invalid_height - 1) as u64;
|
||||||
end_height = std::cmp::min(last_scanned_height + scan_batch_size, latest_block);
|
end_height = std::cmp::min(last_scanned_height + crate::lightwallet::MAX_REORG as u64 + 1, latest_block);
|
||||||
|
|
||||||
warn!("Reorg: reset scanning from {} to {}", last_scanned_height, end_height);
|
warn!("Reorg: reset scanning from {} to {}", last_scanned_height, end_height);
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -659,15 +659,17 @@ impl LightWallet {
|
|||||||
return "Error: Can't add key while wallet is encrypted".to_string();
|
return "Error: Can't add key while wallet is encrypted".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = self.tkeys.read().unwrap().len() as u32;
|
// PBKDF2 (the slow part) outside the lock; hold ONE write guard across read-modify-write.
|
||||||
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
|
||||||
|
let mut tkeys = self.tkeys.write().unwrap();
|
||||||
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
let mut pos = tkeys.len() as u32; // same start index as today (counts imported keys too - unchanged)
|
||||||
let address = self.address_from_sk(&sk);
|
loop {
|
||||||
|
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
||||||
self.tkeys.write().unwrap().push(WalletTKey::new_hdkey(sk, address.clone()));
|
let address = self.address_from_sk(&sk);
|
||||||
|
if tkeys.iter().any(|t| t.address == address) { pos += 1; continue; }
|
||||||
address
|
tkeys.push(WalletTKey::new_hdkey(sk, address.clone()));
|
||||||
|
return address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_taddr(&mut self, sk: String, birthday: u64) -> String {
|
pub fn import_taddr(&mut self, sk: String, birthday: u64) -> String {
|
||||||
|
|||||||
+21
-1
@@ -1,5 +1,7 @@
|
|||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
use pairing::bls12_381::{Bls12};
|
use pairing::bls12_381::{Bls12};
|
||||||
@@ -26,6 +28,24 @@ use zcash_primitives::{
|
|||||||
use zcash_primitives::zip32::ExtendedSpendingKey;
|
use zcash_primitives::zip32::ExtendedSpendingKey;
|
||||||
|
|
||||||
|
|
||||||
|
// C241: wallet open re-parsed the 169-byte ExtendedFullViewingKey once PER NOTE. Memoize the decode,
|
||||||
|
// keyed by the exact 169 raw bytes (content-addressed, so the result is always identical to a fresh
|
||||||
|
// parse). Thread-local and naturally bounded by the handful of distinct keys a wallet holds.
|
||||||
|
thread_local! {
|
||||||
|
static EFVK_CACHE: RefCell<HashMap<[u8; 169], ExtendedFullViewingKey>> = RefCell::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_efvk_cached<R: Read>(r: &mut R) -> io::Result<ExtendedFullViewingKey> {
|
||||||
|
let mut raw = [0u8; 169];
|
||||||
|
r.read_exact(&mut raw)?;
|
||||||
|
if let Some(k) = EFVK_CACHE.with(|c| c.borrow().get(&raw).cloned()) {
|
||||||
|
return Ok(k);
|
||||||
|
}
|
||||||
|
let k = ExtendedFullViewingKey::read(&raw[..])?; // identical parser on identical bytes
|
||||||
|
EFVK_CACHE.with(|c| c.borrow_mut().insert(raw, k.clone()));
|
||||||
|
Ok(k)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct BlockData {
|
pub struct BlockData {
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
pub hash: BlockHash,
|
pub hash: BlockHash,
|
||||||
@@ -147,7 +167,7 @@ impl SaplingNoteData {
|
|||||||
|
|
||||||
let account = reader.read_u64::<LittleEndian>()? as usize;
|
let account = reader.read_u64::<LittleEndian>()? as usize;
|
||||||
|
|
||||||
let extfvk = ExtendedFullViewingKey::read(&mut reader)?;
|
let extfvk = read_efvk_cached(&mut reader)?;
|
||||||
|
|
||||||
let mut diversifier_bytes = [0u8; 11];
|
let mut diversifier_bytes = [0u8; 11];
|
||||||
reader.read_exact(&mut diversifier_bytes)?;
|
reader.read_exact(&mut diversifier_bytes)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user