diff --git a/node/libs/roles/src/proto/validator.proto b/node/libs/roles/src/proto/validator.proto index 87600b30..969705c6 100644 --- a/node/libs/roles/src/proto/validator.proto +++ b/node/libs/roles/src/proto/validator.proto @@ -25,12 +25,16 @@ message LeaderSelectionMode { RoundRobin round_robin = 1; Sticky sticky = 2; Weighted weighted = 3; + Rota rota = 4; } message RoundRobin{} message Sticky{ optional PublicKey key = 1; // required } message Weighted{} + message Rota { + repeated PublicKey keys = 1; // required + } } diff --git a/node/libs/roles/src/validator/conv.rs b/node/libs/roles/src/validator/conv.rs index 0cef59da..e7f6964f 100644 --- a/node/libs/roles/src/validator/conv.rs +++ b/node/libs/roles/src/validator/conv.rs @@ -463,6 +463,15 @@ impl ProtoFmt for LeaderSelectionMode { Ok(LeaderSelectionMode::Sticky(PublicKey::read(key)?)) } proto::leader_selection_mode::Mode::Weighted(_) => Ok(LeaderSelectionMode::Weighted), + proto::leader_selection_mode::Mode::Rota(inner) => { + let _ = required(&inner.keys.first()).context("keys")?; + let pks = inner + .keys + .iter() + .map(PublicKey::read) + .collect::, _>>()?; + Ok(LeaderSelectionMode::Rota(pks)) + } } } fn build(&self) -> Self::Proto { @@ -484,6 +493,13 @@ impl ProtoFmt for LeaderSelectionMode { proto::leader_selection_mode::Weighted {}, )), }, + LeaderSelectionMode::Rota(pks) => proto::LeaderSelectionMode { + mode: Some(proto::leader_selection_mode::Mode::Rota( + proto::leader_selection_mode::Rota { + keys: pks.iter().map(|pk| pk.build()).collect(), + }, + )), + }, } } } diff --git a/node/libs/roles/src/validator/messages/consensus.rs b/node/libs/roles/src/validator/messages/consensus.rs index 882bdcee..2056a380 100644 --- a/node/libs/roles/src/validator/messages/consensus.rs +++ b/node/libs/roles/src/validator/messages/consensus.rs @@ -64,6 +64,9 @@ pub enum LeaderSelectionMode { /// Select pseudo-randomly, based on validators' weights. Weighted, + + /// Select on a rotation of specific validator keys. + Rota(Vec), } /// Calculates the pseudo-random eligibility of a leader based on the input and total weight. @@ -178,6 +181,11 @@ impl Committee { let index = self.index(pk).unwrap(); self.get(index).unwrap().key.clone() } + LeaderSelectionMode::Rota(pks) => { + let index = view_number.0 as usize % pks.len(); + let index = self.index(&pks[index]).unwrap(); + self.get(index).unwrap().key.clone() + } } } @@ -313,6 +321,14 @@ impl Genesis { if self.validators.index(pk).is_none() { anyhow::bail!("leader_selection sticky mode public key is not in committee"); } + } else if let LeaderSelectionMode::Rota(pks) = &self.leader_selection { + for pk in pks { + if self.validators.index(pk).is_none() { + anyhow::bail!( + "leader_selection rota mode public key is not in committee: {pk:?}" + ); + } + } } Ok(()) diff --git a/node/libs/roles/src/validator/messages/tests.rs b/node/libs/roles/src/validator/messages/tests.rs index 32ac4d98..c570177f 100644 --- a/node/libs/roles/src/validator/messages/tests.rs +++ b/node/libs/roles/src/validator/messages/tests.rs @@ -103,6 +103,29 @@ fn test_sticky() { } } +#[test] +fn test_rota() { + let ctx = ctx::test_root(&ctx::RealClock); + let rng = &mut ctx.rng(); + let committee = validator_committee(); + let mut want = Vec::new(); + for _ in 0..3 { + want.push( + committee + .get(rng.gen_range(0..committee.len())) + .unwrap() + .key + .clone(), + ); + } + let rota = LeaderSelectionMode::Rota(want.clone()); + for _ in 0..100 { + let vn: ViewNumber = rng.gen(); + let pk = &want[vn.0 as usize % want.len()]; + assert_eq!(*pk, committee.view_leader(vn, &rota)); + } +} + /// Hardcoded view numbers. fn views() -> impl Iterator { [8394532, 2297897, 9089304, 7203483, 9982111] diff --git a/node/libs/roles/src/validator/testonly.rs b/node/libs/roles/src/validator/testonly.rs index df7c09c6..fc400fef 100644 --- a/node/libs/roles/src/validator/testonly.rs +++ b/node/libs/roles/src/validator/testonly.rs @@ -267,9 +267,13 @@ impl Distribution for Standard { impl Distribution for Standard { fn sample(&self, rng: &mut R) -> LeaderSelectionMode { - match rng.gen_range(0..=2) { + match rng.gen_range(0..=3) { 0 => LeaderSelectionMode::RoundRobin, 1 => LeaderSelectionMode::Sticky(rng.gen()), + 3 => LeaderSelectionMode::Rota({ + let n = rng.gen_range(1..=3); + rng.sample_iter(Standard).take(n).collect() + }), _ => LeaderSelectionMode::Weighted, } } diff --git a/node/tools/src/tests.rs b/node/tools/src/tests.rs index 79f6a56c..37e5f1c3 100644 --- a/node/tools/src/tests.rs +++ b/node/tools/src/tests.rs @@ -15,6 +15,14 @@ impl Distribution for EncodeDist { let i = rng.gen_range(0..genesis.validators.len()); genesis.leader_selection = LeaderSelectionMode::Sticky(genesis.validators.get(i).unwrap().key.clone()); + } else if let LeaderSelectionMode::Rota(pks) = genesis.leader_selection { + let n = pks.len(); + let i = rng.gen_range(0..genesis.validators.len()); + let mut pks = Vec::new(); + for _ in 0..n { + pks.push(genesis.validators.get(i).unwrap().key.clone()); + } + genesis.leader_selection = LeaderSelectionMode::Rota(pks); } AppConfig { server_addr: self.sample(rng),