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
parent c61c211dfe
commit d603a54618
6 changed files with 133 additions and 10 deletions

View File

@@ -76,7 +76,7 @@ std::vector<int> bech32HrpExpand(const std::string& hrp)
} // namespace
bool isValidBase58Check(const std::string& s)
bool decodeBase58Check(const std::string& s, std::vector<std::uint8_t>& payloadOut)
{
if (s.size() < 5 || s.size() > 256) return false;
std::vector<std::uint8_t> data;
@@ -88,7 +88,15 @@ bool isValidBase58Check(const std::string& s)
unsigned char h2[crypto_hash_sha256_BYTES];
crypto_hash_sha256(h1, data.data(), payloadLen);
crypto_hash_sha256(h2, h1, sizeof(h1));
return std::memcmp(h2, data.data() + payloadLen, 4) == 0;
if (std::memcmp(h2, data.data() + payloadLen, 4) != 0) return false;
payloadOut.assign(data.begin(), data.begin() + payloadLen);
return true;
}
bool isValidBase58Check(const std::string& s)
{
std::vector<std::uint8_t> payload;
return decodeBase58Check(s, payload);
}
bool isValidBech32(const std::string& s)
@@ -127,5 +135,18 @@ bool isValidBech32(const std::string& s)
return bech32Polymod(combined) == 1; // original Bech32 constant (Sapling, not Bech32m)
}
std::string bech32Hrp(const std::string& s)
{
if (!isValidBech32(s)) return {};
// isValidBech32 already rejected mixed case and guaranteed a non-empty HRP before the
// final '1' separator, so lower-casing and splitting there recovers the HRP verbatim.
std::string lower(s);
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
const std::size_t sep = lower.rfind('1');
if (sep == std::string::npos) return {};
return lower.substr(0, sep);
}
} // namespace util
} // namespace dragonx

View File

@@ -11,7 +11,9 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace dragonx {
namespace util {
@@ -20,9 +22,18 @@ namespace util {
// (transparent R-addresses). Version-byte agnostic by design.
bool isValidBase58Check(const std::string& s);
// Decodes `s` as Base58Check; on success returns true and fills `payloadOut` with the
// decoded bytes EXCLUDING the trailing 4-byte checksum (i.e. version byte + data). Lets
// callers inspect the version byte / payload length (e.g. to tell a WIF from an address).
bool decodeBase58Check(const std::string& s, std::vector<std::uint8_t>& payloadOut);
// True if `s` is a valid Bech32 string (Sapling zs-addresses). The HRP is taken
// from the string itself and folded into the checksum, so no HRP is hardcoded.
bool isValidBech32(const std::string& s);
// Returns the (lower-cased) human-readable prefix of a valid Bech32 string, or "" if
// `s` is not valid Bech32. The HRP identifies the key/address type (e.g. "zivks").
std::string bech32Hrp(const std::string& s);
} // namespace util
} // namespace dragonx