From 86f14eed076298066a085838d3a512c68c03e5bd Mon Sep 17 00:00:00 2001 From: Lldenaurois <ljdenaurois@gmail.com> Date: Wed, 4 Aug 2021 11:49:21 -0400 Subject: [PATCH] node/approval-voting: Continue to migrate tests to subsystem tests (#3471) * node/approval-voting: Continue to migrate tests to subsystem tests * node/approval-voting: Continue to implement subsystem tests. * Add more tests * node/approval-voting: Add more tests * node/approval-voting: Difficuly determining the proper test scenario for threshold tests * node/approval-voting: Introduce should_trigger_assignment tests * node/approval-voting: Finalize threshold tests * node/approval-voting: Address Feedback and add comments * node/approval-voting: Tidy up approval tests * Tidy up tests * Fix rustfmt --- polkadot/node/core/approval-voting/src/lib.rs | 13 +- .../core/approval-voting/src/old_tests.rs | 1925 -------------- .../approval-voting/src/persisted_entries.rs | 31 - .../node/core/approval-voting/src/tests.rs | 2355 +++++++++++++---- 4 files changed, 1862 insertions(+), 2462 deletions(-) delete mode 100644 polkadot/node/core/approval-voting/src/old_tests.rs diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index d168f43b5bf..0d5c745fb90 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -92,9 +92,6 @@ use crate::{ #[cfg(test)] mod tests; -#[cfg(test)] -mod old_tests; - const APPROVAL_SESSIONS: SessionIndex = 6; const APPROVAL_CHECKING_TIMEOUT: Duration = Duration::from_secs(120); const APPROVAL_CACHE_SIZE: usize = 1024; @@ -455,8 +452,9 @@ impl Wakeups { Some(tick) => { clock.wait(tick).await; match self.wakeups.entry(tick) { - Entry::Vacant(_) => - panic!("entry is known to exist since `first` was `Some`; qed"), + Entry::Vacant(_) => { + panic!("entry is known to exist since `first` was `Some`; qed") + }, Entry::Occupied(mut entry) => { let (hash, candidate_hash) = entry.get_mut().pop() .expect("empty entries are removed here and in `schedule`; no other mutation of this map; qed"); @@ -919,8 +917,9 @@ async fn handle_actions( .await; match confirmation_rx.await { - Err(oneshot::Canceled) => - tracing::warn!(target: LOG_TARGET, "Dispute coordinator confirmation lost",), + Err(oneshot::Canceled) => { + tracing::warn!(target: LOG_TARGET, "Dispute coordinator confirmation lost",) + }, Ok(ImportStatementsResult::ValidImport) => {}, Ok(ImportStatementsResult::InvalidImport) => tracing::warn!( target: LOG_TARGET, diff --git a/polkadot/node/core/approval-voting/src/old_tests.rs b/polkadot/node/core/approval-voting/src/old_tests.rs deleted file mode 100644 index a122bce7df8..00000000000 --- a/polkadot/node/core/approval-voting/src/old_tests.rs +++ /dev/null @@ -1,1925 +0,0 @@ -// Copyright 2021 Parity Technologies (UK) Ltd. -// This file is part of Polkadot. - -// Polkadot is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Polkadot is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Polkadot. If not, see <http://www.gnu.org/licenses/>. - -use super::{ - approval_db::v1::Config, - backend::{Backend, BackendWriteOp}, - *, -}; -use polkadot_node_primitives::approval::{ - AssignmentCert, AssignmentCertKind, DelayTranche, VRFOutput, VRFProof, RELAY_VRF_MODULO_CONTEXT, -}; -use polkadot_node_subsystem::messages::{AllMessages, HighestApprovedAncestorBlock}; -use polkadot_node_subsystem_test_helpers::make_subsystem_context; -use polkadot_primitives::v1::{ - CandidateDescriptor, CoreIndex, DisputeStatement, GroupIndex, ValidDisputeStatementKind, - ValidatorSignature, -}; -use sp_core::testing::TaskExecutor; - -use assert_matches::assert_matches; -use bitvec::order::Lsb0 as BitOrderLsb0; -use parking_lot::Mutex; -use sp_keyring::sr25519::Keyring as Sr25519Keyring; -use std::{pin::Pin, sync::Arc}; - -const SLOT_DURATION_MILLIS: u64 = 5000; - -const DATA_COL: u32 = 0; -const NUM_COLUMNS: u32 = 1; - -const TEST_CONFIG: Config = Config { col_data: DATA_COL }; - -fn make_db() -> DbBackend { - let db_writer: Arc<dyn KeyValueDB> = Arc::new(kvdb_memorydb::create(NUM_COLUMNS)); - DbBackend::new(db_writer.clone(), TEST_CONFIG) -} - -fn overlay_txn<T, F>(db: &mut T, mut f: F) -where - T: Backend, - F: FnMut(&mut OverlayedBackend<'_, T>), -{ - let mut overlay_db = OverlayedBackend::new(db); - f(&mut overlay_db); - let write_ops = overlay_db.into_write_ops(); - db.write(write_ops).unwrap(); -} - -fn slot_to_tick(t: impl Into<Slot>) -> crate::time::Tick { - crate::time::slot_number_to_tick(SLOT_DURATION_MILLIS, t.into()) -} - -#[derive(Default, Clone)] -struct MockClock { - inner: Arc<Mutex<MockClockInner>>, -} - -impl MockClock { - fn new(tick: Tick) -> Self { - let me = Self::default(); - me.inner.lock().set_tick(tick); - me - } -} - -impl Clock for MockClock { - fn tick_now(&self) -> Tick { - self.inner.lock().tick - } - - fn wait(&self, tick: Tick) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> { - let rx = self.inner.lock().register_wakeup(tick, true); - - Box::pin(async move { - rx.await.expect("i exist in a timeless void. yet, i remain"); - }) - } -} - -// This mock clock allows us to manipulate the time and -// be notified when wakeups have been triggered. -#[derive(Default)] -struct MockClockInner { - tick: Tick, - wakeups: Vec<(Tick, oneshot::Sender<()>)>, -} - -impl MockClockInner { - fn set_tick(&mut self, tick: Tick) { - self.tick = tick; - self.wakeup_all(tick); - } - - fn wakeup_all(&mut self, up_to: Tick) { - // This finds the position of the first wakeup after - // the given tick, or the end of the map. - let drain_up_to = - self.wakeups.binary_search_by_key(&(up_to + 1), |w| w.0).unwrap_or_else(|i| i); - - for (_, wakeup) in self.wakeups.drain(..drain_up_to) { - let _ = wakeup.send(()); - } - } - - // If `pre_emptive` is true, we compare the given tick to the internal - // tick of the clock for an early return. - // - // Otherwise, the wakeup will only trigger alongside another wakeup of - // equal or greater tick. - // - // When the pre-emptive wakeup is disabled, this can be used in combination with - // a preceding call to `set_tick` to wait until some other wakeup at that same tick - // has been triggered. - fn register_wakeup(&mut self, tick: Tick, pre_emptive: bool) -> oneshot::Receiver<()> { - let (tx, rx) = oneshot::channel(); - - let pos = self.wakeups.binary_search_by_key(&tick, |w| w.0).unwrap_or_else(|i| i); - - self.wakeups.insert(pos, (tick, tx)); - - if pre_emptive { - // if `tick > self.tick`, this won't wake up the new - // listener. - self.wakeup_all(self.tick); - } - - rx - } -} - -struct MockAssignmentCriteria<Compute, Check>(Compute, Check); - -impl<Compute, Check> AssignmentCriteria for MockAssignmentCriteria<Compute, Check> -where - Compute: Fn() -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment>, - Check: Fn() -> Result<DelayTranche, criteria::InvalidAssignment>, -{ - fn compute_assignments( - &self, - _keystore: &LocalKeystore, - _relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory, - _config: &criteria::Config, - _leaving_cores: Vec<( - CandidateHash, - polkadot_primitives::v1::CoreIndex, - polkadot_primitives::v1::GroupIndex, - )>, - ) -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment> { - self.0() - } - - fn check_assignment_cert( - &self, - _claimed_core_index: polkadot_primitives::v1::CoreIndex, - _validator_index: ValidatorIndex, - _config: &criteria::Config, - _relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory, - _assignment: &polkadot_node_primitives::approval::AssignmentCert, - _backing_group: polkadot_primitives::v1::GroupIndex, - ) -> Result<polkadot_node_primitives::approval::DelayTranche, criteria::InvalidAssignment> { - self.1() - } -} - -impl<F> - MockAssignmentCriteria< - fn() -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment>, - F, - > -{ - fn check_only(f: F) -> Self { - MockAssignmentCriteria(Default::default, f) - } -} - -fn blank_state() -> State { - State { - session_window: RollingSessionWindow::new(APPROVAL_SESSIONS), - keystore: Arc::new(LocalKeystore::in_memory()), - slot_duration_millis: SLOT_DURATION_MILLIS, - clock: Box::new(MockClock::default()), - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - } -} - -fn single_session_state(index: SessionIndex, info: SessionInfo) -> State { - State { - session_window: RollingSessionWindow::with_session_info( - APPROVAL_SESSIONS, - index, - vec![info], - ), - ..blank_state() - } -} - -fn garbage_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert { - let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT); - let msg = b"test-garbage"; - let mut prng = rand_core::OsRng; - let keypair = schnorrkel::Keypair::generate_with(&mut prng); - let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg)); - let out = inout.to_output(); - - AssignmentCert { kind, vrf: (VRFOutput(out), VRFProof(proof)) } -} - -fn sign_approval( - key: Sr25519Keyring, - candidate_hash: CandidateHash, - session_index: SessionIndex, -) -> ValidatorSignature { - key.sign(&ApprovalVote(candidate_hash).signing_payload(session_index)).into() -} - -#[derive(Clone)] -struct StateConfig { - session_index: SessionIndex, - slot: Slot, - tick: Tick, - validators: Vec<Sr25519Keyring>, - validator_groups: Vec<Vec<ValidatorIndex>>, - needed_approvals: u32, - no_show_slots: u32, - candidate_hash: Option<CandidateHash>, -} - -impl Default for StateConfig { - fn default() -> Self { - StateConfig { - session_index: 1, - slot: Slot::from(0), - tick: 0, - validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob], - validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]], - needed_approvals: 1, - no_show_slots: 2, - candidate_hash: None, - } - } -} - -// one block with one candidate. Alice and Bob are in the assignment keys. -fn some_state(config: StateConfig, db: &mut DbBackend) -> State { - let StateConfig { - session_index, - slot, - tick, - validators, - validator_groups, - needed_approvals, - no_show_slots, - candidate_hash, - } = config; - - let n_validators = validators.len(); - - let state = State { - clock: Box::new(MockClock::new(tick)), - ..single_session_state( - session_index, - SessionInfo { - validators: validators.iter().map(|v| v.public().into()).collect(), - discovery_keys: validators.iter().map(|v| v.public().into()).collect(), - assignment_keys: validators.iter().map(|v| v.public().into()).collect(), - validator_groups: validator_groups.clone(), - n_cores: validator_groups.len() as _, - zeroth_delay_tranche_width: 5, - relay_vrf_modulo_samples: 3, - n_delay_tranches: 50, - no_show_slots, - needed_approvals, - ..Default::default() - }, - ) - }; - let core_index = 0.into(); - - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = candidate_hash.unwrap_or_else(|| CandidateHash(Hash::repeat_byte(0xCC))); - - add_block(db, block_hash, session_index, slot); - - add_candidate_to_block( - db, - block_hash, - candidate_hash, - n_validators, - core_index, - GroupIndex(0), - None, - ); - - state -} - -fn add_block(db: &mut DbBackend, block_hash: Hash, session: SessionIndex, slot: Slot) { - overlay_txn(db, |overlay_db| { - overlay_db.write_block_entry( - approval_db::v1::BlockEntry { - block_hash, - parent_hash: Default::default(), - block_number: 0, - session, - slot, - candidates: Vec::new(), - relay_vrf_story: Default::default(), - approved_bitfield: Default::default(), - children: Default::default(), - } - .into(), - ) - }); -} - -fn add_candidate_to_block( - db: &mut DbBackend, - block_hash: Hash, - candidate_hash: CandidateHash, - n_validators: usize, - core: CoreIndex, - backing_group: GroupIndex, - candidate_receipt: Option<CandidateReceipt>, -) { - let mut block_entry = db.load_block_entry(&block_hash).unwrap().unwrap(); - - let mut candidate_entry = - db.load_candidate_entry(&candidate_hash).unwrap().unwrap_or_else(|| { - approval_db::v1::CandidateEntry { - session: block_entry.session(), - block_assignments: Default::default(), - candidate: candidate_receipt.unwrap_or_default(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; n_validators], - } - .into() - }); - - block_entry.add_candidate(core, candidate_hash); - - candidate_entry.add_approval_entry( - block_hash, - approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group, - our_assignment: None, - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; n_validators], - approved: false, - } - .into(), - ); - - overlay_txn(db, |overlay_db| { - overlay_db.write_block_entry(block_entry.clone()); - overlay_db.write_candidate_entry(candidate_entry.clone()); - }) -} - -fn import_assignment<F>( - db: &mut DbBackend, - candidate_hash: &CandidateHash, - block_hash: &Hash, - validator_index: ValidatorIndex, - mut f: F, -) where - F: FnMut(&mut CandidateEntry), -{ - let mut candidate_entry = db.load_candidate_entry(candidate_hash).unwrap().unwrap(); - candidate_entry.approval_entry_mut(block_hash).unwrap().import_assignment( - 0, - validator_index, - 0, - ); - - f(&mut candidate_entry); - - overlay_txn(db, |overlay_db| overlay_db.write_candidate_entry(candidate_entry.clone())); -} - -fn set_our_assignment<F>( - db: &mut DbBackend, - candidate_hash: &CandidateHash, - block_hash: &Hash, - tranche: DelayTranche, - mut f: F, -) where - F: FnMut(&mut CandidateEntry), -{ - let mut candidate_entry = db.load_candidate_entry(&candidate_hash).unwrap().unwrap(); - let approval_entry = candidate_entry.approval_entry_mut(&block_hash).unwrap(); - - approval_entry.set_our_assignment( - approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche, - validator_index: ValidatorIndex(0), - triggered: false, - } - .into(), - ); - - f(&mut candidate_entry); - - overlay_txn(db, |overlay_db| overlay_db.write_candidate_entry(candidate_entry.clone())); -} - -#[test] -fn rejects_bad_assignment() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let assignment_good = IndirectAssignmentCert { - block_hash, - validator: ValidatorIndex(0), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - }; - let mut state = some_state( - StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ); - let candidate_index = 0; - - let mut overlay_db = OverlayedBackend::new(&db); - let res = check_and_import_assignment( - &mut state, - &mut overlay_db, - assignment_good.clone(), - candidate_index, - ) - .unwrap(); - assert_eq!(res.0, AssignmentCheckResult::Accepted); - // Check that the assignment's been imported. - assert_eq!(res.1.len(), 1); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - assert_matches!(write_ops.get(0).unwrap(), BackendWriteOp::WriteCandidateEntry(..)); - db.write(write_ops).unwrap(); - - // unknown hash - let unknown_hash = Hash::repeat_byte(0x02); - let assignment = IndirectAssignmentCert { - block_hash: unknown_hash, - validator: ValidatorIndex(0), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - }; - - let mut overlay_db = OverlayedBackend::new(&db); - let res = check_and_import_assignment(&mut state, &mut overlay_db, assignment, candidate_index) - .unwrap(); - assert_eq!(res.0, AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(unknown_hash))); - assert_eq!(overlay_db.into_write_ops().count(), 0); - - let mut state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| { - Err(criteria::InvalidAssignment) - })), - ..some_state( - StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - // same assignment, but this time rejected - let mut overlay_db = OverlayedBackend::new(&db); - let res = - check_and_import_assignment(&mut state, &mut overlay_db, assignment_good, candidate_index) - .unwrap(); - assert_eq!( - res.0, - AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(ValidatorIndex(0))) - ); - assert_eq!(overlay_db.into_write_ops().count(), 0); -} - -#[test] -fn rejects_assignment_in_future() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 0; - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let assignment = IndirectAssignmentCert { - block_hash, - validator: ValidatorIndex(0), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - }; - - let tick = 9; - let mut state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(move || { - Ok((tick + 20) as _) - })), - ..some_state( - StateConfig { tick, candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - let mut overlay_db = OverlayedBackend::new(&db); - let res = check_and_import_assignment( - &mut state, - &mut overlay_db, - assignment.clone(), - candidate_index, - ) - .unwrap(); - assert_eq!(res.0, AssignmentCheckResult::TooFarInFuture); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - - assert_eq!(write_ops.len(), 0); - db.write(write_ops).unwrap(); - - let mut state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(move || { - Ok((tick + 20 - 1) as _) - })), - ..some_state( - StateConfig { tick, candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - let mut overlay_db = OverlayedBackend::new(&db); - let res = check_and_import_assignment( - &mut state, - &mut overlay_db, - assignment.clone(), - candidate_index, - ) - .unwrap(); - assert_eq!(res.0, AssignmentCheckResult::Accepted); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - - assert_matches!(write_ops.get(0).unwrap(), BackendWriteOp::WriteCandidateEntry(..)); -} - -#[test] -fn rejects_assignment_with_unknown_candidate() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 1; - let assignment = IndirectAssignmentCert { - block_hash, - validator: ValidatorIndex(0), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - }; - - let mut state = some_state(Default::default(), &mut db); - - let mut overlay_db = OverlayedBackend::new(&db); - let res = check_and_import_assignment( - &mut state, - &mut overlay_db, - assignment.clone(), - candidate_index, - ) - .unwrap(); - assert_eq!( - res.0, - AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(candidate_index)) - ); - assert_eq!(overlay_db.into_write_ops().count(), 0); -} - -#[test] -fn assignment_import_updates_candidate_entry_and_schedules_wakeup() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - - let candidate_index = 0; - let assignment = IndirectAssignmentCert { - block_hash, - validator: ValidatorIndex(0), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - }; - - let mut state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - let mut overlay_db = OverlayedBackend::new(&db); - let (res, actions) = check_and_import_assignment( - &mut state, - &mut overlay_db, - assignment.clone(), - candidate_index, - ) - .unwrap(); - - assert_eq!(res, AssignmentCheckResult::Accepted); - assert_eq!(actions.len(), 1); - - assert_matches!( - actions.get(0).unwrap(), - Action::ScheduleWakeup { - block_hash: b, - candidate_hash: c, - tick, - .. - } => { - assert_eq!(b, &block_hash); - assert_eq!(c, &candidate_hash); - assert_eq!(tick, &slot_to_tick(0 + 2)); // current tick + no-show-duration. - } - ); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(1, write_ops.len()); - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(ref c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert!(c_entry.approval_entry(&block_hash).unwrap().is_assigned(ValidatorIndex(0))); - } - ); -} - -#[test] -fn rejects_approval_before_assignment() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - let vote = IndirectSignedApprovalVote { - block_hash, - candidate_index: 0, - validator: ValidatorIndex(0), - signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1), - }; - - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = - check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap(); - - assert_eq!(res, ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(ValidatorIndex(0)))); - assert!(actions.is_empty()); - assert_eq!(overlay_db.into_write_ops().count(), 0); -} - -#[test] -fn rejects_approval_if_no_candidate_entry() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - let vote = IndirectSignedApprovalVote { - block_hash, - candidate_index: 0, - validator: ValidatorIndex(0), - signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1), - }; - - overlay_txn(&mut db, |overlay_db| overlay_db.delete_candidate_entry(&candidate_hash)); - - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = - check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap(); - - assert_eq!( - res, - ApprovalCheckResult::Bad(ApprovalCheckError::InvalidCandidate(0, candidate_hash)) - ); - assert!(actions.is_empty()); - assert_eq!(overlay_db.into_write_ops().count(), 0); -} - -#[test] -fn rejects_approval_if_no_block_entry() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let validator_index = ValidatorIndex(0); - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { candidate_hash: Some(candidate_hash), ..Default::default() }, - &mut db, - ) - }; - - let vote = IndirectSignedApprovalVote { - block_hash, - candidate_index: 0, - validator: ValidatorIndex(0), - signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1), - }; - - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index, |_| {}); - - overlay_txn(&mut db, |overlay_db| overlay_db.delete_block_entry(&block_hash)); - - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = - check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap(); - - assert_eq!(res, ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(block_hash))); - assert!(actions.is_empty()); - assert_eq!(overlay_db.into_write_ops().count(), 0); -} - -#[test] -fn accepts_and_imports_approval_after_assignment() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let validator_index = ValidatorIndex(0); - - let candidate_index = 0; - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { - validators: vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - ], - validator_groups: vec![ - vec![ValidatorIndex(0), ValidatorIndex(1)], - vec![ValidatorIndex(2)], - ], - needed_approvals: 2, - candidate_hash: Some(candidate_hash), - ..Default::default() - }, - &mut db, - ) - }; - - let vote = IndirectSignedApprovalVote { - block_hash, - candidate_index, - validator: validator_index, - signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1), - }; - - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index, |_| {}); - - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = - check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap(); - - assert_eq!(res, ApprovalCheckResult::Accepted); - - assert_eq!(actions.len(), 1); - - assert_matches!( - &actions[0], - Action::InformDisputeCoordinator { - dispute_statement, - candidate_hash: c_hash, - validator_index: v, - .. - } => { - assert_matches!( - dispute_statement.statement(), - &DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking) - ); - - assert_eq!(c_hash, &candidate_hash); - assert_eq!(v, &validator_index); - } - ); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(ref c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert!(!c_entry.approval_entry(&block_hash).unwrap().is_approved()); - } - ); -} - -#[test] -fn second_approval_import_only_schedules_wakeups() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let validator_index = ValidatorIndex(0); - let validator_index_b = ValidatorIndex(1); - - let candidate_index = 0; - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { - validators: vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - ], - validator_groups: vec![ - vec![ValidatorIndex(0), ValidatorIndex(1)], - vec![ValidatorIndex(2)], - ], - needed_approvals: 2, - candidate_hash: Some(candidate_hash), - ..Default::default() - }, - &mut db, - ) - }; - - let vote = IndirectSignedApprovalVote { - block_hash, - candidate_index, - validator: validator_index, - signature: sign_approval(Sr25519Keyring::Alice, candidate_hash, 1), - }; - - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index, |candidate_entry| { - assert!(!candidate_entry.mark_approval(validator_index)); - }); - - // There is only one assignment, so nothing to schedule if we double-import. - - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = - check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote.clone(), |r| r) - .unwrap(); - - assert_eq!(res, ApprovalCheckResult::Accepted); - assert!(actions.is_empty()); - assert_eq!(overlay_db.into_write_ops().count(), 0); - - // After adding a second assignment, there should be a schedule wakeup action. - - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_b, |_| {}); - - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = - check_and_import_approval(&state, &mut overlay_db, &Metrics(None), vote, |r| r).unwrap(); - - assert_eq!(res, ApprovalCheckResult::Accepted); - assert_eq!(actions.len(), 1); - assert_eq!(overlay_db.into_write_ops().count(), 0); - - assert_matches!( - actions.get(0).unwrap(), - Action::ScheduleWakeup { .. } => {} - ); -} - -#[test] -fn import_checked_approval_updates_entries_and_schedules() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let validator_index_a = ValidatorIndex(0); - let validator_index_b = ValidatorIndex(1); - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { - validators: vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - ], - validator_groups: vec![ - vec![ValidatorIndex(0), ValidatorIndex(1)], - vec![ValidatorIndex(2)], - ], - needed_approvals: 2, - candidate_hash: Some(candidate_hash), - ..Default::default() - }, - &mut db, - ) - }; - - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_a, |_| {}); - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_b, |_| {}); - - { - let mut overlay_db = OverlayedBackend::new(&db); - let actions = import_checked_approval( - &state, - &mut overlay_db, - &Metrics(None), - db.load_block_entry(&block_hash).unwrap().unwrap(), - candidate_hash, - db.load_candidate_entry(&candidate_hash).unwrap().unwrap(), - ApprovalSource::Remote(validator_index_a), - ); - - assert_eq!(actions.len(), 1); - assert_matches!( - actions.get(0).unwrap(), - Action::ScheduleWakeup { - block_hash: b_hash, - candidate_hash: c_hash, - .. - } => { - assert_eq!(b_hash, &block_hash); - assert_eq!(c_hash, &candidate_hash); - } - ); - - let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - assert_matches!( - write_ops.get_mut(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => { - assert!(!c_entry.approval_entry(&block_hash).unwrap().is_approved()); - assert!(c_entry.mark_approval(validator_index_a)); - } - ); - - db.write(write_ops).unwrap(); - } - - { - let mut overlay_db = OverlayedBackend::new(&db); - let actions = import_checked_approval( - &state, - &mut overlay_db, - &Metrics(None), - db.load_block_entry(&block_hash).unwrap().unwrap(), - candidate_hash, - db.load_candidate_entry(&candidate_hash).unwrap().unwrap(), - ApprovalSource::Remote(validator_index_b), - ); - assert_eq!(actions.len(), 1); - - let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 2); - - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteBlockEntry(b_entry) => { - assert_eq!(b_entry.block_hash(), block_hash); - assert!(b_entry.is_fully_approved()); - assert!(b_entry.is_candidate_approved(&candidate_hash)); - } - ); - - assert_matches!( - write_ops.get_mut(1).unwrap(), - BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved()); - assert!(c_entry.mark_approval(validator_index_b)); - } - ); - } -} - -#[test] -fn assignment_triggered_by_all_with_less_than_threshold() { - let block_hash = Hash::repeat_byte(0x01); - - let mut candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: 1, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - // 1-of-4 - candidate_entry.approval_entry_mut(&block_hash).unwrap().import_assignment( - 0, - ValidatorIndex(0), - 0, - ); - - candidate_entry.mark_approval(ValidatorIndex(0)); - - let tranche_now = 1; - assert!(should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::All, - tranche_now, - )); -} - -#[test] -fn assignment_not_triggered_by_all_with_threshold() { - let block_hash = Hash::repeat_byte(0x01); - - let mut candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: 1, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - // 2-of-4 - candidate_entry.approval_entry_mut(&block_hash).unwrap().import_assignment( - 0, - ValidatorIndex(0), - 0, - ); - - candidate_entry.approval_entry_mut(&block_hash).unwrap().import_assignment( - 0, - ValidatorIndex(1), - 0, - ); - - candidate_entry.mark_approval(ValidatorIndex(0)); - candidate_entry.mark_approval(ValidatorIndex(1)); - - let tranche_now = 1; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::All, - tranche_now, - )); -} - -#[test] -fn assignment_not_triggered_if_already_triggered() { - let block_hash = Hash::repeat_byte(0x01); - - let candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: 1, - validator_index: ValidatorIndex(4), - triggered: true, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - let tranche_now = 1; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::All, - tranche_now, - )); -} - -#[test] -fn assignment_not_triggered_by_exact() { - let block_hash = Hash::repeat_byte(0x01); - - let candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: 1, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - let tranche_now = 1; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::Exact { needed: 2, next_no_show: None, tolerated_missing: 0 }, - tranche_now, - )); -} - -#[test] -fn assignment_not_triggered_more_than_maximum() { - let block_hash = Hash::repeat_byte(0x01); - let maximum_broadcast = 10; - - let candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: maximum_broadcast + 1, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - let tranche_now = 50; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::Pending { - maximum_broadcast, - clock_drift: 0, - considered: 10, - next_no_show: None, - }, - tranche_now, - )); -} - -#[test] -fn assignment_triggered_if_at_maximum() { - let block_hash = Hash::repeat_byte(0x01); - let maximum_broadcast = 10; - - let candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: maximum_broadcast, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - let tranche_now = maximum_broadcast; - assert!(should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::Pending { - maximum_broadcast, - clock_drift: 0, - considered: 10, - next_no_show: None, - }, - tranche_now, - )); -} - -#[test] -fn assignment_not_triggered_if_at_maximum_but_clock_is_before() { - let block_hash = Hash::repeat_byte(0x01); - let maximum_broadcast = 10; - - let candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: maximum_broadcast, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - let tranche_now = 9; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::Pending { - maximum_broadcast, - clock_drift: 0, - considered: 10, - next_no_show: None, - }, - tranche_now, - )); -} - -#[test] -fn assignment_not_triggered_if_at_maximum_but_clock_is_before_with_drift() { - let block_hash = Hash::repeat_byte(0x01); - let maximum_broadcast = 10; - - let candidate_entry: CandidateEntry = { - let approval_entry = approval_db::v1::ApprovalEntry { - tranches: Vec::new(), - backing_group: GroupIndex(0), - our_assignment: Some(approval_db::v1::OurAssignment { - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - tranche: maximum_broadcast, - validator_index: ValidatorIndex(4), - triggered: false, - }), - our_approval_sig: None, - assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - approved: false, - }; - - approval_db::v1::CandidateEntry { - candidate: Default::default(), - session: 1, - block_assignments: vec![(block_hash, approval_entry)].into_iter().collect(), - approvals: bitvec::bitvec![BitOrderLsb0, u8; 0; 4], - } - .into() - }; - - let tranche_now = 10; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::Pending { - maximum_broadcast, - clock_drift: 1, - considered: 10, - next_no_show: None, - }, - tranche_now, - )); -} - -#[test] -fn wakeups_next() { - let mut wakeups = Wakeups::default(); - - let b_a = Hash::repeat_byte(0); - let b_b = Hash::repeat_byte(1); - - let c_a = CandidateHash(Hash::repeat_byte(2)); - let c_b = CandidateHash(Hash::repeat_byte(3)); - - wakeups.schedule(b_a, 0, c_a, 1); - wakeups.schedule(b_a, 0, c_b, 4); - wakeups.schedule(b_b, 1, c_b, 3); - - assert_eq!(wakeups.first().unwrap(), 1); - - let clock = MockClock::new(0); - let clock_aux = clock.clone(); - - let test_fut = Box::pin(async move { - assert_eq!(wakeups.next(&clock).await, (1, b_a, c_a)); - assert_eq!(wakeups.next(&clock).await, (3, b_b, c_b)); - assert_eq!(wakeups.next(&clock).await, (4, b_a, c_b)); - assert!(wakeups.first().is_none()); - assert!(wakeups.wakeups.is_empty()); - - assert_eq!( - wakeups.block_numbers.get(&0).unwrap(), - &vec![b_a].into_iter().collect::<HashSet<_>>(), - ); - assert_eq!( - wakeups.block_numbers.get(&1).unwrap(), - &vec![b_b].into_iter().collect::<HashSet<_>>(), - ); - - wakeups.prune_finalized_wakeups(0); - - assert!(wakeups.block_numbers.get(&0).is_none()); - assert_eq!( - wakeups.block_numbers.get(&1).unwrap(), - &vec![b_b].into_iter().collect::<HashSet<_>>(), - ); - - wakeups.prune_finalized_wakeups(1); - - assert!(wakeups.block_numbers.get(&0).is_none()); - assert!(wakeups.block_numbers.get(&1).is_none()); - }); - - let aux_fut = Box::pin(async move { - clock_aux.inner.lock().set_tick(1); - // skip direct set to 3. - clock_aux.inner.lock().set_tick(4); - }); - - futures::executor::block_on(futures::future::join(test_fut, aux_fut)); -} - -#[test] -fn wakeup_earlier_supersedes_later() { - let mut wakeups = Wakeups::default(); - - let b_a = Hash::repeat_byte(0); - let c_a = CandidateHash(Hash::repeat_byte(2)); - - wakeups.schedule(b_a, 0, c_a, 4); - wakeups.schedule(b_a, 0, c_a, 2); - wakeups.schedule(b_a, 0, c_a, 3); - - let clock = MockClock::new(0); - let clock_aux = clock.clone(); - - let test_fut = Box::pin(async move { - assert_eq!(wakeups.next(&clock).await, (2, b_a, c_a)); - assert!(wakeups.first().is_none()); - assert!(wakeups.reverse_wakeups.is_empty()); - }); - - let aux_fut = Box::pin(async move { - clock_aux.inner.lock().set_tick(2); - }); - - futures::executor::block_on(futures::future::join(test_fut, aux_fut)); -} - -#[test] -fn import_checked_approval_sets_one_block_bit_at_a_time() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let candidate_receipt_2 = CandidateReceipt::<Hash> { - descriptor: CandidateDescriptor::default(), - commitments_hash: Hash::repeat_byte(0x02), - }; - let candidate_hash_2 = candidate_receipt_2.hash(); - - let validator_index_a = ValidatorIndex(0); - let validator_index_b = ValidatorIndex(1); - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { - validators: vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - ], - validator_groups: vec![ - vec![ValidatorIndex(0), ValidatorIndex(1)], - vec![ValidatorIndex(2)], - ], - needed_approvals: 2, - candidate_hash: Some(candidate_hash), - ..Default::default() - }, - &mut db, - ) - }; - - add_candidate_to_block( - &mut db, - block_hash, - candidate_hash_2, - 3, - CoreIndex(1), - GroupIndex(1), - Some(candidate_receipt_2), - ); - - let setup_candidate = |db: &mut DbBackend, c_hash| { - import_assignment(db, &c_hash, &block_hash, validator_index_a, |candidate_entry| { - let approval_entry = candidate_entry.approval_entry_mut(&block_hash).unwrap(); - approval_entry.import_assignment(0, validator_index_b, 0); - assert!(!candidate_entry.mark_approval(validator_index_a)); - }) - }; - - setup_candidate(&mut db, candidate_hash); - setup_candidate(&mut db, candidate_hash_2); - - let mut overlay_db = OverlayedBackend::new(&db); - let actions = import_checked_approval( - &state, - &mut overlay_db, - &Metrics(None), - db.load_block_entry(&block_hash).unwrap().unwrap(), - candidate_hash, - db.load_candidate_entry(&candidate_hash).unwrap().unwrap(), - ApprovalSource::Remote(validator_index_b), - ); - - assert_eq!(actions.len(), 0); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 2); - - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteBlockEntry(b_entry) => { - assert_eq!(b_entry.block_hash(), block_hash); - assert!(!b_entry.is_fully_approved()); - assert!(b_entry.is_candidate_approved(&candidate_hash)); - assert!(!b_entry.is_candidate_approved(&candidate_hash_2)); - } - ); - - assert_matches!( - write_ops.get(1).unwrap(), - BackendWriteOp::WriteCandidateEntry(c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved()); - } - ); - - db.write(write_ops).unwrap(); - - let mut overlay_db = OverlayedBackend::new(&db); - let actions = import_checked_approval( - &state, - &mut overlay_db, - &Metrics(None), - db.load_block_entry(&block_hash).unwrap().unwrap(), - candidate_hash_2, - db.load_candidate_entry(&candidate_hash_2).unwrap().unwrap(), - ApprovalSource::Remote(validator_index_b), - ); - - assert_eq!(actions.len(), 1); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 2); - - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteBlockEntry(b_entry) => { - assert_eq!(b_entry.block_hash(), block_hash); - assert!(b_entry.is_fully_approved()); - assert!(b_entry.is_candidate_approved(&candidate_hash)); - assert!(b_entry.is_candidate_approved(&candidate_hash_2)); - } - ); - - assert_matches!( - write_ops.get(1).unwrap(), - BackendWriteOp::WriteCandidateEntry(c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash_2); - assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved()); - } - ); -} - -#[test] -fn approved_ancestor_all_approved() { - let mut db = make_db(); - - let block_hash_1 = Hash::repeat_byte(0x01); - let block_hash_2 = Hash::repeat_byte(0x02); - let block_hash_3 = Hash::repeat_byte(0x03); - let block_hash_4 = Hash::repeat_byte(0x04); - - let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC)); - - let slot = Slot::from(1); - let session_index = 1; - - let add_block = |db: &mut DbBackend, block_hash, approved| { - add_block(db, block_hash, session_index, slot); - - let mut block_entry = db.load_block_entry(&block_hash).unwrap().unwrap(); - block_entry.add_candidate(CoreIndex(0), candidate_hash); - if approved { - block_entry.mark_approved_by_hash(&candidate_hash); - } - - overlay_txn(db, |overlay_db| overlay_db.write_block_entry(block_entry.clone())); - }; - - add_block(&mut db, block_hash_1, true); - add_block(&mut db, block_hash_2, true); - add_block(&mut db, block_hash_3, true); - add_block(&mut db, block_hash_4, true); - - let pool = TaskExecutor::new(); - let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone()); - - let test_fut = Box::pin(async move { - let overlay_db = OverlayedBackend::new(&db); - assert_matches!( - handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default()) - .await, - Ok(Some(HighestApprovedAncestorBlock { hash, number, .. } )) => { - assert_eq!((block_hash_4, 4), (hash, number)); - } - ) - }); - - let aux_fut = Box::pin(async move { - assert_matches!( - handle.recv().await, - AllMessages::ChainApi(ChainApiMessage::BlockNumber(target, tx)) => { - assert_eq!(target, block_hash_4); - let _ = tx.send(Ok(Some(4))); - } - ); - - assert_matches!( - handle.recv().await, - AllMessages::ChainApi(ChainApiMessage::Ancestors { - hash, - k, - response_channel: tx, - }) => { - assert_eq!(hash, block_hash_4); - assert_eq!(k, 4 - (0 + 1)); - let _ = tx.send(Ok(vec![block_hash_3, block_hash_2, block_hash_1])); - } - ); - }); - - futures::executor::block_on(futures::future::join(test_fut, aux_fut)); -} - -#[test] -fn approved_ancestor_missing_approval() { - let mut db = make_db(); - - let block_hash_1 = Hash::repeat_byte(0x01); - let block_hash_2 = Hash::repeat_byte(0x02); - let block_hash_3 = Hash::repeat_byte(0x03); - let block_hash_4 = Hash::repeat_byte(0x04); - - let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC)); - - let slot = Slot::from(1); - let session_index = 1; - - let add_block = |db: &mut DbBackend, block_hash, approved| { - add_block(db, block_hash, session_index, slot); - - let mut block_entry = db.load_block_entry(&block_hash).unwrap().unwrap(); - block_entry.add_candidate(CoreIndex(0), candidate_hash); - if approved { - block_entry.mark_approved_by_hash(&candidate_hash); - } - - overlay_txn(db, |overlay_db| overlay_db.write_block_entry(block_entry.clone())); - }; - - add_block(&mut db, block_hash_1, true); - add_block(&mut db, block_hash_2, true); - add_block(&mut db, block_hash_3, false); - add_block(&mut db, block_hash_4, true); - - let pool = TaskExecutor::new(); - let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone()); - - let test_fut = Box::pin(async move { - let overlay_db = OverlayedBackend::new(&db); - assert_matches!( - handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default()) - .await, - Ok(Some(HighestApprovedAncestorBlock { hash, number, .. })) => { - assert_eq!((block_hash_2, 2), (hash, number)); - } - ) - }); - - let aux_fut = Box::pin(async move { - assert_matches!( - handle.recv().await, - AllMessages::ChainApi(ChainApiMessage::BlockNumber(target, tx)) => { - assert_eq!(target, block_hash_4); - let _ = tx.send(Ok(Some(4))); - } - ); - - assert_matches!( - handle.recv().await, - AllMessages::ChainApi(ChainApiMessage::Ancestors { - hash, - k, - response_channel: tx, - }) => { - assert_eq!(hash, block_hash_4); - assert_eq!(k, 4 - (0 + 1)); - let _ = tx.send(Ok(vec![block_hash_3, block_hash_2, block_hash_1])); - } - ); - }); - - futures::executor::block_on(futures::future::join(test_fut, aux_fut)); -} - -#[test] -fn process_wakeup_trigger_assignment_launch_approval() { - let mut db = make_db(); - - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let slot = Slot::from(1); - let session_index = 1; - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state( - StateConfig { - validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob], - validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]], - needed_approvals: 2, - session_index, - slot, - candidate_hash: Some(candidate_hash), - ..Default::default() - }, - &mut db, - ) - }; - - let mut overlay_db = OverlayedBackend::new(&db); - let actions = process_wakeup(&state, &mut overlay_db, block_hash, candidate_hash, 1).unwrap(); - - assert!(actions.is_empty()); - assert_eq!(overlay_db.into_write_ops().count(), 0); - - set_our_assignment(&mut db, &candidate_hash, &block_hash, 0, |_| {}); - - let mut overlay_db = OverlayedBackend::new(&db); - let actions = process_wakeup(&state, &mut overlay_db, block_hash, candidate_hash, 1).unwrap(); - - assert_eq!(actions.len(), 2); - - assert_matches!( - actions.get(0).unwrap(), - Action::LaunchApproval { - candidate_index, - .. - } => { - assert_eq!(candidate_index, &0); - } - ); - - assert_matches!( - actions.get(1).unwrap(), - Action::ScheduleWakeup { - tick, - .. - } => { - assert_eq!(tick, &slot_to_tick(0 + 2)); - } - ); - - let write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert!(c_entry - .approval_entry(&block_hash) - .unwrap() - .our_assignment() - .unwrap() - .triggered() - ); - } - ); -} - -#[test] -fn process_wakeup_schedules_wakeup() { - let mut db = make_db(); - - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let slot = Slot::from(1); - let session_index = 1; - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(10))), - ..some_state( - StateConfig { - validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob], - validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]], - needed_approvals: 2, - session_index, - slot, - candidate_hash: Some(candidate_hash), - ..Default::default() - }, - &mut db, - ) - }; - - set_our_assignment(&mut db, &candidate_hash, &block_hash, 10, |_| {}); - - let mut overlay_db = OverlayedBackend::new(&db); - let actions = process_wakeup(&state, &mut overlay_db, block_hash, candidate_hash, 1).unwrap(); - - assert_eq!(actions.len(), 1); - assert_matches!( - actions.get(0).unwrap(), - Action::ScheduleWakeup { block_hash: b, candidate_hash: c, tick, .. } => { - assert_eq!(b, &block_hash); - assert_eq!(c, &candidate_hash); - assert_eq!(tick, &(slot_to_tick(slot) + 10)); - } - ); - assert_eq!(overlay_db.into_write_ops().count(), 0); -} - -#[test] -fn triggered_assignment_leads_to_recovery_and_validation() {} - -#[test] -fn finalization_event_prunes() {} - -#[test] -fn local_approval_import_always_updates_approval_entry() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let block_hash_2 = Hash::repeat_byte(0x02); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let validator_index = ValidatorIndex(0); - - let state_config = StateConfig { - validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie], - validator_groups: vec![vec![ValidatorIndex(0), ValidatorIndex(1)], vec![ValidatorIndex(2)]], - needed_approvals: 2, - candidate_hash: Some(candidate_hash), - ..Default::default() - }; - - let state = State { - assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| Ok(0))), - ..some_state(state_config.clone(), &mut db) - }; - - add_block(&mut db, block_hash_2, state_config.session_index, state_config.slot); - - add_candidate_to_block( - &mut db, - block_hash_2, - candidate_hash, - state_config.validators.len(), - 1.into(), - GroupIndex(1), - None, - ); - - let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, 1); - let sig_b = sign_approval(Sr25519Keyring::Alice, candidate_hash, 1); - - { - let mut import_local_assignment = |block_hash: Hash| { - set_our_assignment(&mut db, &candidate_hash, &block_hash, 0, |candidate_entry| { - let approval_entry = candidate_entry.approval_entry_mut(&block_hash).unwrap(); - assert!(approval_entry.trigger_our_assignment(0).is_some()); - assert!(approval_entry.local_statements().0.is_some()); - }); - }; - - import_local_assignment(block_hash); - import_local_assignment(block_hash_2); - } - - { - let mut overlay_db = OverlayedBackend::new(&db); - let actions = import_checked_approval( - &state, - &mut overlay_db, - &Metrics(None), - db.load_block_entry(&block_hash).unwrap().unwrap(), - candidate_hash, - db.load_candidate_entry(&candidate_hash).unwrap().unwrap(), - ApprovalSource::Local(validator_index, sig_a.clone()), - ); - - assert_eq!(actions.len(), 0); - - let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - - assert_matches!( - write_ops.get_mut(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert_eq!( - c_entry.approval_entry(&block_hash).unwrap().local_statements().1, - Some(sig_a), - ); - assert!(c_entry.mark_approval(validator_index)); - } - ); - - db.write(write_ops).unwrap(); - } - - { - let mut overlay_db = OverlayedBackend::new(&db); - let actions = import_checked_approval( - &state, - &mut overlay_db, - &Metrics(None), - db.load_block_entry(&block_hash_2).unwrap().unwrap(), - candidate_hash, - db.load_candidate_entry(&candidate_hash).unwrap().unwrap(), - ApprovalSource::Local(validator_index, sig_b.clone()), - ); - - assert_eq!(actions.len(), 0); - - let mut write_ops = overlay_db.into_write_ops().collect::<Vec<BackendWriteOp>>(); - assert_eq!(write_ops.len(), 1); - - assert_matches!( - write_ops.get_mut(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(ref mut c_entry) => { - assert_eq!(&c_entry.candidate.hash(), &candidate_hash); - assert_eq!( - c_entry.approval_entry(&block_hash_2).unwrap().local_statements().1, - Some(sig_b), - ); - assert!(c_entry.mark_approval(validator_index)); - } - ); - } -} - -// TODO [now]: handling `BecomeActive` action broadcasts everything. diff --git a/polkadot/node/core/approval-voting/src/persisted_entries.rs b/polkadot/node/core/approval-voting/src/persisted_entries.rs index aaeeda98b86..f5be1a5d885 100644 --- a/polkadot/node/core/approval-voting/src/persisted_entries.rs +++ b/polkadot/node/core/approval-voting/src/persisted_entries.rs @@ -217,12 +217,6 @@ impl ApprovalEntry { (None, None) } } - - /// For tests: set our assignment. - #[cfg(test)] - pub fn set_our_assignment(&mut self, our_assignment: OurAssignment) { - self.our_assignment = Some(our_assignment); - } } impl From<crate::approval_db::v1::ApprovalEntry> for ApprovalEntry { @@ -294,11 +288,6 @@ impl CandidateEntry { pub fn approval_entry(&self, block_hash: &Hash) -> Option<&ApprovalEntry> { self.block_assignments.get(block_hash) } - - #[cfg(test)] - pub fn add_approval_entry(&mut self, block_hash: Hash, approval_entry: ApprovalEntry) { - self.block_assignments.insert(block_hash, approval_entry); - } } impl From<crate::approval_db::v1::CandidateEntry> for CandidateEntry { @@ -384,26 +373,6 @@ impl BlockEntry { }) } - /// For tests: Add a candidate to the block entry. Returns the - /// index where the candidate was added. - /// - /// Panics if the core is already used. - #[cfg(test)] - pub fn add_candidate(&mut self, core: CoreIndex, candidate_hash: CandidateHash) -> usize { - let pos = self.candidates.binary_search_by_key(&core, |(c, _)| *c).unwrap_err(); - - self.candidates.insert(pos, (core, candidate_hash)); - - // bug in bitvec? - if pos < self.approved_bitfield.len() { - self.approved_bitfield.insert(pos, false); - } else { - self.approved_bitfield.push(false); - } - - pos - } - /// Get the slot of the block. pub fn slot(&self) -> Slot { self.slot diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index fc39815e3e9..9819991641c 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] // Copyright 2021 Parity Technologies (UK) Ltd. // This file is part of Polkadot. @@ -26,7 +25,9 @@ use polkadot_node_subsystem::{ use polkadot_node_subsystem_test_helpers as test_helpers; use polkadot_node_subsystem_util::TimeoutExt; use polkadot_overseer::HeadSupportsParachains; -use polkadot_primitives::v1::{CandidateEvent, CoreIndex, GroupIndex, Header, ValidatorSignature}; +use polkadot_primitives::v1::{ + CandidateEvent, CoreIndex, GroupIndex, Header, Id as ParaId, ValidatorSignature, +}; use std::time::Duration; use assert_matches::assert_matches; @@ -60,14 +61,9 @@ struct TestSyncOracle { struct TestSyncOracleHandle { done_syncing_receiver: oneshot::Receiver<()>, - flag: Arc<AtomicBool>, } impl TestSyncOracleHandle { - fn set_done(&self) { - self.flag.store(false, Ordering::SeqCst); - } - async fn await_mode_switch(self) { let _ = self.done_syncing_receiver.await; } @@ -92,19 +88,13 @@ impl SyncOracle for TestSyncOracle { } // val - result of `is_major_syncing`. -fn make_sync_oracle(val: bool) -> (TestSyncOracle, TestSyncOracleHandle) { +fn make_sync_oracle(val: bool) -> (Box<dyn SyncOracle + Send>, TestSyncOracleHandle) { let (tx, rx) = oneshot::channel(); let flag = Arc::new(AtomicBool::new(val)); + let oracle = TestSyncOracle { flag, done_syncing_sender: Arc::new(Mutex::new(Some(tx))) }; + let handle = TestSyncOracleHandle { done_syncing_receiver: rx }; - ( - TestSyncOracle { flag: flag.clone(), done_syncing_sender: Arc::new(Mutex::new(Some(tx))) }, - TestSyncOracleHandle { flag, done_syncing_receiver: rx }, - ) -} - -fn done_syncing_oracle() -> Box<dyn SyncOracle + Send> { - let (oracle, _) = make_sync_oracle(false); - Box::new(oracle) + (Box::new(oracle), handle) } #[cfg(test)] @@ -172,14 +162,16 @@ impl MockClockInner { fn wakeup_all(&mut self, up_to: Tick) { // This finds the position of the first wakeup after // the given tick, or the end of the map. - let drain_up_to = - self.wakeups.binary_search_by_key(&(up_to + 1), |w| w.0).unwrap_or_else(|i| i); - + let drain_up_to = self.wakeups.partition_point(|w| w.0 <= up_to); for (_, wakeup) in self.wakeups.drain(..drain_up_to) { let _ = wakeup.send(()); } } + fn next_wakeup(&self) -> Option<Tick> { + self.wakeups.iter().map(|w| w.0).next() + } + fn has_wakeup(&self, tick: Tick) -> bool { self.wakeups.binary_search_by_key(&tick, |w| w.0).is_ok() } @@ -196,8 +188,7 @@ impl MockClockInner { fn register_wakeup(&mut self, tick: Tick, pre_emptive: bool) -> oneshot::Receiver<()> { let (tx, rx) = oneshot::channel(); - let pos = self.wakeups.binary_search_by_key(&tick, |w| w.0).unwrap_or_else(|i| i); - + let pos = self.wakeups.partition_point(|w| w.0 <= tick); self.wakeups.insert(pos, (tick, tx)); if pre_emptive { @@ -215,7 +206,7 @@ struct MockAssignmentCriteria<Compute, Check>(Compute, Check); impl<Compute, Check> AssignmentCriteria for MockAssignmentCriteria<Compute, Check> where Compute: Fn() -> HashMap<polkadot_primitives::v1::CoreIndex, criteria::OurAssignment>, - Check: Fn() -> Result<DelayTranche, criteria::InvalidAssignment>, + Check: Fn(ValidatorIndex) -> Result<DelayTranche, criteria::InvalidAssignment>, { fn compute_assignments( &self, @@ -234,13 +225,13 @@ where fn check_assignment_cert( &self, _claimed_core_index: polkadot_primitives::v1::CoreIndex, - _validator_index: ValidatorIndex, + validator_index: ValidatorIndex, _config: &criteria::Config, _relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory, _assignment: &polkadot_node_primitives::approval::AssignmentCert, _backing_group: polkadot_primitives::v1::GroupIndex, ) -> Result<polkadot_node_primitives::approval::DelayTranche, criteria::InvalidAssignment> { - self.1() + self.1(validator_index) } } @@ -255,15 +246,15 @@ impl<F> } } -#[derive(Default)] -struct TestStore { +#[derive(Default, Clone)] +struct TestStoreInner { stored_block_range: Option<StoredBlockRange>, blocks_at_height: HashMap<BlockNumber, Vec<Hash>>, block_entries: HashMap<Hash, BlockEntry>, candidate_entries: HashMap<CandidateHash, CandidateEntry>, } -impl Backend for TestStore { +impl Backend for TestStoreInner { fn load_block_entry(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> { Ok(self.block_entries.get(block_hash).cloned()) } @@ -326,6 +317,49 @@ impl Backend for TestStore { } } +#[derive(Default, Clone)] +pub struct TestStore { + store: Arc<Mutex<TestStoreInner>>, +} + +impl Backend for TestStore { + fn load_block_entry(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> { + let store = self.store.lock(); + store.load_block_entry(block_hash) + } + + fn load_candidate_entry( + &self, + candidate_hash: &CandidateHash, + ) -> SubsystemResult<Option<CandidateEntry>> { + let store = self.store.lock(); + store.load_candidate_entry(candidate_hash) + } + + fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>> { + let store = self.store.lock(); + store.load_blocks_at_height(height) + } + + fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> { + let store = self.store.lock(); + store.load_all_blocks() + } + + fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> { + let store = self.store.lock(); + store.load_stored_blocks() + } + + fn write<I>(&mut self, ops: I) -> SubsystemResult<()> + where + I: IntoIterator<Item = BackendWriteOp>, + { + let mut store = self.store.lock(); + store.write(ops) + } +} + fn garbage_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert { let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT); let msg = b"test-garbage"; @@ -347,22 +381,75 @@ fn sign_approval( type VirtualOverseer = test_helpers::TestSubsystemContextHandle<ApprovalVotingMessage>; -struct TestHarness { - virtual_overseer: VirtualOverseer, - clock: Box<MockClock>, +#[derive(Default)] +struct HarnessConfigBuilder { + sync_oracle: Option<(Box<dyn SyncOracle + Send>, TestSyncOracleHandle)>, + clock: Option<MockClock>, + backend: Option<TestStore>, + assignment_criteria: Option<Box<dyn AssignmentCriteria + Send + Sync + 'static>>, +} + +impl HarnessConfigBuilder { + pub fn assignment_criteria( + &mut self, + assignment_criteria: Box<dyn AssignmentCriteria + Send + Sync + 'static>, + ) -> &mut Self { + self.assignment_criteria = Some(assignment_criteria); + self + } + + pub fn build(&mut self) -> HarnessConfig { + let (sync_oracle, sync_oracle_handle) = + self.sync_oracle.take().unwrap_or_else(|| make_sync_oracle(false)); + + let assignment_criteria = self + .assignment_criteria + .take() + .unwrap_or_else(|| Box::new(MockAssignmentCriteria::check_only(|_| Ok(0)))); + + HarnessConfig { + sync_oracle, + sync_oracle_handle, + clock: self.clock.take().unwrap_or_else(|| MockClock::new(0)), + backend: self.backend.take().unwrap_or_else(|| TestStore::default()), + assignment_criteria, + } + } } -#[derive(Default)] struct HarnessConfig { - tick_start: Tick, - assigned_tranche: DelayTranche, + sync_oracle: Box<dyn SyncOracle + Send>, + sync_oracle_handle: TestSyncOracleHandle, + clock: MockClock, + backend: TestStore, + assignment_criteria: Box<dyn AssignmentCriteria + Send + Sync + 'static>, +} + +impl HarnessConfig { + pub fn backend(&self) -> TestStore { + self.backend.clone() + } +} + +impl Default for HarnessConfig { + fn default() -> Self { + HarnessConfigBuilder::default().build() + } +} + +struct TestHarness { + virtual_overseer: VirtualOverseer, + clock: Box<MockClock>, + sync_oracle_handle: TestSyncOracleHandle, } fn test_harness<T: Future<Output = VirtualOverseer>>( config: HarnessConfig, - sync_oracle: Box<dyn SyncOracle + Send>, test: impl FnOnce(TestHarness) -> T, ) { + let HarnessConfig { sync_oracle, sync_oracle_handle, clock, backend, assignment_criteria } = + config; + let pool = sp_core::testing::TaskExecutor::new(); let (context, virtual_overseer) = test_helpers::make_subsystem_context(pool); @@ -372,26 +459,25 @@ fn test_harness<T: Future<Output = VirtualOverseer>>( Some(&Sr25519Keyring::Alice.to_seed()), ); - let store = TestStore::default(); - - let HarnessConfig { tick_start, assigned_tranche } = config; - - let clock = Box::new(MockClock::new(tick_start)); + let clock = Box::new(clock); let subsystem = run( context, ApprovalVotingSubsystem::with_config( - Config { col_data: test_constants::TEST_CONFIG.col_data, slot_duration_millis: 100u64 }, + Config { + col_data: test_constants::TEST_CONFIG.col_data, + slot_duration_millis: SLOT_DURATION_MILLIS, + }, Arc::new(kvdb_memorydb::create(test_constants::NUM_COLUMNS)), Arc::new(keystore), sync_oracle, Metrics::default(), ), clock.clone(), - Box::new(MockAssignmentCriteria::check_only(move || Ok(assigned_tranche))), - store, + assignment_criteria, + backend, ); - let test_fut = test(TestHarness { virtual_overseer, clock }); + let test_fut = test(TestHarness { virtual_overseer, clock, sync_oracle_handle }); futures::pin_mut!(test_fut); futures::pin_mut!(subsystem); @@ -443,387 +529,124 @@ async fn overseer_signal(overseer: &mut VirtualOverseer, signal: OverseerSignal) .expect(&format!("{:?} is more than enough for sending signals.", TIMEOUT)); } -#[test] -fn blank_subsystem_act_on_bad_block() { - let (oracle, handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; - - let (tx, rx) = oneshot::channel(); - - let bad_block_hash: Hash = Default::default(); - - overseer_send( - &mut virtual_overseer, - FromOverseer::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCert { - block_hash: bad_block_hash.clone(), - validator: 0u32.into(), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { - sample: 0, - }), - }, - 0u32, - tx, - ), - }, - ) - .await; - - handle.await_mode_switch().await; - - assert_matches!( - rx.await, - Ok( - AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(hash)) - ) => { - assert_eq!(hash, bad_block_hash); - } - ); - - virtual_overseer - }); +fn overlay_txn<T, F>(db: &mut T, mut f: F) +where + T: Backend, + F: FnMut(&mut OverlayedBackend<'_, T>), +{ + let mut overlay_db = OverlayedBackend::new(db); + f(&mut overlay_db); + let write_ops = overlay_db.into_write_ops(); + db.write(write_ops).unwrap(); } -#[test] -fn ss_rejects_approval_if_no_block_entry() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; - - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 0; - let validator = ValidatorIndex(0); - let candidate_hash = CandidateReceipt::<Hash>::default().hash(); - let session_index = 1; - - let rx = cai_approval( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - candidate_hash, - session_index, - false, - ) - .await; +fn make_candidate(para_id: ParaId, hash: &Hash) -> CandidateReceipt { + let mut r = CandidateReceipt::default(); + r.descriptor.para_id = para_id; + r.descriptor.relay_parent = hash.clone(); + r +} +async fn check_and_import_approval( + overseer: &mut VirtualOverseer, + block_hash: Hash, + candidate_index: CandidateIndex, + validator: ValidatorIndex, + candidate_hash: CandidateHash, + session_index: SessionIndex, + expect_chain_approved: bool, + expect_coordinator: bool, + signature_opt: Option<ValidatorSignature>, +) -> oneshot::Receiver<ApprovalCheckResult> { + let signature = signature_opt.unwrap_or(sign_approval( + Sr25519Keyring::Alice, + candidate_hash, + session_index, + )); + let (tx, rx) = oneshot::channel(); + overseer_send( + overseer, + FromOverseer::Communication { + msg: ApprovalVotingMessage::CheckAndImportApproval( + IndirectSignedApprovalVote { block_hash, candidate_index, validator, signature }, + tx, + ), + }, + ) + .await; + if expect_chain_approved { assert_matches!( - rx.await, - Ok(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))) => { - assert_eq!(hash, block_hash); + overseer_recv(overseer).await, + AllMessages::ChainSelection(ChainSelectionMessage::Approved(b_hash)) => { + assert_eq!(b_hash, block_hash); } ); - - virtual_overseer - }); -} - -#[test] -fn ss_rejects_approval_before_assignment() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; - - let block_hash = Hash::repeat_byte(0x01); - - let candidate_hash = { - let mut candidate_receipt = CandidateReceipt::<Hash>::default(); - candidate_receipt.descriptor.para_id = 1.into(); - candidate_receipt.descriptor.relay_parent = block_hash; - candidate_receipt.hash() - }; - - let candidate_index = 0; - let validator = ValidatorIndex(0); - let session_index = 1; - - // Add block hash 00. - ChainBuilder::new() - .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) - .build(&mut virtual_overseer) - .await; - - let rx = cai_approval( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - candidate_hash, - session_index, - false, - ) - .await; - + } + if expect_coordinator { assert_matches!( - rx.await, - Ok(ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(v))) => { - assert_eq!(v, validator); + overseer_recv(overseer).await, + AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::ImportStatements { + candidate_hash: c_hash, + pending_confirmation, + .. + }) => { + assert_eq!(c_hash, candidate_hash); + let _ = pending_confirmation.send(ImportStatementsResult::ValidImport); } ); - - virtual_overseer - }); + } + rx } -#[test] -fn ss_rejects_assignment_in_future() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness( - HarnessConfig { - tick_start: 0, - assigned_tranche: TICK_TOO_FAR_IN_FUTURE as _, - ..Default::default() +async fn check_and_import_assignment( + overseer: &mut VirtualOverseer, + block_hash: Hash, + candidate_index: CandidateIndex, + validator: ValidatorIndex, +) -> oneshot::Receiver<AssignmentCheckResult> { + let (tx, rx) = oneshot::channel(); + overseer_send( + overseer, + FromOverseer::Communication { + msg: ApprovalVotingMessage::CheckAndImportAssignment( + IndirectAssignmentCert { + block_hash, + validator, + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), + }, + candidate_index, + tx, + ), }, - Box::new(oracle), - |test_harness| async move { - let TestHarness { mut virtual_overseer, clock } = test_harness; - - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 0; - let validator = ValidatorIndex(0); - - // Add block hash 00. - ChainBuilder::new() - .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) - .build(&mut virtual_overseer) - .await; - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::TooFarInFuture)); - - // Advance clock to make assignment reasonably near. - clock.inner.lock().set_tick(1); - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + ) + .await; + rx +} - virtual_overseer - }, - ); +struct BlockConfig { + slot: Slot, + candidates: Option<Vec<(CandidateReceipt, CoreIndex, GroupIndex)>>, + session_info: Option<SessionInfo>, } -#[test] -fn ss_accepts_duplicate_assignment() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; +struct ChainBuilder { + blocks_by_hash: HashMap<Hash, (Header, BlockConfig)>, + blocks_at_height: BTreeMap<u32, Vec<Hash>>, +} - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 0; - let validator = ValidatorIndex(0); - - // Add block hash 00. - ChainBuilder::new() - .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) - .build(&mut virtual_overseer) - .await; - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate)); - - virtual_overseer - }); -} - -#[test] -fn ss_rejects_assignment_with_unknown_candidate() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; - - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 7; - let validator = ValidatorIndex(0); - - // Add block hash 00. - ChainBuilder::new() - .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) - .build(&mut virtual_overseer) - .await; - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!( - rx.await, - Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex( - candidate_index - ))), - ); - - virtual_overseer - }); -} - -#[test] -fn ss_accepts_and_imports_approval_after_assignment() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; - - let block_hash = Hash::repeat_byte(0x01); - - let candidate_hash = { - let mut candidate_receipt = CandidateReceipt::<Hash>::default(); - candidate_receipt.descriptor.para_id = 1.into(); - candidate_receipt.descriptor.relay_parent = block_hash; - candidate_receipt.hash() - }; - - let candidate_index = 0; - let validator = ValidatorIndex(0); - let session_index = 1; - - // Add block hash 0x01... - ChainBuilder::new() - .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) - .build(&mut virtual_overseer) - .await; - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - - let rx = cai_approval( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - candidate_hash, - session_index, - true, - ) - .await; - - assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); - - virtual_overseer - }); -} -#[test] -fn ss_assignment_import_updates_candidate_entry_and_schedules_wakeup() { - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; - - let block_hash = Hash::repeat_byte(0x01); - - let _candidate_hash = { - let mut candidate_receipt = CandidateReceipt::<Hash>::default(); - candidate_receipt.descriptor.para_id = 1.into(); - candidate_receipt.descriptor.relay_parent = block_hash; - candidate_receipt.hash() - }; - - let candidate_index = 0; - let validator = ValidatorIndex(0); - - // Add block hash 0x01... - ChainBuilder::new() - .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) - .build(&mut virtual_overseer) - .await; - - let rx = - cai_assignment(&mut virtual_overseer, block_hash, candidate_index, validator).await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - - // TODO(ladi): fix - //assert!(clock.inner.lock().has_wakeup(20)); - - virtual_overseer - }); -} - -async fn cai_approval( - overseer: &mut VirtualOverseer, - block_hash: Hash, - candidate_index: CandidateIndex, - validator: ValidatorIndex, - candidate_hash: CandidateHash, - session_index: SessionIndex, - expect_coordinator: bool, -) -> oneshot::Receiver<ApprovalCheckResult> { - let signature = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index); - let (tx, rx) = oneshot::channel(); - overseer_send( - overseer, - FromOverseer::Communication { - msg: ApprovalVotingMessage::CheckAndImportApproval( - IndirectSignedApprovalVote { block_hash, candidate_index, validator, signature }, - tx, - ), - }, - ) - .await; - - if expect_coordinator { - assert_matches!( - overseer_recv(overseer).await, - AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::ImportStatements { - pending_confirmation, - .. - }) => { - let _ = pending_confirmation.send(ImportStatementsResult::ValidImport); - } - ); - } - rx -} - -async fn cai_assignment( - overseer: &mut VirtualOverseer, - block_hash: Hash, - candidate_index: CandidateIndex, - validator: ValidatorIndex, -) -> oneshot::Receiver<AssignmentCheckResult> { - let (tx, rx) = oneshot::channel(); - overseer_send( - overseer, - FromOverseer::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCert { - block_hash, - validator, - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), - }, - candidate_index, - tx, - ), - }, - ) - .await; - rx -} - -struct ChainBuilder { - blocks_by_hash: HashMap<Hash, Header>, - blocks_at_height: BTreeMap<u32, Vec<Hash>>, -} - -impl ChainBuilder { - const GENESIS_HASH: Hash = Hash::repeat_byte(0xff); - const GENESIS_PARENT_HASH: Hash = Hash::repeat_byte(0x00); +impl ChainBuilder { + const GENESIS_HASH: Hash = Hash::repeat_byte(0xff); + const GENESIS_PARENT_HASH: Hash = Hash::repeat_byte(0x00); pub fn new() -> Self { let mut builder = Self { blocks_by_hash: HashMap::new(), blocks_at_height: BTreeMap::new() }; - builder.add_block_inner(Self::GENESIS_HASH, Self::GENESIS_PARENT_HASH, Slot::from(0), 0); + builder.add_block_inner( + Self::GENESIS_HASH, + Self::GENESIS_PARENT_HASH, + 0, + BlockConfig { slot: Slot::from(0), candidates: None, session_info: None }, + ); builder } @@ -831,8 +654,8 @@ impl ChainBuilder { &'a mut self, hash: Hash, parent_hash: Hash, - slot: Slot, number: u32, + config: BlockConfig, ) -> &'a mut Self { assert!(number != 0, "cannot add duplicate genesis block"); assert!(hash != Self::GENESIS_HASH, "cannot add block with genesis hash"); @@ -841,19 +664,19 @@ impl ChainBuilder { "cannot add block with genesis parent hash" ); assert!(self.blocks_by_hash.len() < u8::MAX.into()); - self.add_block_inner(hash, parent_hash, slot, number) + self.add_block_inner(hash, parent_hash, number, config) } fn add_block_inner<'a>( &'a mut self, hash: Hash, parent_hash: Hash, - slot: Slot, number: u32, + config: BlockConfig, ) -> &'a mut Self { - let header = ChainBuilder::make_header(parent_hash, slot, number); + let header = ChainBuilder::make_header(parent_hash, config.slot, number); assert!( - self.blocks_by_hash.insert(hash, header).is_none(), + self.blocks_by_hash.insert(hash, (header, config)).is_none(), "block with hash {:?} already exists", hash, ); @@ -865,15 +688,19 @@ impl ChainBuilder { for (number, blocks) in self.blocks_at_height.iter() { for (i, hash) in blocks.iter().enumerate() { let mut cur_hash = *hash; + let (_, block_config) = + self.blocks_by_hash.get(&cur_hash).expect("block not found"); let mut ancestry = Vec::new(); while cur_hash != Self::GENESIS_PARENT_HASH { - let cur_header = + let (cur_header, _) = self.blocks_by_hash.get(&cur_hash).expect("chain is not contiguous"); ancestry.push((cur_hash, cur_header.clone())); cur_hash = cur_header.parent_hash; } ancestry.reverse(); - import_block(overseer, ancestry.as_ref(), *number, false, i > 0).await; + + import_block(overseer, ancestry.as_ref(), *number, block_config, false, i > 0) + .await; let _: Option<()> = future::pending().timeout(Duration::from_millis(100)).await; } } @@ -902,31 +729,40 @@ impl ChainBuilder { async fn import_block( overseer: &mut VirtualOverseer, hashes: &[(Hash, Header)], - session: u32, + number: u32, + config: &BlockConfig, gap: bool, fork: bool, ) { - let validators = vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob]; - let session_info = SessionInfo { - validators: validators.iter().map(|v| v.public().into()).collect(), - discovery_keys: validators.iter().map(|v| v.public().into()).collect(), - assignment_keys: validators.iter().map(|v| v.public().into()).collect(), - validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]], - n_cores: 6, - needed_approvals: 1, - zeroth_delay_tranche_width: 5, - relay_vrf_modulo_samples: 3, - n_delay_tranches: 50, - no_show_slots: 2, - }; - let (new_head, new_header) = &hashes[hashes.len() - 1]; + let candidates = config.candidates.clone().unwrap_or(vec![( + make_candidate(0.into(), &new_head), + CoreIndex(0), + GroupIndex(0), + )]); + + let session_info = config.session_info.clone().unwrap_or({ + let validators = vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob]; + SessionInfo { + validators: validators.iter().map(|v| v.public().into()).collect(), + discovery_keys: validators.iter().map(|v| v.public().into()).collect(), + assignment_keys: validators.iter().map(|v| v.public().into()).collect(), + validator_groups: vec![vec![ValidatorIndex(0)], vec![ValidatorIndex(1)]], + n_cores: validators.len() as _, + needed_approvals: 1, + zeroth_delay_tranche_width: 5, + relay_vrf_modulo_samples: 3, + n_delay_tranches: 50, + no_show_slots: 2, + } + }); + overseer_send( overseer, FromOverseer::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work( ActivatedLeaf { hash: *new_head, - number: session, + number, status: LeafStatus::Fresh, span: Arc::new(jaeger::Span::Disabled), }, @@ -950,9 +786,9 @@ async fn import_block( RuntimeApiRequest::SessionIndexForChild(s_tx) ) ) => { - let hash = &hashes[session.saturating_sub(1) as usize]; + let hash = &hashes[number.saturating_sub(1) as usize]; assert_eq!(req_block_hash, hash.0.clone()); - s_tx.send(Ok(session.into())).unwrap(); + s_tx.send(Ok(number.into())).unwrap(); } ); @@ -965,7 +801,7 @@ async fn import_block( RuntimeApiRequest::SessionInfo(idx, si_tx), ) ) => { - assert_eq!(session, idx); + assert_eq!(number, idx); assert_eq!(req_block_hash, *new_head); si_tx.send(Ok(Some(session_info.clone()))).unwrap(); } @@ -1000,7 +836,7 @@ async fn import_block( response_channel, }) => { assert_eq!(hash, *new_head); - assert_eq!(k as u32, session - 1); + assert_eq!(k as u32, number - 1); let history: Vec<Hash> = hashes.iter().map(|v| v.0).take(k).collect(); response_channel.send(Ok(history)).unwrap(); }, @@ -1010,25 +846,13 @@ async fn import_block( } } - if session > 0 { + if number > 0 { assert_matches!( overseer_recv(overseer).await, AllMessages::RuntimeApi( RuntimeApiMessage::Request(hash, RuntimeApiRequest::CandidateEvents(c_tx)) ) => { assert_eq!(hash, *new_head); - - let make_candidate = |para_id| { - let mut r = CandidateReceipt::default(); - r.descriptor.para_id = para_id; - r.descriptor.relay_parent = hash; - r - }; - let candidates = vec![ - (make_candidate(1.into()), CoreIndex(0), GroupIndex(2)), - (make_candidate(2.into()), CoreIndex(1), GroupIndex(3)), - ]; - let inclusion_events = candidates.into_iter() .map(|(r, c, g)| CandidateEvent::CandidateIncluded(r, Vec::new().into(), c, g)) .collect::<Vec<_>>(); @@ -1044,9 +868,9 @@ async fn import_block( RuntimeApiRequest::SessionIndexForChild(s_tx) ) ) => { - let hash = &hashes[(session-1) as usize]; + let hash = &hashes[(number-1) as usize]; assert_eq!(req_block_hash, hash.0.clone()); - s_tx.send(Ok(session.into())).unwrap(); + s_tx.send(Ok(number.into())).unwrap(); } ); @@ -1058,10 +882,10 @@ async fn import_block( RuntimeApiRequest::CurrentBabeEpoch(c_tx), ) ) => { - let hash = &hashes[session as usize]; + let hash = &hashes[number as usize]; assert_eq!(req_block_hash, hash.0.clone()); let _ = c_tx.send(Ok(BabeEpoch { - epoch_index: session as _, + epoch_index: number as _, start_slot: Slot::from(0), duration: 200, authorities: vec![(Sr25519Keyring::Alice.public().into(), 1)], @@ -1075,7 +899,7 @@ async fn import_block( ); } - if session == 0 { + if number == 0 { assert_matches!( overseer_recv(overseer).await, AllMessages::ApprovalDistribution(ApprovalDistributionMessage::NewBlocks(v)) => { @@ -1090,44 +914,123 @@ async fn import_block( ) => { assert_eq!(approval_vec.len(), 1); let metadata = approval_vec.pop().unwrap(); - let hash = &hashes[session as usize]; - let parent_hash = &hashes[(session - 1) as usize]; + let hash = &hashes[number as usize]; + let parent_hash = &hashes[(number - 1) as usize]; assert_eq!(metadata.hash, hash.0.clone()); assert_eq!(metadata.parent_hash, parent_hash.0.clone()); - assert_eq!(metadata.slot, Slot::from(session as u64)); + assert_eq!(metadata.slot, config.slot); } ); } } #[test] -fn linear_import_act_on_leaf() { - let session = 3u32; +fn subsystem_rejects_bad_assignment_ok_criteria() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { - let TestHarness { mut virtual_overseer, .. } = test_harness; + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); - let mut head: Hash = ChainBuilder::GENESIS_HASH; + let head: Hash = ChainBuilder::GENESIS_HASH; let mut builder = ChainBuilder::new(); - for i in 1..session { - let slot = Slot::from(i as u64); + let slot = Slot::from(1 as u64); + builder.add_block( + block_hash, + head, + 1, + BlockConfig { slot, candidates: None, session_info: None }, + ); + builder.build(&mut virtual_overseer).await; - let hash = Hash::repeat_byte(i as u8); - builder.add_block(hash, head, slot, i); - head = hash; - } + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); + + // unknown hash + let unknown_hash = Hash::repeat_byte(0x02); + + let rx = check_and_import_assignment( + &mut virtual_overseer, + unknown_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!( + rx.await, + Ok(AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(unknown_hash))), + ); + + virtual_overseer + }); +} + +#[test] +fn subsystem_rejects_bad_assignment_err_criteria() { + let assignment_criteria = + Box::new(MockAssignmentCriteria::check_only(move |_| Err(criteria::InvalidAssignment))); + let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); + test_harness(config, |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); + let head: Hash = ChainBuilder::GENESIS_HASH; + let mut builder = ChainBuilder::new(); + let slot = Slot::from(1 as u64); + builder.add_block( + block_hash, + head, + 1, + BlockConfig { slot, candidates: None, session_info: None }, + ); builder.build(&mut virtual_overseer).await; + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!( + rx.await, + Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(ValidatorIndex(0)))), + ); + + virtual_overseer + }); +} + +#[test] +fn blank_subsystem_act_on_bad_block() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle, .. } = test_harness; + let (tx, rx) = oneshot::channel(); + let bad_block_hash: Hash = Default::default(); + overseer_send( &mut virtual_overseer, FromOverseer::Communication { msg: ApprovalVotingMessage::CheckAndImportAssignment( IndirectAssignmentCert { - block_hash: head, + block_hash: bad_block_hash.clone(), validator: 0u32.into(), cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0, @@ -1140,61 +1043,1515 @@ fn linear_import_act_on_leaf() { ) .await; - assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + sync_oracle_handle.await_mode_switch().await; + + assert_matches!( + rx.await, + Ok( + AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(hash)) + ) => { + assert_eq!(hash, bad_block_hash); + } + ); virtual_overseer }); } #[test] -fn forkful_import_at_same_height_act_on_leaf() { - let session = 3u32; +fn subsystem_rejects_approval_if_no_candidate_entry() { + let config = HarnessConfig::default(); + let store = config.backend(); + test_harness(config, |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; - let (oracle, _handle) = make_sync_oracle(false); - test_harness(Default::default(), Box::new(oracle), |test_harness| async move { + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); + + let candidate_descriptor = make_candidate(1.into(), &block_hash); + let candidate_hash = candidate_descriptor.hash(); + + let head: Hash = ChainBuilder::GENESIS_HASH; + let mut builder = ChainBuilder::new(); + let slot = Slot::from(1 as u64); + builder.add_block( + block_hash, + head, + 1, + BlockConfig { + slot, + candidates: Some(vec![(candidate_descriptor, CoreIndex(1), GroupIndex(1))]), + session_info: None, + }, + ); + builder.build(&mut virtual_overseer).await; + + overlay_txn(&mut store.clone(), |overlay_db| { + overlay_db.delete_candidate_entry(&candidate_hash) + }); + + let session_index = 1; + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + false, + false, + None, + ) + .await; + + assert_matches!( + rx.await, + Ok(ApprovalCheckResult::Bad(ApprovalCheckError::InvalidCandidate(0, hash))) => { + assert_eq!(candidate_hash, hash); + } + ); + + virtual_overseer + }); +} + +#[test] +fn subsystem_rejects_approval_if_no_block_entry() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); + let candidate_hash = CandidateReceipt::<Hash>::default().hash(); + let session_index = 1; + + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + false, + false, + None, + ) + .await; + + assert_matches!( + rx.await, + Ok(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))) => { + assert_eq!(hash, block_hash); + } + ); + + virtual_overseer + }); +} + +#[test] +fn subsystem_rejects_approval_before_assignment() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + + let candidate_hash = { + let mut candidate_receipt = CandidateReceipt::<Hash>::default(); + candidate_receipt.descriptor.para_id = 0.into(); + candidate_receipt.descriptor.relay_parent = block_hash; + candidate_receipt.hash() + }; + + let candidate_index = 0; + let validator = ValidatorIndex(0); + let session_index = 1; + + // Add block hash 00. + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(1), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + false, + false, + None, + ) + .await; + + assert_matches!( + rx.await, + Ok(ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(v))) => { + assert_eq!(v, validator); + } + ); + + virtual_overseer + }); +} + +#[test] +fn subsystem_rejects_assignment_in_future() { + let assignment_criteria = + Box::new(MockAssignmentCriteria::check_only(|_| Ok(TICK_TOO_FAR_IN_FUTURE as _))); + let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); + test_harness(config, |test_harness| async move { + let TestHarness { mut virtual_overseer, clock, sync_oracle_handle: _sync_oracle_handle } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); + + // Add block hash 00. + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(0), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::TooFarInFuture)); + + // Advance clock to make assignment reasonably near. + clock.inner.lock().set_tick(9); + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + virtual_overseer + }); +} + +#[test] +fn subsystem_accepts_duplicate_assignment() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); + + // Add block hash 00. + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(1), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate)); + + virtual_overseer + }); +} + +#[test] +fn subsystem_rejects_assignment_with_unknown_candidate() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 7; + let validator = ValidatorIndex(0); + + // Add block hash 00. + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(1), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!( + rx.await, + Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex( + candidate_index + ))), + ); + + virtual_overseer + }); +} + +#[test] +fn subsystem_accepts_and_imports_approval_after_assignment() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + + let candidate_hash = { + let mut candidate_receipt = CandidateReceipt::<Hash>::default(); + candidate_receipt.descriptor.para_id = 0.into(); + candidate_receipt.descriptor.relay_parent = block_hash; + candidate_receipt.hash() + }; + + let candidate_index = 0; + let validator = ValidatorIndex(0); + let session_index = 1; + + // Add block hash 0x01... + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(1), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + true, + true, + None, + ) + .await; + + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); + + virtual_overseer + }); +} + +#[test] +fn subsystem_second_approval_import_only_schedules_wakeups() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { + mut virtual_overseer, + clock, + sync_oracle_handle: _sync_oracle_handle, + .. + } = test_harness; + + let block_hash = Hash::repeat_byte(0x01); + + let candidate_hash = { + let mut candidate_receipt = CandidateReceipt::<Hash>::default(); + candidate_receipt.descriptor.para_id = 0.into(); + candidate_receipt.descriptor.relay_parent = block_hash; + candidate_receipt.hash() + }; + + let candidate_index = 0; + let validator = ValidatorIndex(0); + let session_index = 1; + + // Add block hash 0x01... + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(0), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + true, + true, + None, + ) + .await; + + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); + + // The clock should already have wakeups from the prior operations. Clear them to assert + // that the second approval adds more wakeups. + assert!(clock.inner.lock().has_wakeup(20)); + clock.inner.lock().wakeup_all(20); + assert!(!clock.inner.lock().has_wakeup(20)); + + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + false, + false, + None, + ) + .await; + + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); + + assert!(clock.inner.lock().has_wakeup(20)); + + virtual_overseer + }); +} + +#[test] +fn subsystem_assignment_import_updates_candidate_entry_and_schedules_wakeup() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { + mut virtual_overseer, + clock, + sync_oracle_handle: _sync_oracle_handle, + .. + } = test_harness; + + let block_hash = Hash::repeat_byte(0x01); + + let candidate_index = 0; + let validator = ValidatorIndex(0); + + // Add block hash 0x01... + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(1), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + assert!(clock.inner.lock().has_wakeup(20)); + + virtual_overseer + }); +} + +#[test] +fn subsystem_process_wakeup_schedules_wakeup() { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { + mut virtual_overseer, + clock, + sync_oracle_handle: _sync_oracle_handle, + .. + } = test_harness; + + let block_hash = Hash::repeat_byte(0x01); + + let candidate_index = 0; + let validator = ValidatorIndex(0); + + // Add block hash 0x01... + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { slot: Slot::from(1), candidates: None, session_info: None }, + ) + .build(&mut virtual_overseer) + .await; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + assert!(clock.inner.lock().has_wakeup(20)); + + // Activate the wakeup present above, and sleep to allow process_wakeups to execute.. + clock.inner.lock().wakeup_all(20); + futures_timer::Delay::new(Duration::from_millis(100)).await; + + // The wakeup should have been rescheduled. + assert!(clock.inner.lock().has_wakeup(20)); + + virtual_overseer + }); +} + +#[test] +fn linear_import_act_on_leaf() { + let session = 3u32; + + test_harness(HarnessConfig::default(), |test_harness| async move { let TestHarness { mut virtual_overseer, .. } = test_harness; - let mut head: Hash = ChainBuilder::GENESIS_HASH; - let mut builder = ChainBuilder::new(); - for i in 1..session { - let slot = Slot::from(i as u64); - let hash = Hash::repeat_byte(i as u8); - builder.add_block(hash, head, slot, i); - head = hash; + let mut head: Hash = ChainBuilder::GENESIS_HASH; + let mut builder = ChainBuilder::new(); + for i in 1..session { + let slot = Slot::from(i as u64); + + let hash = Hash::repeat_byte(i as u8); + builder.add_block( + hash, + head, + i, + BlockConfig { slot, candidates: None, session_info: None }, + ); + head = hash; + } + + builder.build(&mut virtual_overseer).await; + + let (tx, rx) = oneshot::channel(); + + overseer_send( + &mut virtual_overseer, + FromOverseer::Communication { + msg: ApprovalVotingMessage::CheckAndImportAssignment( + IndirectAssignmentCert { + block_hash: head, + validator: 0u32.into(), + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { + sample: 0, + }), + }, + 0u32, + tx, + ), + }, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + + virtual_overseer + }); +} + +#[test] +fn forkful_import_at_same_height_act_on_leaf() { + let session = 3u32; + + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let mut head: Hash = ChainBuilder::GENESIS_HASH; + let mut builder = ChainBuilder::new(); + for i in 1..session { + let slot = Slot::from(i as u64); + let hash = Hash::repeat_byte(i as u8); + builder.add_block( + hash, + head, + i, + BlockConfig { slot, candidates: None, session_info: None }, + ); + head = hash; + } + let num_forks = 3; + let forks = Vec::new(); + + for i in 0..num_forks { + let slot = Slot::from(session as u64); + let hash = Hash::repeat_byte(session as u8 + i); + builder.add_block( + hash, + head, + session, + BlockConfig { slot, candidates: None, session_info: None }, + ); + } + builder.build(&mut virtual_overseer).await; + + for head in forks.into_iter() { + let (tx, rx) = oneshot::channel(); + + overseer_send( + &mut virtual_overseer, + FromOverseer::Communication { + msg: ApprovalVotingMessage::CheckAndImportAssignment( + IndirectAssignmentCert { + block_hash: head, + validator: 0u32.into(), + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { + sample: 0, + }), + }, + 0u32, + tx, + ), + }, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + } + virtual_overseer + }); +} + +#[test] +fn import_checked_approval_updates_entries_and_schedules() { + let config = HarnessConfig::default(); + let store = config.backend(); + test_harness(config, |test_harness| async move { + let TestHarness { + mut virtual_overseer, + clock, + sync_oracle_handle: _sync_oracle_handle, + .. + } = test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let validator_index_a = ValidatorIndex(0); + let validator_index_b = ValidatorIndex(1); + + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Eve, + ]; + let session_info = SessionInfo { + validators: validators.iter().map(|v| v.public().into()).collect(), + validator_groups: vec![ + vec![ValidatorIndex(0), ValidatorIndex(1)], + vec![ValidatorIndex(2)], + vec![ValidatorIndex(3), ValidatorIndex(4)], + ], + needed_approvals: 2, + discovery_keys: validators.iter().map(|v| v.public().into()).collect(), + assignment_keys: validators.iter().map(|v| v.public().into()).collect(), + n_cores: validators.len() as _, + zeroth_delay_tranche_width: 5, + relay_vrf_modulo_samples: 3, + n_delay_tranches: 50, + no_show_slots: 2, + }; + + let candidate_descriptor = make_candidate(1.into(), &block_hash); + let candidate_hash = candidate_descriptor.hash(); + + let head: Hash = ChainBuilder::GENESIS_HASH; + let mut builder = ChainBuilder::new(); + let slot = Slot::from(1 as u64); + builder.add_block( + block_hash, + head, + 1, + BlockConfig { + slot, + candidates: Some(vec![(candidate_descriptor, CoreIndex(0), GroupIndex(0))]), + session_info: Some(session_info), + }, + ); + builder.build(&mut virtual_overseer).await; + + let candidate_index = 0; + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator_index_a, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); + + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator_index_b, + ) + .await; + + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); + + // Clear any wake ups from the assignment imports. + assert!(clock.inner.lock().has_wakeup(20)); + clock.inner.lock().wakeup_all(20); + + let session_index = 1; + let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index); + + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator_index_a, + candidate_hash, + session_index, + false, + true, + Some(sig_a), + ) + .await; + + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted),); + + // Sleep to ensure we get a consistent read on the database. + futures_timer::Delay::new(Duration::from_millis(100)).await; + + // The candidate should not yet be approved and a wakeup should be scheduled on the first + // approval. + let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap(); + assert!(!candidate_entry.approval_entry(&block_hash).unwrap().is_approved()); + assert!(clock.inner.lock().has_wakeup(20)); + + // Clear the wake ups to assert that later approval also schedule wakeups. + clock.inner.lock().wakeup_all(20); + + let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index); + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator_index_b, + candidate_hash, + session_index, + true, + true, + Some(sig_b), + ) + .await; + + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted),); + + // Sleep to ensure we get a consistent read on the database. + // + // NOTE: Since the response above occurs before writing to the database, we are somewhat + // breaking the external consistency of the API by reaching into the database directly. + // Under normal operation, this wouldn't be necessary, since all requests are serialized by + // the event loop and we write at the end of each pass. However, if the database write were + // to fail, a downstream subsystem may expect for this candidate to be approved, and + // possibly take further actions on the assumption that the candidate is approved, when + // that may not be the reality from the database's perspective. This could be avoided + // entirely by having replies processed after database writes, but that would constitute a + // larger refactor and incur a performance penalty. + futures_timer::Delay::new(Duration::from_millis(100)).await; + + // The candidate should now be approved. + let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap(); + assert!(candidate_entry.approval_entry(&block_hash).unwrap().is_approved()); + assert!(clock.inner.lock().has_wakeup(20)); + + virtual_overseer + }); +} + +#[test] +fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() { + let config = HarnessConfig::default(); + let store = config.backend(); + test_harness(config, |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hash = Hash::repeat_byte(0x01); + + let candidate_receipt1 = { + let mut receipt = CandidateReceipt::<Hash>::default(); + receipt.descriptor.para_id = 1.into(); + receipt + }; + let candidate_receipt2 = { + let mut receipt = CandidateReceipt::<Hash>::default(); + receipt.descriptor.para_id = 2.into(); + receipt + }; + let candidate_hash1 = candidate_receipt1.hash(); + let candidate_hash2 = candidate_receipt2.hash(); + let candidate_index1 = 0; + let candidate_index2 = 1; + + let validator1 = ValidatorIndex(0); + let validator2 = ValidatorIndex(1); + let session_index = 1; + + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Eve, + ]; + let session_info = SessionInfo { + validators: validators.iter().map(|v| v.public().into()).collect(), + validator_groups: vec![ + vec![ValidatorIndex(0), ValidatorIndex(1)], + vec![ValidatorIndex(2)], + vec![ValidatorIndex(3), ValidatorIndex(4)], + ], + needed_approvals: 2, + discovery_keys: validators.iter().map(|v| v.public().into()).collect(), + assignment_keys: validators.iter().map(|v| v.public().into()).collect(), + n_cores: validators.len() as _, + zeroth_delay_tranche_width: 5, + relay_vrf_modulo_samples: 3, + n_delay_tranches: 50, + no_show_slots: 2, + }; + + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { + slot: Slot::from(0), + candidates: Some(vec![ + (candidate_receipt1, CoreIndex(1), GroupIndex(1)), + (candidate_receipt2, CoreIndex(1), GroupIndex(1)), + ]), + session_info: Some(session_info), + }, + ) + .build(&mut virtual_overseer) + .await; + + let assignments = vec![ + (candidate_index1, validator1), + (candidate_index2, validator1), + (candidate_index1, validator2), + (candidate_index2, validator2), + ]; + + for (candidate_index, validator) in assignments { + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ) + .await; + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); } - let num_forks = 3; - let forks = Vec::new(); - for i in 0..num_forks { - let slot = Slot::from(session as u64); - let hash = Hash::repeat_byte(session as u8 + i); - builder.add_block(hash, head, slot, session); + let approvals = vec![ + (candidate_index1, validator1, candidate_hash1), + (candidate_index1, validator2, candidate_hash1), + (candidate_index2, validator1, candidate_hash2), + (candidate_index2, validator2, candidate_hash2), + ]; + + for (i, (candidate_index, validator, candidate_hash)) in approvals.iter().enumerate() { + let expect_candidate1_approved = i >= 1; + let expect_candidate2_approved = i >= 3; + let expect_block_approved = expect_candidate2_approved; + + let signature = if *validator == validator1 { + sign_approval(Sr25519Keyring::Alice, *candidate_hash, session_index) + } else { + sign_approval(Sr25519Keyring::Bob, *candidate_hash, session_index) + }; + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + *candidate_index, + *validator, + *candidate_hash, + session_index, + expect_block_approved, + true, + Some(signature), + ) + .await; + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); + + // Sleep to get a consistent read on the database. + futures_timer::Delay::new(Duration::from_millis(200)).await; + + let block_entry = store.load_block_entry(&block_hash).unwrap().unwrap(); + assert_eq!(block_entry.is_fully_approved(), expect_block_approved); + assert_eq!( + block_entry.is_candidate_approved(&candidate_hash1), + expect_candidate1_approved + ); + assert_eq!( + block_entry.is_candidate_approved(&candidate_hash2), + expect_candidate2_approved + ); + } + + virtual_overseer + }); +} + +fn approved_ancestor_test( + skip_approval: impl Fn(BlockNumber) -> bool, + approved_height: BlockNumber, +) { + test_harness(HarnessConfig::default(), |test_harness| async move { + let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = + test_harness; + + let block_hashes = vec![ + Hash::repeat_byte(0x01), + Hash::repeat_byte(0x02), + Hash::repeat_byte(0x03), + Hash::repeat_byte(0x04), + ]; + + let candidate_receipts: Vec<_> = block_hashes + .iter() + .enumerate() + .map(|(i, hash)| { + let mut candidate_receipt = CandidateReceipt::<Hash>::default(); + candidate_receipt.descriptor.para_id = i.into(); + candidate_receipt.descriptor.relay_parent = *hash; + candidate_receipt + }) + .collect(); + + let candidate_hashes: Vec<_> = candidate_receipts.iter().map(|r| r.hash()).collect(); + + let candidate_index = 0; + let validator = ValidatorIndex(0); + + let mut builder = ChainBuilder::new(); + for (i, (block_hash, candidate_receipt)) in + block_hashes.iter().zip(candidate_receipts).enumerate() + { + let parent_hash = if i == 0 { ChainBuilder::GENESIS_HASH } else { block_hashes[i - 1] }; + builder.add_block( + *block_hash, + parent_hash, + i as u32 + 1, + BlockConfig { + slot: Slot::from(i as u64), + candidates: Some(vec![(candidate_receipt, CoreIndex(0), GroupIndex(0))]), + session_info: None, + }, + ); } builder.build(&mut virtual_overseer).await; - for head in forks.into_iter() { - let (tx, rx) = oneshot::channel(); + for (i, (block_hash, candidate_hash)) in + block_hashes.iter().zip(candidate_hashes).enumerate() + { + let rx = check_and_import_assignment( + &mut virtual_overseer, + *block_hash, + candidate_index, + validator, + ) + .await; + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - overseer_send( + if skip_approval(i as BlockNumber + 1) { + continue + } + + let rx = check_and_import_approval( &mut virtual_overseer, - FromOverseer::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCert { - block_hash: head, - validator: 0u32.into(), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { - sample: 0, - }), - }, - 0u32, - tx, - ), + *block_hash, + candidate_index, + validator, + candidate_hash, + i as u32 + 1, + true, + true, + None, + ) + .await; + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); + } + + let target = block_hashes[block_hashes.len() - 1]; + let block_number = block_hashes.len(); + + let (tx, rx) = oneshot::channel(); + overseer_send( + &mut virtual_overseer, + FromOverseer::Communication { + msg: ApprovalVotingMessage::ApprovedAncestor(target, 0, tx), + }, + ) + .await; + + assert_matches!( + overseer_recv(&mut virtual_overseer).await, + AllMessages::ChainApi(ChainApiMessage::BlockNumber(hash, tx)) => { + assert_eq!(target, hash); + tx.send(Ok(Some(block_number as BlockNumber))).unwrap(); + } + ); + + assert_matches!( + overseer_recv(&mut virtual_overseer).await, + AllMessages::ChainApi(ChainApiMessage::Ancestors { + hash, + k, + response_channel: tx, + }) => { + assert_eq!(target, hash); + assert_eq!(k, block_number - (0 + 1)); + let ancestors = block_hashes.iter() + .take(block_number-1) + .rev() + .cloned() + .collect::<Vec<Hash>>(); + tx.send(Ok(ancestors)).unwrap(); + } + ); + + let approved_hash = block_hashes[approved_height as usize - 1]; + let HighestApprovedAncestorBlock { hash, number, .. } = rx.await.unwrap().unwrap(); + assert_eq!(approved_hash, hash); + assert_eq!(number, approved_height); + + virtual_overseer + }); +} + +#[test] +fn subsystem_approved_ancestor_all_approved() { + // Don't skip any approvals, highest approved ancestor should be 4. + approved_ancestor_test(|_| false, 4); +} + +#[test] +fn subsystem_approved_ancestor_missing_approval() { + // Skip approval for the third block, highest approved ancestor should be 2. + approved_ancestor_test(|i| i == 3, 2); +} + +#[test] +fn subsystem_process_wakeup_trigger_assignment_launch_approval() { + let assignment_criteria = Box::new(MockAssignmentCriteria( + || { + let mut assignments = HashMap::new(); + let _ = assignments.insert( + CoreIndex(0), + approval_db::v1::OurAssignment { + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), + tranche: 0, + validator_index: ValidatorIndex(0), + triggered: false, + } + .into(), + ); + assignments + }, + |_| Ok(0), + )); + let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); + let store = config.backend(); + + test_harness(config, |test_harness| async move { + let TestHarness { + mut virtual_overseer, + clock, + sync_oracle_handle: _sync_oracle_handle, + .. + } = test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_receipt = CandidateReceipt::<Hash>::default(); + let candidate_hash = candidate_receipt.hash(); + let slot = Slot::from(1); + let candidate_index = 0; + + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Eve, + ]; + let session_info = SessionInfo { + validators: validators.iter().map(|v| v.public().into()).collect(), + validator_groups: vec![ + vec![ValidatorIndex(0), ValidatorIndex(1)], + vec![ValidatorIndex(2)], + vec![ValidatorIndex(3), ValidatorIndex(4)], + ], + needed_approvals: 2, + discovery_keys: validators.iter().map(|v| v.public().into()).collect(), + assignment_keys: validators.iter().map(|v| v.public().into()).collect(), + n_cores: validators.len() as _, + zeroth_delay_tranche_width: 5, + relay_vrf_modulo_samples: 3, + n_delay_tranches: 50, + no_show_slots: 2, + }; + + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { + slot, + candidates: Some(vec![(candidate_receipt, CoreIndex(0), GroupIndex(0))]), + session_info: Some(session_info), + }, + ) + .build(&mut virtual_overseer) + .await; + + assert!(!clock.inner.lock().has_wakeup(1)); + clock.inner.lock().wakeup_all(1); + + assert!(clock.inner.lock().has_wakeup(slot_to_tick(slot))); + clock.inner.lock().wakeup_all(slot_to_tick(slot)); + + futures_timer::Delay::new(Duration::from_millis(200)).await; + + assert!(clock.inner.lock().has_wakeup(slot_to_tick(slot + 1))); + clock.inner.lock().wakeup_all(slot_to_tick(slot + 1)); + + assert_matches!( + overseer_recv(&mut virtual_overseer).await, + AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeAssignment( + _, + c_index, + )) => { + assert_eq!(candidate_index, c_index); + } + ); + + assert_eq!(clock.inner.lock().wakeups.len(), 0); + + futures_timer::Delay::new(Duration::from_millis(200)).await; + + let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap(); + let our_assignment = + candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap(); + assert!(our_assignment.triggered()); + + virtual_overseer + }); +} + +struct TriggersAssignmentConfig<F1, F2> { + our_assigned_tranche: DelayTranche, + assign_validator_tranche: F1, + no_show_slots: u32, + assignments_to_import: Vec<u32>, + approvals_to_import: Vec<u32>, + ticks: Vec<Tick>, + should_be_triggered: F2, +} + +fn triggers_assignment_test<F1, F2>(config: TriggersAssignmentConfig<F1, F2>) +where + F1: 'static + + Fn(ValidatorIndex) -> Result<DelayTranche, criteria::InvalidAssignment> + + Send + + Sync, + F2: Fn(Tick) -> bool, +{ + let TriggersAssignmentConfig { + our_assigned_tranche, + assign_validator_tranche, + no_show_slots, + assignments_to_import, + approvals_to_import, + ticks, + should_be_triggered, + } = config; + + let assignment_criteria = Box::new(MockAssignmentCriteria( + move || { + let mut assignments = HashMap::new(); + let _ = assignments.insert( + CoreIndex(0), + approval_db::v1::OurAssignment { + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }), + tranche: our_assigned_tranche, + validator_index: ValidatorIndex(0), + triggered: false, + } + .into(), + ); + assignments + }, + assign_validator_tranche, + )); + let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); + let store = config.backend(); + + test_harness(config, |test_harness| async move { + let TestHarness { + mut virtual_overseer, + clock, + sync_oracle_handle: _sync_oracle_handle, + .. + } = test_harness; + + let block_hash = Hash::repeat_byte(0x01); + let candidate_receipt = CandidateReceipt::<Hash>::default(); + let candidate_hash = candidate_receipt.hash(); + let slot = Slot::from(1); + let candidate_index = 0; + + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Eve, + Sr25519Keyring::Ferdie, + ]; + let session_info = SessionInfo { + validators: validators.iter().map(|v| v.public().into()).collect(), + validator_groups: vec![ + vec![ValidatorIndex(0), ValidatorIndex(1)], + vec![ValidatorIndex(2), ValidatorIndex(3)], + vec![ValidatorIndex(4), ValidatorIndex(5)], + ], + needed_approvals: 2, + discovery_keys: validators.iter().map(|v| v.public().into()).collect(), + assignment_keys: validators.iter().map(|v| v.public().into()).collect(), + n_cores: validators.len() as _, + zeroth_delay_tranche_width: 5, + relay_vrf_modulo_samples: 2, + n_delay_tranches: 50, + no_show_slots, + }; + + ChainBuilder::new() + .add_block( + block_hash, + ChainBuilder::GENESIS_HASH, + 1, + BlockConfig { + slot, + candidates: Some(vec![(candidate_receipt, CoreIndex(0), GroupIndex(2))]), + session_info: Some(session_info), }, ) + .build(&mut virtual_overseer) .await; + for validator in assignments_to_import { + let rx = check_and_import_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + ValidatorIndex(validator), + ) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); } + + let n_validators = validators.len(); + for (i, &validator_index) in approvals_to_import.iter().enumerate() { + let expect_chain_approved = 3 * (i + 1) > n_validators; + let rx = check_and_import_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + ValidatorIndex(validator_index), + candidate_hash, + 1, + expect_chain_approved, + true, + Some(sign_approval( + validators[validator_index as usize].clone(), + candidate_hash, + 1, + )), + ) + .await; + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); + } + + let debug = false; + if debug { + step_until_done(&clock).await; + return virtual_overseer + } + + futures_timer::Delay::new(Duration::from_millis(200)).await; + + for tick in ticks { + // Assert that this tick is the next to wake up, requiring the test harness to encode + // all relevant wakeups sequentially. + assert_eq!(Some(tick), clock.inner.lock().next_wakeup()); + + clock.inner.lock().set_tick(tick); + futures_timer::Delay::new(Duration::from_millis(100)).await; + + // Assert that Alice's assignment is triggered at the correct tick. + let candidate_entry = store.load_candidate_entry(&candidate_hash).unwrap().unwrap(); + let our_assignment = + candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap(); + assert_eq!(our_assignment.triggered(), should_be_triggered(tick), "at tick {:?}", tick); + } + virtual_overseer }); } + +// This method is used to generate a trace for an execution of a triggers_assignment_test given a +// starting configuration. The relevant ticks (all scheduled wakeups) are printed after no further +// ticks are scheduled. To create a valid test, a prefix of the relevant ticks should be included +// in the final test configuration, ending at the tick with the desired inputs to +// should_trigger_assignemnt. +async fn step_until_done(clock: &MockClock) { + let mut relevant_ticks = Vec::new(); + loop { + futures_timer::Delay::new(Duration::from_millis(200)).await; + let mut clock = clock.inner.lock(); + if let Some(tick) = clock.next_wakeup() { + println!("TICK: {:?}", tick); + relevant_ticks.push(tick); + clock.set_tick(tick); + } else { + break + } + } + println!("relevant_ticks: {:?}", relevant_ticks); +} + +#[test] +fn subsystem_assignment_triggered_solo_zero_tranche() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 0, + assign_validator_tranche: |_| Ok(0), + no_show_slots: 2, + assignments_to_import: vec![], + approvals_to_import: vec![], + ticks: vec![ + 10, // Alice wakeup, assignment triggered + ], + should_be_triggered: |_| true, + }); +} + +#[test] +fn subsystem_assignment_triggered_by_all_with_less_than_threshold() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 11, + assign_validator_tranche: |_| Ok(0), + no_show_slots: 2, + assignments_to_import: vec![1, 2, 3, 4, 5], + approvals_to_import: vec![2, 4], + ticks: vec![ + 20, // Check for no shows + ], + should_be_triggered: |_| true, + }); +} + +#[test] +fn subsystem_assignment_not_triggered_by_all_with_threshold() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 11, + assign_validator_tranche: |_| Ok(0), + no_show_slots: 2, + assignments_to_import: vec![1, 2, 3, 4, 5], + approvals_to_import: vec![1, 3, 5], + ticks: vec![ + 20, // Check no shows + ], + should_be_triggered: |_| false, + }); +} + +#[test] +fn subsystem_assignment_triggered_if_below_maximum_and_clock_is_equal() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 11, + assign_validator_tranche: |_| Ok(0), + no_show_slots: 2, + assignments_to_import: vec![1], + approvals_to_import: vec![], + ticks: vec![ + 20, // Check no shows + 21, // Alice wakeup, assignment triggered + ], + should_be_triggered: |tick| tick >= 21, + }); +} + +#[test] +fn subsystem_assignment_not_triggered_more_than_maximum() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 3, + assign_validator_tranche: |_| Ok(0), + no_show_slots: 2, + assignments_to_import: vec![2, 3], + approvals_to_import: vec![], + ticks: vec![ + 13, // Alice wakeup + 20, // Check no shows + ], + should_be_triggered: |_| false, + }); +} + +#[test] +fn subsystem_assignment_triggered_if_at_maximum() { + // TODO(ladi): is this possible? + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 11, + assign_validator_tranche: |_| Ok(2), + no_show_slots: 2, + assignments_to_import: vec![1], + approvals_to_import: vec![], + ticks: vec![ + 12, // Bob wakeup + 20, // Check no shows + ], + should_be_triggered: |_| false, + }); +} + +#[test] +fn subsystem_assignment_not_triggered_by_exact() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 2, + assign_validator_tranche: |_| Ok(1), + no_show_slots: 2, + assignments_to_import: vec![2, 3], + approvals_to_import: vec![], + ticks: vec![ + 11, // Charlie and Dave wakeup + ], + should_be_triggered: |_| false, + }); +} + +#[test] +fn subsystem_assignment_not_triggered_if_at_maximum_but_clock_is_before() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 6, + assign_validator_tranche: |validator: ValidatorIndex| Ok(validator.0 as _), + no_show_slots: 0, + assignments_to_import: vec![2, 3, 4], + approvals_to_import: vec![], + ticks: vec![ + 12, // Charlie wakeup + 13, // Dave wakeup + 14, // Eve wakeup + ], + should_be_triggered: |_| false, + }); +} + +#[test] +fn subsystem_assignment_not_triggered_if_at_maximum_but_clock_is_before_with_drift() { + triggers_assignment_test(TriggersAssignmentConfig { + our_assigned_tranche: 5, + assign_validator_tranche: |validator: ValidatorIndex| Ok(validator.0 as _), + no_show_slots: 2, + assignments_to_import: vec![2, 3, 4], + approvals_to_import: vec![], + ticks: vec![ + 12, // Charlie wakeup + 13, // Dave wakeup + 15, // Alice wakeup, noop + 20, // Check no shows + 34, // Eve wakeup + ], + should_be_triggered: |_| false, + }); +} -- GitLab