Skip to content

Conversation

@UdjinM6
Copy link

@UdjinM6 UdjinM6 commented Dec 19, 2025

Issue being fixed or feature implemented

When creating a wallet with an external signer (hardware wallet), the mock signer returns multiple descriptors (BIP44, BIP49, BIP84) for both receive and internal categories. Previously, all valid descriptors were being activated by calling AddActiveScriptPubKeyMan() for each one.

This caused a database key collision since multiple descriptors tried to write to the same key (ACTIVEEXTERNALSPK or ACTIVEINTERNALSPK), resulting in non-deterministic behavior where sometimes BIP44 would be active and sometimes BIP84, depending on database flush timing.

What was done?

The fix follows the pattern from the non-external-signer branch which stores multiple descriptors but only activates certain types (External and Internal, but not CoinJoin). Now we:

  • Store ALL descriptors returned by the signer in m_spk_managers
  • Only activate descriptors matching the BIP44 path pattern /44'/{cointype}'

This ensures deterministic behavior and fixes the intermittent test failure in wallet_signer.py where the test expected m/44' but sometimes got m/84'.

Should fix intermittent test failures like https://github.com/dashpay/dash/actions/runs/20364286460/job/58521843159#step:6:1662

How Has This Been Tested?

Run wallet_signer.py multiple times

Breaking Changes

n/a

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

@UdjinM6 UdjinM6 added this to the 23.1 milestone Dec 19, 2025
@github-actions
Copy link

github-actions bot commented Dec 19, 2025

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

@coderabbitai
Copy link

coderabbitai bot commented Dec 19, 2025

Walkthrough

In SetupDescriptorScriptPubKeyMans for external signers, activation of newly loaded DescriptorScriptPubKeyMan is now conditional. After creating and storing a descriptor-based ScriptPubKeyMan, the code examines the descriptor string for a BIP44 path fragment constructed as "/<BIP32_PURPOSE_STANDARD>'/". Only when that bip44_purpose fragment is present does the code call AddActiveScriptPubKeyMan to activate the manager; otherwise activation is skipped. A comment explaining this gating was added.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Signer as External Signer / Descriptor Source
  participant Wallet as Wallet::SetupDescriptorScriptPubKeyMans
  participant Man as DescriptorScriptPubKeyMan (stored)
  participant Active as ActiveManagers/AddActiveScriptPubKeyMan

  Signer->>Wallet: supply descriptor(s)
  Wallet->>Man: create & store ScriptPubKeyMan for descriptor
  Wallet->>Wallet: inspect descriptor string for bip44_purpose ("/<BIP32_PURPOSE_STANDARD>'/<ExtCoinType>")
  alt descriptor contains bip44_purpose
    Wallet->>Active: AddActiveScriptPubKeyMan(id, internal)
    Active-->>Wallet: activation confirmed
  else descriptor missing bip44_purpose
    Wallet-->>Man: do not activate (remain inactive)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: conditionally activating only BIP44 descriptors for external signers and correcting mock signer behavior.
Description check ✅ Passed The description is clearly related to the changeset, explaining the problem, solution, and testing approach for the BIP44 descriptor activation fix.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f54a88 and 0a72191.

📒 Files selected for processing (3)
  • src/wallet/wallet.cpp
  • test/functional/mocks/invalid_signer.py
  • test/functional/mocks/signer.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/wallet/wallet.cpp
🧰 Additional context used
📓 Path-based instructions (1)
test/functional/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Functional tests in test/functional/ must be written in Python (minimum version specified in .python-version) and depend on dashd and dash-node

Files:

  • test/functional/mocks/signer.py
  • test/functional/mocks/invalid_signer.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: knst
Repo: dashpay/dash PR: 6834
File: test/functional/wallet_mnemonicbits.py:50-51
Timestamp: 2025-09-02T07:34:28.226Z
Learning: CJ (CoinJoin) descriptors with derivation path "9'/1" are intentionally inactive in descriptor wallets, while regular internal/external descriptors with different derivation paths remain active.
Learnt from: kwvg
Repo: dashpay/dash PR: 6543
File: src/wallet/receive.cpp:240-251
Timestamp: 2025-02-06T14:34:30.466Z
Learning: Pull request #6543 is focused on move-only changes and refactoring, specifically backporting from Bitcoin. Behavior changes should be proposed in separate PRs.
Learnt from: kwvg
Repo: dashpay/dash PR: 6761
File: src/chainlock/signing.cpp:247-250
Timestamp: 2025-07-29T14:32:48.369Z
Learning: In PR #6761, kwvg acknowledged a null pointer check issue in ChainLockSigner::Cleanup() method but deferred it to follow-up, consistent with the pattern of avoiding scope creep in refactoring PRs.
Learnt from: UdjinM6
Repo: dashpay/dash PR: 6933
File: src/llmq/utils.cpp:284-298
Timestamp: 2025-11-04T18:24:27.241Z
Learning: In consensus-critical code (such as quorum formation, block validation, or deployment activation logic), do not suggest changes to the logic itself even if the implementation appears theoretically incorrect or off-by-one. Consensus rules, once deployed on the Dash network, must be preserved exactly to avoid network forks. Refactoring PRs should maintain perfect behavioral equivalence. Only suggest logic changes if explicitly accompanied by a DIP (Dash Improvement Proposal) or if the maintainer indicates the consensus rule needs to be changed with appropriate activation logic.
📚 Learning: 2025-09-02T07:34:28.226Z
Learnt from: knst
Repo: dashpay/dash PR: 6834
File: test/functional/wallet_mnemonicbits.py:50-51
Timestamp: 2025-09-02T07:34:28.226Z
Learning: CJ (CoinJoin) descriptors with derivation path "9'/1" are intentionally inactive in descriptor wallets, while regular internal/external descriptors with different derivation paths remain active.

