fix(autoshield): defer to the daemon's own coinbase auto-shield on v1.3.0+
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1241,6 +1241,13 @@ private:
|
|||||||
|
|
||||||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||||||
std::atomic<bool> auto_shield_pending_{false};
|
std::atomic<bool> 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<bool> daemon_autoshield_probe_inflight_{false};
|
||||||
|
|
||||||
// P4: Incremental transaction cache
|
// P4: Incremental transaction cache
|
||||||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||||||
|
|||||||
@@ -714,6 +714,11 @@ void App::onDisconnected(const std::string& reason)
|
|||||||
wallet_seed_status_ = WalletSeedStatus::Unknown;
|
wallet_seed_status_ = WalletSeedStatus::Unknown;
|
||||||
wallet_seed_status_attempts_ = 0;
|
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
|
// Clear RPC result caches
|
||||||
viewtx_cache_.clear();
|
viewtx_cache_.clear();
|
||||||
confirmed_tx_cache_.clear();
|
confirmed_tx_cache_.clear();
|
||||||
@@ -1741,9 +1746,30 @@ void App::refreshCoreData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-shield transparent funds if enabled
|
// Auto-shield transparent funds if enabled. A v1.3.0+ daemon auto-shields coinbase
|
||||||
if (result.balanceOk && settings_ && settings_->getAutoShield() &&
|
// itself; probe z_autoshieldstatus once (while synced, so the daemon is past warmup) and
|
||||||
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing &&
|
// 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<bool>();
|
||||||
|
} 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)) {
|
!auto_shield_pending_.exchange(true)) {
|
||||||
std::string targetZAddr;
|
std::string targetZAddr;
|
||||||
for (const auto& addr : state_.addresses) {
|
for (const auto& addr : state_.addresses) {
|
||||||
|
|||||||
Reference in New Issue
Block a user