Counting sigops in the witness requires context that CheckBlock()
does not have, so it only counts sigops for non-segwit transactions.
It’s useful to document, but it should not be a problem.
The commit message contains some historical context.
Counting sigops in the witness requires context that CheckBlock()
does not have, so it only counts sigops for non-segwit transactions.
It’s useful to document, but it should not be a problem.
The commit message contains some historical context.
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31624.
See the guideline for information on the review process.
Type | Reviewers |
---|---|
ACK | ismaelsadeeq, ryanofsky |
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
4093@@ -4094,6 +4094,7 @@ bool CheckBlock(const CBlock& block, BlockValidationState& state, const Consensu
4094 strprintf("Transaction check failed (tx hash %s) %s", tx->GetHash().ToString(), tx_state.GetDebugMessage()));
4095 }
4096 }
4097+ // This underestimates the number of sigops, because unlike ConnectBlock it does not count the witness:
In commit “doc: warn that CheckBlock() underestimates sigops” (0ac19a98f329fe8952f394509a7a29775ab805b0)
Might be good to say why this check is useful if it’s not a problem for it to be inaccurate.
Might also be good if PR description mentioned motivation for the change. This all looks ok but it’s a bit confusing because it’s not clear what the context is.
The original context was that a miner created an invalid block with too many sigops. I wanted to make sure that it was because they made an invalid patch, and not because our code is broken. I wrote this comment while trying to understand the behaviour better.
It may be a good idea to actually improve the check here, since it’s relied on by getblocktemplate
in proposal mode and by #31981. But that would be another PR imo.
As explained in #31981 (comment) this is fine. TestBlockValidity
also calls the more thorough method.
I don’t know why this check is here, perhaps just a way to quickly reject a block with lots of sigops?
nit: maybe be specific.
0 // This underestimates the number of sigops, because unlike ConnectBlock it does not count the witness sigops and p2sh sigops:
It was introduced by e679ec969c8b22c676ebb10bea1038f6c8f13b33 in preparation for OP_EVAL (which was eventually replaced by P2SH). It seems the initial goal was to have a backward compatible sigop check:
0// This code should be removed when a compatibility-breaking block chain split has passed.
1// Compatibility check for old clients that counted sigops differently:
The commit drops the GetSigOpCount()
helper and copy-pasted its implementation into CheckBlock()
, where it previously called this method. The same commit implements sigops checking in ConnectBlock
.
I still don’t understand in what way this was supposed to help with backward compatibility.
But even if the original intention was for this code to be temporary, things have moved around so much that this would just have to rethought from scratch.
I’ll update the commit message to at least point to this historical commit.
ACK 0ac19a98f329fe8952f394509a7a29775ab805b0 with a nit and another suggestion
The size limit check above the sigops check also underestimates the block size because it does not include the witness.
Counting sigops in the witness and for p2sh requires
context that CheckBlock() does not have, so it only
counts a subset of sigops.
The check here was introduced by Satoshi as a "cleanup" in
f1e1fb4bdef878c8fc1564fa418d44e7541a7e83. With the attempted
introduction of OP_EVAL, it was replaced by the check in
ConnectBlock(). Commit e679ec969c8b22c676ebb10bea1038f6c8f13b33
marked this code as a placeholder for backward compatibility.
Then when P2SH replaced OP_EVAL in 922e8e2929a2e78270868385aa46f96002fbcff3
the phrase "compatibility-breaking" was replaced by a simple
observation that before v0.6 this is how sigops were counted.
It's unclear why the check was kept and there were no review comments
about it.
I expanded the comment to mention that p2sh isn’t counted either.
I also added some history to the commit message. This seems to be a case of: none of the reviewers of #748 brought it up, but there were no reviewers :-)
Also rebased.
The size limit check above the sigops check also underestimates the block size because it does not include the witness.
Well that’s weird. But better to tackle in a different PR.
ACK a04f17a1882407db09b0a07338e12877ac1d9e92
Thanks for taking my suggestion and providing the context.
I will prefer if the size check is also clarified in same PR. Combining the two in same PR might be better to not miss writing the follow-up.
4093@@ -4094,6 +4094,8 @@ bool CheckBlock(const CBlock& block, BlockValidationState& state, const Consensu
4094 strprintf("Transaction check failed (tx hash %s) %s", tx->GetHash().ToString(), tx_state.GetDebugMessage()));
4095 }
4096 }
4097+ // This underestimates the number of sigops, because unlike ConnectBlock it
4098+ // does not count witness and p2sh sigops.
In commit “doc: warn that CheckBlock() underestimates sigops” (a04f17a1882407db09b0a07338e12877ac1d9e92)
This comment is good to have but would be nice to clarify:
Might suggest expanding comment to something like “This check underestimates the number of sigops because it does not count witness and p2sh sigops. This is not a problem for validation because ConnectBlock will check them. Having the check here is useful for mining even though it isn’t perfect, and it could be improved in the future.”
Thanks for clarifying. I got the impression that you did think it was useful from “It may be a good idea to actually improve the check here, since it’s relied on by getblocktemplate
in proposal mode and by #31981” (https://github.com/bitcoin/bitcoin/pull/31624#discussion_r2053597806). But I guess you were saying it could be useful if it were improved, not that it is useful currently.
Anyway I was mostly asking about this because I was confused. Assuming you are happy with PR in current form I will go ahead and merge this later today based on the two ACKs. Obviously there are more changes that could be made but this is a clear improvement over the status quo.
since it’s relied on by getblocktemplate in proposal mode
That was a mistake on my end, because checkBlock
calls connectBlock
which does the full check.
I’m a bit hesitant, because this part of the code has more comments than usual, e.g.
0/**
1 * Pre-version-0.6, Bitcoin always counted CHECKMULTISIGs
2 * as 20 sigops. With pay-to-script-hash, that changed:
3 * CHECKMULTISIGs serialized in scriptSigs are
4 * counted more accurately, assuming they are of the form
5 * ... OP_N CHECKMULTISIG ...
6 */
7unsigned int GetSigOpCount(bool fAccurate) const;
8
9/**
10 * Accurately count sigOps, including sigOps in
11 * pay-to-script-hash transactions:
12 */
13unsigned int GetSigOpCount(const CScript& scriptSig) const;
I’m not sure the ones in this PR add to this in a meaningful way.
clearly states that the check is inaccurate
Isn’t that already clear from calling bool fAccurate
with false
? In a method called GetLegacySigOpCount
with the doc:
0 * Count ECDSA signature operations the old-fashioned (pre-0.6) way
? Seems to me the warnings are already all over the place.
If we need more warnings, can we improve the existing ones instead? Or improve the code to not need so many comments in the first place?
Or improve the code to not need so many comments in the first place?
So IIUC having the comment itself is a downside? I think different people use comments differently. I like when comments are there to help me navigate code and call out things that are notable, even repeat things that are “clear,” because I find most comments easier to read than most code, and I like when I can get an overview of what is happening by just reading the comments and not even looking at the code. I think other people don’t use comments like that and see them more like a red flag or tax that has to be paid when code isn’t expressive enough to communicate what is doing.
So I think the disagreement here may mostly come down to personal preference, but this feedback is helpful, and I will hold off merging this to see if there will be another ack or nack.
Also if have a clear alternative you like better feel free to post or link!
GetSigOpCount(/*fAccurate*/false)
- which isn’t just a dead comment since linters are checking it)?
But here it’s already over-explained, what’s the exact reason for wanting even more comments
I think we probably disagree that the current code is overexplained. At least 3 of us here think that current code is confusing and a04f17a1882407db09b0a07338e12877ac1d9e92 is an improvement.
(but not even considering code changes like
GetSigOpCount(/*fAccurate*/false)
- which isn’t just a dead comment since linters are checking it)?
I didn’t consider this just because I didn’t know it was the clear solution you preferred over this PR. It’s easier to compare two compete alternatives than compare a complete proposal with a partial suggestion. If you can implement your preferred solution in a commit, I think that would be great. It looks like a good change.
For me, comments are most useful in the following cases:
I checked developer-notes.md, and it seems there’s no dedicated section on comments. It would be nice to have one.
What’s currently missing in this PR is a clear explanation of whether the check is redundant or necessary especially given that we already perform a full sigops
count check in ConnectBlock
. If that rationale were explicitly written, I believe it would be the most important part of the comment, similar to the size check above it.
So, I would categorize this PR as addressing point 3.
Even without this PR, as @l0rinc mentioned, someone could dig into the code and eventually realize that it’s an incomplete check. But I think having a short comment above the function call making it explicit is okay.
That said, I don’t have a strong opinion on this. I just think the comment is correct and also gives an additional history of the check for who wants to work on 3 later hence my ACK.
a clear explanation of whether the check is redundant or necessary
The problem is that I don’t know for sure. I think it’s redundant, but I don’t want to mislead some future dev. They should do their own research.
Even without this PR … someone could dig into the code and eventually realize that it’s an incomplete check
The comment is there to prevent someone who doesn’t check from accidentally introducing a consensus bug.
Code review ACK a04f17a1882407db09b0a07338e12877ac1d9e92
Thanks for digging up the history! At least it’s clear how the check get here, though not exactly what it was trying to do.
Combining the two in same PR might be better to not miss writing the follow-up.
Could just open an issue to track it?
4093@@ -4094,6 +4094,8 @@ bool CheckBlock(const CBlock& block, BlockValidationState& state, const Consensu
4094 strprintf("Transaction check failed (tx hash %s) %s", tx->GetHash().ToString(), tx_state.GetDebugMessage()));
4095 }
4096 }
4097+ // This underestimates the number of sigops, because unlike ConnectBlock it
it does not count witness and p2sh sigops
Isn’t it obvious from the name Get*Legacy*SigOpCount
and the parameter GetSigOpCount(bool fAccurate)
called as GetSigOpCount(false)
that new script types aren’t covered and that it’s not meant to be accurate but rather compatible with old behavior?
Instead (or maybe next to) the comments, I’d find it more helpful if we documented what the false
means in GetLegacySigOpCount
:
0unsigned int GetLegacySigOpCount(const CTransaction& tx)
1{
2 unsigned int nSigOps = 0;
3 for (const auto& txin : tx.vin) {
4 nSigOps += txin.scriptSig.GetSigOpCount(/*fAccurate*/false);
5 }
6 for (const auto& txout : tx.vout) {
7 nSigOps += txout.scriptPubKey.GetSigOpCount(/*fAccurate*/false);
8 }
9 return nSigOps;
10}
and maybe to obviate the DecodeOP_N
vs MAX_PUBKEYS_PER_MULTISIG
accuracy difference which isn’t immediately obvious from the code.
Isn’t it obvious from the name
Given our history of bad function names, no :-)
I happened to be working on a PR that’s related to the exact same code path.
I’m not sure whether the call is redundant or not, but I can understand why we want to avoid situations like #9049.
I would prefer a more high-level explanation for the difference between the two calls - explained through code and tests preferably instead of comments - or maybe both.
There are a few minor typos in the commit message as well and it’s a bit hand-wavy - can we find some answers here instead of just repeating the review questions in the commit message?
The purpose of this comment isn’t to be throrough, it’s there to:
I think (2) can be achieved with just a commit message and the fact that our merge script points back to the original PR.
I’m not opposed to writing a longer comment, but I suspect it would take a long time to find a text everyone agrees on. And by the time we do, we might as well change the code itself, which is what I wanted to avoid having to work on.
SigOps
calculation optimization, adding the /*fAccurate=*/
argument to each usage, and renamed GetSigOpCount
to GetLegacySigOpCount
to highlight how this differs from the post-segwit implementation.
I’ve also covered it with an extensive suite of tests and a benchmark, see: https://github.com/bitcoin/bitcoin/pull/32532