fix(import): recognize real DragonX key formats in the import gate

The client-side pre-check rejected legitimate keys before the daemon ever
saw them, surfacing "Unrecognized key format" / a cryptic daemon "Invalid"
error. Two concrete defects plus the brittle heuristic behind them:

- Viewing keys: isViewingKey looked for Zcash's "zxview" extended-FVK
  prefix, but DragonX's z_exportviewingkey emits a Sapling *incoming*
  viewing key (HRP "zivks"), which z_importviewingkey is the only form the
  daemon decodes. Every real DragonX viewing key was refused. (F1)
- Uncompressed transparent WIF: the length+first-char heuristic accepted
  {5,K,L,U} only, but a version-188 uncompressed key starts with '7'. (F2)

Replace the heuristic with structural validation using the existing
checksum validators (F3): add util::decodeBase58Check (checksum-stripped
payload) and util::bech32Hrp (HRP of a valid Bech32 string). Transparent
keys are now accepted by decoding Base58Check and checking the payload is a
33/34-byte secret key with a DragonX SECRET_KEY version byte (188 main/
regtest, 128 testnet) — covering compressed and uncompressed, rejecting
addresses/typos by real checksum. Viewing keys are matched by the real
incoming-VK HRPs (zivks / zivktestsapling / zivkregtestsapling).

The Sweep gate and the dialog's live type indicator run off the same
predicates, so they are fixed too (F4). Messaging now names the likely
cause and appends a wrong-coin/network hint to the daemon's raw "Invalid"
error (F5).

Adds testPrivateKeyImportRecognition plus decodeBase58Check/bech32Hrp
coverage; suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 22:22:29 -05:00
co-authored by Claude Opus 4.8
parent c61c211dfe
commit d603a54618
6 changed files with 133 additions and 10 deletions
+26 -6
View File
@@ -1,9 +1,12 @@
#include "wallet_security_controller.h"
#include "../util/secure_vault.h"
#include "../util/address_validation.h"
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <utility>
#include <vector>
namespace dragonx {
namespace services {
@@ -108,18 +111,35 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
bool WalletSecurityController::isViewingKey(const std::string& key)
{
// Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the
// lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them.
return key.rfind("zxview", 0) == 0;
// DragonX's z_exportviewingkey returns a Sapling *incoming* viewing key (mainnet HRP "zivks");
// z_importviewingkey only decodes that form. Recognize it structurally — a valid Bech32 checksum
// plus a known HRP — instead of a bare prefix, and cover testnet/regtest too. (The old check
// looked for Zcash's "zxview" extended-FVK HRP, which DragonX never emits, so every real viewing
// key was rejected client-side.) Watch-only: reveals the address's funds but cannot spend them.
const std::string hrp = util::bech32Hrp(key);
return hrp == "zivks" // mainnet
|| hrp == "zivktestsapling" // testnet
|| hrp == "zivkregtestsapling"; // regtest
}
bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key)
{
// Sapling z spending key (HRP "secret-extended-key-{main,test,regtest}"). These run ~300 chars,
// past the Bech32 length cap, so match by HRP prefix and let the daemon vet the payload.
if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key
if (key.size() >= 2 && key[0] == 'S' && key[1] == 'K') return true; // Sprout z spending key
// Transparent WIF: base58, ~51-52 chars, common version prefixes.
if (key.size() >= 51 && key.size() <= 52 &&
(key[0] == '5' || key[0] == 'K' || key[0] == 'L' || key[0] == 'U')) return true;
// Transparent WIF: decode Base58Check and confirm it is actually a secret key — version byte plus
// a 32-byte key, optionally a compression flag (payload 33 or 34 bytes). This accepts BOTH the
// compressed ("U…") and uncompressed ("7…") mainnet forms and the testnet form, and rejects
// addresses / typos via the real checksum — the old length+first-char heuristic dropped the
// uncompressed mainnet key (which starts with '7', not one of 5/K/L/U).
std::vector<std::uint8_t> payload;
if (util::decodeBase58Check(key, payload) &&
(payload.size() == 33 || payload.size() == 34) &&
(payload[0] == 188 /* DragonX main/regtest SECRET_KEY */ ||
payload[0] == 128 /* DragonX testnet SECRET_KEY */)) {
return true;
}
return false;
}
+1 -1
View File
@@ -74,7 +74,7 @@ public:
std::size_t minLength = 4);
static KeyKind classifyAddress(const std::string& address);
static KeyKind classifyPrivateKey(const std::string& key);
// True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only).
// True if `key` is a shielded viewing key (Sapling incoming viewing key, "zivks…" — watch-only).
static bool isViewingKey(const std::string& key);
// True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key.
static bool isRecognizedPrivateKey(const std::string& key);