feat(compile): compile taproot descriptor with randomized unspendable internal key#225
feat(compile): compile taproot descriptor with randomized unspendable internal key#225va-an wants to merge 8 commits intobitcoindevkit:masterfrom
Conversation
Pull Request Test Coverage Report for Build 21687075357Details
💛 - Coveralls |
110CodingP
left a comment
There was a problem hiding this comment.
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. 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 :) |
That definitely makes sense.
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 |
I would prefer to keep this PR as is, since @tvpeter has already looked at these changes. |
|
@110CodingP I decided to add the code for removing env vars in tests, as we discussed, so we don't forget to do this in future PRs. I verified locally that this works as expected — the command process will not receive these env vars from the parent process. use std::{env, process::Command};
fn main() {
unsafe {
env::set_var("FOO", "1");
env::set_var("BAR", "2");
env::set_var("BAZ", "3");
}
let cmd = Command::new("printenv")
.env_remove("FOO")
.env_remove("BAR")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&cmd.stdout);
assert!(!stdout.contains("FOO"), "FOO should be removed");
assert!(!stdout.contains("BAR"), "BAR should be removed");
assert!(stdout.contains("BAZ"), "BAZ should still be present");
println!("it's all good man");
}Please take a look — afc76aa. |
| .table() | ||
| .display() | ||
| .map_err(|e| Error::Generic(e.to_string()))?; | ||
| let mut rows = vec![vec!["Descriptor".cell().bold(true), descriptor.cell()]]; |
There was a problem hiding this comment.
the descriptor value is quite long, can you apply the [shorten](https://github.com/bitcoindevkit/bdk-cli/blob/fb7f6c60ca4ac1ad80750391435bbafd7346c714/src/utils.rs#L381) fn so it displays better?
There was a problem hiding this comment.
Done! I also used shorten() for r.
I extracted shorten description into a variable to shorten only the internal key and leave the rest of the descriptor as is.
without pretty:
-> % cargo run --all-features -- compile "pk(ABC)" -t tr
{
"descriptor": "tr(1985c2a86e01bbcdb1b5806422531a73c77e568ecfdd6d2863c158a76628ea50,pk(ABC))#6qsjh5q3",
"r": "e38caa40d0db7cae7037336c982278a7fc6b770e7a80f8bbce0d79976a14d5e1"
}with pretty:
-> % cargo run --all-features -- compile "pk(ABC)" -t tr --pretty
+------------+----------------------------------+
| Descriptor | tr(ccb4...b1d0,pk(ABC))#qrk923py |
+------------+----------------------------------+
| r | a129...7a14 |
+------------+----------------------------------+| let output = Command::new("cargo") | ||
| .args(args) | ||
| .env_remove("NETWORK") | ||
| .env_remove("DATADIR") |
There was a problem hiding this comment.
nit: ig removing DATADIR env variable is not required? Since the compile command does not use it?
There was a problem hiding this comment.
You're right, DATADIR isn't used in compile command.
I'm planning to reuse this command execution code in other tests, so I preemptively cleared all env vars.
787c701 to
b5dac76
Compare
Squashed into 2 commits, but without the shortening approach changes - I've replied about that in the discussion above. |
|
Hey! I noticed that #203 was merged. I merged master into my branch and verified that the shorten() bug no longer reproduces. -> % cargo run --all-features -- compile "pk(ABC)" -t tr --pretty
+------------+----------------------------------+
| Descriptor | tr(e753...c650,pk(ABC))#quuf6809 |
+------------+----------------------------------+
| r | d527...f9c9 |
+------------+----------------------------------+
-> % cargo run --all-features -- compile "pk(ABC)" -t wsh --pretty
+------------+-----------------------+
| Descriptor | wsh(pk(ABC))#8gmtcnaw |
+------------+-----------------------+Please take a look. |
notmandatory
left a comment
There was a problem hiding this comment.
Overall the code looks good, my only concern is with running shell commands as part of the testing, which I'd like to see removed.
Ultimately I'd like to figure out how to do the TR optimizatoin for the pk(A) policy so that it creates the descriptor tr(A) but I don't know if/how to do it or if something would need to be changed upstream.
| let full_cmd = format!("run --features compiler -- {}", cmd); | ||
| let args = shlex::split(&full_cmd).unwrap(); | ||
|
|
||
| let output = Command::new("cargo") |
There was a problem hiding this comment.
I don't see any reason to test the running of these commands. I know some older tests also do that but it's a pattern I don't want to continue and would like to remove. As far as I can tell these tests only verify the clap library is doing its job right, which we do not need to re-test.
I'd rather see the tests only verify the handlers functions return what is expected.
Although not directly related to this PR's changes, during review we agreed to make shlex non-optional since it's used by the default `repl` feature and the package is under 20 KiB. See: bitcoindevkit#225 (comment)
6bcf948 to
741e672
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #225 +/- ##
==========================================
+ Coverage 10.96% 12.34% +1.37%
==========================================
Files 8 8
Lines 2526 2568 +42
==========================================
+ Hits 277 317 +40
- Misses 2249 2251 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
|
||
| let secp = Secp256k1::new(); | ||
| let r_secret = SecretKey::new(&mut rand::thread_rng()); | ||
| r = Some(r_secret.display_secret().to_string()); | ||
|
|
||
| let nums_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)?; | ||
| let nums_point = PublicKey::from_x_only_public_key(nums_key, Parity::Even); | ||
|
|
||
| let internal_key_point = nums_point.add_exp_tweak(&secp, &Scalar::from(r_secret))?; | ||
| let (xonly_internal_key, _) = internal_key_point.x_only_public_key(); | ||
|
|
||
| let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX) | ||
| .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?; | ||
| let tree = TapTree::Leaf(Arc::new(policy.compile::<Tap>()?)); |
There was a problem hiding this comment.
I'm planning to replace compile() with compile_tr() for the tr match branch, and moving the compile step inside the match is preparation for that.
See: #244
There was a problem hiding this comment.
Nope, this won't happen - #244 (comment).
This is exactly what But as I noted in #244 (comment), |
|
|
||
| [dependencies] | ||
| bdk_wallet = { version = "2.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } | ||
| bdk_wallet = { version = "=2.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } |
|
Hey @tvpeter, is there anything else you'd like me to do on my end? Should I squash the follow-up commits down again (a few have piled up since the last squash)? Thanks! |
tvpeter
left a comment
There was a problem hiding this comment.
hi @va-an , I have reviewed again and left one minor nit below.
Also, kindly remove these lines because the gating does not apply to compiler feature tests, and there is no way to run the compile feature tests except all-features are compiled.
Please, once you have effected these changes, tag me to approve.
Thank you.
|
|
||
| [dependencies] | ||
| bdk_wallet = { version = "2.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } | ||
| bdk_wallet = { version = "=2.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } |
@tvpeter done, pls have a look. |
Although not directly related to this PR's changes, during review we agreed to make shlex non-optional since it's used by the default `repl` feature and the package is under 20 KiB. See: bitcoindevkit#225 (comment)
Instead of using a fixed NUMS key as the internal key for taproot descriptors, generate a randomized unspendable key (H + rG) for each compilation. This improves privacy by preventing observers from determining whether key path spending is disabled. The randomness factor `r` is included in the output so the user can verify that the internal key is derived from the NUMS point. Also applies `shorten()` globally in pretty mode and uses `?` operator via dedicated error variants instead of `map_err`.
Add `claims` dev-dependency for ergonomic `assert_ok!`/`assert_err!` macros. Adapt compile tests to verify randomized key behavior: - descriptor structure instead of exact match (key is now random) - presence of `r` field for taproot, absence for other types - uniqueness of `r` across compilations
Move miniscript compilation inside match arms to avoid compiling for all script contexts when only one is needed. Use `tap::Pipe` for concise method chaining in sh/wsh/sh-wsh branches.
- Remove MSRV mention as the project no longer enforces it - Add Conventional Commits and GPG signing links to align with other repos
Rebased onto master to drop the merge commit, which left Cargo.lock with uncommitted changes. The cleaner fix would be to squash these into the commits that introduced the dependencies, but I went with the simpler approach.
5f0acd7 to
053073f
Compare
@tvpeter phew, did it:
|
Description
Part of #218
Depends on #203 (
shorten()fix)Implements randomization of the unspendable internal key for taproot descriptors. This is the first part of #218, which consists of two parts:
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
compilecommand now returns an additionalrfield 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):
Same descriptor compiled again produces different
rand internal key:Other descriptor types remain unchanged:
Tests for
compilecommand have been moved fromhandlers.rsto thetestsdirectory. Since taproot descriptors now generate a random internal key on each invocation, the test for thecompilecommand 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:
cargo fmtandcargo clippybefore committingNew Features:
CHANGELOG.md