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:
@@ -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()
|
||||
{
|
||||
using Refresh = dragonx::services::NetworkRefreshService;
|
||||
@@ -9103,6 +9155,26 @@ void testExplorerBlockCache()
|
||||
EXPECT_TRUE(cache.storeBlock(block12));
|
||||
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());
|
||||
EXPECT_TRUE(reopened.ensureOpen());
|
||||
@@ -19891,6 +19963,7 @@ int main()
|
||||
testResolveExchangeSelection();
|
||||
testTransactionCsv();
|
||||
testEarningsSeries();
|
||||
testResilienceMediums();
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " test assertion(s) failed\n";
|
||||
|
||||
Reference in New Issue
Block a user