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

@@ -5952,6 +5952,66 @@ void testAddressChecksumValidation()
EXPECT_FALSE(isValidBech32("abc1rzg")); // too short / bad checksum
EXPECT_FALSE(isValidBech32("Abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw")); // mixed case
EXPECT_FALSE(isValidBech32("nosalt")); // no separator
// decodeBase58Check exposes the checksum-stripped payload so callers can inspect version/length.
using dragonx::util::decodeBase58Check;
std::vector<std::uint8_t> payload;
EXPECT_TRUE(decodeBase58Check("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", payload));
EXPECT_EQ(payload.size(), (size_t)21); // version(1) + 20-byte hash160, checksum stripped
EXPECT_EQ((int)payload[0], 0); // mainnet P2PKH version byte
EXPECT_FALSE(decodeBase58Check("1A1zP1eP5QGefi2DMPTfTL5SLmv7Divfna", payload)); // bad checksum
// bech32Hrp returns the (lower-cased) HRP of a valid string, or "" when invalid.
using dragonx::util::bech32Hrp;
EXPECT_EQ(bech32Hrp("abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"), std::string("abcdef"));
EXPECT_EQ(bech32Hrp("A12UEL5L"), std::string("a")); // lower-cased
EXPECT_EQ(bech32Hrp("A12UEL5M"), std::string("")); // invalid → empty
}
// Import-key recognition: the client gate must accept every real DragonX key form and reject
// non-keys, so it never blocks a valid import with "Unrecognized key format" (audit F1/F2/F3).
void testPrivateKeyImportRecognition()
{
using dragonx::services::WalletSecurityController;
using KeyKind = WalletSecurityController::KeyKind;
// --- Transparent WIF (DragonX SECRET_KEY version 188; testnet 128) ---
const std::string wifCompressed = "Up3W7uVYkLxCfH91APxjSpkkGBWJyBrm3tt1bCz64V5fpZK9ef3C"; // v188 compressed 'U'
const std::string wifUncompressed = "7JTPumX2kofLQdHKANy8MMLRkmXCmuJcosiv9f4RFqW9oCJXBHD"; // v188 uncompressed '7'
const std::string wifTestnet = "KwFfpDsaF7yxCELuyrH9gP5XL7TAt5b9HPWC1xCQbmrxvhJgMQHb"; // v128 compressed
EXPECT_TRUE(WalletSecurityController::isRecognizedPrivateKey(wifCompressed));
EXPECT_TRUE(WalletSecurityController::isRecognizedPrivateKey(wifUncompressed)); // F2 regression: '7' was rejected
EXPECT_TRUE(WalletSecurityController::isRecognizedPrivateKey(wifTestnet));
EXPECT_TRUE(WalletSecurityController::isRecognizedImportKey(wifUncompressed));
EXPECT_EQ(WalletSecurityController::classifyPrivateKey(wifUncompressed), KeyKind::Transparent);
EXPECT_FALSE(WalletSecurityController::isViewingKey(wifCompressed));
// A corrupted WIF (flipped last char) fails the checksum → caught locally, not at the daemon.
std::string wifBad = wifCompressed;
wifBad.back() = (wifBad.back() == 'C' ? 'D' : 'C');
EXPECT_FALSE(WalletSecurityController::isRecognizedPrivateKey(wifBad));
// A transparent R-address is Base58Check-valid but NOT a key (21-byte payload, not 33/34).
EXPECT_FALSE(WalletSecurityController::isRecognizedPrivateKey("R9NXAVJezHiBnT3ijTpg3JUZre7PxhJWti"));
// --- Sapling incoming viewing key (mainnet HRP "zivks") — the F1 regression ---
const std::string ivk = "zivks1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwxqergd3c8g7rusq45amw7";
EXPECT_TRUE(WalletSecurityController::isViewingKey(ivk));
EXPECT_TRUE(WalletSecurityController::isRecognizedImportKey(ivk));
EXPECT_FALSE(WalletSecurityController::isRecognizedPrivateKey(ivk)); // a viewing key can't spend
// The stale Zcash "zxview…" prefix is NOT a DragonX viewing key (and not valid Bech32 here).
EXPECT_FALSE(WalletSecurityController::isViewingKey("zxviews1abcdef"));
// --- Sapling z spending key (recognized by HRP prefix; daemon vets the long payload) ---
const std::string zspend = "secret-extended-key-main1qxxxxxxxxxxxxxxxxxxxx";
EXPECT_TRUE(WalletSecurityController::isRecognizedPrivateKey(zspend));
EXPECT_TRUE(WalletSecurityController::isRecognizedImportKey(zspend));
EXPECT_EQ(WalletSecurityController::classifyPrivateKey(zspend), KeyKind::Shielded);
// --- Garbage / empty ---
EXPECT_FALSE(WalletSecurityController::isRecognizedImportKey(""));
EXPECT_FALSE(WalletSecurityController::isRecognizedImportKey("hello world"));
}
// Live probe of a real lite server (env-gated). Validates CONNECT_ONLY latency + IP capture.
@@ -7195,6 +7255,7 @@ int main()
testHushChatShuffledReceive();
testWalletFileProbe();
testAddressChecksumValidation();
testPrivateKeyImportRecognition();
testLiteServerProbeLive();
testXmrigLiveInstall();
testGeneratedResourceBehavior();