Skip to content

Commit 781abd0

Browse files
fix(spend): true cumulative run spend across resume/pause (oracle accuracy)
spentUdvpn is a BALANCE-DELTA — "spent since the last balance query" — reset to 0 at every balance re-anchor (run start, resume, paused-balance recovery). It does double duty: the remaining-balance calc (balanceUdvpn - spentUdvpn) needs the delta, but the run's RECORDED Net Spend needs the cumulative. So a resumed/paused run recorded only spend-since-last-anchor, under-reporting the on-chain spend oracle the tester publishes. Fix: add a separate state.runSpentUdvpn = total udvpn paid for the current run — incremented at every payment site (all 7: node-test ×2, session ×2, pipeline ×3), reset ONLY at fresh-pass starts (runAudit !resume, retest/plan/sub-plan starts, startFreshRun, clearActiveRunView, /api/clear), NEVER reset by a balance re-anchor, preserved across resume, persisted UNCAPPED in the snapshot. The run's recorded spend (index.json, SQLite spent_udvpn) + Net Spend display now come from runSpentUdvpn; spentUdvpn stays the balance-delta for remaining-balance only. Two parallel adversarial reviews: #1 confirmed accumulator integrity (every payment site, every reset, clean semantic separation); #2 caught a CRITICAL the first missed — saveCurrentRun's C-1(b) guard read spentUdvpn for liveSpent, so resuming an already-saved run recorded the stale pre-resume total and dropped the resume-pass spend. Fixed: liveSpent now reads runSpentUdvpn. Test suite green (188/31/45/35).
1 parent c312317 commit 781abd0

4 files changed

Lines changed: 60 additions & 28 deletions

File tree

audit/node-test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,9 @@ export async function testNode(client, account, privkey, node, opts, preSessionI
421421
}
422422

423423
state.spentUdvpn += thisCostUdvpn + 200000;
424+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + thisCostUdvpn + 200000;
424425
state.balance = `${(Math.max(0, state.balanceUdvpn - state.spentUdvpn) / 1_000_000).toFixed(4)} P2P (est. remaining)`;
425-
state.estimatedTotalCost = `${(state.spentUdvpn / 1_000_000).toFixed(4)} P2P`;
426+
state.estimatedTotalCost = `${(state.runSpentUdvpn / 1_000_000).toFixed(4)} P2P`;
426427
if (broadcast) broadcast('state', { state });
427428

428429
sessionId = extractSessionId(txResult);
@@ -488,8 +489,9 @@ export async function testNode(client, account, privkey, node, opts, preSessionI
488489
state._freshSessionIds.set(node.address, String(sessionId));
489490
}
490491
state.spentUdvpn += thisCostUdvpn + 200000;
492+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + thisCostUdvpn + 200000;
491493
state.balance = `${(Math.max(0, state.balanceUdvpn - state.spentUdvpn) / 1_000_000).toFixed(4)} P2P (est. remaining)`;
492-
state.estimatedTotalCost = `${(state.spentUdvpn / 1_000_000).toFixed(4)} P2P`;
494+
state.estimatedTotalCost = `${(state.runSpentUdvpn / 1_000_000).toFixed(4)} P2P`;
493495
if (broadcast) broadcast('state', { state });
494496
if (broadcast) broadcast('log', { msg: ` Fresh session ${sessionId} — waiting for chain + node indexing...` });
495497
await waitForSessionActive(node.address, account.address, 20_000, sessionId);

