-
Notifications
You must be signed in to change notification settings - Fork 37.7k
wallet: Avoid underpaying transaction fees when signing taproot spends #23502
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
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. 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. |
c61221d
to
bec622b
Compare
bec622b
to
f0a9151
Compare
@@ -570,14 +571,17 @@ class DummySignatureCreator final : public BaseSignatureCreator { | |||
bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* tweak, SigVersion sigversion) const override | |||
{ | |||
sig.assign(64, '\000'); | |||
if (m_include_sighash) { | |||
sig.push_back((unsigned char)SIGHASH_ALL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could just do sig.assign(64 + m_include_sighash, '\000');
in the previous line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO that's a bit harder to read and I don't like the implicit bool to int conversion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have to agree with @achow101 here. Even though the alternative formulation is shorter ,and correct, I understand the intent of the code better this way.
f0a9151
to
6c2f69d
Compare
6c2f69d
to
ca85b71
Compare
Not sure if I understand this correctly, but the underestimation could also lead to a situation where minrelaytxfee is not met, right? If so, maybe a good first step would be to prevent that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 7f927b7 - LGTM
I also verified the test does not pass on master.
src/script/sign.cpp
Outdated
smallest_result_stack = result_stack; | ||
smallest_result_stack_size = stack_size; | ||
} | ||
if (use_largest && (largest_result_stack.size() == 0 || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit 7f927b7
same above and below
if (use_largest && (largest_result_stack.size() == 0 || | |
if (use_largest && (largest_result_stack.empty() || |
src/script/sign.cpp
Outdated
for (const auto& [key, control_blocks] : sigdata.tr_spenddata.scripts) { | ||
const auto& [script, leaf_ver] = key; | ||
std::vector<std::vector<unsigned char>> result_stack; | ||
if (SignTaprootScript(provider, creator, sigdata, leaf_ver, script, result_stack)) { | ||
result_stack.emplace_back(std::begin(script), std::end(script)); // Push the script | ||
result_stack.push_back(*control_blocks.begin()); // Push the smallest control block | ||
uint64_t stack_size = GetSerializeSize(result_stack, PROTOCOL_VERSION); | ||
if (smallest_result_stack.size() == 0 || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you retouch
if (smallest_result_stack.size() == 0 || | |
if (smallest_result_stack.empty() || |
@@ -32,6 +32,9 @@ class BaseSignatureCreator { | |||
/** Create a singular (non-script) signature. */ | |||
virtual bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const =0; | |||
virtual bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const =0; | |||
|
|||
/** Choose the largest stack size for worst case size estimation when using this signer */ | |||
virtual bool UseLargest() const =0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slightly philosophical question: If the wallet knows how to satisfy two script paths, will it use the more expensive one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. This change only affects dummy signing, but for actual signing, it will still use smallest solution.
7f927b7
to
60b61f8
Compare
Sometimes we need to know whether we should be choosing the largest stack size for worse case size estimation. BaseSignatureCreator can report that to us as we only need to do this when using DummySignatureCreator.
When estimating the size of a taproot witness (i.e. DummySignatureCreator is being used), use the largest stack size rather than the smallest. This avoids underpaying fees. Also updates the tests to make sure that Taproot spends do not underestimate the fee
60b61f8
to
5345b80
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reACK 5345b80
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK. But for the approach i would prefer something like #26573 instead of making the signing logic still more intricate. But that can also happen afterward.
Regarding having a more accurate estimation, i think we should eventually hint to what material in a descriptor will be available at signing time. But i'm not sure yet how. For your interest elsewhere people are testing some approaches to solve this problem: rust-bitcoin/rust-miniscript#481.
The approach in this PR also has a few useful side effects. Notably, right now a PSBT that has script paths will not fill in pubkey details for the scripts. However the changes in this PR should also fix that. |
This is my first time commenting on a PR, so I apologize if I am breaking any protocol or etiquette here. I compiled and ran this branch and it completely solved this issue described above (PSBT script paths with missing pubkey details); Further details; The code in this PR solved my issue completely. I compiled this branch and was able to easily create the PSBT with the proper input data, sign it and broadcast. Here is the signet Txid-> 72632a6c09ece682e2fe07796497ca5235280d908d027ee6900a6273e33dd2ae I'm not knowledgeable or skilled enough to know more about this code, but as a user, I know it solved my problem. (And as a user there's no bigger problem then when you can't spend from an address that you control the keys to) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
How would this behave in the context of changeless transactions? Would we be overestimating the target and dropping an excess to the fees? Does that get recognized when we compare coin selection solutions?
if (smallest_result_stack.empty() || | ||
stack_size < smallest_result_stack_size) { | ||
smallest_result_stack = result_stack; | ||
smallest_result_stack_size = stack_size; | ||
} | ||
if (use_largest && (largest_result_stack.empty() || | ||
stack_size > largest_result_stack_size)) { | ||
largest_result_stack = result_stack; | ||
largest_result_stack_size = stack_size; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These make sense to me, but if we correctly estimated the maximum and minimum, how would they ever get used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow your question.
The estimated stack size is used to choose which result to return as multiple results could be valid. In normal signing, we use the smallest valid stack. In estimating, we use the largest.
All overages due to size estimation differences are dropped to fees. The output values are set based upon the estimated size and they cannot be changed after signing. Once signing is completed and the final transaction created, if it ends up being smaller than estimated, whatever could have been saved will not be saved.
No. Prior to actual signing, all sizes use the size estimator, which this PR changes to use the worst case scenario. |
Superseded by #26573 |
The current taproot signing code will end up creating transactions which do not meet the feerate target. This is primarily seen when spending a taproot output with the script path. In this scenario, because the public key is known, the dummy signer would incorrectly choose to create a dummy signed transaction that used the key path spend when the actual transaction would use script path. This results in a transaction that has a significantly lower fee rate because script path spends are much larger than key path spends. This is fixed by choosing the witness with the largest stack size rather than the smallest when doing size estimation.
Unfortunately this change can result in massively overestimating the transaction fees which is undesirable. More discussion is needed to find a good solution to avoiding that problem.
There is an additional issue in the PSBT workflow where the sighash type would be appended during finalizing, but not during size estimation. That results in a transaction that is slightly larger than estimated so there is a lower feerate. This is resolved by having the DUMMY_MAXIMUM_SIGNATURE_CREATOR include the sighash type so that when funding with a watchonly wallet, we will estimate enough fee for the largest possible signature (as we do for ECDSA with high R).
Tests are updated to catch these case.
wallet_taproot.py
will now use a set feerate and check if the result meets that feerate.