-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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
Award one credit per dequeued vote when processing VoteStateUpdate in… #25743
Award one credit per dequeued vote when processing VoteStateUpdate in… #25743
Conversation
#[test] | ||
#[allow(clippy::type_complexity)] | ||
#[allow(clippy::field_reassign_with_default)] | ||
fn test_vote_state_update_increment_credits() { |
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.
+1 for tests
is this in tandem with #25662 ? or are you going to split it out of there |
programs/vote/src/vote_state/mod.rs
Outdated
), | ||
// Complex expiry: start with 1 - 31 in the tower, then locally vote for 35, 36, 37, 38, 42, 43, 46, 47, | ||
// 48, 49, 50, 53. None of the intermediate tower states land as tx, only the resulting complete state | ||
// does. This test very succinctly illustrates the way that "timely vote credits" are an approximation of |
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.
Much of this comment is out of date, it was relevant when this change included the "timely vote credits" stuff. I can remove it if you like. I hope that it's just prep work for a future change though ....
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.
let's only keep the relevant comments for now
programs/vote/src/vote_state/mod.rs
Outdated
.unwrap_or(false) | ||
{ | ||
// For each finalized slot, there was one voted-on slot in the new vote state that was responsible | ||
// for finalizing it. Each of those votes is awarded 1 credit. |
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: Remove extra whitespace
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 just let rustfmt do its thing. i.e. I don't know exactly what whitespace you're talking about or how you would prefer it to be formatted. I'm just using rustfmt.
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.
extra whitespace before the word Each
programs/vote/src/vote_state/mod.rs
Outdated
let mut credits = 0; | ||
|
||
for _ in new_state.iter().rev() { | ||
credits += 1; | ||
finalized_slot_count -= 1; | ||
if finalized_slot_count == 0 { | ||
break; | ||
} | ||
} |
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.
hmm, why is credits
not just equivalent to finalized_slot_count
, i.e. the number of slots in the current vote state less than the new root?
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 is somewhat vestigial since it came from my other change where I was actually calculating vote credits based on latency.
But I left it in for paranoia about "what if for some reason the number of votes that have been calculated as retired is greater than the number of votes in the tower", which shouldn't be possible. But I didn't want to allow a bug to be exploited for additional credits with some weirdly crafted new tower state that somehow slipped through other checks.
I admit that it's just a protection against an impossible scenario. I will remove if you like and just use finalized_slot_count as the number of credits to award.
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.
what if for some reason the number of votes that have been calculated as retired is greater than the number of votes in the tower"
Yeah this shouldn't be possible given we iterated through the tower earlier and found all the smaller slots. I think just using finalized_slot_count
is cleaner and correct.
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.
technically could be right? e.g
Old state: root 0, votes 1, 2, 3, ..., 30
New state: root 16, votes 17, 18, 19, 20, 21, 300
Technically this could be a valid state transition if vote transactions weren't landing + threshold checks and lockouts expiring. The 16 votes that contributed to moving the root could have all been popped off.
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.
Either way seems like the correct option is to use finalized_slot_count
, but might cause problems if you're using the top of the tower for computing latency based credits.
programs/vote/src/vote_state/mod.rs
Outdated
// (vote_state_lockouts: Vec<(Slot, u32)>, | ||
// vote_state_root: Option<Slot>, | ||
// vote_state_update_lockouts: Vec<(Slot, u32)>, | ||
// vote_state_update_root: Option<Slot>, | ||
// expected_credits: u64) |
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.
large tuples should be converted to structs for the benefit of named fields, i.e.
struct TestVoteData {}
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.
If you can show me how to then declare convenient data like I have here without tons of structural verbage that would be fine.
Honest question by the way. I am somewhat new to Rust. I don't know how these things are typically done. But I do not want to have to write:
let test_data : Vec =
vec![ TestVoteData { vote_state_lockouts : xxx, vote_state_root: yyy, ... },
TestVoteData { vote_state_lockouts: xxx, vote_state_root: yyy, ... },
...];
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.
But I do not want to have to write:
Unfortunately, I don't know of a better way. Having the struct makes the code a lot more readable as it clearly delineates start and end of individual items in the list, and makes each field self documenting.
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.
+1 on using the struct
programs/vote/src/vote_state/mod.rs
Outdated
let mut vote_state = VoteState::default(); | ||
|
||
vote_state.votes = vote_state_votes.clone(); | ||
|
||
vote_state.root_slot = vote_state_root; |
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 think we should do two things:
- Not directly assign the
.votes
androot_slot
, instead callprocess_new_vote_state()
to initialize the vote state and theroot
- Create a #[test] only constructor for initializing this a VoteState via suggestion 1. above called
new_from_lockouts
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.
But why? It's a test case and I think that the overly strict rules of your rust linter can just be relaxed for it ...
I guess one reason would be so that changes can be made to the VoteState struct without as much direct coupling with how this test is written ...
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.
The reason is process_new_vote_state()
is an safe state transition/way to modify the VoteState, and we want to ensure we only create/modify VoteStates in ways the API envisioned. Directly modifying individual fields breaks these guarantees.
programs/vote/src/vote_state/mod.rs
Outdated
assert_eq!( | ||
vote_state.process_new_vote_state( | ||
vote_state_update_votes.clone(), | ||
vote_state_update_root, | ||
None, | ||
100, | ||
None, | ||
), | ||
Ok(()) | ||
); | ||
|
||
assert_eq!(vote_state.epoch_credits[0].1, 1); | ||
|
||
let mut vote_state = VoteState::default(); | ||
|
||
vote_state.votes = vote_state_votes; | ||
|
||
vote_state.root_slot = vote_state_root; |
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 should be made redundant by the suggested constructor above.
Also there should be no need to reinitialize the vote state after construction, should directly be able to call process_new_vote_state
on the vote_state_update_votes
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.
But there are two things being tested:
- Vote state update without the feature set
- Vote state update with the feature set
The state being tested has to be reset between these two, which is what that code is doing.
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 see, that's a subtetly I missed, thanks for the clarification!
Here we can more clearly condense this function by parameterizing on feature set, i.e.
do_test_vote_state_update_increment_credits_with_feature_set(feature_set: Option<>
and then two separate tests:
#[test] do_test_vote_state_update_increment_credits_with_feature_set(Some())
#[test] do_test_vote_state_update_increment_credits_with_feature_set(None)
programs/vote/src/vote_state/mod.rs
Outdated
// Complex expiry: start with 1 - 31 in the tower, then locally vote for 35, 36, 37, 38, 42, 43, 46, 47, | ||
// 48, 49, 50, 53. None of the intermediate tower states land as tx, only the resulting complete state | ||
// does. This test very succinctly illustrates the way that "timely vote credits" are an approximation of | ||
// the latencies involved, because vote expiry causes the votes immediately before expired votes to be | ||
// charged with "worse" latency than they actually had (because expiry caused them to look like they had | ||
// more latency when cast than they did). The only easy solution to this is to track the voted-in slot | ||
// along with each voted-on slot, and use those values when computing vote credits. But this would | ||
// require changes to the Lockout struct to add a new value ("credits to award for this vote") and that | ||
// would then require changes to the serialized format of the tower, which seems like an extensive and | ||
// risky undertaking. | ||
( |
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.
Because these situations are quite complex, for correctness I would prefer if we used the existing build_vote_state()
function present in this module to accurately recreate and verify the votes created the tower you anticipated.
In that function, you pass in
- An ordered vector of votes that represents *every( vote you are making. These should include any expired votes as well.
slot_hashes: &[(Slot, Hash)]
with a hash for each slot you input into 1.
This will simulate creating the tower and ultimately spit out a vote state. That way, you can actually verify that the output vote state matches your expectations (expired votes were actually expired for instance).
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.
OK that's a good idea. I will do so.
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.
OK would you also recommend asserting that the resulting lockouts queue is as the test case expects it to be?
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 the assert would be nice to check your assumptions!
@AshwinSekar plan is to merge this one first, because the rest of #25662 requires a governance vote |
25662 is now obsolete; I was leaving it open just so that your comments are not lost. 25662 would be a follow-up to happen only if this change is accepted, and only after proper governance stuff is done. |
I hope that what I have done here is OK now. I have re-created the unit test to simply ensure that the new VoteStateUpdate counts credits the same way as the old Vote processing did. |
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. @carllin want me to own the feature gate for this?
Change looks good, just one nit |
…struction, to match vote rewards of Vote instruction.
2efe202
to
003f24d
Compare
Hi, I've rebased to master, incorporated carllin's most recent feedback about process_vote_unchecked, and squashed down into a single commit. |
I can't tell - are these build failures real? |
#25743) * Award one credit per dequeued vote when processing VoteStateUpdate instruction, to match vote rewards of Vote instruction. * Update feature pubkey to one owned by cc (ashwin) Co-authored-by: Ashwin Sekar <[email protected]> (cherry picked from commit cbb0f07) # Conflicts: # programs/stake/src/stake_instruction.rs # sdk/src/feature_set.rs
…struction,
to match vote rewards of Vote instruction.
Problem
VoteStateUpdate instruction processing does not award one credit per vote dequeued from the Tower. This is a difference in how vote credits are awarded and has not received community consensus nor governance permissions.
This change will bring the VoteStateUpdate instruction processing in line with how Vote instructions are processed with regards to credit award.
Summary of Changes
Modified VoteStateUpdate processing to count dequeued slots and then award that many credits. Added some unit tests.
Feature Gate issue:
#27091