These are a few refactors and non-invasive improvements to the descriptors code to prepare for adding Taproot descriptors.
None of the commits change behavior in any way, except the last one which improves error reporting a bit.
These are a few refactors and non-invasive improvements to the descriptors code to prepare for adding Taproot descriptors.
None of the commits change behavior in any way, except the last one which improves error reporting a bit.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
Reviewers, 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.
594- out = Merge(out, subprovider);
595+ FlatSigningProvider subprovider;
596+ for (const auto& subarg : m_subdescriptor_args) {
597+ std::vector<CScript> outscripts;
598+ if (!subarg->ExpandHelper(pos, arg, read_cache, outscripts, subprovider, write_cache)) return false;
599+ if (outscripts.size() != 1 && m_subdescriptor_args.size() != 1) return false;
606 for (auto& entry : entries) {
607 pubkeys.push_back(entry.first);
608 out.origins.emplace(entry.first.GetID(), std::make_pair<CPubKey, KeyOriginInfo>(CPubKey(entry.first), std::move(entry.second)));
609 }
610- if (m_subdescriptor_arg) {
611+ if (m_subdescriptor_args.size() == 1) {
m_subdescriptor_args
length 1 a special case?
@achow101 @Sjors Good that you raise that point.
The original reason why this behavior of MakeScripts-is-invoked-once-per-evaluation existed was to support things like combo()
, where a single evaluation at a single index results in multiple scriptPubKeys. This was particularly complicated to combine with descriptors that support multiple subdescriptors (like tr()
) - there isn’t really a straightforward way of doing that (cartesian product? requiring the same number of evaluations from each and combining pairwise? …?). I didn’t want to deal with that, so I added the restriction that this once-per-evaluation only existed for descriptors with 1 subdescriptor.
This is all silly, because we don’t ever need it. combo()
is the only descriptor that produces multiple output scripts, and it is only permitted at the top level - so it’s not even possible to have it as a subdescriptor. I’ve just removed support for multiple-outputscripts-from-subscriptor entirely in a separate commit now. Bye.
ACK 05c633e
Conceptually I find it marginally simpler if you reverse the first and second commit, so the first step is to remove the unused loop:
0 if (m_subdescriptor_arg) {
1 const auto& subscript = subscripts[0];
2 out.scripts.emplace(CScriptID(subscript), subscript);
3 output_scripts = MakeScripts(pubkeys, &subscript, out);
4 } else {
5 output_scripts = MakeScripts(pubkeys, nullptr, out);
6 }
And then the second commit drops out.scripts.emplace
and moves that into MakeScripts
(which is also where combo
does this).
To sanity check my understanding: key_exp_index
is currently used for multisig descriptors to create a seperate cache for each participating key. With taproot, you might have a root key (key_exp_index == 0
), one branch with a key key_exp_index == 2
, and another branch with a 3 key multisig (key_exp_index 3,4,5
). Is that how it works?
Conceptually I find it marginally simpler if you reverse the first and second commit
Ok, done.
With taproot, you might have a root key (
key_exp_index == 0
), one branch with a keykey_exp_index == 2
, and another branch with a 3 key multisig (key_exp_index 3,4,5
). Is that how it works?
I think you skipped the 1, but otherwise: exactly.
re-ACK 35cbef1c483ac9905331f8035d32600e105baab4
Only change was commit reordering.
511
512 public:
513- DescriptorImpl(std::vector<std::unique_ptr<PubkeyProvider>> pubkeys, std::unique_ptr<DescriptorImpl> script, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_arg(std::move(script)) {}
514+ DescriptorImpl(std::vector<std::unique_ptr<PubkeyProvider>> pubkeys, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_args() {}
515+ DescriptorImpl(std::vector<std::unique_ptr<PubkeyProvider>> pubkeys, std::unique_ptr<DescriptorImpl> script, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_args(Vector(std::move(script))) {}
516+ DescriptorImpl(std::vector<std::unique_ptr<PubkeyProvider>> pubkeys, std::vector<std::unique_ptr<DescriptorImpl>> scripts, const std::string& name) : m_pubkey_args(std::move(pubkeys)), m_name(name), m_subdescriptor_args(std::move(scripts)) {}
warning: unused member function 'DescriptorImpl'
596+ std::vector<CScript> outscripts;
597+ if (!subarg->ExpandHelper(pos, arg, read_cache, outscripts, subprovider, write_cache)) return false;
598+ assert(outscripts.size() == 1);
599+ subscripts.emplace_back(std::move(outscripts[0]));
600 }
601+ out = Merge(std::move(out), std::move(subprovider));
621@@ -621,9 +622,9 @@ class DescriptorImpl : public Descriptor
622 if (!p->GetPrivKey(pos, provider, key)) continue;
623 out.keys.emplace(key.GetPubKey().GetID(), key);
624 }
625- if (m_subdescriptor_arg) {
626+ for (const auto& arg : m_subdescriptor_args) {
627 FlatSigningProvider subprovider;
FlatSigningProvider
is enough, same as you do in ExpandHelper
558- if (pos++) ret += ",";
559- std::string tmp;
560- if (!scriptarg->ToStringHelper(arg, tmp, priv, normalized)) return false;
561- ret += std::move(tmp);
562- }
563+ if (!ToStringSubScriptHelper(arg, ret, priv, normalized, pos)) return false;
nit: I find it clearer If we keep placing of the first comma inside ToStringHelper
and do not pass pos
into ToStringSubScriptHelper
. Something like:
0std::string subscript;
1if (!ToStringSubScriptHelper(arg, subscript, priv, normalized)) return false;
2if (pos && subscript.size()) ret += ",";
3out = std::move(ret) + std::move(subscript) + ")";
There are currently two DescriptorImpl subclasses that rely on the functionality
that ExpandHelper automatically adds subscripts to the output SigningProvider.
Taproot descriptors will have subscripts, but we don't want them in the
SigningProvider's bare script field. To avoid them ending up there, move this
functionality into the specific classes' MakeScripts implementation.
So far, no descriptor exists that supports more than one sub-script
descriptor. This will change with taproot, so prepare for this by
changing the m_subdescriptor_arg from a unique_ptr to a vector of
unique_ptr's.
This has no effect for now, as the only fragments with sub-script
expressions (sh, wsh) only allow one, and don't have key expressions
in them.
A future Taproot descriptor will however violate both, and we want
the keys in different sub-scripts to be assigned non-overlapping
cache indices.
This will allow subclasses to overwrite the serialization of subscript
arguments without needing to reimplement all the rest of the ToString
logic.
This is a preparation for parsing xonly pubkeys, which will complicate
this logic. It's cleaner to put the decision logic close to the public
key parsing itself.
This changes all context dependent checks in the parser to be
disjunctions of equality checks, rather than also including inequalities.
This makes sure that adding a new context enum in the future won't change
semantics for existing checks.
The error messages are also made a bit more consistent.
ToStringSubScriptHelper
signature
1065@@ -1045,6 +1066,9 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t key_exp_index, Span<const c
1066 }
1067 auto bytes = ParseHex(str);
1068 return std::make_unique<RawDescriptor>(CScript(bytes.begin(), bytes.end()));
1069+ } else if (Func("raw", expr)) {
1070+ error = "Can only have raw() at top level";
addr()
?