-
Notifications
You must be signed in to change notification settings - Fork 37.7k
refactor, wallet: get serialized size of CRecipient
s directly
#30050
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
refactor, wallet: get serialized size of CRecipient
s directly
#30050
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. 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. |
@@ -1141,6 +1129,16 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( | |||
result.GetWaste(), | |||
result.GetSelectedValue()); | |||
|
|||
// vouts to the payees | |||
for (const auto& recipient : vecSend) |
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.
would it make sense to preallocate the vouts before pushing?
txNew.vout.reserve(txNew.vout.size() + vecSend.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.
Trying to keep this a move-only refactor, so would prefer to leave it for now. I end up touching this code again in #28201; will make a note of this for that PR.
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.
Split it out to a new PR so that we don't forget it: https://github.com/bitcoin/bitcoin/pull/30093/files
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.
Rebased #30093 after this merge - it became trivial after your changes
@@ -994,6 +994,8 @@ static util::Result<CreatedTransactionResult> CreateTransactionInternal( | |||
|
|||
// Set the long term feerate estimate to the wallet's consolidate feerate | |||
coin_selection_params.m_long_term_feerate = wallet.m_consolidate_feerate; | |||
// Static vsize overhead + outputs vsize. 4 nVersion, 4 nLocktime, 1 input count, 1 witness overhead (dummy, flag, stack size) | |||
coin_selection_params.tx_noinputs_size = 10 + GetSizeOfCompactSize(vecSend.size()); // bytes for output count |
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: found it a bit confusing that we have two different style comments for the same 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.
Would prefer to not change for now to keep this a move-only commit.
src/wallet/spend.cpp
Outdated
{ | ||
CTxOut txout(recipient.nAmount, GetScriptForDestination(recipient.dest)); | ||
|
||
if (IsDust(txout, wallet.chain().relayDustFee())) { |
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: it makes sense to keep this check before coin selection to return early and avoid wasting time
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.
Yeah, I had considered this but it would require refactoring IsDust
in a non-trivial way, since IsDust
is used outside of the wallet and as such doesn't really know what a CRecipient
is.
It's definitely possible, since all you really need to check for dust is the serialized size of the output and the value, but doing the refactor here blows up the scope of this PR quite a bit. I think it would be better to handle this in a separate PR, i.e. refactor IsDust
so it can be used for both CRecipients
and CTxOuts
. Happy to open a follow-up PR, or at the very least leave a TODO
comment here.
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.
Actually, I just realized I was over complicating this: you don't need to refactor IsDust
, you just need to wrap it with an IsDust
function that turns a CRecipient
into a CTxOut
, same as GetSerializeSizeForRecipient
. Will update.
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.
Updated, thanks @S3RK !
b44f002
to
f004b16
Compare
Updated b44f002 -> f004b16 (serialize-size-for-crecipient-00 -> serialize-size-for-crecipient-01, compare)
|
src/wallet/wallet.cpp
Outdated
@@ -4559,4 +4559,18 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const | |||
} | |||
return std::nullopt; | |||
} | |||
|
|||
size_t GetSerializeSizeForRecipient(const CRecipient& recipient) |
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: I think the visitor pattern in GetSerializeSizeForRecipient
and IsDust
in this case makes the code less clear, but I could be missing something
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.
p.s. I don't see how the callable will be applied to more than one destination, so in my mind it's just easier to inline the callable and drop the std::visit
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.
Regarding the visitor pattern, CTxDestination
is a std::variant and we need the visitor to know to use WitnessV1Taproot
when calculating the serialized size for both GetSerializeSizeForRecipient
and IsDust
when the CTxDestination
is a V0SilentPayments
destination. See 797e21c.
However, you're correct this isn't really clear in this PR, so I'll update the commit message to explain this.
Regarding making the functions callable, I also wasn't sure about this. I noticed a few places where IsDust
is used in the RPC calls, which is why I decided to make it callable here, but we can always remove them from the header if we end up only using it in CreateTransactionInternal
.
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 must also be missing something here because you should be able to write this as:
size_t GetSerializeSizeForRecipient(const CRecipient& recipient)
{
return ::GetSerializeSize(CTxOut(recipient.nAmount, GetScriptForDestination(recipient.dest)));
}
And the follow-up PR should also compile this code:
size_t GetSerializeSizeForRecipient(const CRecipient& recipient)
{
// A Silent Payements address is instructions on how to create a WitnessV1Taproot output
// Luckily, we know exactly how big a single WitnessV1Taproot output is, so return the serialization for that
// For everything else, convert it to a CTxOut and get the serialized size
if (std::get_if<V0SilentPaymentDestination>(&recipient.dest) != nullptr) {
return ::GetSerializeSize(CTxOut(recipient.nAmount, GetScriptForDestination(WitnessV1Taproot())));
} else {
return ::GetSerializeSize(CTxOut(recipient.nAmount, GetScriptForDestination(recipient.dest)));
}
}
Still, I'm also not sure about this decoupling commit.
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.
@S3RK , @furszy you're right: get_if
works here, as well. For some reason I was thinking std::get_if
doesn't check if the type exists in the std::variant
at compile time, hence using a visitor was better. But std::get_if
does check that the type is included in the variant; its just that the visitor can actually do compile time checks to ensure all the cases are covered. But in the silent payments example, we don't need exhaustive coverage so the visitor is likely overkill. I've updated the functions to remove the visitor.
I also moved these into spend.cpp
for now. After looking around a bit, I don't think I will need them anywhere else and if they are, I can move them to a spend_util.h/cpp
file, per @furszy 's suggestion.
f004b16
to
4e1ccb3
Compare
Rebased to fix merge conflict and update commit message to better explain the motivation for using the visitor patter, per @S3RK 's feedback |
src/wallet/wallet.h
Outdated
size_t GetSerializeSizeForRecipient(const CRecipient& recipient); | ||
bool IsDust(const CRecipient& recipient, const CFeeRate& dustRelayFee); | ||
|
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.
Structural topic (not really for this PR):
These two functions, and also CRecipient
seem to fit better on a new spend_util.h/cpp
file rather than here.
Also TransactionChangeType
seem to fit better inside spend.h/cpp
rather than here.
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.
IIRC, I tried to move CRecipient
out of wallet.h
awhile back and it ended up being more complicated than I expected? But in general I agree.
src/wallet/spend.cpp
Outdated
@@ -9,7 +9,6 @@ | |||
#include <consensus/validation.h> | |||
#include <interfaces/chain.h> | |||
#include <numeric> | |||
#include <policy/policy.h> |
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.
This include should still be needed. The CreateTransactionInternal
still calls GetDustThreshold
.
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.
Fixed.
src/wallet/wallet.cpp
Outdated
@@ -4559,4 +4559,18 @@ std::optional<CKey> CWallet::GetKey(const CKeyID& keyid) const | |||
} | |||
return std::nullopt; | |||
} | |||
|
|||
size_t GetSerializeSizeForRecipient(const CRecipient& recipient) |
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 must also be missing something here because you should be able to write this as:
size_t GetSerializeSizeForRecipient(const CRecipient& recipient)
{
return ::GetSerializeSize(CTxOut(recipient.nAmount, GetScriptForDestination(recipient.dest)));
}
And the follow-up PR should also compile this code:
size_t GetSerializeSizeForRecipient(const CRecipient& recipient)
{
// A Silent Payements address is instructions on how to create a WitnessV1Taproot output
// Luckily, we know exactly how big a single WitnessV1Taproot output is, so return the serialization for that
// For everything else, convert it to a CTxOut and get the serialized size
if (std::get_if<V0SilentPaymentDestination>(&recipient.dest) != nullptr) {
return ::GetSerializeSize(CTxOut(recipient.nAmount, GetScriptForDestination(WitnessV1Taproot())));
} else {
return ::GetSerializeSize(CTxOut(recipient.nAmount, GetScriptForDestination(recipient.dest)));
}
}
Still, I'm also not sure about this decoupling commit.
4e1ccb3
to
ad55985
Compare
Now that a CRecipient holds a CTxDestination, we can get the serialized size and determine if the output is dust using the CRecipient directly. This does not change any current behavior, but provides a nice generalization that can be used to apply special logic to a CTxDestination serialization and dust calculations in the future. Specifically, in a later PR when support for `V0SilentPayment` destinations is added, we need to use `WitnessV1Taproot` as the scriptPubKey for serialized size calcuations whenever the `CRecipient` destination is a `V0SilentPayment` destination.
Move the output serialization size and dust calculation into the loop where the outputs are iterated over to calculate the total sum. Move the code for adding the the txoutputs to the transaction to after coin selection. While this code structure generally follows a more logical flow, the primary motivation for moving the code for adding outputs to the transaction sets us up nicely for silent payments (in a future PR): we need to know the input set before generating the final output scriptPubKeys.
ad55985
to
a9c7300
Compare
reACK a9c7300 Looks even simpler now, thanks for incorporating feedback. |
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.
tACK a9c7300
make, test/functional
are successful.
From what I understand about Silent Payments, the calculation of the recipient address is dependent on the input hash for which it makes sense to push down the txoutputs
addition after coin selection.
Left a suggestion regarding commits but not a blocker.
CTxOut txout(recipient.nAmount, GetScriptForDestination(recipient.dest)); | ||
|
||
// Include the fee cost for outputs. | ||
coin_selection_params.tx_noinputs_size += GetSerializeSizeForRecipient(recipient); | ||
|
||
if (IsDust(recipient, wallet.chain().relayDustFee())) { | ||
return util::Error{_("Transaction amount too small")}; | ||
} | ||
txNew.vout.push_back(txout); |
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.
Although being a bit pedantic, but as this commit is supposed to be move-only, the removal of CTxOut txout...
in line 1107 and the replacement of push_back
with emplace_back
in line 1115 could have been part of the previous commit to avoid any confusion for future readers.
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.
Will leave for now so as to not require reACKs for a house keeping change, but will reorder if I end up needing retouch. Thanks!
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 functionality wise. Not blocking if others are happy with the current code.
Have to admit that I'm not fan of the multiple CTxOut
creations and GetScriptForDestination
calls when we could cache the CTxOut
early in the process, leaving the silent payment destinations empty, just so they can be modified later on. The first commit GetSerializeSizeForRecipient
and IsDust
decouplings seem more like an unneeded indirection rather than something worth doing to me atm. But as said above, not blocking if others are happy with it.
ACK a9c7300
I don't like that since it overloads the meaning of an empty scriptPubKey. This would also not work as well for future silent payments versions since information about the version. |
Broken out from #28201
In order to estimate fees properly, we need to know what the final serialized transaction size will be. This PR refactors
CreateTransactionInternal
to:CRecipient
: this sets us up in a future PR to calculate the serialized size of silent paymentCTxDestinations
(see 797e21c)GetSerializeSizeForRecipient
to move the serialize size calculation to before coin selection and the output creation to after coin selection: this also sets us up for silent payments sending in a future PR in that silent payments outputs cannot be created until after the inputs to the transaction have been selectedAside from the silent payments use case, I think this structure logically makes more sense. As a reminder, move-only commits are best reviewed with something like
git diff -w --color-moved=dimmed-zebra