fix(audit): batch 1 — latent unlock/path/rescan correctness bugs

Four correctness fixes surfaced by the codebase audit:

1. Lite address-book path bug: AddressBook::getDefaultPath() hardcoded
   "ObsidianDragon" while Settings::getDefaultPath() uses DRAGONX_APP_NAME,
   so the Lite build wrote addressbook.json into the full-node app's config
   dir instead of ObsidianDragonLite/. Now uses DRAGONX_APP_NAME on all
   platforms (no macOS path change — the getConfigDir consolidation, which
   would move the macOS dir, is deferred).

2. PIN-unlock lockout bypass: the PIN path duplicated unlockWallet's
   success/lockout logic and its RPC-error branch bumped the attempt counter
   but skipped the escalating-lockout math entirely — a PIN user hitting an
   RPC error escaped the lockout curve. Extracted App::applyUnlockSuccess()
   and App::applyUnlockFailure() and routed all unlock paths (passphrase,
   PIN vault-fail, PIN RPC-fail) through them, so the lockout curve now
   applies uniformly. (noVault stays a mode-switch, not a failed attempt.)

3. Witness/rescan reset drift: the ~11-line rescan+witness completion reset
   was copy-pasted at four sites (app.cpp x2, app_network.cpp x2); adding a
   witness field and missing a copy would leave stale progress. Folded into
   App::resetWitnessRescanProgress(). Left the distinct phase-transition
   reset in App::update() untouched (it sets witness_phase, not 0).

4. Truncation underflow: send_tab/receive_tab's size_t TruncateAddress
   copies computed (maxLen - 3) without guarding maxLen <= 3, which wraps
   and throws std::out_of_range on short inputs. Added the guard.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:54:36 -05:00
parent 530bf24abf
commit f8150434d4
7 changed files with 89 additions and 94 deletions

View File

@@ -72,7 +72,9 @@ static bool s_generating_address = false;
// Helpers
// ============================================================================
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) {
if (addr.length() <= maxLen) return addr;
// Guard maxLen <= 3 before the unsigned (maxLen - 3): otherwise it wraps and the
// trailing substr(length - halfLen) throws std::out_of_range on short inputs.
if (maxLen <= 3 || addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}

View File

@@ -148,7 +148,9 @@ static double GetAvailableBalance(App* app) {
}
static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) {
if (addr.length() <= maxLen) return addr;
// Guard maxLen <= 3 before the unsigned (maxLen - 3): otherwise it wraps and the
// trailing substr(length - halfLen) throws std::out_of_range on short inputs.
if (maxLen <= 3 || addr.length() <= maxLen) return addr;
size_t halfLen = (maxLen - 3) / 2;
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}