-
Notifications
You must be signed in to change notification settings - Fork 38.7k
[refactor] Make TransactionWithinChainLimit more flexible #12634
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
promag
left a comment
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.
which is different from "0", which means "no ancestors/descendants in mempool".
I believe "1" means "no ancestors/descendants in mempool".
src/txmempool.cpp
Outdated
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.
Snake case is for variables.
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.
It seems like we are moving to snake case, e.g. for new code like
Line 92 in a34ac6a
| inline void bit_set(uint32_t s) |
Maybe I'm mistaken on that one. Will check recent merges.
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.
From the developer notes:
- Variable and namespace names are all lowercase, and may use
_to
separate words (snake_case). - Class names, function names and method names are UpperCamelCase
(PascalCase).
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.
Wow, okay. I totally missed that, 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.
Fixed.
src/txmempool.h
Outdated
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 not in the mempool could return 0 since GetCountWithAncestors and GetCountWithDescendants are always > 0 (self is counted). In that case you can revert s/uint64_t/int64_t.
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 looked in the code and it seems to me they can be both 0 and that self is not counted.
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.
@NicolasDorier have you checked these?
Line 30 in a34ac6a
| nCountWithDescendants = 1; |
Line 36 in a34ac6a
| nCountWithAncestors = 1; |
Line 317 in a34ac6a
| assert(int64_t(nCountWithDescendants) > 0); |
Line 326 in a34ac6a
| assert(int64_t(nCountWithAncestors) > 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.
woops, completely missed that indeed. I failed to notice how the clients were forcing this invariant though.
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.
Got confused when I saw that which seemed to me it could be 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.
I made the change for a reason but that reason may have been flawed. I'll try it with uint64_t and 0 again. 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.
The thing is, the check in SelectCoinsMinConf checks if the chain limit value >= nMaxAncestors, which is 0 for no chained ancestors in the code. With the old method, this would be true, but with a 0 returned, it would be false.
I could change the 0 to a 1 to fix it, which may be a better solution here, and is probably what I will end up doing.
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 I do that, the semantics becomes misleading. It says 'max ancestors' so 1 makes it sound like it allows 1 ancestor or descendant, when in reality it only counts itself. I'm gonna keep the change to signed, for now.
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.
Indeed, now:
with @promag suggestion, if nMaxAncestors==0 and the tx is not in mempool then mempool.ChainLimitValue(pcoin->GetHash()) >= nMaxAncestors evaluate to true.
While before !mempool.TransactionWithinChainLimit(pcoin->GetHash(), nMaxAncestors) evaluate to false.
667592e to
b0928df
Compare
|
utACK b0928df |
b0928df to
8a32f56
Compare
38fd625 to
06a937a
Compare
06a937a to
8154223
Compare
|
utACK 8154223. |
|
I find this whole thing unnecessarily confusing. Not your change in particular, more how the code works today. There are two separate config limits, But further, the descendants check isn't even right: the documentation says "Do not accept transactions if any ancestor would have or more in-mempool descendants". So the descendants check really needs to check the descendants count on all ancestors, not just the direct parent. Regardless of all that, even just looking at the ancestors limit, which is easier to check, I think it should not be a strict equality check, whereas it is currently in the code. If trying to spend an output from a transaction with 3 ancestors, the spending transaction would have 4 ancestors. Say the ancestor limit is 3. The code currently would call that spend ineligible because GetCountWithAncestors returns 3, and there is a strict < check, whereas the spend should be allowed because the ancestor count is not above the limit. So that is to say |
|
@jimpo Yeah it's not entirely straightforward that's for sure, and I think further consideration should be given. I don't think this PR makes the situation worse, though. |
|
Well, I think it does make things worse because it formalizes the concept of a |
|
@jimpo Thanks for spelling it out. To summarize:
Does that seem accurate? |
|
Delving into this a bit further, the ascendant vs descendant thing is quite different. There's usually only one ascendant per transaction except if several transactions were merged together, while there can be a tree of transactions as descendants. I'm wondering if this should instead be "max unconfirmed group size", and default to the sum of the default values for max descendants/ascendants. |
|
Actually, I think the more straightforward fix here is to fix This PR can then be rewritten to take both max ancestors and max descendants. That would address your concerns, right? |
8154223 to
d120f0d
Compare
Would you mind checking that out and seeing if it looks okay to you? |
d120f0d to
d8864ab
Compare
src/txmempool.cpp
Outdated
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.
Maybe add some bounds-checking asserts here ala assert(int64_t(nCountWithDescendants) > 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.
I'm going to switch back to unsigned, so no need.
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.
Definitely a big step in the right direction.
src/wallet/wallet.cpp
Outdated
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.
commit: Switch to GetTransactionAncestry() in OutputEligibleForSpending
I think these should be strict > checks, in which case you don't need to switch to signed results. This makes sense logically because ancestors and descendants counts includes the transaction queried, so if the count is at the maximum allowable, it is still valid to create one more in the chain.
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.
It means having to change the parameter for all the calls (bump by 1), but I think you're right. The -1 hack is bad. Fixing.
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 had to do one minor tweak, but otherwise the current values worked even with the change. I also switched from nMaxChainLength to using max_ancestors and max_descendants with their corresponding defaults. (See 6ab847bce76224ff46552dd647b542fa70350a91.)
src/txmempool.cpp
Outdated
| for (;;) { | ||
| const setEntries& parents = GetMemPoolParents(top); | ||
| if (parents.size() == 0) break; | ||
| top = *parents.begin(); |
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.
commit: mempool: Fix max descendants check
You actually need to recurse down all parents and take the max GetCountWithDescendants of all ancestors. A transaction can have multiple chains of ancestry with different descendants counts. This is also why it kind of makes sense to take the descendants limit as a parameter to this method rather than (or in addition to) returning it -- because it can limit the recursion.
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 was afraid that would be too resource intensive and potential for DoS. Doing a 'depth first optimistic pass' seemed like a good estimate.
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.
Hmm, I understand the concern, but this doesn't match the advertised behavior of the flag. Also, the ancestor limit can be used to limit the depth of recursion. I'd like some more opinions on this.
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.
After thinking about it for a bit, I am not convinced it's particularly DoS vulnerable. I pushed a commit (to squash) that calculates it the way it is advertised.
d1a2f21 to
d9b765b
Compare
src/wallet/wallet.cpp
Outdated
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 underflows if nMaxChainLength is 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.
Fixed (I set nMaxChainLength to std::max(1, ...)).
src/wallet/wallet.cpp
Outdated
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 underflows if max_ancestors or max_descendants is 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.
Fixed (set both to std::max(1, ...)).
src/txmempool.cpp
Outdated
| for (;;) { | ||
| const setEntries& parents = GetMemPoolParents(top); | ||
| if (parents.size() == 0) break; | ||
| top = *parents.begin(); |
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.
Hmm, I understand the concern, but this doesn't match the advertised behavior of the flag. Also, the ancestor limit can be used to limit the depth of recursion. I'd like some more opinions on this.
d9b765b to
cde1022
Compare
|
From https://botbot.me/freenode/bitcoin-core-dev/msg/100337736/ on motivation for this change:
|
|
ACK e39efa8c9da85737393ca93bcda711774115702c |
e39efa8 to
5ad55d2
Compare
|
Squashed fix-commits into 5ad55d2. |
src/test/mempool_tests.cpp
Outdated
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
TransactionWithinChainLimits would take a 'limit' and check it against ascendants and descendants. This is changed to take an explicit max ancestors and max descendants value, and to test the corresponding value against its corresponding max.
The chain limits check for max descendants would check the descendants of the transaction itself even though the description for -limitdescendantcount says 'any ancestor'. This commit corrects the descendant count check by finding the top parent transaction in the mempool and comparing against that.
Instead of combining the -limitancestorcount and -limitdescendantcount into a nMaxChainLength, this commit uses each one separately in the coin eligibility filters.
src/txmempool.cpp
Outdated
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: should be faster if you read the return value from the insertion, rather than find & insert.
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.
Didn't realize set::insert told you if the insertion happened. Nice!
5ad55d2 to
6c47fb2
Compare
6c47fb2 to
f77e1d3
Compare
| bool CTxMemPool::TransactionWithinChainLimit(const uint256& txid, size_t chainLimit) const { | ||
| uint64_t CTxMemPool::CalculateDescendantMaximum(txiter entry) const { | ||
| // find parent with highest descendant count | ||
| std::vector<txiter> candidates; |
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: more true to form to use a std::stack here, although I see they're not used otherwise in the codebase fwiw.
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,
std::vector<txiter> candidates = {entry};and remove push_back below.
|
utACK f77e1d3 |
promag
left a comment
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.
utACK f77e1d3.
| bool CTxMemPool::TransactionWithinChainLimit(const uint256& txid, size_t chainLimit) const { | ||
| uint64_t CTxMemPool::CalculateDescendantMaximum(txiter entry) const { | ||
| // find parent with highest descendant count | ||
| std::vector<txiter> candidates; |
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,
std::vector<txiter> candidates = {entry};and remove push_back below.
| } | ||
|
|
||
| bool CTxMemPool::TransactionWithinChainLimit(const uint256& txid, size_t chainLimit) const { | ||
| uint64_t CTxMemPool::CalculateDescendantMaximum(txiter entry) const { |
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 const txiter& entry.
|
utACK f77e1d3 I'm going to ignore last-minute nits here, sorry. This has too many reviews to hold it up on nits. |
f77e1d3 test: Add MempoolAncestryTests (Karl-Johan Alm) a08d76b mempool: Calculate descendant maximum thoroughly (Karl-Johan Alm) 6d35683 wallet: Switch to using ancestor/descendant limits (Karl-Johan Alm) 6888195 wallet: Strictly greater than for ancestor caps (Karl-Johan Alm) 322b12a Remove deprecated TransactionWithinChainLimit (Karl-Johan Alm) 4784751 Switch to GetTransactionAncestry() in OutputEligibleForSpending (Karl-Johan Alm) 475a385 Add GetTransactionAncestry to CTxMemPool for general purpose chain limit checking (Karl-Johan Alm) 46847d6 mempool: Fix max descendants check (Karl-Johan Alm) b9ef21d mempool: Add explicit max_descendants (Karl-Johan Alm) Pull request description: Currently, `TransactionWithinChainLimit` is restricted to single-output use, and needs to be called every time for different limits. If it is replaced with a chain limit value calculator, that can be called once and reused, and is generally more flexible (see e.g. #12257). Update: this PR now corrects usage of max ancestors / max descendants, including calculating the correct max descendant value, as advertised for the two limits. ~~This change also makes `nMaxAncestors` signed, as the replacement method will return `-1` for "not in the mempool", which is different from "0", which means "no ancestors/descendants in mempool".~~ ~~This is a subset of #12257.~~ Tree-SHA512: aa59c849360542362b3126c0e29d44d3d58f11898e277d38c034dc4b86a5b4500f77ac61767599ce878c876b5c446fec9c02699797eb2fa41e530ec863a00cf9
|
Sorry I haven't dug into this change myself, but might this have a material performance impact on wallets with many unconfirmed coins, if the mempool is full? |
|
@sdaftuar I honestly doubt it, unless they have a TON of ancestors in the mempool. I was initially hesitant to make the ancestor check "thorough" but couldn't come up with a reason why it would ever be the case that you have enough transactions in the mempool to make the ancestor check bog down your system significantly. |
… flexible f77e1d3 test: Add MempoolAncestryTests (Karl-Johan Alm) a08d76b mempool: Calculate descendant maximum thoroughly (Karl-Johan Alm) 6d35683 wallet: Switch to using ancestor/descendant limits (Karl-Johan Alm) 6888195 wallet: Strictly greater than for ancestor caps (Karl-Johan Alm) 322b12a Remove deprecated TransactionWithinChainLimit (Karl-Johan Alm) 4784751 Switch to GetTransactionAncestry() in OutputEligibleForSpending (Karl-Johan Alm) 475a385 Add GetTransactionAncestry to CTxMemPool for general purpose chain limit checking (Karl-Johan Alm) 46847d6 mempool: Fix max descendants check (Karl-Johan Alm) b9ef21d mempool: Add explicit max_descendants (Karl-Johan Alm) Pull request description: Currently, `TransactionWithinChainLimit` is restricted to single-output use, and needs to be called every time for different limits. If it is replaced with a chain limit value calculator, that can be called once and reused, and is generally more flexible (see e.g. bitcoin#12257). Update: this PR now corrects usage of max ancestors / max descendants, including calculating the correct max descendant value, as advertised for the two limits. ~~This change also makes `nMaxAncestors` signed, as the replacement method will return `-1` for "not in the mempool", which is different from "0", which means "no ancestors/descendants in mempool".~~ ~~This is a subset of bitcoin#12257.~~ Tree-SHA512: aa59c849360542362b3126c0e29d44d3d58f11898e277d38c034dc4b86a5b4500f77ac61767599ce878c876b5c446fec9c02699797eb2fa41e530ec863a00cf9
Currently,
TransactionWithinChainLimitis restricted to single-output use, and needs to be called every time for different limits. If it is replaced with a chain limit value calculator, that can be called once and reused, and is generally more flexible (see e.g. #12257).Update: this PR now corrects usage of max ancestors / max descendants, including calculating the correct max descendant value, as advertised for the two limits.
This change also makesnMaxAncestorssigned, as the replacement method will return-1for "not in the mempool", which is different from "0", which means "no ancestors/descendants in mempool".This is a subset of #12257.