From d80f8489c918b8ed3697c354ae058ec9ccb35eb1 Mon Sep 17 00:00:00 2001 From: Lldenaurois Date: Thu, 8 Jul 2021 12:30:12 -0400 Subject: [PATCH] Approval voting full subsystem tests (#3391) * node/approval-voting: Introduce Backend trait and Overlaybackend This commit introduces a Backend trait and attempts to move away from the Action model via an OverlayBackend as in the ChainSelection subsystem. * node/approval-voting: Add WriteOps for StoredBlockRange and BlocksAtHeight * node/approval-voting: Add load_all_blocks to overlay * node/approval-voting: Get all module tests to pass. This commit modifies all tests to ensure tests are passing. * node/approval-voting: Address oversights in the previous commit This commit addresses some oversights in the prior commit. 1. Inner errors in backend.write were swallowed 2. One-off write functions removed to avoid useless abstraction 3. Touch-ups in general * node/approval-voting: Move from TestDB to dyn KeyValueDB This commit removes the TestDB from tests.rs and replaces it with an in-memory kvdb. * node/approval-voting: Address feedback * node/approval-voting: Add license to ops.rs * node/approval-voting: Address second-pass feedback * Add TODO * node/approval-voting: Bump spec_version * node/approval-voting: Address final comments. * node/approval-voting: Introduce framework for full subsystem tests * node/approval-voting: Introduce basic tests to attempt to provide coverage via full subsystem tests * node/approval-voting: Introduce Chainbuilder --- Cargo.lock | 1 + node/core/approval-voting/Cargo.toml | 1 + node/core/approval-voting/src/import.rs | 10 +- node/core/approval-voting/src/lib.rs | 7 +- node/core/approval-voting/src/old_tests.rs | 1964 +++++++++++++++ node/core/approval-voting/src/tests.rs | 2534 +++++++------------- 6 files changed, 2887 insertions(+), 1630 deletions(-) create mode 100644 node/core/approval-voting/src/old_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 4c61633547ca..a1414fef8edf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6044,6 +6044,7 @@ dependencies = [ "polkadot-node-subsystem-util", "polkadot-overseer", "polkadot-primitives", + "rand 0.8.4", "rand_core 0.5.1", "sc-client-api", "sc-keystore", diff --git a/node/core/approval-voting/Cargo.toml b/node/core/approval-voting/Cargo.toml index 9385380df585..7fe8dccc9e8b 100644 --- a/node/core/approval-voting/Cargo.toml +++ b/node/core/approval-voting/Cargo.toml @@ -42,3 +42,4 @@ maplit = "1.0.2" polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers" } assert_matches = "1.4.0" kvdb-memorydb = "0.10.0" +rand = "0.8" diff --git a/node/core/approval-voting/src/import.rs b/node/core/approval-voting/src/import.rs index 7f5558782f5c..247b0d28d9da 100644 --- a/node/core/approval-voting/src/import.rs +++ b/node/core/approval-voting/src/import.rs @@ -557,7 +557,7 @@ pub(crate) async fn handle_new_head<'a>( } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::approval_db::v1::DbBackend; use polkadot_node_subsystem_test_helpers::make_subsystem_context; @@ -565,11 +565,11 @@ mod tests { use polkadot_primitives::v1::{SessionInfo, ValidatorIndex}; use polkadot_node_subsystem::messages::AllMessages; use sp_core::testing::TaskExecutor; - use sp_runtime::{Digest, DigestItem}; - use sp_consensus_babe::{ + pub(crate) use sp_runtime::{Digest, DigestItem}; + pub(crate) use sp_consensus_babe::{ Epoch as BabeEpoch, BabeEpochConfiguration, AllowedSlots, }; - use sp_consensus_babe::digests::{CompatibleDigestItem, PreDigest, SecondaryVRFPreDigest}; + pub(crate) use sp_consensus_babe::digests::{CompatibleDigestItem, PreDigest, SecondaryVRFPreDigest}; use sp_keyring::sr25519::Keyring as Sr25519Keyring; use assert_matches::assert_matches; use merlin::Transcript; @@ -650,7 +650,7 @@ mod tests { } // used for generating assignments where the validity of the VRF doesn't matter. - fn garbage_vrf() -> (VRFOutput, VRFProof) { + pub(crate) fn garbage_vrf() -> (VRFOutput, VRFProof) { let key = Sr25519Keyring::Alice.pair(); let key: &schnorrkel::Keypair = key.as_ref(); diff --git a/node/core/approval-voting/src/lib.rs b/node/core/approval-voting/src/lib.rs index d2fb201d0df3..f471488324e3 100644 --- a/node/core/approval-voting/src/lib.rs +++ b/node/core/approval-voting/src/lib.rs @@ -85,9 +85,13 @@ use crate::backend::{Backend, OverlayedBackend}; #[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; +const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds. const LOG_TARGET: &str = "parachain::approval-voting"; /// Configuration for the approval voting subsystem @@ -1420,9 +1424,8 @@ fn check_and_import_assignment( assignment: IndirectAssignmentCert, candidate_index: CandidateIndex, ) -> SubsystemResult<(AssignmentCheckResult, Vec)> { - const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds. - let tick_now = state.clock.tick_now(); + let block_entry = match db.load_block_entry(&assignment.block_hash)? { Some(b) => b, None => return Ok((AssignmentCheckResult::Bad( diff --git a/node/core/approval-voting/src/old_tests.rs b/node/core/approval-voting/src/old_tests.rs new file mode 100644 index 000000000000..d21bde2dddfe --- /dev/null +++ b/node/core/approval-voting/src/old_tests.rs @@ -0,0 +1,1964 @@ +// 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 . + +use super::*; +use super::approval_db::v1::Config; +use super::backend::{Backend, BackendWriteOp}; +use polkadot_primitives::v1::{CandidateDescriptor, CoreIndex, GroupIndex, ValidatorSignature}; +use polkadot_node_primitives::approval::{ + AssignmentCert, AssignmentCertKind, VRFOutput, VRFProof, + RELAY_VRF_MODULO_CONTEXT, DelayTranche, +}; +use polkadot_node_subsystem_test_helpers::make_subsystem_context; +use polkadot_node_subsystem::messages::AllMessages; +use sp_core::testing::TaskExecutor; + +use parking_lot::Mutex; +use bitvec::order::Lsb0 as BitOrderLsb0; +use std::pin::Pin; +use std::sync::Arc; +use sp_keyring::sr25519::Keyring as Sr25519Keyring; +use assert_matches::assert_matches; + +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 = Arc::new(kvdb_memorydb::create(NUM_COLUMNS)); + DbBackend::new(db_writer.clone(), TEST_CONFIG) +} + +fn overlay_txn(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) -> crate::time::Tick { + crate::time::slot_number_to_tick(SLOT_DURATION_MILLIS, t.into()) +} + +#[derive(Default, Clone)] +struct MockClock { + inner: Arc>, +} + +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 + 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); + +impl AssignmentCriteria for MockAssignmentCriteria +where + Compute: Fn() -> HashMap, + Check: Fn() -> Result +{ + 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 { + 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 { + self.1() + } +} + +impl MockAssignmentCriteria< + fn() -> HashMap, + 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, + validator_groups: Vec>, + needed_approvals: u32, + no_show_slots: u32, + candidate_hash: Option, +} + +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, +) { + 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( + 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( + 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::::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::>(); + 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::::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::>(); + + 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::>(); + 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::::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::>(); + 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::::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::::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::::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::::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(), 0); + + let write_ops = overlay_db.into_write_ops().collect::>(); + 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::::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::::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::>(); + 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::>(); + 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::>(), + ); + assert_eq!( + wakeups.block_numbers.get(&1).unwrap(), + &vec![b_b].into_iter().collect::>(), + ); + + 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::>(), + ); + + 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::::default().hash(); + let candidate_receipt_2 = CandidateReceipt:: { + 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::>(); + 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::>(); + 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_eq!( + handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default()) + .await.unwrap(), + Some((block_hash_4, 4)), + ) + }); + + 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_eq!( + handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default()) + .await.unwrap(), + Some((block_hash_2, 2)), + ) + }); + + 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::::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::>(); + 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::::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::::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::>(); + 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::>(); + 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/node/core/approval-voting/src/tests.rs b/node/core/approval-voting/src/tests.rs index c539f4256636..c1de651e4d9c 100644 --- a/node/core/approval-voting/src/tests.rs +++ b/node/core/approval-voting/src/tests.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] // Copyright 2021 Parity Technologies (UK) Ltd. // This file is part of Polkadot. @@ -15,47 +16,115 @@ // along with Polkadot. If not, see . use super::*; -use super::approval_db::v1::Config; -use super::backend::{Backend, BackendWriteOp}; -use polkadot_primitives::v1::{CandidateDescriptor, CoreIndex, GroupIndex, ValidatorSignature}; +use std::time::Duration; +use polkadot_overseer::HeadSupportsParachains; +use polkadot_primitives::v1::{ + CoreIndex, GroupIndex, ValidatorSignature, Header, CandidateEvent, +}; +use polkadot_node_subsystem::{ActivatedLeaf, ActiveLeavesUpdate, LeafStatus}; use polkadot_node_primitives::approval::{ AssignmentCert, AssignmentCertKind, VRFOutput, VRFProof, RELAY_VRF_MODULO_CONTEXT, DelayTranche, }; -use polkadot_node_subsystem_test_helpers::make_subsystem_context; -use polkadot_node_subsystem::messages::AllMessages; -use sp_core::testing::TaskExecutor; +use polkadot_node_subsystem_test_helpers as test_helpers; +use polkadot_node_subsystem::messages::{AllMessages, ApprovalVotingMessage, AssignmentCheckResult}; +use polkadot_node_subsystem_util::TimeoutExt; use parking_lot::Mutex; -use bitvec::order::Lsb0 as BitOrderLsb0; use std::pin::Pin; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use sp_keyring::sr25519::Keyring as Sr25519Keyring; +use sp_keystore::CryptoStore; use assert_matches::assert_matches; +use super::import::tests::{ + BabeEpoch, BabeEpochConfiguration, AllowedSlots, Digest, garbage_vrf, DigestItem, PreDigest, + SecondaryVRFPreDigest, CompatibleDigestItem, +}; +use super::approval_db::v1::StoredBlockRange; +use super::backend::BackendWriteOp; + const SLOT_DURATION_MILLIS: u64 = 5000; -const DATA_COL: u32 = 0; -const NUM_COLUMNS: u32 = 1; +#[derive(Clone)] +struct TestSyncOracle { + flag: Arc, + done_syncing_sender: Arc>>>, +} + +struct TestSyncOracleHandle { + done_syncing_receiver: oneshot::Receiver<()>, + flag: Arc, +} -const TEST_CONFIG: Config = Config { - col_data: DATA_COL, -}; +impl TestSyncOracleHandle { + fn set_done(&self) { + self.flag.store(false, Ordering::SeqCst); + } -fn make_db() -> DbBackend { - let db_writer: Arc = Arc::new(kvdb_memorydb::create(NUM_COLUMNS)); - DbBackend::new(db_writer.clone(), TEST_CONFIG) + async fn await_mode_switch(self) { + let _ = self.done_syncing_receiver.await; + } } -fn overlay_txn(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(); +impl SyncOracle for TestSyncOracle { + fn is_major_syncing(&mut self) -> bool { + let is_major_syncing = self.flag.load(Ordering::SeqCst); + + if !is_major_syncing { + if let Some(sender) = self.done_syncing_sender.lock().take() { + let _ = sender.send(()); + } + } + + is_major_syncing + } + + fn is_offline(&mut self) -> bool { + unimplemented!("not used in network bridge") + } +} + +// val - result of `is_major_syncing`. +fn make_sync_oracle(val: bool) -> (TestSyncOracle, TestSyncOracleHandle) { + let (tx, rx) = oneshot::channel(); + let flag = Arc::new(AtomicBool::new(val)); + + ( + TestSyncOracle { + flag: flag.clone(), + done_syncing_sender: Arc::new(Mutex::new(Some(tx))), + }, + TestSyncOracleHandle { + flag, + done_syncing_receiver: rx, + } + ) +} + +fn done_syncing_oracle() -> Box { + let (oracle, _) = make_sync_oracle(false); + Box::new(oracle) +} + +#[cfg(test)] +pub mod test_constants { + use crate::approval_db::v1::Config as DatabaseConfig; + const DATA_COL: u32 = 0; + pub(crate) const NUM_COLUMNS: u32 = 1; + + pub(crate) const TEST_CONFIG: DatabaseConfig = DatabaseConfig { + col_data: DATA_COL, + }; +} + +struct MockSupportsParachains; + +impl HeadSupportsParachains for MockSupportsParachains { + fn head_supports_parachains(&self, _head: &Hash) -> bool { + true + } } fn slot_to_tick(t: impl Into) -> crate::time::Tick { @@ -116,6 +185,10 @@ impl MockClockInner { } } + fn has_wakeup(&self, tick: Tick) -> bool { + self.wakeups.binary_search_by_key(&tick, |w| w.0).is_ok() + } + // If `pre_emptive` is true, we compare the given tick to the internal // tick of the clock for an early return. // @@ -184,24 +257,78 @@ impl MockAssignmentCriteria< } } -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) })), - } +#[derive(Default)] +struct TestStore { + stored_block_range: Option, + blocks_at_height: HashMap>, + block_entries: HashMap, + candidate_entries: HashMap, } -fn single_session_state(index: SessionIndex, info: SessionInfo) -> State { - State { - session_window: RollingSessionWindow::with_session_info( - APPROVAL_SESSIONS, - index, - vec![info], - ), - ..blank_state() +impl Backend for TestStore { + fn load_block_entry( + &self, + block_hash: &Hash, + ) -> SubsystemResult> { + Ok(self.block_entries.get(block_hash).cloned()) + } + + fn load_candidate_entry( + &self, + candidate_hash: &CandidateHash, + ) -> SubsystemResult> { + Ok(self.candidate_entries.get(candidate_hash).cloned()) + } + + fn load_blocks_at_height( + &self, + height: &BlockNumber, + ) -> SubsystemResult> { + Ok(self.blocks_at_height.get(height).cloned().unwrap_or_default()) + } + + fn load_all_blocks(&self) -> SubsystemResult> { + let mut hashes: Vec<_> = self.block_entries.keys().cloned().collect(); + + hashes.sort_by_key(|k| self.block_entries.get(k).unwrap().block_number()); + + Ok(hashes) + } + + fn load_stored_blocks(&self) -> SubsystemResult> { + Ok(self.stored_block_range.clone()) + } + + fn write(&mut self, ops: I) -> SubsystemResult<()> + where I: IntoIterator + { + for op in ops { + match op { + BackendWriteOp::WriteStoredBlockRange(stored_block_range) => { + self.stored_block_range = Some(stored_block_range); + } + BackendWriteOp::WriteBlocksAtHeight(h, blocks) => { + self.blocks_at_height.insert(h, blocks); + } + BackendWriteOp::DeleteBlocksAtHeight(h) => { + let _ = self.blocks_at_height.remove(&h); + } + BackendWriteOp::WriteBlockEntry(block_entry) => { + self.block_entries.insert(block_entry.block_hash(), block_entry); + } + BackendWriteOp::DeleteBlockEntry(hash) => { + let _ = self.block_entries.remove(&hash); + } + BackendWriteOp::WriteCandidateEntry(candidate_entry) => { + self.candidate_entries.insert(candidate_entry.candidate_receipt().hash(), candidate_entry); + } + BackendWriteOp::DeleteCandidateEntry(candidate_hash) => { + let _ = self.candidate_entries.remove(&candidate_hash); + } + } + } + + Ok(()) } } @@ -227,1752 +354,913 @@ fn sign_approval( key.sign(&ApprovalVote(candidate_hash).signing_payload(session_index)).into() } -#[derive(Clone)] -struct StateConfig { - session_index: SessionIndex, - slot: Slot, - tick: Tick, - validators: Vec, - validator_groups: Vec>, - needed_approvals: u32, - no_show_slots: u32, - candidate_hash: Option, -} +type VirtualOverseer = test_helpers::TestSubsystemContextHandle; -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, - } - } +struct TestHarness { + virtual_overseer: VirtualOverseer, + clock: Box, } -// 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 +#[derive(Default)] +struct HarnessConfig { + tick_start: Tick, + assigned_tranche: DelayTranche, } -fn add_block( - db: &mut DbBackend, - block_hash: Hash, - session: SessionIndex, - slot: Slot, +fn test_harness>( + config: HarnessConfig, + sync_oracle: Box, + test: impl FnOnce(TestHarness) -> T, ) { - 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())); -} + let pool = sp_core::testing::TaskExecutor::new(); + let (context, virtual_overseer) = test_helpers::make_subsystem_context(pool); -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, -) { - 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(), + let keystore = LocalKeystore::in_memory(); + let _ = keystore.sr25519_generate_new( + polkadot_primitives::v1::PARACHAIN_KEY_TYPE_ID, + Some(&Sr25519Keyring::Alice.to_seed()), ); - overlay_txn(db, |overlay_db| { - overlay_db.write_block_entry(block_entry.clone()); - overlay_db.write_candidate_entry(candidate_entry.clone()); - }) -} - -fn import_assignment( - 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( - 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())); -} + let store = TestStore::default(); -#[test] -fn rejects_bad_assignment() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::::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::>(); - 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); -} + let HarnessConfig { + tick_start, + assigned_tranche, + } = config; -#[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::::default().hash(); - let assignment = IndirectAssignmentCert { - block_hash, - validator: ValidatorIndex(0), - cert: garbage_assignment_cert( - AssignmentCertKind::RelayVRFModulo { - sample: 0, + let clock = Box::new(MockClock::new(tick_start)); + let subsystem = run( + context, + ApprovalVotingSubsystem::with_config( + Config{ + col_data: test_constants::TEST_CONFIG.col_data, + slot_duration_millis: 100u64, }, + Arc::new(kvdb_memorydb::create(test_constants::NUM_COLUMNS)), + Arc::new(keystore), + sync_oracle, + Metrics::default(), ), - }; - - 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::>(); - - 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::>(); - assert_eq!(write_ops.len(), 1); - - assert_matches!( - write_ops.get(0).unwrap(), - BackendWriteOp::WriteCandidateEntry(..) + clock.clone(), + Box::new(MockAssignmentCriteria::check_only(move || { Ok(assigned_tranche) })), + store, ); -} - -#[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::::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); + let test_fut = test(TestHarness { + virtual_overseer, + clock, + }); - 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. - } - ); + futures::pin_mut!(test_fut); + futures::pin_mut!(subsystem); - let write_ops = overlay_db.into_write_ops().collect::>(); - 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))); - } - ); + futures::executor::block_on(future::join(async move { + let mut overseer = test_fut.await; + overseer_signal(&mut overseer, OverseerSignal::Conclude).await; + }, subsystem)).1.unwrap(); } -#[test] -fn rejects_approval_before_assignment() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::::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); +async fn overseer_send( + overseer: &mut VirtualOverseer, + msg: FromOverseer, +) { + tracing::trace!("Sending message:\n{:?}", &msg); + overseer + .send(msg) + .timeout(TIMEOUT) + .await + .expect(&format!("{:?} is enough for sending messages.", TIMEOUT)); } -#[test] -fn rejects_approval_if_no_candidate_entry() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::::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), - }; - +async fn overseer_recv( + overseer: &mut VirtualOverseer, +) -> AllMessages { + let msg = overseer_recv_with_timeout(overseer, TIMEOUT) + .await + .expect(&format!("{:?} is enough to receive messages.", TIMEOUT)); - overlay_txn(&mut db, |overlay_db| overlay_db.delete_candidate_entry(&candidate_hash)); + tracing::trace!("Received message:\n{:?}", &msg); - 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); + msg } -#[test] -fn rejects_approval_if_no_block_entry() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::::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); +async fn overseer_recv_with_timeout( + overseer: &mut VirtualOverseer, + timeout: Duration, +) -> Option { + tracing::trace!("Waiting for message..."); + overseer + .recv() + .timeout(timeout) + .await } -#[test] -fn accepts_and_imports_approval_after_assignment() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::::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(), 0); - - let write_ops = overlay_db.into_write_ops().collect::>(); - 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()); - } - ); +const TIMEOUT: Duration = Duration::from_millis(2000); +async fn overseer_signal( + overseer: &mut VirtualOverseer, + signal: OverseerSignal, +) { + overseer + .send(FromOverseer::Signal(signal)) + .timeout(TIMEOUT) + .await + .expect(&format!("{:?} is more than enough for sending signals.", TIMEOUT)); } #[test] -fn second_approval_import_only_schedules_wakeups() { - let mut db = make_db(); - let block_hash = Hash::repeat_byte(0x01); - let candidate_hash = CandidateReceipt::::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); +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; - // After adding a second assignment, there should be a schedule wakeup action. + let (tx, rx) = oneshot::channel(); - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_b, |_| {}); + 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; - let mut overlay_db = OverlayedBackend::new(&db); - let (actions, res) = check_and_import_approval( - &state, - &mut overlay_db, - &Metrics(None), - vote, - |r| r - ).unwrap(); + handle.await_mode_switch().await; - assert_eq!(res, ApprovalCheckResult::Accepted); - assert_eq!(actions.len(), 1); - assert_eq!(overlay_db.into_write_ops().count(), 0); + assert_matches!( + rx.await, + Ok( + AssignmentCheckResult::Bad(AssignmentCheckError::UnknownBlock(hash)) + ) => { + assert_eq!(hash, bad_block_hash); + } + ); - assert_matches!( - actions.get(0).unwrap(), - Action::ScheduleWakeup { .. } => {} - ); + virtual_overseer + }); } #[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::::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) - }; +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; - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_a, |_| {}); - import_assignment(&mut db, &candidate_hash, &block_hash, validator_index_b, |_| {}); + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); + let candidate_hash = CandidateReceipt::::default().hash(); + let session_index = 1; - { - 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(), + let rx = cai_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, candidate_hash, - db.load_candidate_entry(&candidate_hash).unwrap().unwrap(), - ApprovalSource::Remote(validator_index_a), - ); + session_index, + ).await; - 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); + rx.await, + Ok(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))) => { + assert_eq!(hash, block_hash); } ); - let mut write_ops = overlay_db.into_write_ops().collect::>(); - 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)); - } - ); + virtual_overseer + }); +} - db.write(write_ops).unwrap(); - } +#[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 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 block_hash = Hash::repeat_byte(0x01); - assert_matches!( - actions.get(0).unwrap(), - Action::NoteApprovedInChainSelection(h) => { - assert_eq!(h, &block_hash); - } - ); + let candidate_hash = { + let mut candidate_receipt = CandidateReceipt::::default(); + candidate_receipt.descriptor.para_id = 1.into(); + candidate_receipt.descriptor.relay_parent = block_hash; + candidate_receipt.hash() + }; - let mut write_ops = overlay_db.into_write_ops().collect::>(); - assert_eq!(write_ops.len(), 2); + let candidate_index = 0; + let validator = ValidatorIndex(0); + let session_index = 1; - 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)); - } - ); + // 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, + ).await; 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)); + rx.await, + Ok(ApprovalCheckResult::Bad(ApprovalCheckError::NoAssignment(v))) => { + assert_eq!(v, validator); } ); - } -} -#[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, - )); + virtual_overseer + }); } #[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, - }; +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() + }, 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; - 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() - }; + assert_eq!(rx.await, Ok(AssignmentCheckResult::TooFarInFuture)); - // 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, - )); -} + // Advance clock to make assignment reasonably near. + clock.inner.lock().set_tick(1); -#[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, - }; + let rx = cai_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ).await; - 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() - }; + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - let tranche_now = 1; - assert!(!should_trigger_assignment( - candidate_entry.approval_entry(&block_hash).unwrap(), - &candidate_entry, - RequiredTranches::All, - tranche_now, - )); + virtual_overseer + }); } #[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() - }; +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; - 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, - )); -} + let block_hash = Hash::repeat_byte(0x01); + let candidate_index = 0; + let validator = ValidatorIndex(0); -#[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, - }; + // Add block hash 00. + ChainBuilder::new() + .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) + .build(&mut virtual_overseer) + .await; - 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 rx = cai_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ).await; - 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, - )); -} + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); -#[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, - }; + let rx = cai_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ).await; - 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() - }; + assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate)); - 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, - )); + virtual_overseer + }); } #[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, - }; +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; - 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 block_hash = Hash::repeat_byte(0x01); + let candidate_index = 7; + let validator = ValidatorIndex(0); - 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, - )); -} + // Add block hash 00. + ChainBuilder::new() + .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) + .build(&mut virtual_overseer) + .await; -#[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, - }; + let rx = cai_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ).await; - 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() - }; + assert_eq!( + rx.await, + Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(candidate_index))), + ); - 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, - )); + virtual_overseer + }); } #[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)); +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; - wakeups.schedule(b_a, 0, c_a, 1); - wakeups.schedule(b_a, 0, c_b, 4); - wakeups.schedule(b_b, 1, c_b, 3); + let block_hash = Hash::repeat_byte(0x01); - assert_eq!(wakeups.first().unwrap(), 1); + let candidate_hash = { + let mut candidate_receipt = CandidateReceipt::::default(); + candidate_receipt.descriptor.para_id = 1.into(); + candidate_receipt.descriptor.relay_parent = block_hash; + candidate_receipt.hash() + }; - let clock = MockClock::new(0); - let clock_aux = clock.clone(); + let candidate_index = 0; + let validator = ValidatorIndex(0); + let session_index = 1; - 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()); + // Add block hash 0x01... + ChainBuilder::new() + .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) + .build(&mut virtual_overseer) + .await; - assert_eq!( - wakeups.block_numbers.get(&0).unwrap(), - &vec![b_a].into_iter().collect::>(), - ); - assert_eq!( - wakeups.block_numbers.get(&1).unwrap(), - &vec![b_b].into_iter().collect::>(), - ); + let rx = cai_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ).await; - wakeups.prune_finalized_wakeups(0); + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - assert!(wakeups.block_numbers.get(&0).is_none()); - assert_eq!( - wakeups.block_numbers.get(&1).unwrap(), - &vec![b_b].into_iter().collect::>(), - ); + let rx = cai_approval( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + candidate_hash, + session_index, + ).await; - wakeups.prune_finalized_wakeups(1); + assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted)); - assert!(wakeups.block_numbers.get(&0).is_none()); - assert!(wakeups.block_numbers.get(&1).is_none()); + 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 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); - }); + let block_hash = Hash::repeat_byte(0x01); - futures::executor::block_on(futures::future::join(test_fut, aux_fut)); -} + let _candidate_hash = { + let mut candidate_receipt = CandidateReceipt::::default(); + candidate_receipt.descriptor.para_id = 1.into(); + candidate_receipt.descriptor.relay_parent = block_hash; + candidate_receipt.hash() + }; -#[test] -fn wakeup_earlier_supersedes_later() { - let mut wakeups = Wakeups::default(); + let candidate_index = 0; + let validator = ValidatorIndex(0); - let b_a = Hash::repeat_byte(0); - let c_a = CandidateHash(Hash::repeat_byte(2)); + // Add block hash 0x01... + ChainBuilder::new() + .add_block(block_hash, ChainBuilder::GENESIS_HASH, Slot::from(1), 1) + .build(&mut virtual_overseer) + .await; - 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 rx = cai_assignment( + &mut virtual_overseer, + block_hash, + candidate_index, + validator, + ).await; - let clock = MockClock::new(0); - let clock_aux = clock.clone(); + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - 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()); - }); + // TODO(ladi): fix + //assert!(clock.inner.lock().has_wakeup(20)); - let aux_fut = Box::pin(async move { - clock_aux.inner.lock().set_tick(2); + virtual_overseer }); - - 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::::default().hash(); - let candidate_receipt_2 = CandidateReceipt:: { - 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); +async fn cai_approval( + overseer: &mut VirtualOverseer, + block_hash: Hash, + candidate_index: CandidateIndex, + validator: ValidatorIndex, + candidate_hash: CandidateHash, + session_index: SessionIndex, +) -> oneshot::Receiver { + 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; + rx +} - 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) - }; +async fn cai_assignment( + overseer: &mut VirtualOverseer, + block_hash: Hash, + candidate_index: CandidateIndex, + validator: ValidatorIndex, +) -> oneshot::Receiver { + 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 +} - add_candidate_to_block( - &mut db, - block_hash, - candidate_hash_2, - 3, - CoreIndex(1), - GroupIndex(1), - Some(candidate_receipt_2), - ); +struct ChainBuilder { + blocks_by_hash: HashMap, + blocks_at_height: BTreeMap>, +} - 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), - ); +impl ChainBuilder { + const GENESIS_HASH: Hash = Hash::repeat_byte(0xff); + const GENESIS_PARENT_HASH: Hash = Hash::repeat_byte(0x00); - assert_eq!(actions.len(), 0); + 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 + } - let write_ops = overlay_db.into_write_ops().collect::>(); - assert_eq!(write_ops.len(), 2); + pub fn add_block<'a>( + &'a mut self, + hash: Hash, + parent_hash: Hash, + slot: Slot, + number: u32, + ) -> &'a mut Self { + assert!(number != 0, "cannot add duplicate genesis block"); + assert!(hash != Self::GENESIS_HASH, "cannot add block with genesis hash"); + assert!(parent_hash != Self::GENESIS_PARENT_HASH, "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) + } - 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)); - } - ); + fn add_block_inner<'a>( + &'a mut self, + hash: Hash, + parent_hash: Hash, + slot: Slot, + number: u32, + ) -> &'a mut Self { + let header = ChainBuilder::make_header(parent_hash, slot, number); + assert!( + self.blocks_by_hash.insert(hash, header).is_none(), + "block with hash {:?} already exists", hash, + ); + self.blocks_at_height.entry(number).or_insert_with(Vec::new).push(hash); + self + } - 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()); + pub async fn build(&self, overseer: &mut VirtualOverseer) { + for (number, blocks) in self.blocks_at_height.iter() { + for (i, hash) in blocks.iter().enumerate() { + let mut cur_hash = *hash; + let mut ancestry = Vec::new(); + while cur_hash != Self::GENESIS_PARENT_HASH { + 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; + let _: Option<()> = future::pending().timeout(Duration::from_millis(100)).await; + } } - ); - - 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); + fn make_header( + parent_hash: Hash, + slot: Slot, + number: u32, + ) -> Header { + let digest = { + let mut digest = Digest::default(); + let (vrf_output, vrf_proof) = garbage_vrf(); + digest.push(DigestItem::babe_pre_digest(PreDigest::SecondaryVRF( + SecondaryVRFPreDigest { + authority_index: 0, + slot, + vrf_output, + vrf_proof, + } + ))); + digest + }; - assert_matches!( - actions.get(0).unwrap(), - Action::NoteApprovedInChainSelection(h) => { - assert_eq!(h, &block_hash); + Header { + digest, + extrinsics_root: Default::default(), + number, + state_root: Default::default(), + parent_hash, } - ); + } + } + +async fn import_block( + overseer: &mut VirtualOverseer, + hashes: &[(Hash, Header)], + session: u32, + 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 write_ops = overlay_db.into_write_ops().collect::>(); - assert_eq!(write_ops.len(), 2); + let (new_head, new_header) = &hashes[hashes.len() - 1]; + overseer_send( + overseer, + FromOverseer::Signal(OverseerSignal::ActiveLeaves(ActiveLeavesUpdate::start_work(ActivatedLeaf { + hash: *new_head, + number: session, + status: LeafStatus::Fresh, + span: Arc::new(jaeger::Span::Disabled), + })), + )).await; 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)); + overseer_recv(overseer).await, + AllMessages::ChainApi(ChainApiMessage::BlockHeader(head, h_tx)) => { + assert_eq!(*new_head, head); + h_tx.send(Ok(Some(new_header.clone()))).unwrap(); } ); 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()); + overseer_recv(overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request( + req_block_hash, + RuntimeApiRequest::SessionIndexForChild(s_tx) + ) + ) => { + let hash = &hashes[session.saturating_sub(1) as usize]; + assert_eq!(req_block_hash, hash.0.clone()); + s_tx.send(Ok(session.into())).unwrap(); } ); -} - -#[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, + if !fork { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request( + req_block_hash, + RuntimeApiRequest::SessionInfo(idx, si_tx), + ) + ) => { + assert_eq!(session, idx); + assert_eq!(req_block_hash, *new_head); + si_tx.send(Ok(Some(session_info.clone()))).unwrap(); + } ); - 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 mut _ancestry_step = 0; + if gap { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ChainApi(ChainApiMessage::Ancestors { + hash, + k, + response_channel, + }) => { + assert_eq!(hash, *new_head); + let history: Vec = hashes.iter().map(|v| v.0).take(k).collect(); + let _ = response_channel.send(Ok(history)); + _ancestry_step = k; + } + ); - let pool = TaskExecutor::new(); - let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone()); + for i in 0.._ancestry_step { + match overseer_recv(overseer).await { + AllMessages::ChainApi(ChainApiMessage::BlockHeader(_, h_tx)) => { + let (hash, header) = hashes[i as usize].clone(); + assert_eq!(hash, *new_head); + h_tx.send(Ok(Some(header))).unwrap(); + } + AllMessages::ChainApi(ChainApiMessage::Ancestors { + hash, + k, + response_channel, + }) => { + assert_eq!(hash, *new_head); + assert_eq!(k as u32, session-1); + let history: Vec = hashes.iter().map(|v| v.0).take(k).collect(); + response_channel.send(Ok(history)).unwrap(); + } + _ => unreachable!{}, + } + } + } - let test_fut = Box::pin(async move { - let overlay_db = OverlayedBackend::new(&db); - assert_eq!( - handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default()) - .await.unwrap(), - Some((block_hash_4, 4)), - ) - }); + } - let aux_fut = Box::pin(async move { + if session > 0 { assert_matches!( - handle.recv().await, - AllMessages::ChainApi(ChainApiMessage::BlockNumber(target, tx)) => { - assert_eq!(target, block_hash_4); - let _ = tx.send(Ok(Some(4))); + 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::>(); + c_tx.send(Ok(inclusion_events)).unwrap(); } ); 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])); + overseer_recv(overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request( + req_block_hash, + RuntimeApiRequest::SessionIndexForChild(s_tx) + ) + ) => { + let hash = &hashes[(session-1) as usize]; + assert_eq!(req_block_hash, hash.0.clone()); + s_tx.send(Ok(session.into())).unwrap(); } ); - }); - - 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, + assert_matches!( + overseer_recv(overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request( + req_block_hash, + RuntimeApiRequest::CurrentBabeEpoch(c_tx), + ) + ) => { + let hash = &hashes[session as usize]; + assert_eq!(req_block_hash, hash.0.clone()); + let _ = c_tx.send(Ok(BabeEpoch { + epoch_index: session as _, + start_slot: Slot::from(0), + duration: 200, + authorities: vec![(Sr25519Keyring::Alice.public().into(), 1)], + randomness: [0u8; 32], + config: BabeEpochConfiguration { + c: (1, 4), + allowed_slots: AllowedSlots::PrimarySlots, + }, + })); + } ); + } - 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_eq!( - handle_approved_ancestor(&mut ctx, &overlay_db, block_hash_4, 0, &Default::default()) - .await.unwrap(), - Some((block_hash_2, 2)), - ) - }); - - let aux_fut = Box::pin(async move { + if session == 0 { assert_matches!( - handle.recv().await, - AllMessages::ChainApi(ChainApiMessage::BlockNumber(target, tx)) => { - assert_eq!(target, block_hash_4); - let _ = tx.send(Ok(Some(4))); + overseer_recv(overseer).await, + AllMessages::ApprovalDistribution(ApprovalDistributionMessage::NewBlocks(v)) => { + assert_eq!(v.len(), 0usize); } ); - + } else { 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])); + overseer_recv(overseer).await, + AllMessages::ApprovalDistribution( + ApprovalDistributionMessage::NewBlocks(mut approval_vec) + ) => { + 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]; + 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)); } ); - }); - - 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::::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); +fn linear_import_act_on_leaf() { + let session = 3u32; - assert_matches!( - actions.get(0).unwrap(), - Action::LaunchApproval { - candidate_index, - .. - } => { - assert_eq!(candidate_index, &0); - } - ); - - assert_matches!( - actions.get(1).unwrap(), - Action::ScheduleWakeup { - tick, + let (oracle, _handle) = make_sync_oracle(false); + test_harness(Default::default(), Box::new(oracle), |test_harness| async move { + let TestHarness { + mut virtual_overseer, .. - } => { - assert_eq!(tick, &slot_to_tick(0 + 2)); - } - ); - - let write_ops = overlay_db.into_write_ops().collect::>(); - 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::::default().hash(); - let slot = Slot::from(1); - let session_index = 1; + } = test_harness; - 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) - }; + let mut head: Hash = ChainBuilder::GENESIS_HASH; + let mut builder = ChainBuilder::new(); + for i in 1..session { + let slot = Slot::from(i as u64); - set_our_assignment(&mut db, &candidate_hash, &block_hash, 10, |_| {}); + let hash = Hash::repeat_byte(i as u8); + builder.add_block(hash, head, slot, i); + head = hash; + } - let mut overlay_db = OverlayedBackend::new(&db); - let actions = process_wakeup( - &state, - &mut overlay_db, - block_hash, - candidate_hash, - 1, - ).unwrap(); + builder.build(&mut virtual_overseer).await; - 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() { + 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; -#[test] -fn finalization_event_prunes() { + assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); + virtual_overseer + }); } #[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::::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::>(); - 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(); - } +fn forkful_import_at_same_height_act_on_leaf() { + let session = 3u32; - { - 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::>(); - 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)); - } - ); - } + 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 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 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); + } + 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 + }); } - -// TODO [now]: handling `BecomeActive` action broadcasts everything.