Skip to content

Commit 0e04a3e

Browse files
fix(payments): retest reuses live session instead of re-paying (double-pay #2)
With autoCancelAfterTest OFF (the default) main-pass sessions stay ACTIVE after testing, so the in-runAudit retest blocks (internet-failure + Iron-Rule) that did clearPaidNodes()+re-test created a SECOND paid session per node while the first deposit was still locked — a real double-pay from the live wallet. Fix (reviewed): remove clearPaidNodes() from those two retest blocks so nodes keep their paid flag, and change node-test.js's retest branch to re-query findExistingSession and REUSE the still-active session (no new payment); it clears the paid flag and pays fresh only when no active session is found (autoCancel ON cancelled it, or it expired). Mirrors the proven !retestMode recovery path. Verified by adversarial review: findExistingSession returns ONLY active sessions (status 1/'active'; cancelled inactive_pending filtered at session.js:133), so reuse can never resurrect a dead session and autoCancel-ON correctly pays fresh. Manual retest (clearPaidNodes at run start), TEST RUN, balance-pause, and orphan- cancel paths unaffected. Test suite green (188/31/45/35).
1 parent 0f5bed4 commit 0e04a3e

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

audit/node-test.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,31 @@ export async function testNode(client, account, privkey, node, opts, preSessionI
294294
}
295295
}
296296
if (!sessionId && isPaid(node.address) && state.retestMode) {
297-
// In retest: clear the stale paid flag for THIS node only, so it can be
298-
// re-paid. clearPaidNodes() (clear-all) here was a double-pay vector — it
299-
// dropped every other node's paid flag too, re-opening the dup-pay guard.
300-
clearPaidNode(node.address);
297+
// Retest of an already-paid node. With autoCancelAfterTest OFF (the default)
298+
// the main-pass session is still ACTIVE on-chain — re-query and REUSE it so
299+
// we don't pay twice while the first deposit is locked (double-pay vector).
300+
// Only if no session is found (autoCancel cancelled it, or it expired) do we
301+
// clear the paid flag so a fresh payment is made below. Mirrors the
302+
// !retestMode "charged-yet-untested" recovery above.
303+
let reSid = null;
304+
try {
305+
reSid = await findExistingSession(node.address, account.address, broadcast);
306+
if (!reSid) {
307+
await sleep(5_000);
308+
invalidateSessionCache();
309+
reSid = await findExistingSession(node.address, account.address, broadcast);
310+
}
311+
} catch (reErr) {
312+
console.error('[node-test] retest session re-query failed:', reErr?.message || String(reErr));
313+
}
314+
if (reSid) {
315+
sessionId = BigInt(reSid);
316+
addToSessionMap(node.address, sessionId);
317+
if (broadcast) broadcast('log', { msg: ` ✓ Retest: reusing existing session ${sessionId} (no new payment)` });
318+
} else {
319+
// Old session gone (cancelled/expired) — allow a fresh payment below.
320+
clearPaidNode(node.address);
321+
}
301322
}
302323

303324
// Balance check — return PAUSE signal instead of failing

audit/pipeline.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,10 @@ export async function runAudit(resume, state, broadcast, preloadedNodes = null,
10811081
broadcast('log', { msg: `\n🌐 Retesting ${internetFailAddrs.size} nodes that failed during internet outage...` });
10821082
state.retestMode = true;
10831083
clearPoisonedSessions();
1084-
clearPaidNodes();
1084+
// Do NOT clearPaidNodes() here — these nodes were paid in the main pass and
1085+
// (autoCancel off) their sessions are still active. Keeping the paid flag
1086+
// lets node-test.js re-query & REUSE the live session instead of paying
1087+
// twice; it pays fresh only if the session is genuinely gone.
10851088
const retestNodes = viableNodes.filter(n => internetFailAddrs.has(n.node.address));
10861089

10871090
for (let ri = 0; ri < retestNodes.length; ri++) {
@@ -1154,7 +1157,9 @@ export async function runAudit(resume, state, broadcast, preloadedNodes = null,
11541157
broadcast('log', { msg: `🔄 Auto-retesting ${failedWithPeers.length} failures with peers > 0 (Iron Rule)...` });
11551158
state.retestMode = true;
11561159
clearPoisonedSessions();
1157-
clearPaidNodes();
1160+
// Do NOT clearPaidNodes() here — same reason as the internet-failure block:
1161+
// keep the paid flag so node-test.js reuses the still-active session instead
1162+
// of double-paying; it pays fresh only if the session is gone.
11581163
const failAddrs = failedWithPeers.map(r => r.address);
11591164
const retestNodes = viableNodes.filter(n => failAddrs.includes(n.node.address));
11601165

0 commit comments

Comments
 (0)