Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ unsafe fn read_utf8(
/// * `address_ptr`/`address_len` — the UTF-8 P2PKH address whose key signs. Must
/// be one of this wallet's own addresses, on the wallet's network, and belong
/// to a signable funds account (BIP44 / BIP32 / CoinJoin /
/// DashPay-receiving). A foreign address, or a watch-only DashPay *external*
/// account's address, fails with
/// DashPay-receiving). A foreign address, a watch-only DashPay *external*
/// account's address, or a resolver that holds no mnemonic for this wallet
/// (the signer's key-unavailable completion) fails with
/// [`PlatformWalletFFIResultCode::ErrorSigningKeyUnavailable`] (31); an
/// unparseable, wrong-network, or non-P2PKH address fails with
/// [`PlatformWalletFFIResultCode::ErrorInvalidParameter`] (2).
Expand Down
126 changes: 47 additions & 79 deletions packages/rs-platform-wallet-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,17 @@ pub enum PlatformWalletFFIResultCode {
/// wallet-operation failure. Not retryable as-is — the key must be
/// (re-)derived first.
///
/// Also produced WITHOUT the signer round-trip, by
/// [`CoreWallet::sign_message`](platform_wallet::CoreWallet::sign_message):
/// a message-signing address that belongs to no signable funds account of
/// this wallet means no key can exist for it, which is the same conclusion
/// this code exists to carry — hosts route both to key repair / address
/// correction rather than to an opaque wallet-operation failure.
/// Also produced by
/// [`CoreWallet::sign_message`](platform_wallet::CoreWallet::sign_message),
/// both without a signer round-trip (a message-signing address that
/// belongs to no signable funds account of this wallet, so no key can
/// exist for it) and from the signer itself (the resolver reported no
/// mnemonic stored — `MnemonicResolverCoreSigner::NotFound`, whose
/// rendering stamps the same machine prefix at position 0 and which
/// `sign_message` promotes to the typed
/// `MessageSigningKeyUnavailable` before adding context). Hosts route
/// all of these to key repair / address correction rather than to an
/// opaque wallet-operation failure.
ErrorSigningKeyUnavailable = 31,

NotFound = 98, // Used exclusively for all the Option that are retuned as errors
Expand Down Expand Up @@ -446,76 +451,35 @@ impl From<PlatformWalletError> for PlatformWalletFFIResult {
PlatformWalletError::MessageSigningMessageInvalid { .. } => {
PlatformWalletFFIResultCode::ErrorInvalidParameter
}
// A second, signer-free producer of code 31 (the arm above is the
// first): a message-signing address that belongs to no signable
// funds account means no key can exist for it — the same conclusion
// the code carries — so hosts route it to key repair / address
// correction instead of an opaque wallet-operation failure.
// A second producer of code 31 (the arm above is the first),
// reached without any marker inspection at this layer: message
// signing found no signable account for the address, or the signer
// reported its key missing (the reserved marker at position 0 of
// its rendering, which `sign_message` promotes to this typed
// variant BEFORE composing any context string). Either way no key
// can sign as things stand, so hosts route it to key repair /
// address correction instead of an opaque failure.
PlatformWalletError::MessageSigningKeyUnavailable { .. } => {
PlatformWalletFFIResultCode::ErrorSigningKeyUnavailable
}
// NOTE: `MessageSigningFailed` is deliberately NOT matched, so it
// falls to the `ErrorUnknown` catch-all below. Its causes are
// internal invariant breaks (a public key that does not own the
// address, no recovery id that recovers it) which should read as a
// bug rather than as a key-repair prompt, and it carries the
// signer's own `Display`, which reaches the host in the message
// either way.
// address, no recovery id that recovers it) or an unclassifiable
// signer failure, which should read as a bug rather than as a
// key-repair prompt; it carries the signer's own `Display`, which
// reaches the host in the message either way.
//
// It is NOT promoted to code 31 by the key-unavailable arm above,
// and that is deliberate rather than an oversight. That arm matches
// STRUCTURALLY — `Sdk(Protocol(Generic(s)))` with the marker at
// position 0 — because #4183's review rejected sniffing the marker
// as a substring of the rendered error: a foreign signer can merely
// mention the token in human-readable text. `MessageSigningFailed`
// is a different variant, and `sign_message` composes its `reason`
// as "signer rejected the digest at {path}: {e}", so the marker
// could only ever appear mid-string. Matching it here would mean
// exactly the substring sniff that review ruled out.
//
// Consequence worth knowing: a signer-reported key-unavailable
// condition reaching `sign_message` surfaces as ErrorUnknown, not
// 31. That is NOT closable at this layer, and — having chased it —
// not closable at the producer either without an upstream change.
// The type chain is the whole story:
//
// * `preserve_signer_key_unavailable_or` (platform-wallet's own
// helper, #4183) takes a `dash_sdk::Error` and matches
// `Protocol(Generic(s))` with the marker at position 0. It is
// the right tool — for the STATE-TRANSITION signing paths
// (document replace, DPNS, token transfer), whose failures ARE
// `dash_sdk::Error`, which is where it is used.
// * Message signing does not use that surface at all. It calls
// key-wallet's `Signer::sign_ecdsa`, whose error is the
// associated type `S::Error`, bounded only by
// `Display + Send + Sync + 'static`. There is no enum to match:
// no `dash_sdk::Error`, no `ProtocolError`, nothing structural.
// * The one production impl, `MnemonicResolverCoreSigner`
// (rs-sdk-ffi), has `Error = MnemonicResolverSignerError` — a
// typed enum that never stamps the marker. Its `NotFound`
// ("mnemonic not found in keychain") IS the key-unavailable
// case, but nothing distinguishes it once it is `Display`ed.
// * The marker is produced only in `rs-sdk-ffi`'s state-transition
// completion callback (`SignResult = Result<Vec<u8>,
// ProtocolError>`), never on a `Signer::sign_ecdsa` path.
//
// So a position-0 check on the signer's rendering would have zero
// producers today, and a `contains` check is the substring sniff
// #4183's review rejected. Note the marker constant IS visible here
// now (#4183 mirrored it as
// `platform_wallet::error::SIGNER_KEY_UNAVAILABLE_PREFIX`, pinned
// byte-identical by a compile-time assertion in this crate) — the
// blocker is the error TYPE, not the constant, which corrects an
// earlier note in this file's history.
//
// The fix belongs upstream, in ONE of:
// (a) `MnemonicResolverCoreSigner` rendering its key-unavailable
// variants with the marker at position 0, after which a
// position-0 check in `sign_message` becomes principled; or
// (b) key-wallet tightening `Signer::Error` so callers can match
// a typed key-unavailable variant instead of a string.
// Both change shared, externally-consumed surfaces and want their
// own review; neither is in scope for message signing.
// The one signer failure with typed meaning never lands here:
// key-wallet's `Signer::Error` is bounded only by `Display`, so a
// key-unavailable backend stamps the reserved marker at position 0
// of its rendering (`MnemonicResolverCoreSigner::NotFound`), and
// `sign_message` recognizes exactly that — a position-0 check on
// the UNWRAPPED rendering, never a substring sniff of a composed
// reason (#4183 review) — and returns the typed
// `MessageSigningKeyUnavailable` mapped above. By the time a
// `MessageSigningFailed` reason exists, any marker in it sits
// mid-string and is deliberately not matched.
_ => PlatformWalletFFIResultCode::ErrorUnknown,
};
PlatformWalletFFIResult::err(code, error.to_string())
Expand Down Expand Up @@ -1068,10 +1032,12 @@ mod tests {
);
}

/// A second producer of code 31, reached with no signer round-trip and no
/// marker sniffing at all: the wallet simply holds no key for the address.
/// Hosts branch on it to correct the address or repair the key, so it must
/// not flatten to ErrorUnknown.
/// A second producer of code 31, reached with no marker inspection at
/// this layer: message signing concluded no key can sign for the address
/// (no signable account owns it, or the signer reported the key missing
/// and `sign_message` promoted the position-0 marker to this typed
/// variant). Hosts branch on it to correct the address or repair the key,
/// so it must not flatten to ErrorUnknown.
#[test]
fn message_signing_key_unavailable_maps_to_code_31() {
let err = PlatformWalletError::MessageSigningKeyUnavailable {
Expand Down Expand Up @@ -1180,12 +1146,14 @@ mod tests {
/// `MessageSigningMessageInvalid` mapping to ErrorInvalidParameter. What
/// remains here is genuinely internal.
///
/// #4183's key-unavailable promotion does NOT reach this variant, by
/// design: it matches `Sdk(Protocol(Generic(s)))` structurally with the
/// marker at position 0, because that review rejected sniffing the marker as
/// a substring. `sign_message` composes `reason` as
/// "signer rejected the digest at {path}: {e}", so a marker could only ever
/// sit mid-string here. See the NOTE on the mapping arm.
/// A signer-reported key-unavailable failure never lands on this variant:
/// `sign_message` checks the reserved marker at position 0 of the signer's
/// UNWRAPPED rendering and returns the typed
/// `MessageSigningKeyUnavailable` before composing `reason` as
/// "signer rejected the digest at {path}: {e}". Once a reason exists, any
/// marker in it sits mid-string, and matching it there would be the
/// substring sniff #4183's review rejected. See the NOTE on the mapping
/// arm.
#[test]
fn message_signing_failed_falls_through_to_unknown() {
let internal = PlatformWalletError::MessageSigningFailed {
Expand Down
46 changes: 30 additions & 16 deletions packages/rs-platform-wallet/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,23 @@ pub enum PlatformWalletError {
#[error("the message to sign for address {address} is not valid UTF-8: {reason}")]
MessageSigningMessageInvalid { address: String, reason: String },

/// [`CoreWallet::sign_message`] was given a well-formed P2PKH address for
/// the right network that this wallet holds no signing key for: it belongs
/// to no *signable* funds account (BIP44 / BIP32 / CoinJoin /
/// DashPay-receiving), or it belongs to a watch-only DashPay **external**
/// account — a contact's receiving address, whose keys we never had.
/// [`CoreWallet::sign_message`] holds no usable signing key for a
/// well-formed P2PKH address on the right network. Two producers:
///
/// Distinct from a signer *failure*: nothing was attempted, because no
/// derivation path resolves the address. Carries no retry value as-is; the
/// caller must supply an address the wallet owns.
/// * **Address resolution** — the address belongs to no *signable* funds
/// account (BIP44 / BIP32 / CoinJoin / DashPay-receiving), or it belongs
/// to a watch-only DashPay **external** account (a contact's receiving
/// address, whose keys we never had). No signer is invoked.
/// * **The signer itself** — the backend reported its key missing, stamped
/// as the reserved [`SIGNER_KEY_UNAVAILABLE_PREFIX`] at position 0 of its
/// error rendering (`MnemonicResolverCoreSigner::NotFound` in
/// production: the keychain holds no mnemonic for the wallet).
/// `sign_message` checks that marker BEFORE prepending any context, so
/// the condition stays typed across the FFI.
///
/// Either way the conclusion is the same — no key can sign for this
/// address as things stand — so hosts route both to key repair / address
/// correction (FFI code 31). Carries no retry value as-is.
///
/// [`CoreWallet::sign_message`]: crate::wallet::core::CoreWallet::sign_message
#[error(
Expand Down Expand Up @@ -156,14 +164,14 @@ pub enum PlatformWalletError {
/// the host mirror-enum churn that follows one.
///
/// Deliberately NOT given a dedicated FFI code: [`Signer::Error`] is
/// generic and bounded only by `Display`, so it cannot be classified
/// structurally here. Signer failures — including a signer-reported
/// key-unavailable completion — remain `MessageSigningFailed` and fall
/// through to `ErrorUnknown`. Only [`MessageSigningKeyUnavailable`]
/// (address resolution failing before a signer is ever invoked) reaches
/// FFI code 31. See the `MessageSigningFailed` arm's NOTE in
/// `platform-wallet-ffi`'s error conversion for the full type chain and
/// the upstream change that would be needed to close this gap.
/// generic and bounded only by `Display`, so what lands here cannot be
/// classified structurally and falls through to `ErrorUnknown`. The one
/// signer failure with a typed meaning — a key-unavailable rendering with
/// [`SIGNER_KEY_UNAVAILABLE_PREFIX`] at position 0 — never reaches this
/// variant: `sign_message` promotes it to
/// [`MessageSigningKeyUnavailable`] (FFI code 31) before any context
/// string is composed. See the `MessageSigningFailed` arm's NOTE in
/// `platform-wallet-ffi`'s error conversion.
///
/// [`MessageSigningKeyUnavailable`]: Self::MessageSigningKeyUnavailable
/// [`Signer::Error`]: key_wallet::signer::Signer::Error
Expand Down Expand Up @@ -604,6 +612,12 @@ pub fn promote_address_nonce_error_or_sdk(error: dash_sdk::Error) -> PlatformWal

/// The reserved machine prefix that a typed `SigningKeyUnavailable` signer
/// completion stamps at the **start** of its `ProtocolError::Generic` payload.
/// Also stamped at position 0 of `MnemonicResolverCoreSigner::NotFound`'s
/// `Display`, which is how a missing key stays recognizable across key-wallet's
/// `Signer` surface (whose error type is only `Display`) — `sign_message`
/// checks this prefix on the signer's rendering before adding any context and
/// promotes the failure to the typed
/// [`PlatformWalletError::MessageSigningKeyUnavailable`].
///
/// Canonically owned by the signer-completion boundary as
/// [`rs_sdk_ffi::DASH_SDK_SIGNER_ERR_KEY_UNAVAILABLE_PREFIX`]. It is mirrored
Expand Down
Loading
Loading