Applied to files:

  • test/functional/mocks/signer.py
🧬 Code graph analysis (1)
test/functional/mocks/signer.py (1)
src/wallet/rpc/wallet.cpp (1)
  • account (250-250)
🔇 Additional comments (3)
test/functional/mocks/invalid_signer.py (1)

23-33: LGTM! Mock correctly simplified to return only BIP44 descriptors.

The mock now returns a single pkh descriptor with the BIP44 derivation path (44'/1'/...) for both receive and internal categories. This aligns with the production fix that only activates BIP44 descriptors for external signers, eliminating the database key collisions from multiple descriptor types.

test/functional/mocks/signer.py (2)

23-33: LGTM! getdescriptors correctly updated for BIP44-only support.

The mock now returns only BIP44 (pkh with 44'/1' path) descriptors, matching the production behavior that only activates BIP44 descriptors from external signers. This eliminates the non-deterministic test failures caused by multiple descriptor types competing for activation.


42-44: LGTM! Expected descriptor correctly updated to match BIP44 path.

The displayaddress expected descriptor path was correctly updated from 84'/1' (BIP84) to 44'/1' (BIP44), aligning with the wallet's new behavior of only activating BIP44 descriptors for external signers. The address derivation path (/0/0) and the resulting test address remain consistent.


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.

Copy link
Collaborator

@knst knst left a comment

Choose a reason for hiding this comment

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

strange, I still have this type of failures on my localhost:

2/12 - wallet_signer.py --descriptors failed, Duration: 2 s

stdout:
2025-12-24T18:09:13.469000Z TestFramework (INFO): PRNG seed is: 3735113496137477052
2025-12-24T18:09:13.471000Z TestFramework (INFO): Initializing test directory /tmp/test_runner_∋_🏃_20251225_010908/wallet_signer_3
2025-12-24T18:09:14.383000Z TestFramework (ERROR): Assertion failed
Traceback (most recent call last):
  File "/DASH/test/functional/test_framework/test_framework.py", line 163, in main
    self.run_test()
    ~~~~~~~~~~~~~^^
  File "/DASH/test/functional/wallet_signer.py", line 62, in run_test
    self.test_valid_signer()
    ~~~~~~~~~~~~~~~~~~~~~~^^
  File "/DASH/test/functional/wallet_signer.py", line 105, in test_valid_signer
    assert_equal(address_info['hdkeypath'], "m/44'/1'/0'/0/0")
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/DASH/test/functional/test_framework/util.py", line 74, in assert_equal
    raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
AssertionError: not(m/84'/1'/0'/0/0 == m/44'/1'/0'/0/0)
2025-12-24T18:09:14.434000Z TestFramework (INFO): Stopping nodes
2025-12-24T18:09:14.836000Z TestFramework (WARNING): Not cleaning up dir /tmp/test_runner_∋_🏃_20251225_010908/wallet_signer_3
2025-12-24T18:09:14.836000Z TestFramework (ERROR): Test failed. Test logging available at /tmp/test_runner_∋_🏃_20251225_010908/wallet_signer_3/test_framework.log
2025-12-24T18:09:14.836000Z TestFramework (ERROR): 
2025-12-24T18:09:14.836000Z TestFramework (ERROR): Hint: Call /DASH/test/functional/combine_logs.py '/tmp/test_runner_∋_🏃_20251225_010908/wallet_signer_3' to consolidate all logs
2025-12-24T18:09:14.836000Z TestFramework (ERROR): 
2025-12-24T18:09:14.836000Z TestFramework (ERROR): If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.
2025-12-24T18:09:14.836000Z TestFramework (ERROR): https://github.com/dashpay/dash/issues
2025-12-24T18:09:14.836000Z TestFramework (ERROR): 

@UdjinM6 UdjinM6 marked this pull request as draft December 30, 2025 00:27
@UdjinM6 UdjinM6 changed the title fix: only activate BIP44 descriptors for external signers fix: only activate BIP44 descriptors for external signers, fix mocked signers Dec 30, 2025
UdjinM6 and others added 3 commits December 30, 2025 13:46
When creating a wallet with an external signer (hardware wallet), the
mock signer returns multiple descriptors (BIP44, BIP49, BIP84) for both
receive and internal categories. Previously, all valid descriptors were
being activated by calling AddActiveScriptPubKeyMan() for each one.

This caused a database key collision since multiple descriptors tried to
write to the same key (ACTIVEEXTERNALSPK or ACTIVEINTERNALSPK), resulting
in non-deterministic behavior where sometimes BIP44 would be active and
sometimes BIP84, depending on database flush timing.

The fix follows the pattern from the non-external-signer branch which
stores multiple descriptors but only activates certain types (External
and Internal, but not CoinJoin). Now we:
- Store ALL descriptors returned by the signer in m_spk_managers
- Only activate descriptors matching the BIP44 path pattern /44'/{cointype}'

This ensures deterministic behavior and fixes the intermittent test
failure in wallet_signer.py where the test expected m/44' but sometimes
got m/84'.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
pkh should use purpose 44'
@UdjinM6 UdjinM6 force-pushed the fix_ext_wal_act_desc branch from c934b4f to 0a72191 Compare December 30, 2025 10:46
@UdjinM6 UdjinM6 marked this pull request as ready for review December 30, 2025 12:19
@UdjinM6
Copy link
Author

UdjinM6 commented Dec 30, 2025

Added 2 fixes for mocked signers and rebased

Copy link
Member

@PastaPastaPasta PastaPastaPasta left a comment

Choose a reason for hiding this comment

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

utACK 0a72191

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.

3 participants