fix(send): accept P2SH/multisig recipients in the send + URI address gates
Same defect class as the import-key fix: a hardcoded prefix/length pre-filter
layered over the checksum validators rejected valid addresses before the daemon
saw them. The send-screen recipient gate required a[0]=='R', and the payment-URI
parser accepted only 'R'/'t' with rigid length bands — so every valid P2SH /
multisig address (DragonX SCRIPT_ADDRESS=85 → 'b…') was silently refused, leaving
the Send button disabled with no usable recipient.
Centralize recipient recognition in util/address_validation:
- isTransparentAddress: Base58Check with a 21-byte version+hash160 payload —
covers P2PKH ('R…', v60) AND P2SH ('b…', v85) on every network, rejects WIF
keys / typos by real checksum.
- isShieldedAddress: Bech32 + a Sapling payment-address HRP (zs / ztestsapling /
zregtestsapling), distinguishing a payment address from a viewing key.
- isValidRecipientAddress: either of the above.
send_tab's two validity helpers (the single choke point for all 5 call sites) and
the payment-URI format check now route through these. The URI parser now
checksum-validates the recipient (fail-fast on transcription errors) rather than
being prefix/length-only.
Tests use real checksummed vectors (P2PKH/P2SH/shielded, WIF- and typo-rejection);
testPaymentUri updated off its old fake fixed-char addresses. Suite green (1/1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -148,5 +148,26 @@ std::string bech32Hrp(const std::string& s)
|
||||
return lower.substr(0, sep);
|
||||
}
|
||||
|
||||
bool isTransparentAddress(const std::string& s)
|
||||
{
|
||||
std::vector<std::uint8_t> payload;
|
||||
// version byte (1) + hash160 (20) = 21 bytes, checksum stripped. Covers P2PKH ('R', v60) and
|
||||
// P2SH/multisig ('b', v85); the daemon vets the exact version byte for the active network.
|
||||
return decodeBase58Check(s, payload) && payload.size() == 21;
|
||||
}
|
||||
|
||||
bool isShieldedAddress(const std::string& s)
|
||||
{
|
||||
const std::string hrp = bech32Hrp(s);
|
||||
return hrp == "zs" // mainnet Sapling payment address
|
||||
|| hrp == "ztestsapling" // testnet
|
||||
|| hrp == "zregtestsapling"; // regtest
|
||||
}
|
||||
|
||||
bool isValidRecipientAddress(const std::string& s)
|
||||
{
|
||||
return isTransparentAddress(s) || isShieldedAddress(s);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -35,5 +35,18 @@ bool isValidBech32(const std::string& s);
|
||||
// `s` is not valid Bech32. The HRP identifies the key/address type (e.g. "zivks").
|
||||
std::string bech32Hrp(const std::string& s);
|
||||
|
||||
// True if `s` is a transparent (Base58Check) address — P2PKH *or* P2SH/multisig. Accepts any
|
||||
// address whose payload is a 21-byte version+hash160, so it covers both the 'R…' (v60) and 'b…'
|
||||
// (v85 script) forms on every DragonX network and rejects WIF keys / typos by checksum. Version-byte
|
||||
// agnostic by design — a bare prefix check ('R' only) silently drops valid P2SH recipients.
|
||||
bool isTransparentAddress(const std::string& s);
|
||||
|
||||
// True if `s` is a shielded Sapling payment address (HRP "zs" / "ztestsapling" / "zregtestsapling"),
|
||||
// with a valid Bech32 checksum. Distinguishes a payment address from a viewing key (e.g. "zivks…").
|
||||
bool isShieldedAddress(const std::string& s);
|
||||
|
||||
// True if `s` is any address a payment can be sent to (transparent or shielded).
|
||||
bool isValidRecipientAddress(const std::string& s);
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "payment_uri.h"
|
||||
#include "address_validation.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
@@ -161,20 +162,11 @@ PaymentURI parsePaymentURI(const std::string& uri)
|
||||
return result;
|
||||
}
|
||||
|
||||
// Basic address format check. NOTE: this is format-only by design — the send flow
|
||||
// checksum-validates the recipient (isValidBase58Check / shielded check) before broadcasting,
|
||||
// so an invalid-checksum address parsed here can never actually be sent to.
|
||||
bool validFormat = false;
|
||||
|
||||
// z-address: starts with 'zs' and is 78+ chars
|
||||
if (result.address[0] == 'z' && result.address.size() >= 78) {
|
||||
validFormat = true;
|
||||
}
|
||||
// t-address: starts with 'R' (DragonX) or 't' (HUSH) and is ~34 chars
|
||||
else if ((result.address[0] == 'R' || result.address[0] == 't') &&
|
||||
result.address.size() >= 26 && result.address.size() <= 36) {
|
||||
validFormat = true;
|
||||
}
|
||||
// Address format check via the shared, structure-based recognizers (checksum + real DragonX
|
||||
// address types). This accepts shielded ("zs…"), P2PKH ("R…") and P2SH/multisig ("b…") forms —
|
||||
// the old prefix/length heuristic rejected P2SH and hardcoded a 't' prefix DragonX never emits.
|
||||
const bool validFormat = isShieldedAddress(result.address) ||
|
||||
isTransparentAddress(result.address);
|
||||
|
||||
if (!validFormat) {
|
||||
result.error = "Invalid address format";
|
||||
|
||||
Reference in New Issue
Block a user