Skip to content

Conversation

achow101
Copy link
Member

@achow101 achow101 commented Nov 13, 2021

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.

@DrahtBot
Copy link
Contributor

DrahtBot commented Nov 14, 2021

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

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK aureleoules
Concept ACK darosior, murchandamus

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:

  • #28212 (test: verify spend from 999-of-999 taproot multisig wallet by eriknylund)

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.

@@ -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);
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

@bitcoin bitcoin deleted a comment from 2310Mas Dec 7, 2021
@flack
Copy link
Contributor

flack commented Jan 29, 2022

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.

Copy link
Contributor

@aureleoules aureleoules left a 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.

smallest_result_stack = result_stack;
smallest_result_stack_size = stack_size;
}
if (use_largest && (largest_result_stack.size() == 0 ||
Copy link
Contributor

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

Suggested change
if (use_largest && (largest_result_stack.size() == 0 ||
if (use_largest && (largest_result_stack.empty() ||

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 ||
Copy link
Contributor

Choose a reason for hiding this comment

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

if you retouch

Suggested change
if (smallest_result_stack.size() == 0 ||
if (smallest_result_stack.empty() ||

@fanquake fanquake requested a review from instagibbs October 10, 2022 07:36
@@ -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;
Copy link
Member

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?

Copy link
Member Author

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.

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
Copy link
Contributor

@aureleoules aureleoules left a comment

Choose a reason for hiding this comment

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

reACK 5345b80

Copy link
Member

@darosior darosior left a 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.

@achow101
Copy link
Member Author

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.

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.

@FractalEncrypt
Copy link

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.

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;
I created a multisig address using deriveaddresses and funded it. Then I tried to create a PSBT to spend from the funded address. I was unable to create a PSBT that could be properly processed in 24.0.1. After creating the PSBT and using walletprocesspsbt in my wallet with private keys disabled, it created PSBTs missing the 'taproot_bip32_derivs' on all the multisig inputs, so my signing wallets couldn't sign the transaction.
This is kinda crazy since I was able to create a multisig address and fund it - but now I can't spend from it.

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)

Copy link
Contributor

@murchandamus murchandamus left a 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?

Comment on lines +276 to +284
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;
Copy link
Contributor

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?

Copy link
Member Author

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.

@achow101
Copy link
Member Author

How would this behave in the context of changeless transactions? Would we be overestimating the target and dropping an excess to the fees?

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.

Does that get recognized when we compare coin selection solutions?

No. Prior to actual signing, all sizes use the size estimator, which this PR changes to use the worst case scenario.

@achow101
Copy link
Member Author

achow101 commented Sep 7, 2023

Superseded by #26573

@achow101 achow101 closed this Sep 7, 2023
@bitcoin bitcoin locked and limited conversation to collaborators Sep 6, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.