audit/pipeline.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ export function createState() {
394394
balanceUdvpn: 0,
395395
estimatedTotalCost: null,
396396
spentUdvpn: 0,
397+
runSpentUdvpn: 0,
397398
startedAt: null,
398399
completedAt: null,
399400
errorMessage: null,
@@ -612,6 +613,7 @@ export async function runAudit(resume, state, broadcast, preloadedNodes = null,
612613
state.balanceUdvpn = parseInt(balRes?.amount || '0', 10);
613614
state.balance = `${(state.balanceUdvpn / 1_000_000).toFixed(4)} P2P`;
614615
state.spentUdvpn = 0;
616+
if (!resume) state.runSpentUdvpn = 0; // cumulative run spend — preserve across resume
615617

616618
if (resume) {
617619
broadcast('log', { msg: `Resuming audit with ${results.length} existing results...` });
@@ -1292,6 +1294,7 @@ export async function runRetestSkips(skipAddrs, state, broadcast) {
12921294
state.balance = `${(state.balanceUdvpn / 1_000_000).toFixed(4)} P2P`;
12931295
// retest displays only this pass's spend; the run's stored total is accumulated on persist (see persistActiveRun).
12941296
state.spentUdvpn = 0;
1297+
state.runSpentUdvpn = 0; // fresh PASS — accumulates onto saved run's prior via persistActiveRun
12951298
broadcast('state', { state });
12961299

12971300
const v2rayAvailable = await checkV2Ray();
@@ -1481,6 +1484,7 @@ export async function runPlanTest(planId, state, broadcast) {
14811484
state.balanceUdvpn = parseInt(balRes?.amount || '0', 10);
14821485
state.balance = `${(state.balanceUdvpn / 1_000_000).toFixed(4)} P2P`;
14831486
state.spentUdvpn = 0;
1487+
state.runSpentUdvpn = 0; // fresh pass — cumulative run spend
14841488
broadcast('state', { state });
14851489

14861490
const v2rayAvailable = await checkV2Ray();
@@ -1506,6 +1510,7 @@ export async function runPlanTest(planId, state, broadcast) {
15061510
throw new Error(`Subscribe tx failed code=${subResult.code}: ${subResult.rawLog}`);
15071511
}
15081512
state.spentUdvpn += 200000;
1513+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + 200000;
15091514
for (const event of (subResult.events || [])) {
15101515
if (/subscription/i.test(event.type)) {
15111516
for (const attr of event.attributes) {
@@ -1655,6 +1660,7 @@ export async function runPlanTest(planId, state, broadcast) {
16551660
throw new Error(`Session tx failed code=${sessResult.code}: ${sessResult.rawLog}`);
16561661
}
16571662
state.spentUdvpn += 200000;
1663+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + 200000;
16581664

16591665
for (const event of (sessResult.events || [])) {
16601666
if (/session/i.test(event.type)) {
@@ -1813,6 +1819,7 @@ export async function runSubPlanTest(planId, subscriptionId, granterAddr, state,
18131819
state.balanceUdvpn = parseInt(balRes?.amount || '0', 10);
18141820
state.balance = `${(state.balanceUdvpn / 1_000_000).toFixed(4)} P2P`;
18151821
state.spentUdvpn = 0;
1822+
state.runSpentUdvpn = 0; // fresh pass — cumulative run spend
18161823
broadcast('state', { state });
18171824

18181825
// Pre-broadcast fee grant verification — abort with structured error if missing/expired.
@@ -2092,6 +2099,7 @@ export async function runSubPlanTest(planId, subscriptionId, granterAddr, state,
20922099
throw new Error(`Session tx failed code=${sessResult.code}: ${sessResult.rawLog}`);
20932100
}
20942101
state.spentUdvpn += 200000;
2102+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + 200000;
20952103
} else {
20962104
_spBroadcast('log', { msg: ` Starting session (fee-granted by ${granterAddr.slice(0, 12)}…)` });
20972105
sessResult = await broadcastWithFeeGrant(client, account.address, [sessMsg], granterAddr, '', _spBroadcast);

core/session.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,11 +378,16 @@ export async function submitBatchPayment(client, account, denom, gigabytes, batc
378378
const list = pricingMode === 'hours' ? (node.hourly_prices || []) : (node.gigabyte_prices || []);
379379
const priceEntry = list.find(p => p.denom === denom);
380380
const units = pricingMode === 'hours' ? sessionHours : gigabytes;
381-
if (priceEntry) state.spentUdvpn += Math.round(parseFloat(priceEntry.quote_value) || 0) * units;
381+
if (priceEntry) {
382+
const _amt = Math.round(parseFloat(priceEntry.quote_value) || 0) * units;
383+
state.spentUdvpn += _amt;
384+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + _amt;
385+
}
382386
});
383387
state.spentUdvpn += 200000 * n;
388+
state.runSpentUdvpn = (state.runSpentUdvpn || 0) + 200000 * n;
384389
state.balance = `${(Math.max(0, state.balanceUdvpn - state.spentUdvpn) / 1_000_000).toFixed(4)} P2P (est. remaining)`;
385-
state.estimatedTotalCost = `${(state.spentUdvpn / 1_000_000).toFixed(4)} P2P`;
390+
state.estimatedTotalCost = `${(state.runSpentUdvpn / 1_000_000).toFixed(4)} P2P`;
386391
if (broadcast) broadcast('state', { state });
387392
}
388393
result._reusedAddrs = reusedAddrs;

server.js

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ function saveStateSnapshot(force = false) {
238238
baselineHistory: state.baselineHistory,
239239
nodeSpeedHistory: state.nodeSpeedHistory,
240240
spentUdvpn: state.spentUdvpn,
241+
runSpentUdvpn: state.runSpentUdvpn ?? 0,
241242
balanceUdvpn: state.balanceUdvpn,
242243
balance: state.balance,
243244
estimatedTotalCost: state.estimatedTotalCost,
@@ -603,20 +604,22 @@ function saveCurrentRun(label) {
603604
// C-1(b) guard: if the currently-active run is ALREADY a saved run (its index
604605
// entry exists AND aliases the same SQLite dbRunId we'd write to), do NOT
605606
// allocate a new run number / create a duplicate index entry / overwrite the
606-
// SQLite spend downward. This happens after load+retest (or retest-then-Save):
607-
// state.activeDbRunId still points at the saved run while state.spentUdvpn may
608-
// hold only a pass-only figure. Re-persist into the SAME run with
609-
// accumulateSpend=false so we never reduce stored spend, then return the
610-
// existing number. persistActiveRun keeps index + SQLite in lockstep.
607+
// SQLite spend downward. This happens after load+retest (or resume-then-Save):
608+
// state.activeDbRunId still points at the saved run. Re-persist into the SAME
609+
// run with accumulateSpend=false so we never reduce stored spend, then return
610+
// the existing number. persistActiveRun keeps index + SQLite in lockstep.
611611
if (state.activeRunNumber != null && state.activeDbRunId != null) {
612612
const idxGuard = loadRunsIndex();
613613
const existing = idxGuard.runs.find(r => r.number === state.activeRunNumber);
614614
if (existing && existing.dbRunId != null && Number(existing.dbRunId) === Number(state.activeDbRunId)) {
615-
// Never reduce stored spend: only re-persist (write-through) when the live
616-
// figure is at least the stored cumulative. accumulateSpend=false treats
617-
// state.spentUdvpn as the full cumulative (not additive on top of stored).
615+
// Compare against the CUMULATIVE run spend (runSpentUdvpn), NOT the
616+
// balance-delta spentUdvpn: when resuming an already-saved run, the index
617+
// entry still holds the pre-resume total while the post-resume spend lives
618+
// only in runSpentUdvpn — reading spentUdvpn here would record the stale
619+
// (smaller) figure and silently drop the resume-pass spend from the
620+
// on-chain oracle. accumulateSpend=false treats this as the full cumulative.
618621
const storedSpent = Number(existing.spentUdvpn) || 0;
619-
const liveSpent = Number(state.spentUdvpn) || 0;
622+
const liveSpent = Number(state.runSpentUdvpn) || 0;
620623
const passSpent = Math.max(storedSpent, liveSpent);
621624
state.activeRunSaved = true;
622625
try {
@@ -689,7 +692,7 @@ function saveCurrentRun(label) {
689692
failed: failed.length,
690693
pass10: pass10.length,
691694
sdk: state.activeSDK,
692-
spentUdvpn: Number(state.spentUdvpn) || 0,
695+
spentUdvpn: Number(state.runSpentUdvpn) || 0,
693696
dbRunId: state.activeDbRunId || null,
694697
auditLog: state.auditLogPath ? path.basename(state.auditLogPath) : null,
695698
};
@@ -706,7 +709,7 @@ function saveCurrentRun(label) {
706709
finished_at: Date.now(),
707710
node_count: results.length,
708711
pass_count: passed.length,
709-
spent_udvpn: Number(state.spentUdvpn) || 0,
712+
spent_udvpn: Number(state.runSpentUdvpn) || 0,
710713
});
711714
} catch (dbErr) {
712715
console.error(`[db] updateRunOnFinish failed: ${dbErr.message}`);
@@ -770,7 +773,7 @@ function persistActiveRun(label, { accumulateSpend = true, passSpent = null } =
770773
const priorSpent = accumulateSpend ? (Number(entry?.spentUdvpn) || 0) : 0;
771774
// Snapshot the pass spend the caller captured (defends against the idle
772775
// balance refresher zeroing state.spentUdvpn mid-gap); fall back to live state.
773-
const thisPassSpent = passSpent != null ? (Number(passSpent) || 0) : (Number(state.spentUdvpn) || 0);
776+
const thisPassSpent = passSpent != null ? (Number(passSpent) || 0) : (Number(state.runSpentUdvpn) || 0);
774777
const cumulativeSpent = priorSpent + thisPassSpent;
775778
if (!entry) {
776779
// M-2: no index entry for the active run → create one so index + SQLite
@@ -864,6 +867,7 @@ function loadRunIntoState(num) {
864867
}
865868
}
866869
state.spentUdvpn = _spent;
870+
state.runSpentUdvpn = _spent; // loaded run's stored cumulative spend
867871
state.estimatedTotalCost = _spent > 0 ? `${(_spent / 1_000_000).toFixed(4)} P2P` : '0 P2P';
868872
// Live Log follows the loaded run: replace the rolling buffer with this run's
869873
// saved execution log (empty if the run predates per-run log capture).
@@ -990,6 +994,7 @@ function clearActiveRunView() {
990994
state.activeRunSaved = false;
991995
state.status = 'idle';
992996
state.spentUdvpn = 0;
997+
state.runSpentUdvpn = 0;
993998
state.estimatedTotalCost = '0 P2P';
994999
state.auditLogPath = null;
9951000
// Charts + transient run pointers also belong to the now-deleted run: without
@@ -1062,6 +1067,9 @@ function rehydrateState(results) {
10621067
// Only restore for display purposes, capped to prevent negative.
10631068
if (snap.balanceUdvpn) state.balanceUdvpn = snap.balanceUdvpn;
10641069
if (snap.spentUdvpn) state.spentUdvpn = Math.min(snap.spentUdvpn, state.balanceUdvpn);
1070+
// runSpentUdvpn is a cumulative payment total (not a balance delta) — restore
1071+
// uncapped so a stop→bounce→resume preserves the true recorded run spend.
1072+
if (snap.runSpentUdvpn != null) state.runSpentUdvpn = snap.runSpentUdvpn;
10651073
const remaining = Math.max(0, state.balanceUdvpn - state.spentUdvpn);
10661074
state.balance = `${(remaining / 1_000_000).toFixed(4)} P2P`;
10671075
if (snap.estimatedTotalCost) state.estimatedTotalCost = snap.estimatedTotalCost;
@@ -2251,6 +2259,7 @@ function startFreshRun(label, { mode = 'p2p', plan_id = null } = {}) {
22512259
state.retryCount = 0;
22522260
state.estimatedTotalCost = '0 P2P';
22532261
state.spentUdvpn = 0;
2262+
state.runSpentUdvpn = 0;
22542263
// Surface the active mode so the UI can render a clear "what's running" badge:
22552264
// 'subscription' (sub-plan), 'p2p', 'test'. Plan id is null unless mode === 'subscription'.
22562265
state.runMode = mode;
@@ -2557,12 +2566,15 @@ app.post('/api/retest-skips', adminOnly, async (req, res) => {
25572566
const tracked = withBatchTracking(broadcast, state.runMode || 'p2p');
25582567
runRetestSkips(skipAddrs, state, tracked).then(() => {
25592568
// Snapshot this-pass spend NOW, before the idle balance refresher can zero
2560-
// state.spentUdvpn in the gap before persist. Then reset state.spentUdvpn to
2561-
// the true cumulative so the live header + any later Save reflect the real total.
2562-
const passSpent = Number(state.spentUdvpn) || 0;
2569+
// state.spentUdvpn in the gap before persist. runRetestSkips resets the
2570+
// cumulative accumulator (runSpentUdvpn) to 0 at its top, so this-pass spend
2571+
// lives in runSpentUdvpn. Then set BOTH state.spentUdvpn (balance-delta) and
2572+
// state.runSpentUdvpn (cumulative) to the true running total so the live
2573+
// header + any later Save reflect the real total.
2574+
const passSpent = Number(state.runSpentUdvpn) || 0;
25632575
try {
25642576
const r = persistActiveRun(undefined, { passSpent });
2565-
if (r) state.spentUdvpn = r.cumulativeSpent;
2577+
if (r) { state.spentUdvpn = r.cumulativeSpent; state.runSpentUdvpn = r.cumulativeSpent; }
25662578
} catch (e) { console.error('[retest-skips] persistActiveRun failed:', e.message); }
25672579
}).catch(err => {
25682580
state.status = 'error';
@@ -2593,12 +2605,14 @@ app.post('/api/retest-fails', adminOnly, async (req, res) => {
25932605
}
25942606
const tracked = withBatchTracking(broadcast, state.runMode || 'p2p');
25952607
runRetestSkips(failAddrs, state, tracked).then(() => {
2596-
// Snapshot this-pass spend before the idle refresher can zero it; reset
2597-
// state.spentUdvpn to the true cumulative for the live header + later Save.
2598-
const passSpent = Number(state.spentUdvpn) || 0;
2608+
// Snapshot this-pass spend (from runSpentUdvpn, the cumulative accumulator
2609+
// which runRetestSkips reset to 0 at its top) before the idle refresher can
2610+
// zero it; set BOTH state.spentUdvpn (balance-delta) and state.runSpentUdvpn
2611+
// (cumulative) to the true cumulative for the live header + later Save.
2612+
const passSpent = Number(state.runSpentUdvpn) || 0;
25992613
try {
26002614
const r = persistActiveRun(undefined, { passSpent });
2601-
if (r) state.spentUdvpn = r.cumulativeSpent;
2615+
if (r) { state.spentUdvpn = r.cumulativeSpent; state.runSpentUdvpn = r.cumulativeSpent; }
26022616
} catch (e) { console.error('[retest-fails] persistActiveRun failed:', e.message); }
26032617
}).catch(err => {
26042618
state.status = 'error';
@@ -2727,6 +2741,7 @@ app.post('/api/clear', adminOnly, (req, res) => {
27272741
// leave spend/total/currentNode stale, so the header would still show the old
27282742
// Net Spend / Total. Reset those transient fields too for a consistent wipe.
27292743
state.spentUdvpn = 0;
2744+
state.runSpentUdvpn = 0;
27302745
state.estimatedTotalCost = '0 P2P';
27312746
state.totalNodes = 0;
27322747
state.currentNode = null;
@@ -2922,12 +2937,14 @@ app.post('/api/auto-retest', adminOnly, async (req, res) => {
29222937
const tracked = withBatchTracking(broadcast, state.runMode || 'p2p');
29232938
runRetestSkips(retestable.map(r => r.address), state, tracked).then(() => {
29242939
// Persist back to the SAME run number — no new run is created.
2925-
// Snapshot this-pass spend before the idle refresher can zero it; reset
2926-
// state.spentUdvpn to the true cumulative for the live header + later Save.
2927-
const passSpent = Number(state.spentUdvpn) || 0;
2940+
// Snapshot this-pass spend from runSpentUdvpn (the cumulative accumulator
2941+
// runRetestSkips reset to 0 at its top) before the idle refresher can zero
2942+
// it; set BOTH state.spentUdvpn (balance-delta) and state.runSpentUdvpn
2943+
// (cumulative) to the true cumulative for the live header + later Save.
2944+
const passSpent = Number(state.runSpentUdvpn) || 0;
29282945
try {
29292946
const r = persistActiveRun(undefined, { passSpent });
2930-
if (r) state.spentUdvpn = r.cumulativeSpent;
2947+
if (r) { state.spentUdvpn = r.cumulativeSpent; state.runSpentUdvpn = r.cumulativeSpent; }
29312948
} catch (e) { console.error('[auto-retest] persistActiveRun failed:', e.message); }
29322949
}).catch(err => {
29332950
state.status = 'error';

0 commit comments

Comments
 (0)