-
Notifications
You must be signed in to change notification settings - Fork 658
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(runtime) - Mitigate the receipt size limit bug #12633
Conversation
… bandwidth requests
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #12633 +/- ##
===========================================
+ Coverage 1.66% 70.61% +68.94%
===========================================
Files 669 847 +178
Lines 119942 173400 +53458
Branches 119942 173400 +53458
===========================================
+ Hits 2001 122448 +120447
+ Misses 117838 45825 -72013
- Partials 103 5127 +5024
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
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
/// NewReceipt validation. Tolerates some receipts that wouldn't pass new validation. It has to | ||
/// be less strict because: | ||
/// 1) Older receipts might have been created before new validation rules. | ||
/// 2) There is a bug which allows to create receipts that are above the size limit. Runtime has |
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 thought it's not possible to create them above the size limit but that the receipt may increase in size later?
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.
Here I used "create" in a broader sense, I wanted to say that it's possible for a large receipt to occur.
And when a max size value is returned, the created receipt is larger than max_receipt_size
, so I'd say that counts as "create"
runtime/runtime/src/verifier.rs
Outdated
/// 2) There is a bug which allows to create receipts that are above the size limit. Runtime has | ||
/// to handle them gracefully until the receipt size limit bug is fixed. | ||
/// See https://github.com/near/nearcore/issues/12606 for details. | ||
OldReceipt, |
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 can see that the delayed and incoming receipts are validated but the buffered receipts are not, as far as I can see. Is it an omission?
- Is the validation for old receipts actually necessary or is it just an extra protection? Those receipts are a result of on chain computation so I would assume we trust them here already.
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 think validating old receipts is just a sanity check. When an old receipt fails validation the runtime panics, so we don't expect this validation to ever fail. I think it's fine to not validate buffered receipts.
if size > max_receipt_size { | ||
if size > max_receipt_size { |
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 condition got duplicated
if size > max_receipt_size { | ||
tracing::warn!( | ||
target: "runtime", | ||
"try_forward observed a receipt with size exceeding the size limit! receipt_id: {} size: {} size_limit: {}", |
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: use structured logging - set the values as fields rather than in the formatted message
tracing::warn!(
receipt_id=?..
size,
max_size,
"try_forward observed a receipt with size exceeding the size limit"
);
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 find these structured logs a bit harder to read than manually formatted messages, but I guess that's the proper way to do it. Changed to structured.
|
||
// A function call will generate a new receipt. Size of this receipt will be equal to | ||
// `max_receipt_size`, it'll pass validation, but then `output_data_receivers` will be modified and | ||
// the receipt's size will go above max_receipt_size. The receipt should be rejected, but currently |
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.
Can you assert that this condition is actually triggered?
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.
Added an assert that there really was a receipt with size above max_receipt_size
.
/// Returns a value of size "value_size". | ||
/// Accepts json args, e.g {"value_size": 1000} |
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.
just out of curiosity, why not pass it as simple integer instead of the json?
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.
Json is easy to use and it's readable. I find it easier to think about than manually serializing/deserializing integers. And it's easier to add more arguments to a function.
#[allow(unused)] | ||
extern "C" { |
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.
- Is all of it needed?
- Is there any way we can reuse the existing one or refactor it to avoid copy pasting?
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.
Yeah there's a lot of new functions. I tried to use the existing ones, but I want to do very specific things and the existing ones don't really do what I want to do.
It'd be nice if there was an easy way to create a per-test contract, maybe inside the test code itself. One giant contract gets a bit chaotic :/
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'd be nice if there was an easy way to create a per-test contract, maybe inside the test code itself. One giant contract gets a bit chaotic :/
So there are some programmatically constructed contracts in near-test-contracts
but trust me the experience maintaining those is quite a bit worse as you end up having to hand-roll WebAssembly, pretty much.
If we wanted to make it conversible from Rust I think it would still be possible technically, but has very significant drawbacks. There's this saying about maintainability along the lines of "code is written once, read many times", but this quote gets cuts off before it gets to "and compiled a further thousand times more." Unfortunately compiling and linking up a huge number of Rust binaries would be a huge hit for our development experience.
While I don't necessarily object to having a couple additional contract crates, I think in general we should probably aim to have as few of these as possible and try to achieve a good maintainability/readability within the confines of a crate (by e.g. splitting the code up into modules – this also naturally enables reuse of the duplicate code at hand.)
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.
No blocking comments from me but see inline for my general thoughts.
#[allow(unused)] | ||
extern "C" { |
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'd be nice if there was an easy way to create a per-test contract, maybe inside the test code itself. One giant contract gets a bit chaotic :/
So there are some programmatically constructed contracts in near-test-contracts
but trust me the experience maintaining those is quite a bit worse as you end up having to hand-roll WebAssembly, pretty much.
If we wanted to make it conversible from Rust I think it would still be possible technically, but has very significant drawbacks. There's this saying about maintainability along the lines of "code is written once, read many times", but this quote gets cuts off before it gets to "and compiled a further thousand times more." Unfortunately compiling and linking up a huge number of Rust binaries would be a huge hit for our development experience.
While I don't necessarily object to having a couple additional contract crates, I think in general we should probably aim to have as few of these as possible and try to achieve a good maintainability/readability within the confines of a crate (by e.g. splitting the code up into modules – this also naturally enables reuse of the duplicate code at hand.)
runtime/runtime/src/lib.rs
Outdated
@@ -1933,6 +1936,7 @@ impl Runtime { | |||
&processing_state.apply_state.config.wasm_config.limit_config, | |||
receipt, | |||
protocol_version, | |||
ValidateReceiptMode::OldReceipt, |
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.
Hm, maybe it would be better to name this variant something along the lines of StoredReceipt
or something like that? My intuition is that for delayed receipts "old" is perhaps still applicable, sure, but incoming receipts don't feel like they would qualify for the "old" qualifier.
At the same time I'm trying to wrap around if it is really alright to not validate the incoming receipts for their size? I guess not doing so is the reason why this PR doesn't actually fix the associated issue fully?
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.
Hm, maybe it would be better to name this variant something along the lines of StoredReceipt or something like that? My intuition is that for delayed receipts "old" is perhaps still applicable, sure, but incoming receipts don't feel like they would qualify for the "old" qualifier.
Stored
doesn't really fit for incoming receipts. Maybe "ExistingReceipt"? The distinction is between receipts that were already created and validated and totally new ones
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.
At the same time I'm trying to wrap around if it is really alright to not validate the incoming receipts for their size? I guess not doing so is the reason why this PR doesn't actually fix the associated issue fully?
I think it's fine because all incoming receipts were created by runtime, which we trust fully. We can trust that the runtime doesn't create receipts that are too big, invalid, etc.
In this case it turned out that it's possible to bypass runtime's size check, and because of that the runtime produced invalid receipts. Fixing that would require adding proper max_receipt_size
checks when applying an action receipt. This is a bit complicated, so for now we have a mitigation.
// See https://github.com/near/nearcore/issues/12606 | ||
let max_receipt_size = apply_state.config.wasm_config.limit_config.max_receipt_size; | ||
if size > max_receipt_size { | ||
tracing::warn!( |
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.
How frequently might this happen in production? If this warning is straightforward to trigger, then this probably shouldn't be a warn
: we will inevitably get validators asking if something is wrong with their node and how to fix this warning.
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 don't really expect this to happen with normal traffic. Someone would have to deliberately try to trigger it, or be doing some testing with max size receipts.
But tbh I don't see a lot of value in this warning, even if it got triggered, the only effect is that validators will get upset, we won't really react to it in any way. So maybe it's better to downgrade it to a debug-level log 👍
There is a bug (#12606) which allows to create receipts which are slightly larger than
max_receipt_size
.Let's mitigate the issue by making the runtime able to handle receipts that are above
max_receipt_size
.First the size limit check is relaxed to check only new receipts. Then the receipt forwarding logic is modified to pretend that all receipts have
congestion_size
belowmax_receipt_size
.This mostly matches the code that was added in
2.3.1
and2.4.0
, but this PR contains one more change that is needed for bandwidth scheduler. Before bandwidth scheduler we were able to forward receipts that are slightly above themax_receipt_size
because the big outgoing limit was 4.5 MiB andtry_forward
pretended that all receipts are at most 4 MiB. With bandwidth scheduler this changes, bandwidth scheduler expects all receipts to be at mostmax_receipt_size
, otherwise there's no guarantee that the receipt will ever be forwarded. To deal with this there's the change which pretends that all receipts are at mostmax_receipt_size
when generating bandwidth requests.This is just a mitigation, a proper fix will be done in the future.
One more advantage of this patch is that it should allow us to reduce
max_receipt_size
without crazy protocol upgrade logic. We could reduce it to e.g 2MB and the scheduler would still be able to handle 4MB receipts created in the previous protocol version.I added some of tests which try to create a receipt above max size and check that everything still works fine.
As usual the
test_transaction_limit_for_local_congestion
test broke when I added more methods to thetest-contract-rs
contract. For some reason this test is very sensitive to contract size and the congestion level rapidly drops when size of the contract increases. To deal with this I added a separate small contract that is used exclusively by these congestion control tests, that should fix the issue once and for all.The change is divided into commits for easier review.