Join GitHub today
GitHub is home to over 20 million developers working together to host and review code, manage projects, and build software together.
Near-Bugfix: Optimization: Minimize the number of times it is checked that no money... #8498
Conversation
dcousens
and 1 other
commented on an outdated diff
Aug 11, 2016
| { | ||
| - // This doesn't trigger the DoS code on purpose; if it did, it would make it easier | ||
| - // for an attacker to attempt to split the network. | ||
| - if (!inputs.HaveInputs(tx)) | ||
| - return state.Invalid(false, 0, "", "Inputs unavailable"); | ||
| + // are the actual inputs available? | ||
| + if (!inputs.HaveInputs(tx)) | ||
| + return state.DoS(100, false, REJECT_INVALID, "bad-txns-inputs-missingorspent", false, | ||
| + strprintf("%s: inputs missing/spent", __func__)); |
jtimon
Member
|
laanwj
added the
Consensus
label
Aug 11, 2016
|
Needed rebase. Besides, the previous version contained a bug. |
jtimon
referenced
this pull request
Sep 1, 2016
Closed
[WIP] "Lockfree" Checkqueue Implementation #8464
|
Needed rebase after renaming main.o, see #9260 (although needed, the rebase was clean in this case) |
| @@ -646,18 +646,20 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState& state, const C | ||
| } | ||
| } | ||
| - // are the actual inputs available? | ||
| + // This redundant check doesn't trigger the DoS code on purpose; if it did, it would make it easier | ||
| + // for an attacker to attempt to split the network (Consensus::CheckTxInputs also checks this). |
sipa
Apr 9, 2017
Owner
I think you can change the view.HaveInputs(tx) below into a Consensus::CheckTxInputs call, doing some of the checks slightly earlier.
jtimon
Apr 18, 2017
•
Member
Perhaps the new comment could be clearer, but the view.HaveInputs(tx) check below sets state.DoS(0, ...) instead of state.DoS(100, ...) like Consensus::CheckTxInputs would do.
Feel more than free to rephrase it in a way that would have been more clear to you.
| @@ -1318,16 +1318,14 @@ int GetSpendHeight(const CCoinsViewCache& inputs) | ||
| return pindexPrev->nHeight + 1; | ||
| } | ||
| -namespace Consensus { | ||
| -bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight) | ||
| +bool Consensus::CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& nFees) |
dcousens
Apr 9, 2017
•
Contributor
Rather than forcing a bogus nFees (or unused) everywhere, just wrap this function with an overload containing an unused nFees stack variable.
Simpler. Less diff.
Also means anyone reviewing at some later date won't get confused as to whether nFees is actually used or not.
jtimon
Apr 18, 2017
Member
The only place where this makes sense is in txmempool.cpp. There you can do a much nicer wrapper there. The diff won't be smaller in total but there will be more lines in red (which people tend to like). I considered this before and I rembember @sipa said it would be fine to just remove the check in txmempool.cpp but I can't find the comment and he changed his mind about it. I will rewrite this for you to see and I'm happy to squash it.
|
Added a commit that may make @dcousens happier or not, thanks for making me remember. EDIT: I would be also be happy to take the function out of the consensus namespace "for free" here. That was a mistake on my part. |
ryanofsky
reviewed
May 8, 2017
utACK 1590c19
PR was initially confusing to me, but here are my notes on what it does:
- Changes
CheckTxInputsto return DoS level100and codeREJECT_INVALIDinstead of0and0. - Adds
CheckTxInputsnFeesin/out argument - Removes
CheckTxInputscall fromCheckInputs. Avoids changing behavior by having the two callers ofCheckInputs(AcceptToMemoryPoolWorkerandConnectBlock) callCheckTxInputsthemselves. - Changes
AcceptToMemoryPoolWorkerandConnectBlockto use fees returned byCheckTxInputsinstead of computing fees themselves. - Updates calls to
CheckTxInputsinCTxMemPool::check. No impact there because mempool code ignores returned validation state and fees.
| @@ -624,7 +624,7 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState& state, const C | ||
| CCoinsView dummy; | ||
| CCoinsViewCache view(&dummy); | ||
| - CAmount nValueIn = 0; | ||
| + CAmount nFees = 0; |
ryanofsky
May 8, 2017
Contributor
In commit "Optimization: Minimize the number of times it is checked"
Would be good to declare nFees below close to where it is actually used.
| @@ -1820,6 +1814,8 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd | ||
| blockundo.vtxundo.reserve(block.vtx.size() - 1); | ||
| std::vector<PrecomputedTransactionData> txdata; | ||
| txdata.reserve(block.vtx.size()); // Required so that pointers to individual PrecomputedTransactionData don't get invalidated | ||
| + const int nHeight = pindex->pprev == NULL ? 0 : pindex->pprev->nHeight + 1; | ||
| + const int64_t nSpendHeight = block.vtx.size() > 1 ? GetSpendHeight(view) : nHeight; |
ryanofsky
May 8, 2017
Contributor
In commit "Optimization: Minimize the number of times it is checked"
Can you add a comment explaining this line? Why do transactions in a block with vtx.size() of >= 2 have a different spend height than transactions in a block with vtx.size() == 1? Is nSpendHeight even used in a block with vtx.size() == 1?
jtimon
May 18, 2017
Member
If the block only has the coinbase tx before GetSpendHeight(view) would have not been called at all and starting doing so seems unnecessarily costly, see https://github.com/bitcoin/bitcoin/blob/master/src/validation.cpp#L1306
I was actually hoping that someone suggested to just remove this local variable directly and just use nHeight directly for the coinbase maturity check, assuming that is safe. I think so but would like others to confirm before trying it.
|
Needed rebase, hopefully fixed all nits. |
|
Tests for this PR seem to be failing currently, maybe due to the SpendHeight change? Only changes since my previous review were rebasing post-#8329 and the nFees and nSpendHeight changes commented on above. |
|
Yes, the problem was in that change but not on stop using GetSpendHeight, just added +1 to the height by mistake. Updated. |
|
Needed rebase after #10195, it also needs more review now (AcceptToMemoryPoolWorker may return different DoS pnctuation [100 instead of 0] now, happy to fix if that's a problem). |
|
Some discussion of this in IRC https://botbot.me/freenode/bitcoin-core-dev/msg/87002756/ |
ryanofsky
reviewed
Jun 14, 2017
utACK 4d9e9a5. Only changes since last review were removing a stray comment and fixing the spendheight failures.
| - if (!view.HaveInputs(tx)) | ||
| - return state.DoS(100, error("ConnectBlock(): inputs missing/spent"), | ||
| - REJECT_INVALID, "bad-txns-inputs-missingorspent"); | ||
| + if (!Consensus::CheckTxInputs(tx, state, view, pindex->nHeight, nFees)) |
ryanofsky
Jun 14, 2017
Contributor
In commit "Optimization: Minimize the number of times"
If I understand correctly, it would be more correct to pass SpendHeight(view) instead of pindex->nHeight as the nSpendHeight argument, but in this case it doesn't matter because the value is only used for coinbase transactions, and this isn't a coinbase transaction? Or maybe there are other reasons why pindex->nHeight is ok to pass?
Either way, I think this is confusing, and that there should be a comment here explaining the pindex->nHeight value. Or maybe you could delete the nSpendHeight argument and have CheckTxInputs call GetSpendHeight(inputs) when needed. This seems like it would be a simplification and I don't think there should be a performance cost because CheckTxInputs seems to be skipped for coinbase transactions in all cases except for one call that invokes GetSpendHeight anyway.
jtimon
Jun 17, 2017
Member
I don't think it would be more correct (but I'm glad to be corrected) and performance would sufffer.
I think pindex->nHeight because that's actually what we want to validate here. In acceptToMemPool and CTxMemPool::check you don't know the height. Here, nSpendHeight should always be lower or equal to pindex->nHeight.
I'm happy to add a comment but not sure what it should be. Peharps the comment should be in GetSpendHeight instead of ConnectBlock().
ryanofsky
Jun 21, 2017
Contributor
I'm happy to add a comment but not sure what it should be.
Comment could just say why it is better to pass pindex->nHeight as nSpendHeight here instead of GetSpendHeight(view). If the two values are always equal, or have some other relation, comment could just say what that relationship is. If this is too in the weeds, definitely feel free to skip this. I'm just suggesting what I think would make the code clearer for me.
Alternately, I don't know what you think of my suggestion to eliminate the nSpendHeight argument and just call GetSpendHeight in CheckTxInputs. It does look to me like this would be logically equivalent and not effect performance, though maybe there's another reason not to do it.
jtimon
Jun 22, 2017
Member
Part of the optimization here is not calling GetSpendHeight for every tx in the block, and your suggestion would eliminate that part of the optimization.
For the comment, what about "We know the height, so we don't need to GetSpendHeight"?
it if more people agree.
I'm really not sure that comment would add that much value, but I don't mind adding
sdaftuar
Jun 23, 2017
Contributor
I agree with @jtimon that it makes more sense for ConnectBlock to pass in pindex->nHeight, rather than get the spend height from the view, even though those are the same. We could add an assert that the view's best block has the same hash as pindex->pprev if that makes the code clearer -- but we have bigger problems than just coinbase maturity if the view is out of sync with our chain!
ryanofsky
reviewed
Jun 21, 2017
Not sure if this needs more review. This PR has two utACKs from dcousens and me, and a concept ack from sipa.
| - if (!view.HaveInputs(tx)) | ||
| - return state.DoS(100, error("ConnectBlock(): inputs missing/spent"), | ||
| - REJECT_INVALID, "bad-txns-inputs-missingorspent"); | ||
| + if (!Consensus::CheckTxInputs(tx, state, view, pindex->nHeight, nFees)) |
ryanofsky
Jun 21, 2017
Contributor
I'm happy to add a comment but not sure what it should be.
Comment could just say why it is better to pass pindex->nHeight as nSpendHeight here instead of GetSpendHeight(view). If the two values are always equal, or have some other relation, comment could just say what that relationship is. If this is too in the weeds, definitely feel free to skip this. I'm just suggesting what I think would make the code clearer for me.
Alternately, I don't know what you think of my suggestion to eliminate the nSpendHeight argument and just call GetSpendHeight in CheckTxInputs. It does look to me like this would be logically equivalent and not effect performance, though maybe there's another reason not to do it.
sdaftuar
suggested changes
Jun 23, 2017
Concept ACK. I think this will slightly speed up validation time; I'm doing a benchmark to see if this is observable. Left one request and some style nits below.
| @@ -24,7 +26,7 @@ namespace Consensus { | ||
| * This does not modify the UTXO set. This does not check scripts and sigs. | ||
| * Preconditions: tx.IsCoinBase() is false. | ||
| */ | ||
| -bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight); | ||
| +bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& nFees); |
sdaftuar
Jun 23, 2017
Contributor
I think it's somewhat confusing that CheckTxInputs() will be doing calculations on accumulated fee values, and not just the particular transaction passed in.
Can we just have CheckTxInputs return the fee of the transaction passed in, and then the caller can decide what additional checks should happen? It would make more sense to me that the MoneyRange() check on total block fees happen in ConnectBlock(), rather than here (though I do think conceptually that is a reasonable check for us to add).
Alternatively, if we decide to go with the accumulated fees being checked here, I'd strongly prefer to rename this variable to something like "accumulated_fees" to make it clear what this is, and update the documentation to explain it. But I'd prefer to move the accumulation to the caller, and leave this function as operating on a single transaction.
jtimon
Jun 25, 2017
Member
If the fee is returned, then we can't also return a bool, and we would need to duplicate the check in ConnectBlock, AcceptToMemeoryPool and CTxMemPool::check. And at that point the additional complexity wouldn't be worth it IMO.
We could also pass a fees input/output parameter that doesn't accumulate (although you can already enjoy that functionality by simply passing 0 to the variable). In that case, the duplication would only need to duplicateMoneyrange check in ConnectBlock, but I don't really see it as an improvement over this.
Agreed, accumulated_fees would be more readable with only 2 more lines needing to change
sipa
Jun 27, 2017
•
Owner
@jtimon You can 'return' the fee in a tuple, or as a pass-by-reference value. So for example CheckTxInputs would have CAmount& return_fee field, and return_fee would just be assigned that transaction's fee. The caller can then do the accumulated_fee += return_fee logic. This involves no duplication, and still makes CheckTxInputs purely operate on data for a single transaction.
jtimon
Jun 27, 2017
Member
If the concern is that the addition with other fees is not done within the function, as documented the caller can set the in/out argument to zero before calling. I could also always return only the value for the single transaction without accumulating anything (although I don't see the gain, it's just more code), at that point the argument would be out only, not in/out.
From there, to return the output argument as a tuple or pair I think it's just making things uglier and unnecessarily complicated.
instead of:
CAmount tx_fee = 0; // You will initialize the variable even if the function starts setting it to zero
if (!CheckTxInputs(..., tx_fee))
return false;
accumulated_fee += tx_fee;
You would have something like:
struct CheckTxInputsReturn
{
bool fValid;
CAmount tx_fee;
}
CheckTxInputsReturn ret = CheckTxInputs(...);
if (!ret.fValid)
return false;
accumulated_fee += ret.tx_fee;
I think that's incredibly ugly. Specially since I don't see the problem with how it is currently used when you don't want to accumulate any fees:
CAmount tx_fee = 0;
if (!CheckTxInputs(..., tx_fee))
return false;
Please, I invite you to write your suggestions on top of this and see for yourselves that they are not an improvement but actually the contrary.
If people agree that those IMO much uglier solutions are better, I will squash your suggestions, no problem.
sipa
Jun 27, 2017
Owner
I was just clarifying that @sdaftuar's suggestion does not imply duplication of logic. I personally think that it's a bit cleaner to do the summing of fees outside of the validation of a single function, but it's an unimportant nit for sure.
jtimon
Jun 27, 2017
Member
Oh, sorry, the duplication comment was a mistake. Before I was checking the moneyRange for the accumulated fees and not just for the single tx fees, and I thought that check would need to be duplicated outside, but that was a change in functionality that I shouldn't be doing (and btw was missed in review).
Pedantically adding that check would be a softfork, but it should be impossible that the MoneyRange check on the accumulated fees fails anyway.
I can not accumulate anything if you guys prefer that, that's a simple change (even though I still can't understand why would you prefer that), reading your previous comment again better, you were also considering the pass by reference (what is done now), not necessarily the tuple (which is what I would be against in principle because of its ugliness).
| + } // end LOCK(pool.cs) | ||
| + | ||
| + CAmount nFees = 0; | ||
| + if (!Consensus::CheckTxInputs(tx, state, view, GetSpendHeight(view), nFees)) |
| - if (!view.HaveInputs(tx)) | ||
| - return state.DoS(100, error("ConnectBlock(): inputs missing/spent"), | ||
| - REJECT_INVALID, "bad-txns-inputs-missingorspent"); | ||
| + if (!Consensus::CheckTxInputs(tx, state, view, pindex->nHeight, nFees)) |
sdaftuar
Jun 23, 2017
Contributor
I agree with @jtimon that it makes more sense for ConnectBlock to pass in pindex->nHeight, rather than get the spend height from the view, even though those are the same. We could add an assert that the view's best block has the same hash as pindex->pprev if that makes the code clearer -- but we have bigger problems than just coinbase maturity if the view is out of sync with our chain!
| - if (!view.HaveInputs(tx)) | ||
| - return state.DoS(100, error("ConnectBlock(): inputs missing/spent"), | ||
| - REJECT_INVALID, "bad-txns-inputs-missingorspent"); | ||
| + if (!Consensus::CheckTxInputs(tx, state, view, pindex->nHeight, nFees)) |
| - if (!inputs.HaveInputs(tx)) | ||
| - return state.Invalid(false, 0, "", "Inputs unavailable"); | ||
| + // are the actual inputs available? | ||
| + if (!inputs.HaveInputs(tx)) |
|
Hopefully fixed @sdaftuar 's nits. |
TheBlueMatt
reviewed
Jun 27, 2017
•
| @@ -22,9 +24,12 @@ namespace Consensus { | ||
| /** | ||
| * Check whether all inputs of this transaction are valid (no double spends and amounts) | ||
| * This does not modify the UTXO set. This does not check scripts and sigs. | ||
| + * @param[in,out] accumulated_fees this serves to get the fees of the tx as output. |
| * Preconditions: tx.IsCoinBase() is false. | ||
| */ | ||
| -bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight); | ||
| +bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& accumulated_fees); |
| + if (!Consensus::CheckTxInputs(tx, state, view, pindex->nHeight, tx_fees)) { | ||
| + return error("%s: Consensus::CheckTxInputs: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); | ||
| + } | ||
| + nFees += tx_fees; |
TheBlueMatt
Jun 27, 2017
Contributor
While you're at it, can you please re-add the MoneyRange checks here that Gavin removed years ago (and maybe update the PR title to indicate that you're fixing Gavin's near-bug while also decreasing the number of CCoinsView map lookups).
TheBlueMatt
Jun 27, 2017
Contributor
Specifically, commit 8d7849b broke the check at 8d7849b#diff-7ec3c68a81efff79b6ca22ac1f1eabbaR1110 and made it useless, which very narrowly didn't cause a major consensus failure and re-introduction of the original bitcoin-printing overflow bug.
|
Squashed the non-accumulator commit. |
jtimon
changed the title from
Optimization: Minimize the number of times it is checked that no money... to Near-Bugfix: Optimization: Minimize the number of times it is checked that no money...
Jul 4, 2017
| + nFees += tx_fees; | ||
| + if (!MoneyRange(nFees)) { | ||
| + state.DoS(100, false, REJECT_INVALID, "bad-txns-accumulated-fee-outofrange"); | ||
| + return error("%s: %s, %s", __func__, tx.GetHash().ToString(), FormatStateMessage(state)); |
TheBlueMatt
Jul 11, 2017
Contributor
Why not use the normal formatting here of return state.DoS(100, error(), ...)?
|
Fixed latest nit. |
jtimon commentedAug 11, 2016
•
edited
...is created by individual transactions to 2 places (but call only once in each):
fees per tx one extra time )
Also call tx.GetValueOut() only once per call of CheckTxInputs (instead of 2)
For more motivation:
https://github.com/bitcoin/bitcoin/blob/master/src/main.cpp#L1493
jtimon/bitcoin@0.13-consensus-inputs...jtimon:0.13-consensus-inputs-comments
EDIT: partially replaces #6445
Near-Bugfix as pointed out in #8498 (comment)