Skip to content

fix: show user-friendly error for duplicate identity keys#729

Merged
lklimek merged 3 commits into
v1.0-devfrom
fix/714-duplicate-key-error
Mar 11, 2026
Merged

fix: show user-friendly error for duplicate identity keys#729
lklimek merged 3 commits into
v1.0-devfrom
fix/714-duplicate-key-error

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Mar 11, 2026

Summary

Closes #714

  • Replace raw base64-encoded CBOR error with actionable user-friendly messages when adding a duplicate key to an identity
  • Match structured SDK error variants (ConsensusError::StateError) — no string-based pattern matching
  • Add three TaskError variants with actionable Display messages
  • broadcast_error() returns typed TaskError directly — no String round-trip
  • add_key_to_identity() and run_identity_task() return Result<..., TaskError> for end-to-end typed error propagation

Error messages

Scenario SDK Variant Message
Duplicate key data DuplicatedIdentityPublicKeyStateError "This public key is already registered on the platform. Try a different key."
Duplicate key hash DuplicatedIdentityPublicKeyIdStateError "This key hash is already registered on the platform. Try a different key."
Contract bounds conflict IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError "This key conflicts with an existing key bound to contract {id}. Use a different key or purpose."
Unknown broadcast error Any other SdkError "Broadcasting error: ..." (generic fallback)

Note: Only unique key types (ECDSA_SECP256K1, BLS12_381) require platform-wide uniqueness. Non-unique types (ECDSA_HASH160, BIP13_SCRIPT_HASH, EDDSA_25519_HASH160) can be shared across identities — error messages intentionally avoid claiming "all keys must be globally unique".

Test plan

🤖 Co-authored by Claudius the Magnificent AI Agent

Replace raw base64-encoded CBOR error with actionable messages when
adding a duplicate key to an identity. Match structured SDK error
variants (ConsensusError::StateError) instead of string parsing.

- Add TaskError variants: DuplicateIdentityPublicKey,
  DuplicateIdentityPublicKeyId, IdentityPublicKeyContractBoundsConflict
- Add broadcast_error_message() helper matching both SDK error paths
  (StateTransitionBroadcastError and Protocol/ConsensusError)
- 5 unit tests covering all variants + fallback
- Manual test scenarios document

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

These changes address unreadable internal errors when attempting to add duplicate keys to identities. They introduce three new error variants to properly categorize duplicate key scenarios and implement error message mapping logic to convert SDK consensus errors into user-friendly messages, supported by manual test documentation covering five core scenarios and edge cases.

Changes

Cohort / File(s) Summary
Error Type Definitions
src/backend_task/error.rs
Added three new TaskError variants: DuplicateIdentityPublicKey and DuplicateIdentityPublicKeyId for duplicate key scenarios, plus IdentityPublicKeyContractBoundsConflict for contract-bound key conflicts, each with descriptive user-facing messages.
Error Handling Logic
src/backend_task/identity/add_key_to_identity.rs
Introduced broadcast_error_message helper function to parse SDK consensus errors and map them to appropriate TaskError variants; integrated this function into the IdentityAddKey broadcast flow; added unit tests validating error mapping for duplicates, contract conflicts, and unknown errors.
Test Documentation
docs/ai-design/2026-03-11-duplicate-key-error/manual-test-scenarios.md
Added comprehensive manual test scenarios covering duplicate public key data, key ID conflicts, contract bounds violations, unrecognized broadcast errors, and successful recovery; includes edge cases for rapid submissions, wallet state changes, network issues, and contract ID length.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 A error once cryptic, now speaks plain and clear,
No base64 shadows to cause users fear,
Duplicate keys now have messages bright,
Each scenario tested, each banner just right,
The identity service hops forward with cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: show user-friendly error for duplicate identity keys' directly and clearly describes the primary change in the PR - replacing raw broadcast errors with user-friendly messages when adding duplicate keys.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #714: parses broadcast error messages, maps them to structured TaskError variants with user-friendly messages, and implements error handling in the IdentityAddKey flow with comprehensive unit tests.
Out of Scope Changes check ✅ Passed All changes are scoped to issue #714 requirements: error variant definitions, broadcast error message handler, integration into IdentityAddKey flow, and unit tests with documentation. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/714-duplicate-key-error

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

