feat(wallet): defer + retry a merge that hits a transient mempool conflict
A consolidation batch can be rejected by the daemon with bad-txns-shielded-requirements-not-met (REJECT_DUPLICATE) when it clashes with an earlier, still-unconfirmed batch — the batch outran its predecessors into the mempool. That isn't a real failure: once the earlier batches confirm, the same merge succeeds. Previously it failed the whole address. Now a merge OP failure whose message is a mempool conflict (shielded- / joinsplit-requirements-not-met, txn-mempool-conflict, inputs-spent — new pure helper isDeferrableMergeOpError) HOLDS the address for kRunMempoolClearSec (2 min) instead of failing it, then retries from where it left off; up to kRunMaxDeferRetries consecutive defers (reset on any success), after which it is a genuine failure. Deferrals don't feed the halt breaker, and since the address outcome stays non-failed the run verdict reads Clean when the retry succeeds. applyOpState takes `now` to stamp the hold; next() waits on a deferred address until its window passes. Unit-tested (testConsolidationDefer): the classifier; and a driven run where an op reports the conflict → the address is deferred (not failed), held before the window and retried after it. Build + tests + hygiene green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7395,6 +7395,52 @@ void testNoteConsolidation()
|
||||
}
|
||||
}
|
||||
|
||||
// A merge op that fails with a transient mempool conflict defers its address and retries once the window
|
||||
// clears, instead of failing it — so a batch that outran an earlier, still-unconfirmed batch recovers.
|
||||
void testConsolidationDefer()
|
||||
{
|
||||
using namespace dragonx::services;
|
||||
|
||||
// The classifier: mempool-conflict reject codes defer; a genuine "no funds" failure does not.
|
||||
EXPECT_TRUE(isDeferrableMergeOpError("18: bad-txns-shielded-requirements-not-met"));
|
||||
EXPECT_TRUE(isDeferrableMergeOpError("txn-mempool-conflict"));
|
||||
EXPECT_TRUE(isDeferrableMergeOpError("bad-txns-inputs-spent"));
|
||||
EXPECT_FALSE(isDeferrableMergeOpError("could not find any funds"));
|
||||
EXPECT_FALSE(isDeferrableMergeOpError(""));
|
||||
|
||||
ConsolidationRun run;
|
||||
RunConfig cfg; cfg.limit = 10; cfg.maxBatches = 3; cfg.fee = 0.0001;
|
||||
std::vector<NoteGroup> groups{ {"zA", 100, 50.0} };
|
||||
EXPECT_TRUE(run.start(groups, cfg, 0.0));
|
||||
RunEnv env; env.nodeUsable = true; // idle + callable (callsAllowed defaults true; the rest false)
|
||||
|
||||
// A batch is submitted for address 0.
|
||||
RunStep s = run.next(env, 1.0);
|
||||
EXPECT_TRUE(s.action == RunAction::Call && s.addrIdx == 0);
|
||||
run.markCallPosted(0, 1.0);
|
||||
MergeCallResult cr; cr.opid = "op1"; cr.merging = 10; cr.remaining = 90;
|
||||
run.onCallResult(run.gen(), 0, cr, dragonx::rpc::RpcFailureKind::None, 0.5, 2.0);
|
||||
EXPECT_FALSE(run.addresses()[0].done);
|
||||
|
||||
// Its op is polled and reports a mempool conflict.
|
||||
EXPECT_TRUE(run.next(env, 3.0).action == RunAction::Poll);
|
||||
run.markPollPosted(3.0);
|
||||
std::vector<OpReport> reps{ {"op1", "failed", "18: bad-txns-shielded-requirements-not-met", ""} };
|
||||
run.onPollResult(run.gen(), true, reps, 4.0);
|
||||
|
||||
// Deferred, not failed: still live, held, and counted as a defer (not an address failure).
|
||||
EXPECT_TRUE(run.active());
|
||||
EXPECT_FALSE(run.addresses()[0].done);
|
||||
EXPECT_TRUE(run.addresses()[0].outcome != AddrOutcome::Failed);
|
||||
EXPECT_EQ(run.addresses()[0].defers, 1);
|
||||
EXPECT_EQ(run.summary().addressesFailed, 0);
|
||||
|
||||
// Held before the window, retried after it.
|
||||
EXPECT_TRUE(run.next(env, 5.0).action != RunAction::Call);
|
||||
RunStep retry = run.next(env, 4.0 + kRunMempoolClearSec + 1.0);
|
||||
EXPECT_TRUE(retry.action == RunAction::Call && retry.addrIdx == 0);
|
||||
}
|
||||
|
||||
void testDatadirLockGate()
|
||||
{
|
||||
using dragonx::daemon::EmbeddedDaemon;
|
||||
@@ -20298,6 +20344,7 @@ int main()
|
||||
testOwnershipRecord();
|
||||
testDebugLogTailRules();
|
||||
testNoteConsolidation();
|
||||
testConsolidationDefer();
|
||||
testScopeScanAndLockedNotes();
|
||||
testMergeCallResult();
|
||||
testOpReports();
|
||||
|
||||
Reference in New Issue
Block a user