Skip to content
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

solana-program: use VoteState::deserialize_into on non-bpf #35036

Closed
wants to merge 3 commits into from

Conversation

2501babe
Copy link
Member

@2501babe 2501babe commented Feb 1, 2024

Problem

the custom VoteState parser can be much faster. also it now makes sense to expose this to non-bpf contexts so validators can benefit from faster vote parsing. however we still want to preserve V0_23_5 support for maximum safety

Summary of Changes

previously, the logic went something like "if on bpf, use the new parser, otherwise use bincode," and the new parser did not support V0_23_5 for the sake of code simplicity, as they should in theory never be encountered. now the logic goes "always use the new parser. inside the new parser, if V0_23_5 and on bpf, error. if V0_23_5 and not on bpf, fall back to bincode"

also i made the parser appreciably faster. it is now 7x faster than bincode on average, can be up to 20x faster in the worst case, and is never slower

HANA custom vs bincode
* we win: 10000, they win: 0
* our max 22.422µs, their max 471.89µs
* our avg 5.76µs, their avg 35.956µs
* avg speedup 30.195µs

we also introduce a new function VoteState::deserialize_with_bincode() which is identical to the original VoteState::deserialize(). this is a temporary measure while we work on a (potentially feature gated) upgrade to use the new parser in the core runtime

Comment on lines -402 to -407
let minimum_size =
serialized_size(vote_state).map_err(|_| InstructionError::InvalidAccountData)?;
if (input.len() as u64) < minimum_size {
return Err(InstructionError::InvalidAccountData);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check is unnecessary, the cursor position check on line 421 covers all cases of too-short buffers. in fact all it ends up doing is imposing a compute cost on correct inputs while saving compute for malformed inputs. presuming an account owner check precedes deserialization, we should never encounter them

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming this is correct -- any one of the reads will fail properly if the buffer isn't big enough

@2501babe 2501babe force-pushed the 20240201_bettervsde branch 2 times, most recently from 5d65d6d to de728f7 Compare February 1, 2024 11:59
Copy link

codecov bot commented Feb 1, 2024

Codecov Report

Attention: 2 lines in your changes are missing coverage. Please review.

Comparison is base (0569304) 81.6% compared to head (7e1464a) 81.6%.
Report is 4 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #35036   +/-   ##
=======================================
  Coverage    81.6%    81.6%           
=======================================
  Files         830      830           
  Lines      224828   224867   +39     
=======================================
+ Hits       183504   183561   +57     
+ Misses      41324    41306   -18     

@2501babe 2501babe force-pushed the 20240201_bettervsde branch 3 times, most recently from 546b76f to b9c5bec Compare February 2, 2024 07:42
Comment on lines +385 to +388
// this only exists for the sake of the feature gated upgrade to the new parser; do not use it
#[doc(hidden)]
#[allow(clippy::used_underscore_binding)]
pub fn deserialize_with_bincode(_input: &[u8]) -> Result<Self, InstructionError> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i waffled a lot on whether to try and do the feature gate and vote program/runtime parser upgrade all in this pr or split into two, but the fact that some functions that want to use VoteState::deserialize() dont have access to the featureset struct made me decide the changes are too complicated and should be done separately. this function is identical to the original VoteState::deserialize() and i replace calls to that with this in a few places

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense.

Rather than having this temporarily-private function though, how about just having the call-sites do bincode::deserialize themselves?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

figured this was cleaner because other callsites would need to use VoteStateVersions and InstructionError. i could make it a pub(crate) function instead of a struct method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah pub(crate) doesnt work because multiple crates and inlining bincode would mean i need to add it to and then remove it from Cargo.tomls. i think ill just do all this in one shot in #35079 rather than create an uncomfortable intermediate state

@2501babe 2501babe marked this pull request as ready for review February 2, 2024 10:44
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look great! Just a couple of small comments

Comment on lines +385 to +388
// this only exists for the sake of the feature gated upgrade to the new parser; do not use it
#[doc(hidden)]
#[allow(clippy::used_underscore_binding)]
pub fn deserialize_with_bincode(_input: &[u8]) -> Result<Self, InstructionError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense.

Rather than having this temporarily-private function though, how about just having the call-sites do bincode::deserialize themselves?

Comment on lines -402 to -407
let minimum_size =
serialized_size(vote_state).map_err(|_| InstructionError::InvalidAccountData)?;
if (input.len() as u64) < minimum_size {
return Err(InstructionError::InvalidAccountData);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming this is correct -- any one of the reads will fail properly if the buffer isn't big enough

Comment on lines +149 to +156
#[test]
fn test_prior_voters_serialized_size() {
let vote_state = VoteState::default();
assert_eq!(
serialized_size(&vote_state.prior_voters).unwrap(),
PRIOR_VOTERS_SERIALIZED_SIZE
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this test!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants