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.
ACK, seems good to me. Waiting for squash and will review again.
I don't intend to squash. They're all independent improvements.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--174a7506f384e20aa4161008e828411d-->
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;
5a044ac64f3f51dc89a772f512d403c7baa12ae2: I'm a bit confused what this new check is doing
Gone.
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) {
5a044ac64f3f51dc89a772f512d403c7baa12ae2: why is m_subdescriptor_args length 1 a special case?
Gone.
Code review 45bc0c04e0f2ea6dcce734652f45cabc6d10d7d5 looks good, but I got confused in one place...
Partial ACK. Same questions as Sjors.
@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.
Thanks, I'll review again after rebase.
Rebased.
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:
if (m_subdescriptor_arg) {
const auto& subscript = subscripts[0];
out.scripts.emplace(CScriptID(subscript), subscript);
output_scripts = MakeScripts(pubkeys, &subscript, out);
} else {
output_scripts = MakeScripts(pubkeys, nullptr, out);
}
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?
ACK 05c633e0efaf300aa3353303f77c0a622647a588
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'
Fixed.
re-ACK 35cbef1c483ac9905331f8035d32600e105baab4 modulo unused constructor
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));
Q: why do we need to move the first argument?
It doesn't matter now apparently, as Merge takes const lvalue references only. But a more efficient variant could be added that takes advantage of inputs that aren't needed anymore.
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;
nit: I think one instance of FlatSigningProvider is enough, same as you do in ExpandHelper
Done. In fact, I don't need any at all.
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:
std::string subscript;
if (!ToStringSubScriptHelper(arg, subscript, priv, normalized)) return false;
if (pos && subscript.size()) ret += ",";
out = std::move(ret) + std::move(subscript) + ")";
Done.
Code Review ACK 394e387. Nice and clear improvements!
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.
Rebased, and addressed all comments I think.
reACK 0b188b7
The only changes are addressing review comments above. 1) removing unused constructor 2) removing redundant signing provided instance creation 3) simplifying ToStringSubScriptHelper signature
re-ACK 0b188b7
re-ACK 0b188b751f970027c52729e0c223cc9257669322
@instagibbs what do you think?
Seems like 3 ACKs is enough? not familiar enough with this code anymore to give a proper ACK
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";
why is there no new test for this and for addr()?