wallet, descriptor: Fix MuSig private key completeness checks on importdescriptors#35493
wallet, descriptor: Fix MuSig private key completeness checks on importdescriptors#35493w0xlt wants to merge 3 commits into
importdescriptors#35493Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35493. ReviewsSee the guideline and AI policy for information on the review process.
If your review is incorrectly listed, please copy-paste ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
pablomartin4btc
left a comment
There was a problem hiding this comment.
Approach ACK.
I verified the regression by running the functional test without the fix, where the fully-private rawtr(musig(...)) import incorrectly warns that not all private keys were provided. With this PR, the fully-private MuSig descriptor imports without warnings, while the partial-private case still warns as expected.
The approach makes sense to me: as the previous origin/pubkey counting heuristic does not correctly model MuSig descriptors where the aggregate key is derived from the participant keys. The added functional test covers both the regression and the expected warning case.
|
Approach ACK. The fix looks okay --- a/test/functional/wallet_musig.py
+++ b/test/functional/wallet_musig.py
@@ -217,6 +217,26 @@ class WalletMuSigTest(BitcoinTestFramework):
assert_equal(res[1]["success"], True)
assert_equal(res[1]["warnings"], [missing_keys_warning])
+ # Check musig participant completeness for aggregate-key derivation descriptors.
+ # Full private participant set should not warn, partial should.
+ res = wallet.importdescriptors([
+ {
+ "desc": descsum_create(f"rawtr(musig({keys[0][0]},{keys[1][0]})/0/*)"),
+ "timestamp": "now",
+ "range": [0, 1],
+ },
+ {
+ "desc": descsum_create(f"rawtr(musig({keys[0][0]},{keys[1][1]})/1/*)"),
+ "timestamp": "now",
+ "range": [0, 1],
+ },
+ ])
+
+ assert_equal(res[0]["success"], True)
+ assert "warnings" not in res[0]
+ assert_equal(res[1]["success"], True)
+ assert_equal(res[1]["warnings"], [missing_keys_warning])
+ |
|
Good catch, Concept ACK 9d75efa. |
|
Concept ACK |
| assert "musig2_pubnonces" in dec["inputs"][0] | ||
| assert "musig2_partial_sigs" not in dec["inputs"][0] | ||
|
|
||
| def test_importdescriptors_private_key_warnings(self): |
There was a problem hiding this comment.
This test is more suitable for wallet_importdescriptors.py file.
Even the missing_keys_warning is hardcoded there few times that could be extracted out.
bitcoin/test/functional/wallet_importdescriptors.py
Lines 682 to 707 in 6921f5d
This file wallet_musig.py is more suitable for testing the musig signing flow, the file name unfortunately falls short of its intent.
|
|
||
| def test_importdescriptors_private_key_warnings(self): | ||
| self.log.info("Testing importdescriptors MuSig private key warnings") | ||
| _, keys = self.create_wallets_and_keys_from_pattern("rawtr(musig($0,$1))") |
There was a problem hiding this comment.
The creation of multiple wallets to extract the keys is not needed for this test.
Related to moving this test, there are already few hardcoded xprvs and xpubs in wallet_importdescriptors.py that are sufficient for this test, keeping the test light.
bitcoin/test/functional/wallet_importdescriptors.py
Lines 580 to 589 in 6921f5d
3f11191 to
86dbbea
Compare
|
@b-l-u-e Thanks. Done. |
|
Rebased. |
| "desc": descsum_create(f"rawtr(musig({xprv1}/84h/0h/0h/0,{acc_xprv2}/1))"), | ||
| "timestamp": "now", | ||
| }, | ||
| { | ||
| "desc": descsum_create(f"rawtr(musig({xprv1}/84h/0h/0h/2,{acc_xpub2}/3))"), | ||
| "timestamp": "now", | ||
| }, | ||
| { | ||
| "desc": descsum_create(f"rawtr(musig({xprv1}/84h/0h/0h,{acc_xprv2})/0/*)"), | ||
| "timestamp": "now", | ||
| "range": [0, 1], | ||
| }, | ||
| { | ||
| "desc": descsum_create(f"rawtr(musig({xprv1}/84h/0h/0h,{acc_xpub2})/1/*)"), |
There was a problem hiding this comment.
84h/0h/0h
It would need a rebase post merging of PR #35543.
While doing that, let's also avoid hardcoding this derivation path and instead use the variable derivation_path here.
86dbbea to
0fd7b53
Compare
pablomartin4btc
left a comment
There was a problem hiding this comment.
ACK 0fd7b53
Since my last review the test was moved to wallet_importdescriptors.py and simplified, the hardcoded warning string extracted to a constant, the derivation path de-hardcoded to use the existing derivation_path variable, and rebased after #35543. Also addresses @b-l-u-e's suggestion — the test now covers both participant-level and aggregate-level derivation cases.
| } | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(descriptor_musig_have_private_keys) |
There was a problem hiding this comment.
In b768584 "descriptors: require complete MuSig private keys"
Instead of adding a new specific unit test just for this case, we should be updating DoCheck so that it performs the proper checks for all of the descriptors being passed in. Any new test descriptors should be added to descriptor_test with appropriate flags set so that the correct things are being checked.
| virtual void GetPrivKey(int pos, const SigningProvider& arg, FlatSigningProvider& out) const = 0; | ||
|
|
||
| /** Whether private data for this provider is available in arg. */ | ||
| virtual bool HavePrivateKeys(int pos, const SigningProvider& arg) const |
There was a problem hiding this comment.
In b768584 "descriptors: require complete MuSig private keys"
Having pos here is pointless, it has no bearing on whether we do or don't have private keys for this provider. pos is only necessary in GetPrivKey to get a specific private key, but this function only cares about whether we can get private keys at all, especially because all callers pass 0 anyways.
Co-authored-by: b-l-u-e <winnie.gitau282@gmail.com>
0fd7b53 to
0390338
Compare
|
@b-l-u-e I added you as a co-author of the test commit. I had forgotten to do so earlier. |
It’s all good in the hood 😊 |
importdescriptorscurrently checks whether all private keys are present by expanding the descriptor and verifying that every expanded origin pubkey has a private key.This is wrong for MuSig descriptors because expansion includes the synthetic aggregate pubkey. There is no individual private key for that aggregate pubkey, so importing a fully private MuSig descriptor such as
rawtr(musig(A_priv,B_priv))incorrectly returns:This PR fixes the issue by making descriptor private-key completeness account for MuSig participant keys, and by having
importdescriptorsuseDescriptor::HavePrivateKeys()instead of duplicating its own manual completeness check.The functional test covers both cases:
rawtr(musig(A_priv,B_priv))imports without warnings.rawtr(musig(A_priv,B_pub))still warns that not all private keys were provided.