-
Notifications
You must be signed in to change notification settings - Fork 14
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
feat: Submit a slot number alongside nonce #5297
base: main
Are you sure you want to change the base?
Conversation
c07cc38
to
77e7b82
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5297 +/- ##
======================================
- Coverage 71% 71% -0%
======================================
Files 488 489 +1
Lines 84898 84876 -22
Branches 84898 84876 -22
======================================
- Hits 60375 60229 -146
- Misses 21822 21932 +110
- Partials 2701 2715 +14 ☔ 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.
Looks good to me, just some minor comments mostly around naming. I'll take a closer look at tests on Monday.
state-chain/runtime/src/lib.rs
Outdated
@@ -724,6 +724,7 @@ impl pallet_cf_governance::Config for Runtime { | |||
type UpgradeCondition = ( | |||
pallet_cf_validator::NotDuringRotation<Runtime>, | |||
pallet_cf_swapping::NoPendingSwaps<Runtime>, | |||
pallet_cf_environment::NoUsedNonce<Runtime>, |
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 write this as a nested tuple then you don't need to change the trait definition:
type UpgradeCondition = (
pallet_cf_validator::NotDuringRotation<Runtime>,
(
pallet_cf_swapping::NoPendingSwaps<Runtime>,
pallet_cf_environment::NoUsedNonce<Runtime>,
)
);
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.
There's a typo in the file name.
Settings: Member + Parameter + MaybeSerializeDeserialize + Eq, | ||
Hook: OnChangeHook<Identifier, Value> + 'static, | ||
ValidatorId: Member + Parameter + Ord + MaybeSerializeDeserialize, | ||
> Change<Identifier, Value, Settings, Hook, ValidatorId> | ||
> NonceWitnessing<Identifier, Value, Slot, Settings, Hook, ValidatorId> |
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.
Naming nit: we rename this to something specific (nonce witnessing instead of change, 'Slot' which is Solana-specific) but haven't renamed any of the generics to match this more specific purpose (for example nonce account instead of identifier).
I would suggest either we keep a more general name (MonotonicChange? BlockHeight?), or we rename the generics to match the more specific use case. This would make the code easier to reason about (no need to remember whether value
is the nonce or the address etc.).
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.
imo, making the names more specific is better in this case
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.
Why? (Would be good to give a reason otherwise it's just a question of who has the most willingness to argue 😅 )
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 i don't think it matters much, but unless it has a general use better to name it specific. Like we don't name every parameter "variable", the name becomes more general as what it is actually doing becomes more general. Also there are no doubts about how it's intended to be used when it's specifically named.
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.
Yes, but in this case, we're implementing a type with a bunch of generics, so it's pretty clear that it's supposed to work generically. And of course I'm not saying we should name everything variable
and thing
. What I mean is that naming should be as specific as possible within its own context. In this particular case, for example, there's nothing in the functionality of the implementation that is nonce-specific.
) -> Result<VoteComponents<Self>, CorruptStorageError> { | ||
Ok(VoteComponents { | ||
bitmap_component: Some(partial_vote.value), | ||
individual_component: Some((_properties, partial_vote.slot)), |
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.
Why are the properties used here?
edit: I see now, these are vote properties ()
not election properties. Still, they value should have a leading underscore unless it's unused.
use crate::{CorruptStorageError, SharedDataHash}; | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] | ||
pub struct NonceVote<Value, Slot> { |
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.
Similar to the other comment, this vote type seems like it could be used more generally, so maybe there is a better / more general name than NonceVote? MonotonicChangeVote
or something?
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 we can generalise it when it necessary? else it's a little confusing it has a general name and is used for one (quite specific) thing. Though, if we go the way of composing the existing vote storage then this conversation is void.
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.
My view is that if we implement something generically, then it should have generic names. Reading generic code with specifically-named values is confusing because you always have the specific use-case in mind.
counts | ||
.entry(vote.value) | ||
.and_modify(|(count, slots)| { | ||
*count += 1; |
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.
We don't really need this count
, it's already implicitly tracked in the length of slots
.
Some(vote.clone()) | ||
let mut slots = slots.clone(); | ||
let num_slots = slots.len() as u32; | ||
let (_, median_vote, _) = { |
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 avoid calling it the median vote, it's not a median ;)
consensus_slot
or consensus_height
if we're using more general naming?
if previous_value != value { | ||
if let Some((value, slot)) = election_access.check_consensus()?.has_consensus() { | ||
let (identifier, previous_value, previous_slot) = election_access.properties()?; | ||
if previous_value != value && slot > previous_slot { |
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.
Due to the implementation of is_vote_valid, I think this statement should always be true (It's not possible to vote, and therefore not possible to gain consensus, if this condition is violated). So as an extra safety measure, we could add and else { log_or_panic!(..)
here to catch any logic errors?
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 we have the on-chain logic, which should prevent it on its own, without having to rely on correct behaviour from the engines. So I don't mind having this here as a way of ensuring the chain logic is valid independent of engine behaviour - though I do think there should be a comment here that we don't expect this to be false because in general the engines should behave.
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.
Yes I'm not saying we remove it, just that we should add something so that we don't silently fail if the assumption is wrong.
|
||
impl<T: Config> ExecutionCondition for NoUsedNonce<T> { | ||
fn is_satisfied() -> bool { | ||
SolanaAvailableNonceAccounts::<T>::get().len() == 10 |
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 was 7 😅
Could we not use SolanaUnavailableNonceAccounts::<T>::iter().next().is_none()
?
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 was originally 7 indeed. However, I did some optimizations to be able to bump it up to ten, as that is the bottleneck.
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, should been less indirect: we can't assume that it will always be 10. We've already change it once, we might change it again. So the implementation should not assume the number.
Settings: Member + Parameter + MaybeSerializeDeserialize + Eq, | ||
Hook: OnChangeHook<Identifier, Value> + 'static, | ||
ValidatorId: Member + Parameter + Ord + MaybeSerializeDeserialize, | ||
> Change<Identifier, Value, Settings, Hook, ValidatorId> | ||
> NonceWitnessing<Identifier, Value, Slot, Settings, Hook, ValidatorId> |
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.
imo, making the names more specific is better in this case
state-chain/pallets/cf-elections/src/electoral_systems/tests/nonce_witnessing.rs
Outdated
Show resolved
Hide resolved
state-chain/pallets/cf-elections/src/electoral_systems/tests/nonce_witnessing.rs
Outdated
Show resolved
Hide resolved
use crate::{CorruptStorageError, SharedDataHash}; | ||
|
||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, TypeInfo)] | ||
pub struct NonceVote<Value, Slot> { |
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 we can generalise it when it necessary? else it's a little confusing it has a general name and is used for one (quite specific) thing. Though, if we go the way of composing the existing vote storage then this conversation is void.
4f9fa4c
to
7b7fdd3
Compare
I kept everything generic in the end! |
4229a1b
to
4c08f3b
Compare
vote: &Self::Vote, | ||
mut _h: H, | ||
) -> Self::PartialVote { | ||
(*vote).clone() |
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 means we don't have a partial vote, and we always vote the full vote. The plan is to extend this electoral system for the contract witnessing, and so there the value would be much larger in size - and we don't want every validator submitting all that data - we should probably store a hash of the value in the bitmap, and then construct the full vote from the shared data - like we do in the Bitmap
VoteStorage
impl
- keep everything generalized
let mut blocks_height = blocks_height.clone(); | ||
let (_, consensus_block_height, _) = { | ||
blocks_height | ||
.select_nth_unstable(threshold_from_share_count(num_votes) as usize) |
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.
We are computing a threshold here based on the number of votes rather than the number of authorities. Is this intentional? Why?
(
For comparison, for monotonic_median consensus we do:
active_votes.select_nth_unstable((num_authorities - success_threshold) as usize);
)
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.
My idea was to keep only the valid votes into consideration for the slot.
Cause if someone voted for a wrong value their slot could be way off and it doesn't make much sense to keep that into consideration.
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.
Yes, but if we are in this branch of the code, then we know that there are at least success_threshold
valid votes. Our thresholds should always be based on the total number of authorities.
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 I'll switch back to use the authority_count
b86c5ad
to
d41341b
Compare
pub async fn get_durable_nonce<SolRetryRpcClient>( | ||
sol_client: &SolRetryRpcClient, | ||
nonce_account: SolAddress, | ||
) -> Result<Option<SolHash>> | ||
previous_slot: SlotNumber, | ||
) -> Result<Option<MonotonicChangeVote<SolHash, SlotNumber>>> |
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: To keep the idea of a vote within the VoterApi, this fn should not return a vote, it can just return (SolHash, SlotNumber)
. (and it's the VoterApi's job to convert that into a vote)
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 see a test that checks that we can only form consensus if the block increases. (ie. everyone votes for a new value, but at a lower block).
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.
Actually this is not true, we can form consensus even if the block decrease.
Votes are filtered at the moment of voting only, so if we manually create votes that have a lower block we can still form consensus.
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 implementing is_vote_valid()
for the mock so that we can test that these bad votes are correctly rejected!
@@ -152,6 +156,37 @@ fn consensus_when_all_votes_the_same_but_different_slot() { | |||
); | |||
} | |||
|
|||
#[test] | |||
fn votes_with_old_value_or_lower_block_are_rejected() { |
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.
👌
As discussed: the upgrade condition check doesn't work, we can delete it. We need to migrate by deleting all the old elections and then requesting nonce witness elections for any missing nonces. |
Pull Request
Closes: PRO-1635
Checklist
Please conduct a thorough self-review before opening the PR.
Summary
Updated the Change Electoral system to Nonce Electoral system, which takes both nonce value and slot as votes and update the nonce to the new value only if the new value is different than the previous one and the slot is higher than the previous one.