fix(resilience,perf): gate send on a known tip, surface dropped/stale txs, advance cached confs, memoize explorer
Backlog mediums (display/gating/caching only — no send/sweep/encrypt call or args changed): Resilience: - Gate Send/Review on a KNOWN peer-derived tip (SyncInfo::sendTipReady): a fresh reconnect or peerless node has tip_known=false, so its local height isn't a trustworthy network tip — sending is disabled with a clear tooltip until it is. Lite is exempt (its own sync gate). The z_sendmany call is untouched. - History now shows "Dropped" for a conflicted tx (confirmations < 0) and "Expired?" for one unconfirmed > 1h, instead of "Pending" forever (new TransactionInfo::isConflicted/isStalePending; the tab inlines the check on its DisplayTx fields). - A cached z_viewtransaction row's confirmations now advance by the blocks mined since it was sampled (observed_height), instead of freezing at the cached value; never goes backwards; pending (0) isn't extrapolated. Performance: - Explorer block list uses loadRangeCached() (memoized on the cache revision) instead of loadRange(), so it only hits SQLite after the cache changes, not every frame. Tests: testResilienceMediums (tip gate, confirmations-advance, dropped/stale predicates) + loadRangeCached memoization in testExplorerBlockCache (isolated on its own cache). Held: Overview/Balance per-frame list cache, peers clippers, off-thread tx-history cache load (larger refactors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,13 @@ struct TransactionInfo {
|
|||||||
std::string getTypeDisplay() const;
|
std::string getTypeDisplay() const;
|
||||||
bool isConfirmed() const { return confirmations >= 1; }
|
bool isConfirmed() const { return confirmations >= 1; }
|
||||||
bool isMature() const { return confirmations >= 100; }
|
bool isMature() const { return confirmations >= 100; }
|
||||||
|
// gettransaction reports confirmations < 0 for a tx conflicted with the chain (double-spent / reorged out).
|
||||||
|
bool isConflicted() const { return confirmations < 0; }
|
||||||
|
// Unconfirmed for longer than maxAgeSec since the wallet first saw it: most likely dropped from the
|
||||||
|
// mempool or expired. A display heuristic only (timestamp 0 = unknown -> never flagged).
|
||||||
|
bool isStalePending(int64_t now, int64_t maxAgeSec = 3600) const {
|
||||||
|
return confirmations == 0 && timestamp > 0 && now - timestamp > maxAgeSec;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exact field-wise equality: decides whether a refreshed list really differs. `amount` is compared
|
// Exact field-wise equality: decides whether a refreshed list really differs. `amount` is compared
|
||||||
@@ -198,6 +205,11 @@ struct SyncInfo {
|
|||||||
bool tip_known = false;
|
bool tip_known = false;
|
||||||
|
|
||||||
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
|
bool isSynced() const { return tip_known && !syncing && blocks > 0 && blocks >= headers - 2; }
|
||||||
|
|
||||||
|
// Send precondition: the wallet is caught up to a KNOWN peer-derived tip. A fresh (re)connect or a
|
||||||
|
// peerless node has tip_known == false, so its local height can't be trusted as the network tip.
|
||||||
|
// The lite backend never populates tip_known, so lite is exempt (it has its own sync gate).
|
||||||
|
bool sendTipReady(bool liteBuild) const { return liteBuild || (tip_known && !syncing && blocks > 0); }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1056,8 +1056,13 @@ NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseVie
|
|||||||
|
|
||||||
void NetworkRefreshService::appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
void NetworkRefreshService::appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||||
const std::string& txid,
|
const std::string& txid,
|
||||||
const TransactionViewCacheEntry& entry)
|
const TransactionViewCacheEntry& entry,
|
||||||
|
int currentHeight)
|
||||||
{
|
{
|
||||||
|
// A mined entry's confirmations grow by one per block since it was sampled; never go backwards.
|
||||||
|
int entryConfirmations = entry.confirmations;
|
||||||
|
if (entry.confirmations >= 1 && entry.observed_height >= 0 && currentHeight > entry.observed_height)
|
||||||
|
entryConfirmations += currentHeight - entry.observed_height;
|
||||||
for (const auto& out : entry.outgoing_outputs) {
|
for (const auto& out : entry.outgoing_outputs) {
|
||||||
bool alreadyTracked = false;
|
bool alreadyTracked = false;
|
||||||
for (const auto& existing : transactions) {
|
for (const auto& existing : transactions) {
|
||||||
@@ -1089,7 +1094,7 @@ void NetworkRefreshService::appendViewTransactionOutputs(std::vector<Transaction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!foundExistingMetadata) {
|
if (!foundExistingMetadata) {
|
||||||
info.confirmations = entry.confirmations;
|
info.confirmations = entryConfirmations;
|
||||||
info.timestamp = entry.timestamp;
|
info.timestamp = entry.timestamp;
|
||||||
}
|
}
|
||||||
transactions.push_back(std::move(info));
|
transactions.push_back(std::move(info));
|
||||||
@@ -1476,7 +1481,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
auto cached = snapshot.viewTxCache.find(txid);
|
auto cached = snapshot.viewTxCache.find(txid);
|
||||||
if (cached != snapshot.viewTxCache.end()) {
|
if (cached != snapshot.viewTxCache.end()) {
|
||||||
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
if (!trackedSend || !cached->second.outgoing_outputs.empty()) {
|
||||||
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
appendViewTransactionOutputs(result.transactions, txid, cached->second, currentBlockHeight);
|
||||||
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
||||||
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
||||||
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
||||||
@@ -1509,6 +1514,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
||||||
|
entry.observed_height = currentBlockHeight; // confirmations below are sampled at this tip
|
||||||
const bool storedRow = rowBacklog.count(txid) > 0;
|
const bool storedRow = rowBacklog.count(txid) > 0;
|
||||||
if (storedRow && entry.timestamp == 0) {
|
if (storedRow && entry.timestamp == 0) {
|
||||||
const auto settled = settledStoredMeta.find(txid);
|
const auto settled = settledStoredMeta.find(txid);
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ public:
|
|||||||
std::string from_address;
|
std::string from_address;
|
||||||
std::int64_t timestamp = 0;
|
std::int64_t timestamp = 0;
|
||||||
int confirmations = 0;
|
int confirmations = 0;
|
||||||
|
int observed_height = -1; // chain tip when `confirmations` was read (-1 unknown): lets a cached entry advance
|
||||||
struct Output {
|
struct Output {
|
||||||
std::string address;
|
std::string address;
|
||||||
double value = 0.0;
|
double value = 0.0;
|
||||||
@@ -427,9 +428,12 @@ public:
|
|||||||
const std::set<std::string>& miningAddresses = {},
|
const std::set<std::string>& miningAddresses = {},
|
||||||
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
|
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
|
||||||
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
||||||
|
// currentHeight (>=0): the present chain tip; a cached entry that was already mined then reports
|
||||||
|
// confirmations advanced by the blocks added since it was observed (display only).
|
||||||
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||||
const std::string& txid,
|
const std::string& txid,
|
||||||
const TransactionViewCacheEntry& entry);
|
const TransactionViewCacheEntry& entry,
|
||||||
|
int currentHeight = -1);
|
||||||
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
|
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
|
||||||
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
|
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
|
||||||
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
|
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
|
||||||
|
|||||||
@@ -846,7 +846,8 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
|
|
||||||
std::map<int, BlockSummary> cachedBlocks;
|
std::map<int, BlockSummary> cachedBlocks;
|
||||||
if (!pageHeights.empty()) {
|
if (!pageHeights.empty()) {
|
||||||
cachedBlocks = s_recent_block_cache.loadRange(pageHeights.back(), pageHeights.front());
|
// Memoized on the cache revision: SQLite is only hit after the cache changed, not every frame.
|
||||||
|
cachedBlocks = s_recent_block_cache.loadRangeCached(pageHeights.back(), pageHeights.front());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
|
// Fuzzy mode: a non-numeric, non-full-hash query filters the list to cached blocks whose hash
|
||||||
@@ -890,7 +891,7 @@ static void renderRecentBlocks(App* app, float availWidth) {
|
|||||||
}
|
}
|
||||||
std::map<int, BlockSummary> nextCachedBlocks;
|
std::map<int, BlockSummary> nextCachedBlocks;
|
||||||
if (!nextPageHeights.empty()) {
|
if (!nextPageHeights.empty()) {
|
||||||
nextCachedBlocks = s_recent_block_cache.loadRange(nextPageHeights.back(), nextPageHeights.front());
|
nextCachedBlocks = s_recent_block_cache.loadRangeCached(nextPageHeights.back(), nextPageHeights.front());
|
||||||
}
|
}
|
||||||
for (int height : nextPageHeights) {
|
for (int height : nextPageHeights) {
|
||||||
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
|
if (nextCachedBlocks.find(height) == nextCachedBlocks.end()) {
|
||||||
|
|||||||
@@ -917,7 +917,8 @@ void RenderSendConfirmPopup(App* app) {
|
|||||||
// since Review. Don't broadcast a now-invalid transaction.
|
// since Review. Don't broadcast a now-invalid transaction.
|
||||||
const auto& liveState = app->getWalletState();
|
const auto& liveState = app->getWalletState();
|
||||||
double liveAvail = GetAvailableBalance(app);
|
double liveAvail = GetAvailableBalance(app);
|
||||||
if (!app->isConnected() || liveState.sync.syncing || s_amount <= 0 ||
|
if (!app->isConnected() || liveState.sync.syncing ||
|
||||||
|
!liveState.sync.sendTipReady(app->isLiteBuild()) || s_amount <= 0 ||
|
||||||
!util::canAfford(s_amount, s_fee, liveAvail)) {
|
!util::canAfford(s_amount, s_fee, liveAvail)) {
|
||||||
s_tx_status = TR("send_cannot_send_now");
|
s_tx_status = TR("send_cannot_send_now");
|
||||||
s_status_success = false;
|
s_status_success = false;
|
||||||
@@ -1048,6 +1049,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
}
|
}
|
||||||
bool can_send = app->isConnected() &&
|
bool can_send = app->isConnected() &&
|
||||||
!state.sync.syncing &&
|
!state.sync.syncing &&
|
||||||
|
state.sync.sendTipReady(app->isLiteBuild()) &&
|
||||||
is_valid_address &&
|
is_valid_address &&
|
||||||
s_amount > 0 &&
|
s_amount > 0 &&
|
||||||
s_from_address[0] != '\0' &&
|
s_from_address[0] != '\0' &&
|
||||||
@@ -1072,7 +1074,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
float btnH = std::max(schema::UI().drawElement("tabs.send", "action-btn-min-height").size, schema::UI().drawElement("tabs.send", "action-btn-height").size * vScale);
|
float btnH = std::max(schema::UI().drawElement("tabs.send", "action-btn-min-height").size, schema::UI().drawElement("tabs.send", "action-btn-height").size * vScale);
|
||||||
|
|
||||||
// Explain why Review/Send is greyed while syncing (the fields above are usable now).
|
// Explain why Review/Send is greyed while syncing (the fields above are usable now).
|
||||||
if (state.sync.syncing) {
|
if (state.sync.syncing || (app->isConnected() && !state.sync.sendTipReady(app->isLiteBuild()))) {
|
||||||
Type().textColored(TypeStyle::Caption, Warning(), TR("send_available_when_synced"));
|
Type().textColored(TypeStyle::Caption, Warning(), TR("send_available_when_synced"));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
}
|
}
|
||||||
@@ -1101,6 +1103,8 @@ static void RenderActionButtons(App* app, float width, float vScale,
|
|||||||
material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
material::Tooltip("%s", TR(app->isLiteBuild() ? "lite_no_wallet_short" : "send_tooltip_not_connected"));
|
||||||
else if (state.sync.syncing)
|
else if (state.sync.syncing)
|
||||||
material::Tooltip("%s", TR("send_tooltip_syncing"));
|
material::Tooltip("%s", TR("send_tooltip_syncing"));
|
||||||
|
else if (!state.sync.sendTipReady(app->isLiteBuild()))
|
||||||
|
material::Tooltip("%s", TR("send_tooltip_no_tip"));
|
||||||
else if (s_from_address[0] == '\0')
|
else if (s_from_address[0] == '\0')
|
||||||
material::Tooltip("%s", TR("send_tooltip_select_source"));
|
material::Tooltip("%s", TR("send_tooltip_select_source"));
|
||||||
else if (is_mining_addr)
|
else if (is_mining_addr)
|
||||||
|
|||||||
@@ -855,7 +855,12 @@ void RenderTransactionsTab(App* app)
|
|||||||
{
|
{
|
||||||
const char* statusStr;
|
const char* statusStr;
|
||||||
ImU32 statusCol;
|
ImU32 statusCol;
|
||||||
if (tx.confirmations == 0) {
|
const int64_t nowSec = static_cast<int64_t>(std::time(nullptr));
|
||||||
|
if (tx.confirmations < 0) { // gettransaction < 0: conflicted / double-spent / reorged out
|
||||||
|
statusStr = TR("tx_dropped"); statusCol = Error();
|
||||||
|
} else if (tx.confirmations == 0 && tx.timestamp > 0 && nowSec - tx.timestamp > 3600) {
|
||||||
|
statusStr = TR("tx_stale_pending"); statusCol = Warning();
|
||||||
|
} else if (tx.confirmations == 0) {
|
||||||
statusStr = TR("pending"); statusCol = Warning();
|
statusStr = TR("pending"); statusCol = Warning();
|
||||||
} else if (tx.confirmations < 10) {
|
} else if (tx.confirmations < 10) {
|
||||||
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
|
||||||
|
|||||||
@@ -1511,6 +1511,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["confirmations"] = "Confirmations";
|
strings_["confirmations"] = "Confirmations";
|
||||||
strings_["confirmed"] = "Confirmed";
|
strings_["confirmed"] = "Confirmed";
|
||||||
strings_["pending"] = "Pending";
|
strings_["pending"] = "Pending";
|
||||||
|
strings_["tx_dropped"] = "Dropped";
|
||||||
|
strings_["tx_stale_pending"] = "Expired?";
|
||||||
strings_["baltab_pending_chip"] = "Pending +%.4f";
|
strings_["baltab_pending_chip"] = "Pending +%.4f";
|
||||||
strings_["baltab_pending_tip"] = "Awaiting confirmations — not spendable yet; it moves to your available balance once confirmed.";
|
strings_["baltab_pending_tip"] = "Awaiting confirmations — not spendable yet; it moves to your available balance once confirmed.";
|
||||||
strings_["sent"] = "sent";
|
strings_["sent"] = "sent";
|
||||||
@@ -2703,6 +2705,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_tooltip_not_connected"] = "Not connected to daemon";
|
strings_["send_tooltip_not_connected"] = "Not connected to daemon";
|
||||||
strings_["send_tooltip_select_source"] = "Select a source address first";
|
strings_["send_tooltip_select_source"] = "Select a source address first";
|
||||||
strings_["send_tooltip_syncing"] = "Wait for blockchain to sync";
|
strings_["send_tooltip_syncing"] = "Wait for blockchain to sync";
|
||||||
|
strings_["send_tooltip_no_tip"] = "Waiting for peers to report the network height. Sending is disabled until the wallet knows it is caught up.";
|
||||||
strings_["send_available_when_synced"] = "You can fill in the details now. Sending is available once the wallet is fully synced.";
|
strings_["send_available_when_synced"] = "You can fill in the details now. Sending is available once the wallet is fully synced.";
|
||||||
strings_["send_tooltip_view_only"] = "View-only address — no spending key, cannot send";
|
strings_["send_tooltip_view_only"] = "View-only address — no spending key, cannot send";
|
||||||
strings_["send_total"] = "Total";
|
strings_["send_total"] = "Total";
|
||||||
|
|||||||
@@ -3042,6 +3042,58 @@ void testNetworkRefreshResultModels()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resilience mediums: the send tip-gate, cached view-tx confirmations advancing, and the dropped /
|
||||||
|
// stale-pending display predicates.
|
||||||
|
void testResilienceMediums()
|
||||||
|
{
|
||||||
|
{ // 1) send tip gate
|
||||||
|
dragonx::SyncInfo s;
|
||||||
|
s.blocks = 100; s.headers = 100; s.syncing = false; s.tip_known = false;
|
||||||
|
EXPECT_FALSE(s.sendTipReady(false)); // peerless / fresh connect: tip unknown
|
||||||
|
EXPECT_TRUE(s.sendTipReady(true)); // lite exempt
|
||||||
|
s.tip_known = true;
|
||||||
|
EXPECT_TRUE(s.sendTipReady(false));
|
||||||
|
s.syncing = true;
|
||||||
|
EXPECT_FALSE(s.sendTipReady(false));
|
||||||
|
s.syncing = false; s.blocks = 0;
|
||||||
|
EXPECT_FALSE(s.sendTipReady(false));
|
||||||
|
}
|
||||||
|
{ // 2) cached view entry confirmations advance (never backwards; pending not extrapolated)
|
||||||
|
using R = dragonx::services::NetworkRefreshService;
|
||||||
|
R::TransactionViewCacheEntry e;
|
||||||
|
e.from_address = "zs-from"; e.confirmations = 3; e.timestamp = 500; e.observed_height = 100;
|
||||||
|
R::TransactionViewCacheEntry::Output o; o.address = "zs-dest"; o.value = 1.0; e.outgoing_outputs.push_back(o);
|
||||||
|
std::vector<dragonx::TransactionInfo> rows;
|
||||||
|
R::appendViewTransactionOutputs(rows, "tx1", e, 105);
|
||||||
|
EXPECT_EQ(rows.size(), static_cast<size_t>(1));
|
||||||
|
EXPECT_EQ(rows[0].confirmations, 8); // 3 + (105-100)
|
||||||
|
std::vector<dragonx::TransactionInfo> rows2;
|
||||||
|
R::appendViewTransactionOutputs(rows2, "tx1", e); // no tip -> unchanged
|
||||||
|
EXPECT_EQ(rows2[0].confirmations, 3);
|
||||||
|
std::vector<dragonx::TransactionInfo> rows3;
|
||||||
|
R::appendViewTransactionOutputs(rows3, "tx1", e, 90); // reorg/older tip: never backwards
|
||||||
|
EXPECT_EQ(rows3[0].confirmations, 3);
|
||||||
|
e.confirmations = 0; // pending entries are not extrapolated
|
||||||
|
std::vector<dragonx::TransactionInfo> rows4;
|
||||||
|
R::appendViewTransactionOutputs(rows4, "tx1", e, 110);
|
||||||
|
EXPECT_EQ(rows4[0].confirmations, 0);
|
||||||
|
}
|
||||||
|
{ // 3) dropped / stale-pending predicates
|
||||||
|
dragonx::TransactionInfo t;
|
||||||
|
t.confirmations = -1;
|
||||||
|
EXPECT_TRUE(t.isConflicted());
|
||||||
|
EXPECT_FALSE(t.isStalePending(100000));
|
||||||
|
t.confirmations = 0; t.timestamp = 1000;
|
||||||
|
EXPECT_FALSE(t.isConflicted());
|
||||||
|
EXPECT_FALSE(t.isStalePending(1000 + 3600)); // exactly the limit: still pending
|
||||||
|
EXPECT_TRUE(t.isStalePending(1000 + 3601));
|
||||||
|
t.timestamp = 0;
|
||||||
|
EXPECT_FALSE(t.isStalePending(999999)); // unknown time never flagged
|
||||||
|
t.timestamp = 1000; t.confirmations = 1;
|
||||||
|
EXPECT_FALSE(t.isStalePending(999999)); // mined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void testOperationStatusPollParsing()
|
void testOperationStatusPollParsing()
|
||||||
{
|
{
|
||||||
using Refresh = dragonx::services::NetworkRefreshService;
|
using Refresh = dragonx::services::NetworkRefreshService;
|
||||||
@@ -9103,6 +9155,26 @@ void testExplorerBlockCache()
|
|||||||
EXPECT_TRUE(cache.storeBlock(block12));
|
EXPECT_TRUE(cache.storeBlock(block12));
|
||||||
EXPECT_EQ(cache.loadRange(12, 12)[12].tx_count, 5);
|
EXPECT_EQ(cache.loadRange(12, 12)[12].tx_count, 5);
|
||||||
|
|
||||||
|
{ // loadRangeCached memoizes on revision(): reads don't bump; a store invalidates; updates are seen.
|
||||||
|
// Isolated on its own cache so storing blocks here can't perturb the shared `cache`'s count assertions.
|
||||||
|
fs::path mdir = makeTempDir();
|
||||||
|
ExplorerBlockCache mcache((mdir / "memo.sqlite").string(), (mdir / "memo.json").string());
|
||||||
|
EXPECT_TRUE(mcache.ensureOpen());
|
||||||
|
ExplorerBlockSummary m12; m12.height = 12; m12.hash = "m-12"; m12.tx_count = 5; m12.size = 1200; m12.time = 5200; m12.difficulty = 1.4;
|
||||||
|
EXPECT_TRUE(mcache.storeBlock(m12));
|
||||||
|
const std::uint64_t rev0 = mcache.revision();
|
||||||
|
EXPECT_EQ(mcache.loadRangeCached(12, 12).size(), static_cast<size_t>(1));
|
||||||
|
EXPECT_EQ(mcache.revision(), rev0); // reading does not bump
|
||||||
|
ExplorerBlockSummary m13 = m12; m13.height = 13; m13.hash = "m-13";
|
||||||
|
EXPECT_TRUE(mcache.storeBlock(m13));
|
||||||
|
EXPECT_TRUE(mcache.revision() != rev0); // a store invalidates the memo
|
||||||
|
EXPECT_EQ(mcache.loadRangeCached(12, 13).size(), static_cast<size_t>(2));
|
||||||
|
EXPECT_EQ(mcache.loadRangeCached(12, 13).size(), static_cast<size_t>(2)); // memo hit, same answer
|
||||||
|
m13.tx_count = 99;
|
||||||
|
EXPECT_TRUE(mcache.storeBlock(m13));
|
||||||
|
EXPECT_EQ(mcache.loadRangeCached(13, 13).at(13).tx_count, 99); // sees the update, not a stale memo
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ExplorerBlockCache reopened(databasePath.string(), legacyPath.string());
|
ExplorerBlockCache reopened(databasePath.string(), legacyPath.string());
|
||||||
EXPECT_TRUE(reopened.ensureOpen());
|
EXPECT_TRUE(reopened.ensureOpen());
|
||||||
@@ -19891,6 +19963,7 @@ int main()
|
|||||||
testResolveExchangeSelection();
|
testResolveExchangeSelection();
|
||||||
testTransactionCsv();
|
testTransactionCsv();
|
||||||
testEarningsSeries();
|
testEarningsSeries();
|
||||||
|
testResilienceMediums();
|
||||||
|
|
||||||
if (g_failures != 0) {
|
if (g_failures != 0) {
|
||||||
std::cerr << g_failures << " test assertion(s) failed\n";
|
std::cerr << g_failures << " test assertion(s) failed\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user