From c4f2ec20924d98c1fad64f78de5e8925ad05e4cc Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 13:47:09 -0400 Subject: [PATCH 1/5] feat(dpns): add early validation and helpful error messages for key selection Add comprehensive validation before signing DPNS registration state transitions to catch common errors early and provide clear, actionable error messages to developers. Fixes issue where users accidentally use MASTER key (ID 0) which is not allowed for DPNS registration, resulting in cryptic error messages only after signing attempt. --- packages/js-evo-sdk/src/dpns/facade.ts | 41 ++++++++++++++ packages/wasm-sdk/src/dpns.rs | 78 ++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/packages/js-evo-sdk/src/dpns/facade.ts b/packages/js-evo-sdk/src/dpns/facade.ts index 5f39b83a1d..b346d28d7e 100644 --- a/packages/js-evo-sdk/src/dpns/facade.ts +++ b/packages/js-evo-sdk/src/dpns/facade.ts @@ -33,8 +33,49 @@ export class DpnsFacade { return w.dpnsResolveName(name); } + /** + * Register a DPNS username + * + * @param args.label - The username label (without .dash suffix) + * @param args.identityId - The identity ID that will own the name + * @param args.publicKeyId - The identity key ID to use for signing + * IMPORTANT: Must be a key with: + * - Purpose: AUTHENTICATION (not TRANSFER) + * - Security Level: CRITICAL or HIGH (NOT MASTER) + * Typically use key ID 1 (CRITICAL) or key ID 2 (HIGH) + * @param args.privateKeyWif - The private key in WIF format matching publicKeyId + * @param args.onPreorder - Optional callback called after preorder succeeds + * @returns Registration result with document IDs + * + * @example + * ```javascript + * await sdk.dpns.registerName({ + * label: 'myname', + * identityId: 'xxx', + * publicKeyId: 1, // Use key 1 (CRITICAL) or 2 (HIGH), NOT 0 (MASTER) + * privateKeyWif: 'xxx' + * }); + * ``` + */ async registerName(args: { label: string; identityId: wasm.IdentifierLike; publicKeyId: number; privateKeyWif: string; onPreorder?: Function }): Promise { const { label, identityId, publicKeyId, privateKeyWif, onPreorder } = args; + + // Validate inputs + if (publicKeyId === undefined || publicKeyId === null) { + throw new Error( + 'publicKeyId is required for DPNS registration.\n' + + 'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n' + + 'Common key IDs:\n' + + ' - Key 1: CRITICAL security level\n' + + ' - Key 2: HIGH security level\n' + + 'Do NOT use Key 0 (MASTER security level).' + ); + } + + if (typeof publicKeyId !== 'number' || publicKeyId < 0) { + throw new Error(`publicKeyId must be a non-negative number, got: ${publicKeyId}`); + } + const w = await this.sdk.getWasmSdkConnected(); return w.dpnsRegisterName(label, identityId, publicKeyId, privateKeyWif, onPreorder ?? null); } diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index 515dc732c3..0ec47d41c8 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -5,6 +5,9 @@ use crate::queries::ProofMetadataResponseWasm; use crate::sdk::WasmSdk; use dash_sdk::dpp::document::{Document, DocumentV0Getters}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dash_sdk::dpp::identity::signer::Signer; +use dash_sdk::dpp::identity::{Purpose, SecurityLevel}; use dash_sdk::dpp::platform_value::{string_encoding::Encoding, Value}; use dash_sdk::dpp::prelude::Identifier; use dash_sdk::platform::dpns_usernames::{ @@ -246,6 +249,81 @@ impl WasmSdk { })? .clone(); + // Validate the key meets DPNS requirements + let key_purpose = identity_public_key.purpose(); + let key_security_level = identity_public_key.security_level(); + + // Check purpose + if key_purpose != Purpose::AUTHENTICATION { + return Err(WasmSdkError::invalid_argument(format!( + "Cannot register DPNS name with key ID {}: key has purpose {:?} but AUTHENTICATION is required.\n\ + Use a key with purpose AUTHENTICATION (usually keys 0-2).", + public_key_id, key_purpose + ))); + } + + // Check security level + if key_security_level != SecurityLevel::CRITICAL + && key_security_level != SecurityLevel::HIGH + { + let available_keys: Vec = identity + .public_keys() + .iter() + .filter_map(|(key_id, k)| { + if k.purpose() == Purpose::AUTHENTICATION + && (k.security_level() == SecurityLevel::CRITICAL + || k.security_level() == SecurityLevel::HIGH) + { + let level_name = if k.security_level() == SecurityLevel::CRITICAL { + "CRITICAL" + } else { + "HIGH" + }; + Some( + String::from(" Key ") + + &key_id.to_string() + + ": " + + level_name + + " security level", + ) + } else { + None + } + }) + .collect(); + + let suggestion = if available_keys.is_empty() { + "No suitable keys found in this identity.".to_string() + } else { + format!("Try one of these keys:\n{}", available_keys.join("\n")) + }; + + return Err(WasmSdkError::invalid_argument(format!( + "Cannot register DPNS name with key ID {}: key has {:?} security level but CRITICAL or HIGH is required.\n\ + \n\ + DPNS registration requires a key with:\n\ + - Purpose: AUTHENTICATION\n\ + - Security Level: CRITICAL or HIGH (not MASTER)\n\ + \n\ + {}", + public_key_id, key_security_level, suggestion + ))); + } + + // Validate private key matches public key + if !signer.can_sign_with(&identity_public_key) { + return Err(WasmSdkError::invalid_argument(format!( + "The provided private key does not match public key ID {}.\n\ + \n\ + Public key {} details:\n\ + - Security Level: {:?}\n\ + - Purpose: {:?}\n\ + \n\ + Please verify you're using the correct private key (WIF) for this key.", + public_key_id, public_key_id, key_security_level, key_purpose + ))); + } + thread_local! { static PREORDER_CALLBACK : std::cell::RefCell < Option < js_sys::Function >> = const { std::cell::RefCell::new(None) }; From 9dca14674ca54d8c7b2d5a4aae5117ed43d29826 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 14:15:18 -0400 Subject: [PATCH 2/5] test: add validation tests for registerName publicKeyId parameter --- .../tests/unit/facades/dpns.spec.mjs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs index d17931a650..7177f64dc7 100644 --- a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs @@ -54,4 +54,91 @@ describe('DPNSFacade', () => { expect(wasmSdk.getDpnsUsernameByName).to.be.calledOnceWithExactly('u'); expect(wasmSdk.getDpnsUsernameByNameWithProofInfo).to.be.calledOnceWithExactly('u'); }); + + describe('registerName validation', () => { + it('should throw error when publicKeyId is not provided', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + privateKeyWif: 'someKey', + // publicKeyId intentionally omitted + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('publicKeyId is required'); + expect(error.message).to.include('CRITICAL or HIGH security level'); + expect(error.message).to.include('Do NOT use Key 0'); + } + }); + + it('should throw error when publicKeyId is undefined', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: undefined, + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('publicKeyId is required'); + expect(error.message).to.include('CRITICAL or HIGH security level'); + expect(error.message).to.include('Do NOT use Key 0'); + } + }); + + it('should throw error when publicKeyId is null', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: null, + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('publicKeyId is required'); + } + }); + + it('should throw error when publicKeyId is negative', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: -1, + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('must be a non-negative number'); + expect(error.message).to.include('got: -1'); + } + }); + + it('should throw error when publicKeyId is not a number', async () => { + try { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: '1', + privateKeyWif: 'someKey', + }); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error.message).to.include('must be a non-negative number'); + } + }); + + it('should accept valid publicKeyId', async () => { + await client.dpns.registerName({ + label: 'test', + identityId: 'someId', + publicKeyId: 1, + privateKeyWif: 'someKey', + }); + expect(wasmSdk.dpnsRegisterName).to.be.calledOnce(); + }); + }); }); From f4a93aaf7ba2f839c7c9953e4520ea2eaa2873d8 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 15:32:41 -0400 Subject: [PATCH 3/5] chore: comment cleanup and review updates --- packages/js-evo-sdk/src/dpns/facade.ts | 17 +------------- packages/wasm-sdk/src/dpns.rs | 31 ++++++++++---------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/packages/js-evo-sdk/src/dpns/facade.ts b/packages/js-evo-sdk/src/dpns/facade.ts index b346d28d7e..18c3863092 100644 --- a/packages/js-evo-sdk/src/dpns/facade.ts +++ b/packages/js-evo-sdk/src/dpns/facade.ts @@ -42,20 +42,9 @@ export class DpnsFacade { * IMPORTANT: Must be a key with: * - Purpose: AUTHENTICATION (not TRANSFER) * - Security Level: CRITICAL or HIGH (NOT MASTER) - * Typically use key ID 1 (CRITICAL) or key ID 2 (HIGH) * @param args.privateKeyWif - The private key in WIF format matching publicKeyId * @param args.onPreorder - Optional callback called after preorder succeeds * @returns Registration result with document IDs - * - * @example - * ```javascript - * await sdk.dpns.registerName({ - * label: 'myname', - * identityId: 'xxx', - * publicKeyId: 1, // Use key 1 (CRITICAL) or 2 (HIGH), NOT 0 (MASTER) - * privateKeyWif: 'xxx' - * }); - * ``` */ async registerName(args: { label: string; identityId: wasm.IdentifierLike; publicKeyId: number; privateKeyWif: string; onPreorder?: Function }): Promise { const { label, identityId, publicKeyId, privateKeyWif, onPreorder } = args; @@ -64,11 +53,7 @@ export class DpnsFacade { if (publicKeyId === undefined || publicKeyId === null) { throw new Error( 'publicKeyId is required for DPNS registration.\n' + - 'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n' + - 'Common key IDs:\n' + - ' - Key 1: CRITICAL security level\n' + - ' - Key 2: HIGH security level\n' + - 'Do NOT use Key 0 (MASTER security level).' + 'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n' ); } diff --git a/packages/wasm-sdk/src/dpns.rs b/packages/wasm-sdk/src/dpns.rs index 0ec47d41c8..e78eae3c7b 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -257,7 +257,7 @@ impl WasmSdk { if key_purpose != Purpose::AUTHENTICATION { return Err(WasmSdkError::invalid_argument(format!( "Cannot register DPNS name with key ID {}: key has purpose {:?} but AUTHENTICATION is required.\n\ - Use a key with purpose AUTHENTICATION (usually keys 0-2).", + Use a key with purpose AUTHENTICATION.", public_key_id, key_purpose ))); } @@ -270,24 +270,17 @@ impl WasmSdk { .public_keys() .iter() .filter_map(|(key_id, k)| { - if k.purpose() == Purpose::AUTHENTICATION - && (k.security_level() == SecurityLevel::CRITICAL - || k.security_level() == SecurityLevel::HIGH) - { - let level_name = if k.security_level() == SecurityLevel::CRITICAL { - "CRITICAL" - } else { - "HIGH" - }; - Some( - String::from(" Key ") - + &key_id.to_string() - + ": " - + level_name - + " security level", - ) - } else { - None + if k.purpose() != Purpose::AUTHENTICATION { + return None; + } + match k.security_level() { + SecurityLevel::CRITICAL => { + Some(format!(" Key {}: CRITICAL security level", key_id)) + } + SecurityLevel::HIGH => { + Some(format!(" Key {}: HIGH security level", key_id)) + } + _ => None, } }) .collect(); From 9e06d0d078c97ca191240f5aa367b9f74963b291 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 15:52:17 -0400 Subject: [PATCH 4/5] fix: lint and test fix --- packages/js-evo-sdk/src/dpns/facade.ts | 4 ++-- packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/js-evo-sdk/src/dpns/facade.ts b/packages/js-evo-sdk/src/dpns/facade.ts index 18c3863092..46363a5149 100644 --- a/packages/js-evo-sdk/src/dpns/facade.ts +++ b/packages/js-evo-sdk/src/dpns/facade.ts @@ -52,8 +52,8 @@ export class DpnsFacade { // Validate inputs if (publicKeyId === undefined || publicKeyId === null) { throw new Error( - 'publicKeyId is required for DPNS registration.\n' + - 'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n' + 'publicKeyId is required for DPNS registration.\n' + + 'DPNS requires a key with AUTHENTICATION purpose and CRITICAL or HIGH security level.\n', ); } diff --git a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs index 7177f64dc7..7780c89651 100644 --- a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs @@ -68,7 +68,6 @@ describe('DPNSFacade', () => { } catch (error) { expect(error.message).to.include('publicKeyId is required'); expect(error.message).to.include('CRITICAL or HIGH security level'); - expect(error.message).to.include('Do NOT use Key 0'); } }); @@ -84,7 +83,6 @@ describe('DPNSFacade', () => { } catch (error) { expect(error.message).to.include('publicKeyId is required'); expect(error.message).to.include('CRITICAL or HIGH security level'); - expect(error.message).to.include('Do NOT use Key 0'); } }); From ed2205fe784622b5a46d994d1f91b3a8eb08d1b9 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 24 Dec 2025 11:16:56 -0500 Subject: [PATCH 5/5] test: add missing assertion for null publicKeyId error message --- packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs index 7780c89651..e94fe9027d 100644 --- a/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/dpns.spec.mjs @@ -97,6 +97,7 @@ describe('DPNSFacade', () => { expect.fail('Should have thrown an error'); } catch (error) { expect(error.message).to.include('publicKeyId is required'); + expect(error.message).to.include('CRITICAL or HIGH security level'); } });