lklimek and others added 2 commits March 11, 2026 11:48
… per-identity

DuplicatedIdentityPublicKeyStateError and DuplicatedIdentityPublicKeyIdStateError
are platform-wide uniqueness constraints, not per-identity. Updated error messages
to reflect that keys must be globally unique across all identities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only unique key types (ECDSA_SECP256K1, BLS12_381) require platform-wide
uniqueness. Non-unique types (ECDSA_HASH160, BIP13_SCRIPT_HASH,
EDDSA_25519_HASH160) can be shared across identities. Simplified messages
to avoid misleading users about key uniqueness rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lklimek lklimek requested a review from Copilot March 11, 2026 12:26
@lklimek lklimek marked this pull request as ready for review March 11, 2026 12:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves the UX when adding identity keys by mapping structured Dash SDK broadcast/consensus errors to actionable, user-friendly messages instead of displaying raw/encoded error payloads.

Changes:

  • Add broadcast_error_message() helper to translate specific ConsensusError::StateError variants into user-facing messages.
  • Introduce 3 new TaskError variants for duplicate-key and contract-bounds conflict scenarios.
  • Add unit tests for the supported SDK error paths and a manual testing checklist doc.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/backend_task/identity/add_key_to_identity.rs Adds structured error-to-message mapping for identity key broadcast failures + unit tests.
