feat(merge): run per-address consolidation as a paced background job
"Keep in each address" posted one task that called z_mergetoaddress for every batch of every address back to back; each call is a whole-wallet scan under cs_main, and the daemon runs the ops one at a time anyway. It is now an App-owned services::ConsolidationRun: - one RPC at a time; the next merge only after the previous batch left the queue and a gap as long as the last call took (2-120 s); - pauses while the node syncs, the wallet is locked, a send of ours is in progress, or another wallet scan is running; - failures classified: nothing that may have queued is ever resent; a breaker stops after 3 dead addresses or 3 failed ops in a row; - each merge is charged to the scan budget; one re-read and one forced balance at the end; - it keeps running when the dialog closes: a status-bar chip, a run view with progress, the wait reason and a Stop button. The "Into one" merge can no longer be confirmed twice while its call is in flight. fake_dragonxd gains FAKE_SCAN_SECS, FAKE_MERGE_SERIAL and FAKE_MERGE_FAIL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,9 @@ inherits the GUI's environment.
|
||||
| `FAKE_STUCK_SHUTDOWN` | 0 | the Sep 29 orphan: hold the datadir lock, no listener, ignore SIGTERM, never exit |
|
||||
| `FAKE_WALLET_NOTES` | (off) | a note wallet: `addr:count:amount,...` shielded notes; enables `z_mergetoaddress` + its operations (dragonxd rules: confirmed unlocked notes, a batch locked on return, merged note next block) |
|
||||
| `FAKE_MERGE_SECS` | 6 | how long a `z_mergetoaddress` operation runs before it succeeds |
|
||||
| `FAKE_MERGE_SERIAL` | 0 | `1` = ops run one at a time, like dragonxd's single async worker: a new op is `queued` until the previous one finishes |
|
||||
| `FAKE_MERGE_FAIL` | (off) | `syncing:N` (the Nth and later merges fail with the THROW_IF_SYNCING text), `lockedkey:N` (-8 "Could not find spending key…" after the scan), `opfail:N` (the Nth and later ops end `failed`, their notes unlocked) |
|
||||
| `FAKE_SCAN_SECS` | 0 | the GUI's wallet scans (`z_listunspent`, `listunspent`, `z_gettotalbalance`, `z_listreceivedbyaddress`, `z_mergetoaddress`) hold cs_main this long; `scan-begin`/`scan-end` events |
|
||||
| `FAKE_TIP_UNKNOWN_SECS` | 0 | `longestchain` reads 0 this long after `Done Loading` (a node that hasn't seen an UpdateTip yet) |
|
||||
| `FAKE_LONGEST_AHEAD` / `FAKE_CATCHUP_SECS` | 0 / 0 | the network tip (`longestchain`, `headers`) is N blocks ahead, for that long after `Done Loading` (0 = for good) |
|
||||
| `FAKE_TXCOUNT` | 42 | `getwalletinfo` `txcount` (the GUI's connect gate waits for the tip on wallets of 500+) |
|
||||
|
||||
@@ -424,8 +424,10 @@ struct FakeOp {
|
||||
std::vector<long> noteIds;
|
||||
std::string to;
|
||||
double fee = 0.0;
|
||||
double startAt = 0.0; // FAKE_MERGE_SERIAL: queued until the previous op finished
|
||||
double finishAt = 0.0; // unixMs()/1000.0
|
||||
std::string status = "executing";
|
||||
std::string error; // a failed op's message (FAKE_MERGE_FAIL=opfail:N)
|
||||
long long created = 0;
|
||||
};
|
||||
bool g_noteWallet = false;
|
||||
@@ -434,6 +436,12 @@ std::vector<FakeOp> g_ops;
|
||||
long g_noteSeq = 0;
|
||||
long g_opSeq = 0;
|
||||
double g_mergeSecs = 6.0;
|
||||
// Consolidation-run knobs (C210 manual checks).
|
||||
double g_scanSecs = 0.0; // FAKE_SCAN_SECS: a wallet scan holds cs_main this long
|
||||
bool g_mergeSerial = false; // FAKE_MERGE_SERIAL=1: one op at a time, like dragonxd's single async worker
|
||||
std::string g_mergeFailKind; // FAKE_MERGE_FAIL=syncing:N | lockedkey:N | opfail:N
|
||||
long g_mergeFailFrom = 0;
|
||||
long g_mergeCalls = 0;
|
||||
|
||||
void loadNoteWallet()
|
||||
{
|
||||
@@ -441,6 +449,13 @@ void loadNoteWallet()
|
||||
if (!spec || !*spec) return;
|
||||
g_noteWallet = true;
|
||||
g_mergeSecs = envDouble("FAKE_MERGE_SECS", 6.0);
|
||||
g_mergeSerial = envLong("FAKE_MERGE_SERIAL", 0) == 1;
|
||||
if (const char* f = std::getenv("FAKE_MERGE_FAIL")) {
|
||||
const std::string spec(f);
|
||||
const size_t colon = spec.find(':');
|
||||
g_mergeFailKind = spec.substr(0, colon);
|
||||
g_mergeFailFrom = colon == std::string::npos ? 1 : std::max(1L, std::strtol(spec.c_str() + colon + 1, nullptr, 10));
|
||||
}
|
||||
std::string s(spec);
|
||||
size_t pos = 0;
|
||||
while (pos <= s.size()) {
|
||||
@@ -472,7 +487,15 @@ void settleOps()
|
||||
{
|
||||
const double now = unixMs() / 1000.0;
|
||||
for (auto& op : g_ops) {
|
||||
if (op.status == "queued" && now >= op.startAt) op.status = "executing";
|
||||
if (op.status != "executing" || now < op.finishAt) continue;
|
||||
if (!op.error.empty()) { // FAKE_MERGE_FAIL=opfail: the op fails and its notes are unlocked again
|
||||
for (auto& n : g_notes)
|
||||
if (std::find(op.noteIds.begin(), op.noteIds.end(), n.id) != op.noteIds.end()) n.locked = false;
|
||||
op.status = "failed";
|
||||
event("merge-failed", op.id);
|
||||
continue;
|
||||
}
|
||||
double sum = 0.0;
|
||||
for (long id : op.noteIds) {
|
||||
auto it = std::find_if(g_notes.begin(), g_notes.end(), [&](const FakeNote& n) { return n.id == id; });
|
||||
@@ -488,6 +511,7 @@ json opStatus(const FakeOp& op)
|
||||
{
|
||||
json o = {{"id", op.id}, {"status", op.status}, {"creation_time", op.created}, {"method", "z_mergetoaddress"}};
|
||||
if (op.status == "success") o["result"] = {{"txid", fakeHash(1000000 + std::stol(op.id.substr(5)))}};
|
||||
if (op.status == "failed") o["error"] = {{"code", -6}, {"message", op.error}};
|
||||
return o;
|
||||
}
|
||||
|
||||
@@ -503,7 +527,7 @@ Canned noteWalletReply(const std::string& m, const json& params)
|
||||
const long conf = std::max(0L, h - n.height + 1);
|
||||
arr.push_back({{"txid", fakeHash(5000000 + n.id)}, {"outindex", 0}, {"confirmations", conf},
|
||||
{"spendable", true}, {"address", n.address}, {"amount", n.amount}, {"memo", "f6"},
|
||||
{"change", false}});
|
||||
{"change", false}, {"locked", n.locked}});
|
||||
}
|
||||
c.result = arr;
|
||||
} else if (m == "z_listaddresses") {
|
||||
@@ -535,6 +559,24 @@ Canned noteWalletReply(const std::string& m, const json& params)
|
||||
else from.push_back(a.get<std::string>());
|
||||
}
|
||||
const std::string to = params[1].get<std::string>();
|
||||
++g_mergeCalls;
|
||||
const bool failNow = !g_mergeFailKind.empty() && g_mergeCalls >= g_mergeFailFrom;
|
||||
if (failNow && g_mergeFailKind == "syncing") { // THROW_IF_SYNCING runs before the scan (cheap)
|
||||
c.errCode = -1;
|
||||
c.errMsg = "z_mergetoaddress: Extreme Privacy! Chain still syncing at height " + std::to_string(h) +
|
||||
", aborting to prevent linkability analysis. Please wait until FULLY SYNCED and try again.";
|
||||
return c;
|
||||
}
|
||||
if (g_scanSecs > 0.0) { // GetFilteredNotes: a whole-wallet scan under cs_main
|
||||
event("scan-begin", "m=z_mergetoaddress");
|
||||
std::this_thread::sleep_for(std::chrono::duration<double>(g_scanSecs));
|
||||
event("scan-end", "m=z_mergetoaddress");
|
||||
}
|
||||
if (failNow && g_mergeFailKind == "lockedkey") {
|
||||
c.errCode = -8;
|
||||
c.errMsg = "Could not find spending key for payment address.";
|
||||
return c;
|
||||
}
|
||||
const double fee = params.size() > 2 && params[2].is_number() ? params[2].get<double>() : 0.0001;
|
||||
long limit = params.size() > 4 && params[4].is_number() ? params[4].get<long>() : 90;
|
||||
if (limit <= 0) limit = 1000000;
|
||||
@@ -565,7 +607,16 @@ Canned noteWalletReply(const std::string& m, const json& params)
|
||||
op.to = to;
|
||||
op.fee = fee;
|
||||
op.created = unixMs() / 1000;
|
||||
op.finishAt = unixMs() / 1000.0 + g_mergeSecs;
|
||||
const double nowSec = unixMs() / 1000.0;
|
||||
double prevFinish = nowSec;
|
||||
if (g_mergeSerial) {
|
||||
for (const auto& o : g_ops)
|
||||
if (o.status == "queued" || o.status == "executing") prevFinish = std::max(prevFinish, o.finishAt);
|
||||
}
|
||||
op.startAt = prevFinish;
|
||||
op.finishAt = op.startAt + g_mergeSecs;
|
||||
op.status = op.startAt > nowSec ? "queued" : "executing";
|
||||
if (failNow && g_mergeFailKind == "opfail") op.error = "Proof failed (FAKE_MERGE_FAIL)";
|
||||
g_ops.push_back(op);
|
||||
event("merge", op.id + " notes=" + std::to_string(picked.size()) + " to=" + to);
|
||||
c.result = {{"remainingUTXOs", 0}, {"remainingTransparentValue", 0.0},
|
||||
@@ -581,7 +632,7 @@ Canned noteWalletReply(const std::string& m, const json& params)
|
||||
std::vector<FakeOp> keep;
|
||||
for (const auto& op : g_ops) {
|
||||
const bool match = filter.empty() || std::find(filter.begin(), filter.end(), op.id) != filter.end();
|
||||
const bool finished = op.status != "executing";
|
||||
const bool finished = op.status == "success" || op.status == "failed";
|
||||
if (match && (m == "z_getoperationstatus" || finished)) arr.push_back(opStatus(op));
|
||||
if (!(m == "z_getoperationresult" && match && finished)) keep.push_back(op);
|
||||
}
|
||||
@@ -792,6 +843,13 @@ json execOne(const json& req, int& status)
|
||||
std::lock_guard<std::mutex> lk(g_csMain);
|
||||
c = cannedReply(m, params);
|
||||
if (m == "setgenerate") g_minerOn = !params.empty() && params[0].is_boolean() && params[0].get<bool>();
|
||||
// FAKE_SCAN_SECS: the GUI's wallet-wide scans hold cs_main (z_mergetoaddress sleeps in its own handler).
|
||||
if (g_scanSecs > 0.0 && (m == "z_listunspent" || m == "listunspent" || m == "z_gettotalbalance" ||
|
||||
m == "z_listreceivedbyaddress")) {
|
||||
event("scan-begin", "m=" + m);
|
||||
std::this_thread::sleep_for(std::chrono::duration<double>(g_scanSecs));
|
||||
event("scan-end", "m=" + m);
|
||||
}
|
||||
if (g_noteWallet) {
|
||||
Canned w = noteWalletReply(m, params);
|
||||
if (w.known) c = w;
|
||||
@@ -1363,6 +1421,7 @@ void loadConfig(int argc, char** argv)
|
||||
g_cfg.tipUnknownSecs = envDouble("FAKE_TIP_UNKNOWN_SECS", g_cfg.tipUnknownSecs);
|
||||
g_cfg.longestAhead = std::max(0L, envLong("FAKE_LONGEST_AHEAD", g_cfg.longestAhead));
|
||||
g_cfg.catchupSecs = envDouble("FAKE_CATCHUP_SECS", g_cfg.catchupSecs);
|
||||
g_scanSecs = envDouble("FAKE_SCAN_SECS", 0.0);
|
||||
g_cfg.txcount = envLong("FAKE_TXCOUNT", g_cfg.txcount);
|
||||
g_cfg.height = envLong("FAKE_HEIGHT", g_cfg.height);
|
||||
g_cfg.rebuildTotal = std::max(1L, envLong("FAKE_REBUILD_TOTAL", g_cfg.rebuildTotal));
|
||||
|
||||
Reference in New Issue
Block a user