From 6c0a664e5279df0420af6bd926fcb0ee825cf068 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 13:47:09 -0400 Subject: [PATCH 1/4] 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 166efc7d145..74d5c06d08c 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: string; 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 89945e17695..0cec2145c74 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -2,6 +2,9 @@ use crate::error::WasmSdkError; 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::prelude::Identifier; use dash_sdk::platform::dpns_usernames::{ convert_to_homograph_safe_chars, is_contested_username, is_valid_username, @@ -76,6 +79,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 + ))); + } + // Store the JS callback in a thread-local variable that we can access from the closure thread_local! { static PREORDER_CALLBACK: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; From 6674a044a444f8e968050b9cba4333c54ae10c24 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 14:15:18 -0400 Subject: [PATCH 2/4] 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 a0b2e0da145..8ebc8f672dd 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 79a318d3d2e7e1e75f8492b8bb95b64d6b76eb40 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 15:32:41 -0400 Subject: [PATCH 3/4] 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 74d5c06d08c..780f6202978 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: string; 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 0cec2145c74..e4da250aa51 100644 --- a/packages/wasm-sdk/src/dpns.rs +++ b/packages/wasm-sdk/src/dpns.rs @@ -87,7 +87,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 ))); } @@ -100,24 +100,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 28f639a14c70f555b758a70f4b08a4d8a8e860f6 Mon Sep 17 00:00:00 2001 From: thephez Date: Tue, 28 Oct 2025 15:52:17 -0400 Subject: [PATCH 4/4] 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 780f6202978..9ca171dd285 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 8ebc8f672dd..207168ef156 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'); } });