src/backend_task/error.rs Adds new TaskError variants with user-friendly Display messages.
docs/ai-design/2026-03-11-duplicate-key-error/manual-test-scenarios.md Documents manual verification steps for the new error handling behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/backend_task/error.rs
Comment thread src/backend_task/identity/add_key_to_identity.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/backend_task/error.rs`:
- Around line 68-70: Update the error string for the
DuplicateIdentityPublicKeyId enum variant in error.rs so it correctly states
that the conflict is a duplicate publicKeys[].id within the same identity rather
than a platform-wide key hash collision; replace the current message in the
#[error(...)] attribute for DuplicateIdentityPublicKeyId with text that tells
the user the key ID is already used in this identity and to choose a unique key
ID.

In `@src/backend_task/identity/add_key_to_identity.rs`:
- Around line 25-53: Change broadcast_error_message to return TaskError (not
String): map the SDK consensus cases to the structured TaskError variants
already used (TaskError::DuplicateIdentityPublicKey,
TaskError::DuplicateIdentityPublicKeyId,
TaskError::IdentityPublicKeyContractBoundsConflict { contract_id: format!("{}",
e.contract_id()) }) and for any other/unhandled broadcast error return a fixed,
non-raw TaskError (e.g. TaskError::Generic("Failed to broadcast state
transition".into()) or your project’s dedicated broadcast-failure variant). Then
update the callers that currently do map_err(format!(...)) to accept/propagate
TaskError (adjust the surrounding function signatures to Result<..., TaskError>
and replace map_err(|e| broadcast_error_message(&e)) or equivalent so the
structured TaskError is propagated instead of a String).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa8ec6a6-e70a-4ef7-9f31-05bb89da9597

📥 Commits

Reviewing files that changed from the base of the PR and between fd0bb08 and b9cc1b7.

📒 Files selected for processing (3)
  • docs/ai-design/2026-03-11-duplicate-key-error/manual-test-scenarios.md
  • src/backend_task/error.rs
  • src/backend_task/identity/add_key_to_identity.rs

Comment thread src/backend_task/error.rs
Comment on lines +25 to +53
fn broadcast_error_message(error: &SdkError) -> String {
let consensus_error = match error {
SdkError::StateTransitionBroadcastError(broadcast_err) => broadcast_err.cause.as_ref(),
SdkError::Protocol(ProtocolError::ConsensusError(ce)) => Some(ce.as_ref()),
_ => None,
};

if let Some(ce) = consensus_error {
match ce {
ConsensusError::StateError(StateError::DuplicatedIdentityPublicKeyStateError(_)) => {
return TaskError::DuplicateIdentityPublicKey.to_string();
}
ConsensusError::StateError(StateError::DuplicatedIdentityPublicKeyIdStateError(_)) => {
return TaskError::DuplicateIdentityPublicKeyId.to_string();
}
ConsensusError::StateError(
StateError::IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError(e),
) => {
return TaskError::IdentityPublicKeyContractBoundsConflict {
contract_id: format!("{}", e.contract_id()),
}
.to_string();
}
_ => {}
}
}

format!("Broadcasting error: {}", error)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return a typed, sanitized error from this helper.

broadcast_error_message() flattens the new TaskError variants back into String, so this task still crosses the boundary as Result<_, String> and impl From<String> for TaskError will rebuild them as Generic. The same helper also falls back to format!("Broadcasting error: {}", error), which still surfaces raw SDK text for any unhandled broadcast/consensus error. Return TaskError here, keep the structured variants intact, and use a fixed user message for unknown broadcast failures.

💡 Minimal direction
-fn broadcast_error_message(error: &SdkError) -> String {
+fn broadcast_error(error: &SdkError) -> TaskError {
     let consensus_error = match error {
         SdkError::StateTransitionBroadcastError(broadcast_err) => broadcast_err.cause.as_ref(),
         SdkError::Protocol(ProtocolError::ConsensusError(ce)) => Some(ce.as_ref()),
         _ => None,
     };

     if let Some(ce) = consensus_error {
         match ce {
             ConsensusError::StateError(StateError::DuplicatedIdentityPublicKeyStateError(_)) => {
-                return TaskError::DuplicateIdentityPublicKey.to_string();
+                return TaskError::DuplicateIdentityPublicKey;
             }
             ConsensusError::StateError(StateError::DuplicatedIdentityPublicKeyIdStateError(_)) => {
-                return TaskError::DuplicateIdentityPublicKeyId.to_string();
+                return TaskError::DuplicateIdentityPublicKeyId;
             }
             ConsensusError::StateError(
                 StateError::IdentityPublicKeyAlreadyExistsForUniqueContractBoundsError(e),
             ) => {
-                return TaskError::IdentityPublicKeyContractBoundsConflict {
-                    contract_id: format!("{}", e.contract_id()),
-                }
-                .to_string();
+                return TaskError::IdentityPublicKeyContractBoundsConflict {
+                    contract_id: e.contract_id().to_string(),
+                };
             }
             _ => {}
         }
     }

-    format!("Broadcasting error: {}", error)
+    match error {
+        SdkError::StateTransitionBroadcastError(_)
+        | SdkError::Protocol(ProtocolError::ConsensusError(_)) => {
+            TaskError::Generic("Failed to broadcast the key update. Please try again.".into())
+        }
+        _ => TaskError::Generic(format!("Broadcasting error: {}", error)),
+    }
 }
 ...
-            .map_err(|ref e| broadcast_error_message(e))?;
+            .map_err(|e| broadcast_error(&e))?;

The surrounding task signature and the other local map_err(format!(...)) sites need the matching TaskError conversion too. As per coding guidelines: src/backend_task/**/*.rs: Backend tasks should return Result<T, TaskError> from src/backend_task/error.rs.

Also applies to: 107-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend_task/identity/add_key_to_identity.rs` around lines 25 - 53,
Change broadcast_error_message to return TaskError (not String): map the SDK
consensus cases to the structured TaskError variants already used
(TaskError::DuplicateIdentityPublicKey, TaskError::DuplicateIdentityPublicKeyId,
TaskError::IdentityPublicKeyContractBoundsConflict { contract_id: format!("{}",
e.contract_id()) }) and for any other/unhandled broadcast error return a fixed,
non-raw TaskError (e.g. TaskError::Generic("Failed to broadcast state
transition".into()) or your project’s dedicated broadcast-failure variant). Then
update the callers that currently do map_err(format!(...)) to accept/propagate
TaskError (adjust the surrounding function signatures to Result<..., TaskError>
and replace map_err(|e| broadcast_error_message(&e)) or equivalent so the
structured TaskError is propagated instead of a String).

@lklimek lklimek merged commit ece3659 into v1.0-dev Mar 11, 2026
14 checks passed
@lklimek lklimek deleted the fix/714-duplicate-key-error branch March 11, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Internal error when adding duplicate key on Identities > Add key

2 participants