From ba1d760bb330dd21d40f21864542e21f8d136c17 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 31 Aug 2026 22:33:12 -0500 Subject: [PATCH] fix(autoshield): defer to the daemon's own coinbase auto-shield on v1.3.0+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.3.0 daemons auto-shield coinbase by default (when the HD seed is recoverable — which every ObsidianDragon-created wallet is, via -usemnemonic=1). The wallet also ran its own client-side auto-shield every refresh tick, so both raced for the same coinbase UTXOs and split funds across different z-addresses (the wallet picks the first z_listaddresses entry; the daemon uses a seed-hardened derivation). Probe z_autoshieldstatus once per connection (while synced, so the daemon is past warmup) and skip the wallet's client-side shield when the daemon reports it active. Fail-closed: a pre-1.3.0 daemon has no such RPC, so the probe returns active=false and the wallet keeps shielding — no regression on the currently-bundled v1.0.3. Re-probes on reconnect (handles a live daemon upgrade/swap). Follow-up (not done): drive the Settings "Auto-shield" toggle + disabled_reason from z_autoshieldstatus (O1) — needs runtime verification of the RPC fields on a live v1.3.0 node. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.h | 7 +++++++ src/app_network.cpp | 32 +++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/app.h b/src/app.h index 0bcd80d..fe1fd7c 100644 --- a/src/app.h +++ b/src/app.h @@ -1241,6 +1241,13 @@ private: // Auto-shield guard (prevents concurrent auto-shield operations) std::atomic auto_shield_pending_{false}; + // v1.3.0+ daemons auto-shield coinbase themselves; probe z_autoshieldstatus once per connection and + // defer the wallet's own client-side auto-shield when the daemon is doing it (otherwise both race for + // the same coinbase UTXOs and split funds across different z-addresses). Fail-closed: a pre-1.3.0 + // daemon lacks the RPC → active stays false → the wallet keeps shielding client-side (no regression). + bool daemon_autoshield_probed_ = false; + bool daemon_autoshield_active_ = false; + std::atomic daemon_autoshield_probe_inflight_{false}; // P4: Incremental transaction cache int last_tx_block_height_ = -1; // block height at last full tx fetch diff --git a/src/app_network.cpp b/src/app_network.cpp index 8641598..0c82961 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -714,6 +714,11 @@ void App::onDisconnected(const std::string& reason) wallet_seed_status_ = WalletSeedStatus::Unknown; wallet_seed_status_attempts_ = 0; + // Re-probe whether the daemon auto-shields coinbase on the next connect — it may have been + // upgraded/swapped (e.g. v1.0.3 which has no z_autoshieldstatus -> v1.3.0 which auto-shields). + daemon_autoshield_probed_ = false; + daemon_autoshield_active_ = false; + // Clear RPC result caches viewtx_cache_.clear(); confirmed_tx_cache_.clear(); @@ -1741,9 +1746,30 @@ void App::refreshCoreData() } } - // Auto-shield transparent funds if enabled - if (result.balanceOk && settings_ && settings_->getAutoShield() && - state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing && + // Auto-shield transparent funds if enabled. A v1.3.0+ daemon auto-shields coinbase + // itself; probe z_autoshieldstatus once (while synced, so the daemon is past warmup) and + // defer to it when it's active — otherwise the wallet and the daemon race for the same + // coinbase UTXOs and split funds across different z-addresses. A pre-1.3.0 daemon lacks + // the RPC, so the probe fails closed (active=false) and the wallet keeps shielding. + const bool autoShieldEligible = result.balanceOk && settings_ && settings_->getAutoShield() && + state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing; + if (autoShieldEligible && !daemon_autoshield_probed_ && worker_ && + !daemon_autoshield_probe_inflight_.exchange(true)) { + worker_->post([this]() -> rpc::RPCWorker::MainCb { + bool active = false; + try { + auto st = rpc_->call("z_autoshieldstatus", json::array()); + if (st.is_object() && st.contains("autoshield")) + active = st["autoshield"].get(); + } catch (...) { active = false; } // pre-1.3.0 daemon: method not found + return [this, active]() { + daemon_autoshield_active_ = active; + daemon_autoshield_probed_ = true; + daemon_autoshield_probe_inflight_ = false; + }; + }); + } + if (autoShieldEligible && daemon_autoshield_probed_ && !daemon_autoshield_active_ && !auto_shield_pending_.exchange(true)) { std::string targetZAddr; for (const auto& addr : state_.addresses) {