-
Notifications
You must be signed in to change notification settings - Fork 36.6k
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
Fix off-by-one error w/ nLockTime in the wallet #6183
Conversation
acf2b95
to
6f6710c
Compare
This changes the semantics of a function that is used in consensus. However the only uses of Though now that we're changing this function anyway, I'd prefer splitting up
|
@laanwj +1 Changing the behaviour of a function without renaming it or otherwise breaking old code using it, is asking for trouble. |
Previously due to an off-by-one error the wallet ignored nLockTime-by-height transactions that would be valid in the next block even though they are accepted into the mempool. The transactions wouldn't show up until confirmed, nor would they be included in the unconfirmed balance. Similar to the mempool behavior fix in 665bdd3, the wallet code was calling IsFinalTx() directly without taking into account the fact that doing so tells you if the transaction could have been mined in the *current* block, rather than the next block. To fix this we strip IsFinalTx() of non-consensus-critical functionality, removing the default arguments, and add CheckFinalTx() to check if a transaction will be final in the next block.
6f6710c
to
28bf062
Compare
@laanwj Split into two functions. In doing so I also had to add part #6177 Any thoughts on what should happen at bitcoin/src/test/miner_tests.cpp Line 251 in 28bf062
|
Any reason not to just rebase on top of #6063 ?
I believe testing the new method would be good. This is just another example of how globals make testing more difficult. |
BOOST_CHECK(IsFinalTx(tx2)); | ||
// FIXME: we should *actually* create a new block so the following test | ||
// works; CheckFinalTx() isn't fooled by monkey-patching nHeight. | ||
//BOOST_CHECK(CheckFinalTx(tx)); |
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.
Could just use IsFinalTx
here with manual arguments instead of CheckFinalTx
for these tests?
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.
You could, but it'd create more code to update if the logic of CheckFinalTx() changes for some reason. Remember that this is really a "test-the-test" line of code, not what the test is actually trying to test.
Looks good to me now. @jtimon The reason for renaming the function is outlined in my last post, as well as @luke-jr's reply - as this pull changes the functionality of the function, so to be sure that all caller sites are 'aware' of this it makes sense to rename it. |
if (tx.nLockTime == 0) | ||
return true; | ||
if (nBlockHeight == 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.
Are you sure none of the current callers of IsFinalTx pass nBlockHeight or nBlockTime = 0 ?
Seriously, it seems much safer to keep this function as it is and call the "new"/consensus-friendly one from that one.
If you're breaking the old one, why keep to of them at all?
The only reason I maintain the old one in #6063 is to make sure it keeps being functionally identical. If you can't warranty that, I would just use the consensus-friendly version everywhere.
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 am quite sure of that, particularly since the callers that would do that are all consensus-critical code, and this could lead to a consensus split. Edit: maybe that comes off the wrong way... I mean, because it could lead to a consensus split, I spent a good hour or so examining every call of IsFinalTx() to be sure it'd never be called that way.
Fortunately the only block for which nBlockHeight=0 or nBlockTime=0 is legal is the genesis block so we don't actually have that bug in practice. Additionally if nBlockHeight/nBlockTime are set to zero the function still behaves as you'd expect it to in that circumstance.
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 am quite sure of that, particularly since the callers that would do that are all consensus-critical code, and this could lead to a consensus split. Edit: maybe that comes off the wrong way... I mean, because it could lead to a consensus split, I spent a good hour or so examining every call of IsFinalTx() to be sure it'd never be called that way.
Then why maintain a global-state-dependent function at all?
If we're going to review all the calls and change all those lines, let's just have the stateless version of it.
And at that point, you're only changing one function, shouldn't the criterion "if you change functionality, change the name of the function" precisely apply here?
Fortunately the only block for which nBlockHeight=0 or nBlockTime=0 is legal is the genesis block so we don't actually have that bug in practice.
Well, the genesis block shouldn't be tested since it's correct by definition (is really the first consensus rule), so that's fine.
Additionally if nBlockHeight/nBlockTime are set to zero the function still behaves as you'd expect it to in that circumstance.
No, before the null values were replaced with values from the global state, now they don't.
They clearly function differently (even if you're taking care of it not mattering by adapting things on the caller, the function itself is NOT functionally equivalent to the previous stateful one).
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.
Then why maintain a global-state-dependent function at all?
Huh? The global state-dependent function is for the convenience of callers, who just need to know if the tx will be final by the time the next block comes.
If we're going to review all the calls and change all those lines, let's just have the stateless version of it.
Having only the stateless IsFinalTx() as an option results in most of those calls having useless - and probably soon to be obsolete - Tip()->nHeight, GetAdjustedTime() boilerplate.
And at that point, you're only changing one function, shouldn't the criterion "if you change functionality, change the name of the function" precisely apply here?
The functionality of mixing up a consensus-critical and non-consensus-critical function in one is simply broken.
No, before the null values were replaced with values from the global state, now they don't.
They clearly function differently (even if you're taking care of it not mattering by adapting things on the caller, the function itself is NOT functionally equivalent to the previous stateful one).
Again, the previous design of IsFinalTx() is simply broken and shouldn't have happened; from the point of view of the important stuff - the consensus-critical code - the functionality hasn't changed changed. Fortunately we never had a situation where that broken design bit us in the ass so in spite of it we didn't end up with a forking bug; if the functionality had changed from the point of view of consensus-critical code it'd be a guaranteed forking bug because it's comparing nLockTime to state that is not the same on every node.
@laanwj Well, a caller like IsFinalTx(tx, 0, 0) won't work as expected. |
Personally I prefer the terminology "IsFinalTx()" for the "no-context" consensus-critical check, and "CheckFinalTx()" for the context-dependent non-consensus-critical check. |
As said, Check is being used by other consensus functions (stateless) and IsFinalTx was global-state dependent. For consistency, it would be better to have the consensus version start with Check. But since this may be considered bikesheding, let me explain what the options I see. Let's imagine this in smaller commits that have been squashed and consider stopping at each point.
Let's please stop before 6. Let's forget about naming and making me and possibly other people work more. I propose to use a cleaner git history as the main criterion. My approach is better because it will produce a smaller total diff (since some calls that were using the default values can remain untouched). |
Something you're probably not aware of re: CheckFinalTx() is that I'll be submitting another pull-req once this is merged to change CheckFinalTx() to use GetMedianTime() rather than GetAdjustedTime(). This is important for a number of security related reasons. (most discussed before on #bitcoin-wizards) After that I'll be proposing a soft-fork enforcing this within blocks themselves. My thinking re: not renaming IsFinalTx() is taking that into account, because IsFinalTx() in that case would be relegated to purely consensus-critical function deep in the bowels of validation, with CheckFinalTx() being what you'd expect people to normally be using to answer the question "Is the transaction (effectively) final right now?" (possibly IsFinalTx() might even get renamed to OldIsFinalTx() to further hammer home that point) Now, at some point in the implementation of that BIP I wouldn't be surprised if it makes sense to make it possible to provide a "chain tip" parameter to CheckFinalTx() to tell it what chain you're checking with respect too, and thus what's the median time, but for now the consensus-critical stuff can stay the way it is. Importantly, by changing everything else we get it all onto a new function with guaranteed semantics of "just figure out if this transaction is final according to whatever rules exist" - exactly what you'd expect from a Check*() function! Equally, when that chainTip parameter does get added, we'll be sure to catch everything that needs to be updated in one fell swoop. (if it's not done as a default parameter) Anyway, smaller total diffs aren't necessarily always a good thing. In this case, by changing the function name we help clue people in to think "Why did IsFinalTx() change names?" "What's different about the design now?", hopefully setting the stage to discover things like a future median time rule change. |
I would also expect that function to be stateless!
Then why keep the name IsFinalTx at all when the function is changing from global-state-dependent to stateless?
Ok, I didn't know this. I have to say that passing a CBlockIndex* from the beginning instead of using the global tip would make the function more stateless and my concerns with it being named CheckFinalTx would be lower, specially if it's going to be fully statelss later by not calling GetAdjustedTime(). So, to reiterate and in summary: a function named CheckFinalTx should be as stateless as possible from the beginning: using the global activeTip inside it, even temporarily, seems horrendous to me. |
Note that for the last option, CheckFinalTx could optionally take the time (which callers that use it would set as GetAdjustedTime) which is set to pindexTip->GetMedianTime() inside if it's not provided (or 0 is passed). |
Again, from the point of view of the code that mattered, IsFinalTx() was a stateless function... That it potentially wasn't was an unfortunate bug, not a feature. Equally, we can't get rid of it after a soft-fork and replace it with some CheckFinalTx() or whatnot that takes block indexes, as the old behavior is still needed to verify old blocks. Re: adding CBlockIndex's and the like right now, that needs some discussion and thought, so it's not going to happen in this pull-req. |
At least moving up the use of the global chainActive shouldn't generate much discussion.
As said I would be happy with both:
So even assuming we want to have 2 separate functions for now, there's at least 2 options for getting a stateless (consensus-friendly) one named CheckFinalTx in this PR (so I would be happy to close #6063 and rebase #6051 on top of this). |
If I did that, we'd have to re-touch a whole bunch of code all over again later... Notably, that's the kind of design mistake that lead to the bug this pull-req is fixing (re)appearing in the first place. Anyway, continuing this discussion is a waste of time. @laanwj do whatever you want, just make sure this bug actually gets fixed. |
Fair enough, don't do that then. |
28bf062 Fix off-by-one error w/ nLockTime in the wallet (Peter Todd)
ACK. Merging this as-is. Can discuss naming later but I think the naming is fine. |
Previously due to an off-by-one error the wallet ignored nLockTime-by-height transactions that would be valid in the next block even though they are accepted into the mempool. The transactions wouldn't show up until confirmed, nor would they be included in the unconfirmed balance. Similar to the mempool behavior fix in 665bdd3, the wallet code was calling IsFinalTx() directly without taking into account the fact that doing so tells you if the transaction could have been mined in the *current* block, rather than the next block. To fix this we strip IsFinalTx() of non-consensus-critical functionality, removing the default arguments, and add CheckFinalTx() to check if a transaction will be final in the next block. Github-Pull: #6183 Rebased-From: 28bf062
Previously due to an off-by-one error the wallet ignored nLockTime-by-height transactions that would be valid in the next block even though they are accepted into the mempool. The transactions wouldn't show up until confirmed, nor would they be included in the unconfirmed balance. Similar to the mempool behavior fix in 665bdd3, the wallet code was calling IsFinalTx() directly without taking into account the fact that doing so tells you if the transaction could have been mined in the current block, rather than the next block.
Instead of changing the wallet code, this commit simply changes the semantics of IsFinalTx() when the block height isn't provided to use the height of the next block. The resulting semantics return true if the transaction in question could included in the next block, significantly less confusing and more useful than the previous semantics.