Skip to content

Conversation

@va-an
Copy link
Contributor

@va-an va-an commented Nov 19, 2025

Description

Resolves #218 (part 1/2).

Implements randomization of the unspendable internal key for taproot descriptors. This is the first part of #218, which consists of two parts:

  1. This PR: Randomize unspendable internal key for taproot descriptor
  2. Follow-up: Add verification command to ensure internal key is derived from NUMS key

Split into separate PRs for easier review and iteration, and to allow independent discussion of the verification command implementation, as one of the possible approaches could introduce breaking changes.

Notes to the reviewers

The compile command now returns an additional r field for taproot descriptors (-t tr), containing the randomly generated internal key. Each compilation will produce a different internal key instead of using a fixed NUMS key.

Example output for taproot (first execution):

-> % bdk-cli compile "pk(A)" -t tr
{
  "descriptor": "tr(2dd09dd0355f4b2d5a4886de599786f3c0211652373221c87aba1cd1f7f1e593,pk(A))#anvu48aj",
  "r": "275a58827bd0ad459d6f92e083ddc3d99a03076155691680eb8f3b06380cdcfd"
}

Same descriptor compiled again produces different r and internal key:

-> % bdk-cli compile "pk(A)" -t tr
{
  "descriptor": "tr(801078f69dae7d95631723d4d13e6c32911633d227dcfc24c6b7e32e1e533e6c,pk(A))#f79rr82j",
  "r": "5e3ac63bb20d6a4bfff645279cc63a7472e18066da8826b13cbcb23aecb5c401"
}

Other descriptor types remain unchanged:

-> % bdk-cli compile "pk(A)" -t sh
{
  "descriptor": "sh(pk(A))#k80zhe7s"
}

Tests for compile command have been moved from handlers.rs to the tests directory. Since taproot descriptors now generate a random internal key on each invocation, the test for the compile command has been simplified. I plan to enhance this test in a follow-up PR once the verification command is implemented.

Changelog notice

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo fmt and cargo clippy before committing

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature
  • I've updated CHANGELOG.md

- Add basic tests validating that taproot results contain 'r' field
  while sh results do not
- Fix test execution by enabling 'compiler' feature
- Add error message validation for invalid cases

Future iterations will include more comprehensive test coverage.
@coveralls
Copy link

coveralls commented Nov 19, 2025

Pull Request Test Coverage Report for Build 20664781654

Details

  • 17 of 25 (68.0%) changed or added relevant lines in 1 file are covered.
  • 4 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-1.9%) to 6.004%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/handlers.rs 17 25 68.0%
Files with Coverage Reduction New Missed Lines %
src/handlers.rs 4 11.23%
Totals Coverage Status
Change from base Build 20377635666: -1.9%
Covered Lines: 128
Relevant Lines: 2132

💛 - Coveralls

Copy link
Collaborator

@tvpeter tvpeter left a comment

Choose a reason for hiding this comment

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

Thanks for working on this @va-an
I have left a few comments.

Comment on lines +48 to +50
#[error("Miniscript compiler error: {0}")]
MiniscriptCompilerError(#[from] bdk_wallet::miniscript::policy::compiler::CompilerError),

Copy link
Collaborator

Choose a reason for hiding this comment

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

The above miniscript error wraps all the errors from miniscript including this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this error only for creating policies in a more concise way:
https://github.com/bitcoindevkit/bdk-cli/pull/225/files#diff-148e623ad4cdaab49b81901460099f4efe7900e8c57e4f7a0ff6778589be01f8L896-R905

Which way do you prefer more?

  1. remove error, restore using err_map for policies
  2. keep error and create policies without err_map

compiler = []

[dev-dependencies]
shlex = "1.3.0"
Copy link
Collaborator

Choose a reason for hiding this comment

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

shlex is an optional dependency, so instead of re-installing it under dev dependencies, it's better to make it a non-optional dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

shlex needs to remain both optional and in dev-dependencies:

  • Optional: it's only used by the repl feature, so other builds don't need it
  • Dev-dependencies: CI runs cargo test --no-default-features which excludes repl, but tests still need shlex to compile

Making it non-optional would unnecessarily add the dependency to all builds.

Alternatively, we could accept the dependency overhead but simplify the code and CI in return?

src/handlers.rs Outdated

let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
let internal_key_point = nums_point.combine(&r_point)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you considered the add_exp_tweak method? I think it might be better suited for this use case because it knows one of the factors is a Generator point, whereas combine is for adding arbitrary public points.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, thanks! Done.

@va-an va-an requested a review from tvpeter December 27, 2025 20:46
Copy link
Contributor

@110CodingP 110CodingP left a comment

Choose a reason for hiding this comment

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

Refactoring the tests to usestd::process::Command instead of calling the internal function handle_compile_subcommand make the tests vulnerable to the state of environment variables and makes debugging harder in my opinion. But I have no strong disagreement to the approach in the PR.

src/handlers.rs Outdated
}
}?;
}?
.to_string();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: ig we do not need this to_string here because of the Display implementation of Descriptor<String>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

@va-an
Copy link
Contributor Author

va-an commented Jan 2, 2026

Refactoring the tests to usestd::process::Command instead of calling the internal function handle_compile_subcommand make the tests vulnerable to the state of environment variables and makes debugging harder in my opinion. But I have no strong disagreement to the approach in the PR.

My main idea is to have integration tests, not just unit tests, because that's the closest to how users actually interact with it.
We can have both: unit tests for functions and integration tests for CLI commands. Some duplication, but better coverage overall.

As for the environment variable state concern — we could remove related env vars to isolate integration tests, for example for compile tests:

    let output = Command::new("cargo")
        .args(args)
        .env_remove("NETWORK")
        .env_remove("DATADIR")
        .env_remove("POLICY")
        .env_remove("TYPE")
        .output()
        .unwrap();

This way we can isolate tests from the environment. Maintaining such tests might be a bit less convenient, but I promise to keep an eye on it :)
What do you think?

@va-an va-an requested a review from 110CodingP January 2, 2026 20:27
@110CodingP
Copy link
Contributor

110CodingP commented Jan 3, 2026

My main idea is to have integration tests, not just unit tests, because that's the closest to how users actually interact with it. We can have both: unit tests for functions and integration tests for CLI commands. Some duplication, but better coverage overall.

That definitely makes sense.

As for the environment variable state concern — we could remove related env vars to isolate integration tests, for example for compile tests:

    let output = Command::new("cargo")
        .args(args)
        .env_remove("NETWORK")
        .env_remove("DATADIR")
        .env_remove("POLICY")
        .env_remove("TYPE")
        .output()
        .unwrap();

This way we can isolate tests from the environment. Maintaining such tests might be a bit less convenient, but I promise to keep an eye on it :) What do you think?

Thanks! I did not know that we could remove them.

I agree this could be helpful. Do you think it should be done in a separate PR though? maybe one that also refactors tests/integration.rs or is it better to keep it here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

NUMS_UNSPENDABLE_KEY_HEX is not supposed to be used as is (privacy concerns)

4 participants