-
Notifications
You must be signed in to change notification settings - Fork 651
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(congestion) - Correctly handle receipt gas and size calculation in protocol upgrades #12031
Conversation
@jakmeier and @jancionear Can you have a look? |
Thanks for preparing this. I think the approach can work out but I have a few thougts on it.
Indeed it's quite hacky, the protocol specification becomes a real nightmare as we add layers of serialization hacks upon each ohter.
Once upon a time, the creators of Near decided they need to invent Borsh in order to have a serialization format that is bijective. At least that is my understanding. I think your implementation breaks that. I never found a written down explanation for why exactly a bijective de-serialization is needed. My guess is it has to do with consensus, where a malicious actor could provide 2 different state roots to peers, where both represent the same data but serialized differently. Proofing which of the two is wrong would be impossible without fully recomputing it, since they contain the exact same data when deserisalized. I don't know for sure if we still care about this property, you guys know much better than I how consensus under stateless validation works. I just wanted to bring it to your attention. I imagine you could change the implementation to become bijective again with a bit of extra effort. That's my thoughts so far on your implementation. I hope it helps. |
@jakmeier Thanks!
Yeah it's true that the same byte array may be deserialized differently but in the implementation we always deserialize as Still it is very hacky.
I'm not quite sure how to do that. The constraint is that the new implementation needs to support the old data format. I guess we could attempt something along the lines of "try parse as Receipt, otherwise parse as |
Yeah, but it's still possible to construct two different state roots that both work perfectly fine as the previous state root, if you are trying to do something malicious that requires it. As you are describing it, it seems we only need it to be a surjective serialization, not bijective. But borsh.io specifically says it's bijecive, so I always thought that is relevant. Before SV, I believe challanges were a major concern, that would require that a malicious state root can be proven to be invalid without downloading all the state and doing the full computation for a state transition. This assumption would be broken if you can have two state roots that contain the exact same state, thus it needs to be bijective. Honestly, I'm not sure how much of problem that is with SV. As I understand it, chunk producers, as well as stateless validators, always start with a state root that is verified by a block hash, which itself has been verified using headersync from genesis. Therefore, there is no way to trick them into using the wrong state root, even if it would work to produce the same outcome. A bijection seems good enough. The problem would be if you had some code that just checks that the transition from state root A to B is correct, without verifiying that the previous state root is correct. I think there were discussions about a design like that, where transitions are verified and signed independent of where exactly they are in the block chain. But it hasn't been implemented like this, right?
Sorry I didn't mean it as, just try a bit harder :P What I had in mind was some way to always know which version to expect when deserializing. That requires extra house-keeping until the protocol upgrade has happened. Like, when the protocol upgrade happens, take a note how many receipts there are in each queue and use the old deserialization for those. This would be quite a bit of extra code but it wouldn't be too complicated and it could be removed again once the upgrade is over. |
Ah I think I understand what you mean. We can have the same Receipt placed in state as either However in both cases only one of the possible representations is a result of a correct state transition. What structure should be used is determined by the protocol version. A malicious actor can't just pick and choose which one to use. Worth mentioning that this isn't implemented yet in this PR, I have TODOs in place for that where That being said I'm no longer that confident that it's ok to break the bijectivity. @bowenwang1996 Can you share your wisdom on this PR? Is this hackery fine?
I like that idea although I'm still leaning towards this PR since it is almost ready :) |
The crucial difference is that ReceiptV0 is always serialized back as ReceiptV0. Therefore, serialization is still bijective for receipts. But as I understood your explanation that I quoted on my first comment, you want to decide the version used on storing a
Or maybe I misunderstood, as the protocol version specific behaviour is not implemented, yet. As things are implemented right now, you preserve the version on serialization, which means serialization is still bijective. Thinking about it again, I don't see why you even need to store as |
I added the protocol feature for this change and the handling for protocol upgrade. abbr:
What I proposed is the following:
Other way that I think about it is that in State it looks like either |
Thanks for writing it down so clearly and also for implementing the protocol specific behaviour, as well as the tests that show the behaviour exactly. I guess you could say This makes sense. Sorry for the derailed discussion, your first response should have cleared this up but I didn't understand it. Still, I'm not a fan of the code complexity. The manual tagging, even if well-justified, is unintuitive in my opinion. Sadly, I couldn't find a better alternative... :( As I see it, if we want to store the metadata alongside the receipts, I think we are stuck with this solution. Right now, the code doesn't worry me, only the bit rotting makes me scratch my head. (For a worse alternative, if you are curious, I thought about putting the metadata at the end of the struct. Then you check the length to figure out if you should deserialize as (Receipt,()) or (Receipt, metadata). At first glance, it seemed more intuitive than 3 magic bytes intertwined. But to be honest, it's even worse to maintain. Especially if new receipt versions are added with different lengths, suddenly it might be impossible to to distinguish all cases.) All of this makes me super glad that you thought of making All said, I have nothing against this PR to block it but only reviewed it on a high level. If you want a detailed review for the +1 from me, ping me when it's ready and I'll take another look. |
@jakmeier Thanks! Yeah I'm not too happy about it either but I think it's the best we can do given circumstances. I'm just glad it's localized to the serialization and doesn't leak into apply. We discussed it today in the chain sync meeting and agreed to proceed with this solution. @jancionear Can you do a full review now please? The tests seem to be failing because I forgot to regen some .snap files, I'll fix it tomorrow. |
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.
Looks nice 👍
Left some comments, I think we could include it in protocol 72.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #12031 +/- ##
==========================================
+ Coverage 71.55% 71.57% +0.01%
==========================================
Files 821 821
Lines 165172 165425 +253
Branches 165172 165425 +253
==========================================
+ Hits 118186 118400 +214
- Misses 41843 41870 +27
- Partials 5143 5155 +12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
core/parameters/src/snapshots/near_parameters__config_store__tests__146.json.snap
Outdated
Show resolved
Hide resolved
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.
Sorry for the late review, I had to go debug issues with 2.2.0 and it consumed a lot of my time.
1c56359
to
8742fbc
Compare
Remove lifetimes from types generated by ProtocolSchema tooling. Needed for #12031.
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.
lgtm 👍, left some nits
For now just a request for comments. There are still a few missing pieces like adding tests and gating this feature by a protocol feature. Please let me know if this approach makes sense on a high level.
This PR addresses the issue described in #11923. Please have a look at that first for context.
This is a hacky implementation of option 1:
"Store the receipts together with the gas and size when added and use that when removing the receipt from buffer."
The basic idea is that instead of storing
Receipt
directly in state, it can be wrapped in aStateStoredReceipt
together with the metadata that is needed to correctly handle protocol upgrades.In order to migrate from the old way of storing receipts I'm using a rather hacky solution. I implemented a custom serialization and deserialization for the
StateStoredReceipt
. Using that customization it's possible to discriminate between serialized Receipt and serialized StateStoredReceipt. I added a helper structReceiptOrStateStoredReceipt
and I made it so that both Receipt and StateStoredReceipt can be deserialized directly to it.How to differentiate between
Receipt
andStateStoredReceipt
?The
StateStoredReceipt
can be told apart from Receipt::V0 by the second byte.The
StateStoredReceipt
can be told apart from Receipt::Vn by the first byte.Receipt::V0 and Receipt::V1 can be told apart by the second byte as described in
nearcore/core/primitives/src/receipt.rs
Lines 105 to 118 in 0e5e7c7
How will the migration from
Receipt
toStateStoredReceipt
happen?Receipt
StateStoredReceipt
ReceiptOrStateStoredReceipt
. This covers the fun case where receipts are stored in the old version and read in the new version.