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
|
||||
// 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.
|
||||
// Recompute only when the tx list actually changed (last_tx_update is bumped on every tx refresh;
|
||||
// size() catches same-second content changes). Otherwise this built two unordered_sets over the whole
|
||||
// Recompute only when the tx list actually changed. tx_revision moves on EVERY content change (incl. a
|
||||
// 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.
|
||||
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;
|
||||
for (const auto& tx : state_.transactions) {
|
||||
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_key_ts_ = state_.last_tx_update;
|
||||
sb_unconf_key_n_ = state_.transactions.size();
|
||||
sb_unconf_key_rev_ = state_.tx_revision;
|
||||
}
|
||||
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
|
||||
// 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));
|
||||
// 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),
|
||||
// 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 int chat_unread_cached_ = 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 +
|
||||
// size), not every frame. See App::render().
|
||||
std::int64_t sb_unconf_key_ts_ = -1;
|
||||
std::size_t sb_unconf_key_n_ = 0;
|
||||
// Sidebar unconfirmed-tx badge — recomputed only when the tx list changes (keyed on tx_revision),
|
||||
// not every frame. See App::render().
|
||||
std::uint64_t sb_unconf_key_rev_ = 0; // sentinel 0 never matches: tx_revision starts at 1
|
||||
int sb_unconf_count_ = 0;
|
||||
// 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).
|
||||
|
||||
@@ -451,8 +451,8 @@ json RPCClient::parseRpcResult(long httpCode, const std::string& body, bool scru
|
||||
throw RpcError(errCode, "RPC error: " + err_msg, httpCode);
|
||||
}
|
||||
|
||||
json result = response["result"]; // a COPY of the subobject (operator[] yields an lvalue ref)
|
||||
if (scrubSource) scrubJsonSecrets(response); // zero the discarded tree's secret before it frees (B7)
|
||||
json result = std::move(response["result"]); // move the subtree out: the secret now lives only here
|
||||
if (scrubSource) scrubJsonSecrets(response); // response["result"] is now moved-from (null); harmless (B7)
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -684,7 +684,7 @@ void RPCClient::z_getNewAddress(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)
|
||||
|
||||
@@ -285,9 +285,9 @@ private:
|
||||
// hold curl_mutex_ and have verified impl_->curl.
|
||||
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
||||
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
||||
// response["result"] on success. When scrubSource is true (secret-bearing calls), the intermediate
|
||||
// parse tree's string values are zeroed before it is discarded — parseRpcResult returns a COPY of
|
||||
// the result node, so its own tree would otherwise free the secret un-wiped (B7).
|
||||
// response["result"] on success. The result subtree is moved out of the parse tree, so a secret lives
|
||||
// only in the returned node (which callSecret/callSecretString callers wipe). When scrubSource is true
|
||||
// (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);
|
||||
// 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).
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -932,6 +933,9 @@ void NetworkRefreshService::appendTransparentTransactions(std::vector<Transactio
|
||||
for (const auto& transactionJson : result) {
|
||||
TransactionInfo info;
|
||||
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<double>(transactionJson, "amount")) info.amount = *value;
|
||||
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
|
||||
// call (re-paging 1000 rows at a time formats ~rows^2/2000 rows); skip-paging stays the fallback.
|
||||
const int pageSize = transparentPageSize(snapshot.transparentRowHint);
|
||||
std::unordered_set<std::string> seenPagedRows;
|
||||
for (int skip = 0;; skip += pageSize) {
|
||||
json page = rpc.call("listtransactions", json::array({"", pageSize, skip}));
|
||||
appendTransparentTransactions(result.transactions, knownTxids, page, snapshot.miningAddresses);
|
||||
json page = rpc.call("listtransactions", json::array({"*", pageSize, skip}));
|
||||
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;
|
||||
}
|
||||
result.transparentChainOk = true;
|
||||
@@ -1555,7 +1581,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
|
||||
try {
|
||||
std::set<std::string> recentTxids;
|
||||
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);
|
||||
|
||||
// A coinbase changes category in place (immature -> generate at maturity, or -> orphan on a reorg): drop
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
@@ -499,6 +503,27 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
||||
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()
|
||||
{
|
||||
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
|
||||
// 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();
|
||||
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
||||
// running flag + result slot), never `this`, so detaching is safe.
|
||||
|
||||
@@ -175,6 +175,10 @@ public:
|
||||
|
||||
~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& 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[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({
|
||||
json{{"txid", "transparent-a"}, {"amount", 9.0}, {"confirmations", 1}},
|
||||
json{{"txid", "shielded-change"}, {"change", true}, {"amount", 2.0}},
|
||||
@@ -2411,11 +2425,41 @@ void testNetworkRefreshRpcCollectors()
|
||||
}));
|
||||
auto pagedResult = Refresh::collectTransactionRefreshResult(pagedRpc, pagedSnapshot, 327, 0);
|
||||
EXPECT_TRUE(pagedRpc.methodNames() == std::vector<std::string>({"listtransactions", "listtransactions"}));
|
||||
EXPECT_EQ(pagedRpc.calls[0].params, json::array({"", 1000, 0}));
|
||||
EXPECT_EQ(pagedRpc.calls[1].params, json::array({"", 1000, 1000}));
|
||||
EXPECT_EQ(pagedRpc.calls[0].params, json::array({"*", 1000, 0}));
|
||||
EXPECT_EQ(pagedRpc.calls[1].params, json::array({"*", 1000, 1000}));
|
||||
EXPECT_EQ(pagedResult.transactions.size(), static_cast<size_t>(1001));
|
||||
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)
|
||||
// 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).
|
||||
@@ -2469,7 +2513,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
}));
|
||||
auto recent = Refresh::collectRecentTransactionRefreshResult(recentRpc, recentSnapshot, 123);
|
||||
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.transactions.size(), static_cast<size_t>(3));
|
||||
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
|
||||
// (injected fake) backend, wipes passphrases, and folds status into WalletState (encrypted/locked).
|
||||
void testLiteWalletControllerEncryption()
|
||||
@@ -16194,7 +16295,7 @@ void testHistoryCollectorSweepAndEnrichment()
|
||||
MockRefreshRpc rpc;
|
||||
rpc.addResponse("listtransactions", json::array());
|
||||
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);
|
||||
}
|
||||
{ // Same tip, nothing changed: the transparent rows are reused, no listtransactions at all; a tracked
|
||||
@@ -18868,6 +18969,7 @@ int main()
|
||||
testLiteRolloutPolicyStagedRollout();
|
||||
testLiteRolloutManifestLoader();
|
||||
testLiteWalletControllerRolloutGate();
|
||||
testLiteWalletControllerFlushOnExit();
|
||||
testXmrigReleaseParsing();
|
||||
testXmrigAssetSelection();
|
||||
testXmrigChecksumParsing();
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ use tokio_rustls::{rustls::ClientConfig};
|
||||
use tonic::{Request};
|
||||
|
||||
use threadpool::ThreadPool;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::mpsc::{channel, sync_channel};
|
||||
|
||||
use crate::PubCertificate;
|
||||
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 (tx, rx) = channel::<Option<CompactBlock>>();
|
||||
let (tx, rx) = sync_channel::<Option<CompactBlock>>(2048);
|
||||
let (ftx, frx) = channel();
|
||||
|
||||
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
|
||||
// .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(
|
||||
1_000_000, // 1 MB write buffer
|
||||
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
|
||||
// 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
|
||||
// error 10055). A large batch (25k) cuts that ~50x (~52 connections for a full sync) with
|
||||
// no extra memory — blocks are still streamed + scanned incrementally, not buffered.
|
||||
// error 10055). A large batch (25k) cuts that ~50x (~52 connections for a full sync). Blocks
|
||||
// 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 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);
|
||||
// Reset the scanning heights
|
||||
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);
|
||||
|
||||
|
||||
+10
-8
@@ -659,15 +659,17 @@ impl LightWallet {
|
||||
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 sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
||||
let address = self.address_from_sk(&sk);
|
||||
|
||||
self.tkeys.write().unwrap().push(WalletTKey::new_hdkey(sk, address.clone()));
|
||||
|
||||
address
|
||||
let mut tkeys = self.tkeys.write().unwrap();
|
||||
let mut pos = tkeys.len() as u32; // same start index as today (counts imported keys too - unchanged)
|
||||
loop {
|
||||
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
||||
let address = self.address_from_sk(&sk);
|
||||
if tkeys.iter().any(|t| t.address == address) { pos += 1; continue; }
|
||||
tkeys.push(WalletTKey::new_hdkey(sk, address.clone()));
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
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::sync::Arc;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use pairing::bls12_381::{Bls12};
|
||||
@@ -26,6 +28,24 @@ use zcash_primitives::{
|
||||
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 height: i32,
|
||||
pub hash: BlockHash,
|
||||
@@ -147,7 +167,7 @@ impl SaplingNoteData {
|
||||
|
||||
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];
|
||||
reader.read_exact(&mut diversifier_bytes)?;
|
||||
|
||||
Reference in New Issue
Block a user