Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

descriptors: Be able to specify change and receiving in a single descriptor string #22838

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

achow101
Copy link
Member

@achow101 achow101 commented Aug 30, 2021

It is convenient to have a descriptor which specifies both receiving and change addresses in a single string. However, as discussed in #17190 (comment), it is not feasible to use a generic multipath specification like BIP 88 due to combinatorial blow up and that it would result in unexpected descriptors.

To resolve that problem, this PR proposes a targeted solution which allows only a single pair of 2 derivation indexes to be inserted in the place of a single derivation index. So instead of two descriptor wpkh(xpub.../0/0/*) and wpkh(xpub.../0/1/*) to represent receive and change addresses, this could be written as wpkh(xpub.../0/<0;1>/*). The multipath specifier is of the form <NUM;NUM>. Each NUM can have its own hardened specifier, e.g. <0;1h> is valid. The multipath specifier can also only appear in one path index in the derivation path.

This results in the parser returning two descriptors. The first descriptor uses the first NUM in all pairs present, and the second uses the second NUM. In our implementation, if a multipath descriptor is not provided, a pair is still returned, but the second element is just nullptr.

The wallet will not output the multipath descriptors (yet). Furthermore, when a multipath descriptor is imported, it is expanded to the two descriptors and each imported on its own, with the second descriptor being implicitly for internal (change) addresses. There is no change to how the wallet stores or outputs descriptors (yet).

Note that the path specifier is different from what was proposed. It uses angle brackets and the semicolon because these are unused characters available in the character set and I wanted to avoid conflicts with characters already in use in descriptors.

Closes #17190

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 30, 2021

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK mjdietzx
Concept ACK craigraw
Stale ACK Rspigler, darosior

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #30243 (Tr partial descriptors by Eunovo)
  • #29936 (fuzz: wallet: add target for CreateTransaction by brunoerg)
  • #29396 (rpc: getdescriptorinfo also returns normalized descriptor by araujo88)
  • #29136 (wallet: addhdkey RPC to add just keys to wallets via new void(KEY) descriptor by achow101)
  • #28333 (wallet: Construct ScriptPubKeyMans with all data rather than loaded progressively by achow101)
  • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)
  • #27286 (wallet: Keep track of the wallet's own transaction outputs in memory by achow101)

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.

@Rspigler
Copy link
Contributor

Rspigler commented Sep 1, 2021

Strong concept ACK!

Glad this is getting a fix. As discussed in 17190 and OP, there's more improvements that can come after this.

@mjdietzx
Copy link
Contributor

mjdietzx commented Sep 3, 2021

Concept and Approach ACK. I plan on updating the test and docs introduced in #22067 to take advantage of this in a follow up PR. So I will try to do a more thorough review and testing of this after that is merged and I start on the followup based on this branch. (or if it looks like this will get merged first I will do that earlier)

@Sjors
Copy link
Member

Sjors commented Sep 29, 2021

Note that the path specifier is different from what was proposed. It uses angle brackets and the semicolon because these are unused characters available in the character set and I wanted to avoid conflicts with characters already in use in descriptors.

{0,1} seems more readable and would at least be a subset of BIP 88. The conflict is with taproot descriptors, which use { and , for nesting the scripts.

As an example tr([00000000/1']xpub/{0,1}/*,{pk([00000000/2']xpub/{0,1}/*),pk([00000000/3']xpub/{0,1}/*)}) doesn't look terrible to me. How much does it really complicate the parsing code? They're always either inside () of a descriptor function like pkh() or within [] of an origin (if we even allow that).

Allowing a mix of hardened and unhardened like <0;1h> seems unnecessary to me and might make alternative implementations more complicated (though perhaps it's trivial; I haven't tried).

@achow101
Copy link
Member Author

The conflict is with taproot descriptors, which use { and , for nesting the scripts.

The conflict is actually with the , in a variety of descriptors. The particular issue I had run into was using , in a multi() descriptor. Since , is used to separate the components of multi(), the parser would look for the next , for the next item to parse. If we use , for the multipath specifier, then it would run into that and parse too little. The parser could be changed to not do that, but that was more work than to just use unused characters. { did not cause any issues, but to make it easier for other implementations which may be parsing differently, I decided to go with a different character as well.

@mjdietzx
Copy link
Contributor

I'm doing some more review/testing of this PR, but when I checkout this PR and rebase to master, I get build error:

wallet/rpcwallet.cpp:3387:45: error: no viable conversion from 'std::pair<std::unique_ptr<Descriptor>, std::unique_ptr<Descriptor> >' to 'std::unique_ptr<Descriptor>'
  std::unique_ptr<Descriptor> desc = Parse(desc_str, desc_out, error, true);

@achow101
Copy link
Member Author

Fixed the silent merge conflict.

@mjdietzx
Copy link
Contributor

So in my tests, from this multipath descriptor string: f"wsh(sortedmulti(M},{f'/<0;1>/*,'.join(xpubs)}/<0;1>/*))"

I have a descriptor that looks like this:

{
  'descriptor': 'wsh(sortedmulti(2,tpubDCcZnBitiPCBqsdLWx8Lkc2BrsvVTdZU2Gcioi8yY76HrGSvp4oWyxZ5GUKGVcjdv4wzWbfntfLdcEEfVRamhSgwi1ZxgfNtayQ2QEEjfTv/0/*,tpubDCMtV5vMS9Zn2paRcw7o83ytLY7WHMCGYRSDxhz7HWnZ4ix19EQpi7g3wvYxEvuoXRfLy5XpXJP5VEr7qPzS92TWnVrBMPJYzfupdACC6vP/0/*,tpubDDBi5pEGAmG7k8t2udh8vbYb1Zpwnt5E9bxd86z6xR66r6tYH1aXy5J3kxR2NDBWhVzgcyiAjFw3kCbkf8YcxDx4wjsnvKjKu3SfPVi4dzf/0/*))#shfp6kvs', 
  'descriptor_change': 'wsh(sortedmulti(2,tpubDCcZnBitiPCBqsdLWx8Lkc2BrsvVTdZU2Gcioi8yY76HrGSvp4oWyxZ5GUKGVcjdv4wzWbfntfLdcEEfVRamhSgwi1ZxgfNtayQ2QEEjfTv/1/*,tpubDCMtV5vMS9Zn2paRcw7o83ytLY7WHMCGYRSDxhz7HWnZ4ix19EQpi7g3wvYxEvuoXRfLy5XpXJP5VEr7qPzS92TWnVrBMPJYzfupdACC6vP/1/*,tpubDDBi5pEGAmG7k8t2udh8vbYb1Zpwnt5E9bxd86z6xR66r6tYH1aXy5J3kxR2NDBWhVzgcyiAjFw3kCbkf8YcxDx4wjsnvKjKu3SfPVi4dzf/1/*))#ghxep260', 
  'checksum': 'e82r9xv3', 
  'isrange': True, 
  'issolvable': True, 
  'hasprivatekeys': False
}

But now, multisig.getrawchangeaddress() fails with error: This wallet has no available keys (-4):

2021-10-18T22:26:50.595000Z TestFramework (INFO): Check that every participant's multisig generates the same addresses...
2021-10-18T22:26:50.602000Z TestFramework (ERROR): JSONRPC error
Traceback (most recent call last):
  File "/Users/michaeldietz/Documents/bitcoin/test/functional/test_framework/test_framework.py", line 132, in main
    self.run_test()
  File "/Users/michaeldietz/Documents/bitcoin/test/functional/wallet_multisig_descriptor_psbt.py", line 95, in run_test
    change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]]
  File "/Users/michaeldietz/Documents/bitcoin/test/functional/wallet_multisig_descriptor_psbt.py", line 95, in <listcomp>
    change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]]
  File "/Users/michaeldietz/Documents/bitcoin/test/functional/test_framework/coverage.py", line 49, in __call__
    return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
  File "/Users/michaeldietz/Documents/bitcoin/test/functional/test_framework/authproxy.py", line 144, in __call__
    raise JSONRPCException(response['error'], status)
test_framework.authproxy.JSONRPCException: Error: This wallet has no available keys (-4)

Any ideas? I am updating #22067 to use multipath descriptors and using it as a chance to test this PR more

@achow101
Copy link
Member Author

achow101 commented Oct 18, 2021

@mjdietzx importdescriptors is probably failing. You should check the result of the import and see if there are any errors.

Is your branch pushed so I can have a look?

@mjdietzx
Copy link
Contributor

In my code I actually have:

result = multisig.importdescriptors([
                {
                    "desc": descriptor["descriptor"],
                    "active": True,
                    "timestamp": "now",
                },
            ])
            assert all(r["success"] for r in result)

And that doesn't catch any errors. I will push my branch shortly and reply w/ it

@mjdietzx
Copy link
Contributor

I had an error in my tests - it seems everything in this PR is working properly.

This works:

result = multisig.importdescriptors([
  {
    "desc": descsum_create(f"wsh(sortedmulti({self.M},{f'/<0;1>/*,'.join(xpubs)}/<0;1>/*))"),
    "active": True,
    "timestamp": "now",
  },
])

re the error I posted eariler, I was trying to do this (using getdescriptorinfo instead of descsum_create):

descriptor = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/<0;1>/*,'.join(xpubs)}/<0;1>/*))")
result = multisig.importdescriptors([
    {
        "desc": descriptor["descriptor"],
        "active": True,
        "timestamp": "now",
    },
])

But I was not importing or doing anything with descriptor["descriptor_change"], hence the (obvious in hindsight) error.


We used getdescriptorinfo instead of descsum_create so test/functional/wallet_multisig_descriptor_psbt.py will only use node functions without an additional script for the checksum (see review of #22067 for more info). I wonder if this is useful user feedback at all @achow101? I'm specifically wondering about the interface of getdescriptorinfo now, and if it would make sense to return something like:

{
  descriptor // still the multipath descriptor, similar to output of descsum_create
  // now the broken out descriptors:
  descriptor_receiving
  descriptor_change
 // ... and the other fields are unchanged
}

@achow101
Copy link
Member Author

We used getdescriptorinfo instead of descsum_create so test/functional/wallet_multisig_descriptor_psbt.py will only use node functions without an additional script for the checksum (see review of #22067 for more info). I wonder if this is useful user feedback at all @achow101? I'm specifically wondering about the interface of getdescriptorinfo now, and if it would make sense to return something like:

The checksum field is for that purpose. You can concatenate the descriptor input, #, and that checksum.

src/rpc/misc.cpp Outdated
@@ -170,6 +170,7 @@ static RPCHelpMan getdescriptorinfo()
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys"},
{RPCResult::Type::STR, "descriptor_change", "The change descriptor in canonical form, without private keys. Only if the provided descriptor specifies derivation paths for both receiving and change."},
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still not convinced this is the best interface for getdescriptorinfo

I'd expect descriptor to always be the full descriptor (whether multipath or not). In the case of multipath, I'd then expect two optional attributes descriptor_receiving and descriptor_change to be present in the response.

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: descriptor_change should be optional

Copy link
Member Author

Choose a reason for hiding this comment

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

descriptor cannot currently be the multipath descriptor as generating the string output for one is not yet implemented. I'm not sure on a good approach for doing that yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are you saying something like:

  • The multipath descriptor string input to getdescriptorinfo is parsed/expanded to the two descriptors
  • Now each of these descriptors is converted back ToString and returned as descriptor (first) and descriptor_change (second)

And that we currently don't have a way to go from the two parsed/expanded descriptors back to the single multipath descriptor string?

That does sound tricky, just trying to make sure I understand

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that is correct.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, so what's the downside of something like:

  • Each of these parsed descriptors is converted back ToString and returned as descriptor_receiving (first) and descriptor_change (second)
  • The same multipath descriptor string input to getdescriptorinfo is just returned for the descriptor output (with a checksum added)

Copy link
Contributor

Choose a reason for hiding this comment

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

@achow101 could you entertain me on this, if nothing else it'll help me understand this better as I finish up my review

Copy link
Member Author

Choose a reason for hiding this comment

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

#15986 provides a bit more context for this.

There are a few reasons I would prefer to not change the behavior of the descriptor result. The first is for backwards compatibility. This RPC may already be used in a way that expects the canonicalized descriptor produced by getdescriptorinfo in the descriptor field. Additionally, if a descriptor with a private key were provided, if we were to just output that same string again, then we would be echoing private keys which is something that we want to avoid doing.

Copy link
Member

@Sjors Sjors Jan 28, 2022

Choose a reason for hiding this comment

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

descriptor cannot currently be the multipath descriptor as generating the string output for one is not yet implemented. I'm not sure on a good approach for doing that yet.

Mmm, that has me a bit worried. If a user imports a multipath descriptor, I think we should remember it in that form. And if we embrace multipath descriptors, we should probably also use them for new wallets.

Perhaps a better design would be a MultipathDescriptor subclass that only has a receive and change method, or more neutral: a pair method that returns a pair of Descriptors that then work as usual.

In the future std::pair<Descriptor> can be generalized to std::map<uint32, Descriptor> to allow <NUM,NUM,...,NUM>, etc. That may even be appropriate now: we could treat <0;1> as a typical receive/change setup, and refuse to import any other combination (though that's an RPC level thing, and we might as well use std::pair<Descriptor> given such an import restriction).

src/test/fuzz/descriptor_parse.cpp Outdated Show resolved Hide resolved
src/script/descriptor.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@mjdietzx mjdietzx left a comment

Choose a reason for hiding this comment

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

Getting very close to signing off on this PR, just a few minor things/questions

src/rpc/misc.cpp Outdated
{RPCResult::Type::STR, "address", "the derived addresses"},
}
{
RPCResult{"for single derivation descriptors",
Copy link
Contributor

Choose a reason for hiding this comment

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

I've been thinking through the response returned here. It doesn't seem ideal that the shape of this response differs between a single descriptor and a multipath descriptor. Ie this returns an array of addresses if we pass in a descriptor, but it returns an object if we pass in a multipath descriptor.

Have you thought through an api where the response is more uniform and "feels" the same for both types? I don't necessarily know a better way to do it, wondering if you've thought this through though

Copy link
Member Author

Choose a reason for hiding this comment

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

The primary reason for having the two different return results is to maintain backwards compatibility with anyone who may be using the RPC now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I figured there'd need to be a deprecation process if we wanted to do this. Anyways, if you think it's enough of an improvement to warrant that I'd be happy to do give it a go as a followup PR. Just lmk

src/rpc/misc.cpp Outdated
obj.pushKV("change", DeriveAddresses(descs.second.get(), range_begin, range_end, key_provider));
return obj;
} else {
return addresses;
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to my above comment, if the response is uniform we could potentially simplify this a bit and have a single return at the end of the function

@mjdietzx
Copy link
Contributor

Review ACK 796c439. I also lightly used/tested this by running #23308 on top of this as additional test coverage.

@mjdietzx
Copy link
Contributor

I think there may be another silent merge conflict w/ master:

wallet/test/util.cpp:33:37: error: no viable conversion from 'std::pair<std::unique_ptr<Descriptor>, std::unique_ptr<Descriptor> >' to 'std::unique_ptr<Descriptor>'
        std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);

Can you try rebasing again?

To prepare for returning multipath descriptors which will be a shorthand
for specifying multiple descriptors, change ParseScript's signature to return a
vector.
Multipath specifiers are derivation path indexes of the form `<i;j;k;...>`
used for specifying multiple derivation paths for a descriptor.
Only one multipath specifier is allowed per PubkeyProvider.
This is syntactic sugar which is parsed into multiple distinct descriptors.
One descriptor will have all of the `i` paths, the second all of the `j` paths,
the third all of the `k` paths, and so on.

ParseKeypath will always return a vector of keypaths with the same size
as the multipath specifier. The callers of this function are updated to deal
with this case and return multiple PubkeyProviders. Their callers have
also been updated to handle vectors of PubkeyProviders.
When given a descriptor which contins a multipath derivation specifier,
a vector of descriptors will be returned.
When given a multipath descriptor, derive all of the descriptors.
The derived addresses will be returned in an object
consisting of multiple arrays. For compatibility, when given a single path
descriptor, the addresses are provided in a single array as before.
Instead of applying internal-ness to all keys being imported at the same
time, apply it on a per key basis. So each key that is imported will
carry with it whether it is for the change keypool.
Multipath descriptors will be imported as multiple separate descriptors.
When there are exactly 2 multipath items, the first descriptor will be
for receiving addreses, and the second for change
addresses. When importing a multipath descriptor, 'internal' cannot be
specified.
Multipath descriptors will be imported as multiple separate descriptors.
When there are 2 multipath items, the first descriptor will be for receiving
addresses and the second for change. This mirrors importmulti.
Test that both importmulti and importdescriptors behave as expected when
importing a multipath descriptor.
Comment on lines 790 to 794
||||||| parent of 657176b834d (descriptors: Add DescriptorImpl::Clone)
=======

virtual std::unique_ptr<DescriptorImpl> Clone() const = 0;
>>>>>>> 657176b834d (descriptors: Add DescriptorImpl::Clone)
Copy link
Member

Choose a reason for hiding this comment

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

Rebase issues in ed00e60 that are removed on 06caba0.

@mjdietzx
Copy link
Contributor

Tested ACK cb41e26. Note: I did not do a thorough code review

What I've tested:

  • getdescriptorinfo, importdescriptors, listdescriptors with a miniscript wallet similar to #29156. All works as expected
  • import this wallet from file to Coldcard Mk4 (Edge FW 6.2.2 w/ miniscript support)
    • <0;1> notation works properly as Coldcard has already adopted this convention
    • Coldcard also signs psbt properly
  • I have two PRs which would benefit from this improvement. I rebased #29156 on top of this. This diff simplifies and passes:
diff --git a/test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py b/test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py
index 4f50ae191c..e5f58eb4d2 100755
--- a/test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py
+++ b/test/functional/wallet_miniscript_decaying_multisig_descriptor_psbt.py
@@ -32,35 +32,26 @@ class WalletMiniscriptDecayingMultisigDescriptorPSBTTest(BitcoinTestFramework):
         self.skip_if_no_sqlite()
 
     @staticmethod
-    def _get_xpub(wallet, internal):
+    def _get_xpub(wallet):
         """Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""
-        pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and d["internal"] == internal, wallet.listdescriptors()["descriptors"]))
+        pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh("), wallet.listdescriptors()["descriptors"]))
         # keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices
         # see section 'Key origin identification' in 'doc/descriptors.md' for more details...
         return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0]
 
-    def create_multisig(self, external_xpubs, internal_xpubs):
+    def create_multisig(self, xpubs):
         """The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every signer can do this."""
         self.node.createwallet(wallet_name=f"{self.name}", blank=True, descriptors=True, disable_private_keys=True)
         multisig = self.node.get_wallet_rpc(f"{self.name}")
         # spending policy: `thresh(4,pk(key_1),pk(key_2),pk(key_3),pk(key_4),after(t1),after(t2),after(t3))`
         # IMPORTANT: when backing up your descriptor, the order of key_1...key_4 must be correct!
-        external = multisig.getdescriptorinfo(f"wsh(thresh({self.N},pk({f'),s:pk('.join(external_xpubs)}),sln:after({f'),sln:after('.join(map(str, self.locktimes))})))")
-        internal = multisig.getdescriptorinfo(f"wsh(thresh({self.N},pk({f'),s:pk('.join(internal_xpubs)}),sln:after({f'),sln:after('.join(map(str, self.locktimes))})))")
-        result = multisig.importdescriptors([
-            {  # receiving addresses (internal: False)
-                "desc": external["descriptor"],
-                "active": True,
-                "internal": False,
-                "timestamp": "now",
-            },
-            {  # change addresses (internal: True)
-                "desc": internal["descriptor"],
-                "active": True,
-                "internal": True,
-                "timestamp": "now",
-            },
-        ])
+        descriptors = multisig.getdescriptorinfo(f"wsh(thresh({self.N},pk({f'),s:pk('.join(xpubs)}),sln:after({f'),sln:after('.join(map(str, self.locktimes))})))")
+        result = multisig.importdescriptors([{
+            "desc": desc,
+            "active": True,
+            "internal": "/1/*" in desc,
+            "timestamp": "now",
+        } for desc in descriptors["multipath_expansion"]])
         assert all(r["success"] for r in result)
         return multisig
 
@@ -77,10 +68,10 @@ class WalletMiniscriptDecayingMultisigDescriptorPSBTTest(BitcoinTestFramework):
 
         self.log.info("Create the signer wallets and get their xpubs...")
         signers = [self.node.get_wallet_rpc(self.node.createwallet(wallet_name=f"signer_{i}", descriptors=True)["name"]) for i in range(self.N)]
-        external_xpubs, internal_xpubs = [[self._get_xpub(signer, internal) for signer in signers] for internal in [False, True]]
+        xpubs = [self._get_xpub(signer).replace("/0/*", "/<0;1>/*") for signer in signers]
 
         self.log.info("Create the watch-only decaying multisig using signers' xpubs...")
-        multisig = self.create_multisig(external_xpubs, internal_xpubs)
+        multisig = self.create_multisig(xpubs)
 
         self.log.info("Get a mature utxo to send to the multisig...")
         coordinator_wallet = self.node.get_wallet_rpc(self.node.createwallet(wallet_name=f"coordinator", descriptors=True)["name"])

@darosior
Copy link
Member

Will re-review from scratch next week to finally get this over the finish line.

@pythcoiner
Copy link

pythcoiner commented Jul 8, 2024

I'm not sure if it's the intended behavior, but passing the same NUM twice to importdescriptors does not fail and ends up with a single change descriptor:

self.log.info("Multipath descriptors: duplicate NUM")
self.nodes[1].createwallet(wallet_name="multipath3", descriptors=True, blank=True)
w_multipath = self.nodes[1].get_wallet_rpc("multipath3")
self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<1;1>/*)"),
                      "active": True,
                      "range": 10,
                      "timestamp": "now"},
                      success=True,
                      wallet=w_multipath)
result = w_multipath.listdescriptors()
self.log.info(f"{result=}")
assert_equal(len(result["descriptors"]), 1)

assert_equal(result["descriptors"][0]["desc"],"wpkh(tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H/1/*)#hf9h03qd" )
assert_equal(result["descriptors"][0]["internal"], True)

Edit: is there any reason to accept duplictaes NUM in multipath?

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

Successfully merging this pull request may close these issues.

descriptors: represent multiple derivation paths within one descriptor