// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Tests for Sassafras pezpallet. use crate::*; use mock::*; use pezsp_consensus_sassafras::Slot; fn h2b(hex: &str) -> [u8; N] { array_bytes::hex2array_unchecked(hex) } fn b2h(bytes: [u8; N]) -> String { array_bytes::bytes2hex("", &bytes) } #[test] fn genesis_values_assumptions_check() { new_test_ext(3).execute_with(|| { assert_eq!(Sassafras::authorities().len(), 3); assert_eq!(Sassafras::config(), TEST_EPOCH_CONFIGURATION); }); } #[test] fn post_genesis_randomness_initialization() { let (pairs, mut ext) = new_test_ext_with_pairs(1, false); let pair = &pairs[0]; ext.execute_with(|| { assert_eq!(Sassafras::randomness(), [0; 32]); assert_eq!(Sassafras::next_randomness(), [0; 32]); assert_eq!(Sassafras::randomness_accumulator(), [0; 32]); // Test the values with a zero genesis block hash let _ = initialize_block(1, 123.into(), [0x00; 32].into(), pair); assert_eq!(Sassafras::randomness(), [0; 32]); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("b9497550deeeb4adc134555930de61968a0558f8947041eb515b2f5fa68ffaf7") ); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("febcc7fe9539fe17ed29f525831394edfb30b301755dc9bd91584a1f065faf87") ); let (id1, _) = make_ticket_bodies(1, Some(pair))[0]; // Reset what is relevant System::set_block_number(0); NextRandomness::::set([0; 32]); RandomnessAccumulator::::set([0; 32]); // Test the values with a non-zero genesis block hash let _ = initialize_block(1, 123.into(), [0xff; 32].into(), pair); assert_eq!(Sassafras::randomness(), [0; 32]); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("51c1e3b3a73d2043b3cabae98ff27bdd4aad8967c21ecda7b9465afaa0e70f37") ); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("466bf3007f2e17bffee0b3c42c90f33d654f5ff61eff28b0cc650825960abd52") ); let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; // Ticket ids should be different when next epoch randomness is different assert_ne!(id1, id2); // Reset what is relevant System::set_block_number(0); NextRandomness::::set([0; 32]); RandomnessAccumulator::::set([0; 32]); // Test the values with a non-zero genesis block hash let _ = initialize_block(1, 321.into(), [0x00; 32].into(), pair); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("d85d84a54f79453000eb62e8a17b30149bd728d3232bc2787a89d51dc9a36008") ); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("8a035eed02b5b8642b1515ed19752df8df156627aea45c4ef6e3efa88be9a74d") ); let (id2, _) = make_ticket_bodies(1, Some(pair))[0]; // Ticket ids should be different when next epoch randomness is different assert_ne!(id1, id2); }); } // Tests if the sorted tickets are assigned to each slot outside-in. #[test] fn slot_ticket_id_outside_in_fetch() { let genesis_slot = Slot::from(100); let tickets_count = 6; // Current epoch tickets let curr_tickets: Vec = (0..tickets_count).map(|i| i as TicketId).collect(); // Next epoch tickets let next_tickets: Vec = (0..tickets_count - 1).map(|i| (i + tickets_count) as TicketId).collect(); new_test_ext(0).execute_with(|| { // Some corner cases TicketsIds::::insert((0, 0_u32), 1_u128); // Cleanup (0..3).for_each(|i| TicketsIds::::remove((0, i as u32))); curr_tickets .iter() .enumerate() .for_each(|(i, id)| TicketsIds::::insert((0, i as u32), id)); next_tickets .iter() .enumerate() .for_each(|(i, id)| TicketsIds::::insert((1, i as u32), id)); TicketsMeta::::set(TicketsMetadata { tickets_count: [curr_tickets.len() as u32, next_tickets.len() as u32], unsorted_tickets_count: 0, }); // Before importing the first block the pezpallet always return `None` // This is a kind of special hardcoded case that should never happen in practice // as the first thing the pezpallet does is to initialize the genesis slot. assert_eq!(Sassafras::slot_ticket_id(0.into()), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 100), None); // Initialize genesis slot.. GenesisSlot::::set(genesis_slot); pezframe_system::Pezpallet::::set_block_number(One::one()); // Try to fetch a ticket for a slot before current epoch. assert_eq!(Sassafras::slot_ticket_id(0.into()), None); // Current epoch tickets. assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 0), Some(curr_tickets[1])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 1), Some(curr_tickets[3])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 2), Some(curr_tickets[5])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 3), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 4), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 5), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 6), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 7), Some(curr_tickets[4])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 8), Some(curr_tickets[2])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 9), Some(curr_tickets[0])); // Next epoch tickets (note that only 5 tickets are available) assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 10), Some(next_tickets[1])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 11), Some(next_tickets[3])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 12), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 13), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 14), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 15), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 16), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 17), Some(next_tickets[4])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 18), Some(next_tickets[2])); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 19), Some(next_tickets[0])); // Try to fetch the tickets for slots beyond the next epoch. assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 20), None); assert_eq!(Sassafras::slot_ticket_id(genesis_slot + 42), None); }); } // Different test for outside-in test with more focus on corner case correctness. #[test] fn slot_ticket_id_outside_in_fetch_corner_cases() { new_test_ext(0).execute_with(|| { pezframe_system::Pezpallet::::set_block_number(One::one()); let mut meta = TicketsMetadata { tickets_count: [0, 0], unsorted_tickets_count: 0 }; let curr_epoch_idx = EpochIndex::::get(); let mut epoch_test = |epoch_idx| { let tag = (epoch_idx & 1) as u8; let epoch_start = Sassafras::epoch_start(epoch_idx); // cleanup meta.tickets_count = [0, 0]; TicketsMeta::::set(meta); assert!((0..10).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); meta.tickets_count[tag as usize] += 1; TicketsMeta::::set(meta); TicketsIds::::insert((tag, 0_u32), 1_u128); assert_eq!(Sassafras::slot_ticket_id((epoch_start + 9).into()), Some(1_u128)); assert!((0..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); meta.tickets_count[tag as usize] += 1; TicketsMeta::::set(meta); TicketsIds::::insert((tag, 1_u32), 2_u128); assert_eq!(Sassafras::slot_ticket_id((epoch_start + 0).into()), Some(2_u128)); assert!((1..9).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); meta.tickets_count[tag as usize] += 2; TicketsMeta::::set(meta); TicketsIds::::insert((tag, 2_u32), 3_u128); assert_eq!(Sassafras::slot_ticket_id((epoch_start + 8).into()), Some(3_u128)); assert!((1..8).all(|i| Sassafras::slot_ticket_id((epoch_start + i).into()).is_none())); }; // Even epoch epoch_test(curr_epoch_idx); epoch_test(curr_epoch_idx + 1); }); } #[test] fn on_first_block_after_genesis() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); ext.execute_with(|| { let start_slot = Slot::from(100); let start_block = 1; let digest = initialize_block(start_block, start_slot, Default::default(), &pairs[0]); let common_assertions = || { assert_eq!(Sassafras::genesis_slot(), start_slot); assert_eq!(Sassafras::current_slot(), start_slot); assert_eq!(Sassafras::epoch_index(), 0); assert_eq!(Sassafras::current_epoch_start(), start_slot); assert_eq!(Sassafras::current_slot_index(), 0); assert_eq!(Sassafras::randomness(), [0; 32]); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") ); }; // Post-initialization status assert!(SlotRandomness::::exists()); common_assertions(); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("f0d42f6b7c0d157ecbd788be44847b80a96c290c04b5dfa5d1d40c98aa0c04ed") ); let header = finalize_block(start_block); // Post-finalization status assert!(!SlotRandomness::::exists()); common_assertions(); assert_eq!( Sassafras::randomness_accumulator(), h2b("95a508cf10f877cf0457af3503a6cb3192763d5c15a7b9a58e40dc543efae889"), ); // Header data check assert_eq!(header.digest.logs.len(), 2); assert_eq!(header.digest.logs[0], digest.logs[0]); // Genesis epoch start deposits consensus let consensus_log = pezsp_consensus_sassafras::digests::ConsensusLog::NextEpochData( pezsp_consensus_sassafras::digests::NextEpochDescriptor { authorities: Sassafras::next_authorities().into_inner(), randomness: Sassafras::next_randomness(), config: None, }, ); let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); assert_eq!(header.digest.logs[1], consensus_digest) }) } #[test] fn on_normal_block() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); let start_slot = Slot::from(100); let start_block = 1; let end_block = start_block + 1; ext.execute_with(|| { initialize_block(start_block, start_slot, Default::default(), &pairs[0]); // We don't want to trigger an epoch change in this test. let epoch_length = Sassafras::epoch_length() as u64; assert!(epoch_length > end_block); // Progress to block 2 let digest = progress_to_block(end_block, &pairs[0]).unwrap(); let common_assertions = || { assert_eq!(Sassafras::genesis_slot(), start_slot); assert_eq!(Sassafras::current_slot(), start_slot + 1); assert_eq!(Sassafras::epoch_index(), 0); assert_eq!(Sassafras::current_epoch_start(), start_slot); assert_eq!(Sassafras::current_slot_index(), 1); assert_eq!(Sassafras::randomness(), [0; 32]); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") ); }; // Post-initialization status assert!(SlotRandomness::::exists()); common_assertions(); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("95a508cf10f877cf0457af3503a6cb3192763d5c15a7b9a58e40dc543efae889"), ); let header = finalize_block(end_block); // Post-finalization status assert!(!SlotRandomness::::exists()); common_assertions(); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("5465cb257ad20cd4b9400a9fc85af7b1e2e72b59debd8ca06580dfb76bfca394"), ); // Header data check assert_eq!(header.digest.logs.len(), 1); assert_eq!(header.digest.logs[0], digest.logs[0]); }); } #[test] fn produce_epoch_change_digest_no_config() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); ext.execute_with(|| { let start_slot = Slot::from(100); let start_block = 1; initialize_block(start_block, start_slot, Default::default(), &pairs[0]); // We want to trigger an epoch change in this test. let epoch_length = Sassafras::epoch_length() as u64; let end_block = start_block + epoch_length; let digest = progress_to_block(end_block, &pairs[0]).unwrap(); let common_assertions = || { assert_eq!(Sassafras::genesis_slot(), start_slot); assert_eq!(Sassafras::current_slot(), start_slot + epoch_length); assert_eq!(Sassafras::epoch_index(), 1); assert_eq!(Sassafras::current_epoch_start(), start_slot + epoch_length); assert_eq!(Sassafras::current_slot_index(), 0); println!("[DEBUG] {}", b2h(Sassafras::randomness())); assert_eq!( Sassafras::randomness(), h2b("a49592ef190b96f3eb87bde4c8355e33df28c75006156e8c81998158de2ed49e") ); }; // Post-initialization status assert!(SlotRandomness::::exists()); common_assertions(); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("c4d374ed47b71e1c29e57143db23861916ff2d0c59ead4c51070d42ff4af2830"), ); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("c6d84d1f389853959c39271a38010f2f27abe6ff56cc419cf9e89eafcae1ab5e"), ); let header = finalize_block(end_block); // Post-finalization status assert!(!SlotRandomness::::exists()); common_assertions(); println!("[DEBUG] {}", b2h(Sassafras::next_randomness())); assert_eq!( Sassafras::next_randomness(), h2b("c4d374ed47b71e1c29e57143db23861916ff2d0c59ead4c51070d42ff4af2830"), ); println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator())); assert_eq!( Sassafras::randomness_accumulator(), h2b("6ca02b90e14ef11b3855069794da7e9d4007526b0588c426c3e3533b0b6ade7a"), ); // Header data check assert_eq!(header.digest.logs.len(), 2); assert_eq!(header.digest.logs[0], digest.logs[0]); // Deposits consensus log on epoch change let consensus_log = pezsp_consensus_sassafras::digests::ConsensusLog::NextEpochData( pezsp_consensus_sassafras::digests::NextEpochDescriptor { authorities: Sassafras::next_authorities().into_inner(), randomness: Sassafras::next_randomness(), config: None, }, ); let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); assert_eq!(header.digest.logs[1], consensus_digest) }) } #[test] fn produce_epoch_change_digest_with_config() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); ext.execute_with(|| { let start_slot = Slot::from(100); let start_block = 1; initialize_block(start_block, start_slot, Default::default(), &pairs[0]); let config = EpochConfiguration { redundancy_factor: 1, attempts_number: 123 }; Sassafras::plan_config_change(RuntimeOrigin::root(), config).unwrap(); // We want to trigger an epoch change in this test. let epoch_length = Sassafras::epoch_length() as u64; let end_block = start_block + epoch_length; let digest = progress_to_block(end_block, &pairs[0]).unwrap(); let header = finalize_block(end_block); // Header data check. // Skip pezpallet status checks that were already performed by other tests. assert_eq!(header.digest.logs.len(), 2); assert_eq!(header.digest.logs[0], digest.logs[0]); // Deposits consensus log on epoch change let consensus_log = pezsp_consensus_sassafras::digests::ConsensusLog::NextEpochData( pezsp_consensus_sassafras::digests::NextEpochDescriptor { authorities: Sassafras::next_authorities().into_inner(), randomness: Sassafras::next_randomness(), config: Some(config), }, ); let consensus_digest = DigestItem::Consensus(SASSAFRAS_ENGINE_ID, consensus_log.encode()); assert_eq!(header.digest.logs[1], consensus_digest) }) } #[test] fn segments_incremental_sort_works() { let (pairs, mut ext) = new_test_ext_with_pairs(1, false); let pair = &pairs[0]; let segments_count = 14; let start_slot = Slot::from(100); let start_block = 1; ext.execute_with(|| { let epoch_length = Sassafras::epoch_length() as u64; // -3 just to have the last segment not full... let submitted_tickets_count = segments_count * SEGMENT_MAX_SIZE - 3; initialize_block(start_block, start_slot, Default::default(), pair); // Manually populate the segments to skip the threshold check let mut tickets = make_ticket_bodies(submitted_tickets_count, None); persist_next_epoch_tickets_as_segments(&tickets); // Proceed to half of the epoch (sortition should not have been started yet) let half_epoch_block = start_block + epoch_length / 2; progress_to_block(half_epoch_block, pair); let mut unsorted_tickets_count = submitted_tickets_count; // Check that next epoch tickets sortition is not started yet let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); assert_eq!(meta.tickets_count, [0, 0]); // Follow the incremental sortition block by block progress_to_block(half_epoch_block + 1, pair); unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE - 3; let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count,); assert_eq!(meta.tickets_count, [0, 0]); progress_to_block(half_epoch_block + 2, pair); unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); assert_eq!(meta.tickets_count, [0, 0]); progress_to_block(half_epoch_block + 3, pair); unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); assert_eq!(meta.tickets_count, [0, 0]); progress_to_block(half_epoch_block + 4, pair); unsorted_tickets_count -= 3 * SEGMENT_MAX_SIZE; let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); assert_eq!(meta.tickets_count, [0, 0]); let header = finalize_block(half_epoch_block + 4); // Sort should be finished now. // Check that next epoch tickets count have the correct value. // Bigger ticket ids were discarded during sortition. unsorted_tickets_count -= 2 * SEGMENT_MAX_SIZE; assert_eq!(unsorted_tickets_count, 0); let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, unsorted_tickets_count); assert_eq!(meta.tickets_count, [0, epoch_length as u32]); // Epoch change log should have been pushed as well assert_eq!(header.digest.logs.len(), 1); // No tickets for the current epoch assert_eq!(TicketsIds::::get((0, 0)), None); // Check persistence of "winning" tickets tickets.sort_by_key(|t| t.0); (0..epoch_length as usize).into_iter().for_each(|i| { let id = TicketsIds::::get((1, i as u32)).unwrap(); let body = TicketsData::::get(id).unwrap(); assert_eq!((id, body), tickets[i]); }); // Check removal of "loosing" tickets (epoch_length as usize..tickets.len()).into_iter().for_each(|i| { assert!(TicketsIds::::get((1, i as u32)).is_none()); assert!(TicketsData::::get(tickets[i].0).is_none()); }); // The next block will be the first produced on the new epoch. // At this point the tickets are found already sorted and ready to be used. let slot = Sassafras::current_slot() + 1; let number = System::block_number() + 1; initialize_block(number, slot, header.hash(), pair); let header = finalize_block(number); // Epoch changes digest is also produced assert_eq!(header.digest.logs.len(), 2); }); } #[test] fn tickets_fetch_works_after_epoch_change() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); let pair = &pairs[0]; let start_slot = Slot::from(100); let start_block = 1; let submitted_tickets = 300; ext.execute_with(|| { initialize_block(start_block, start_slot, Default::default(), pair); // We don't want to trigger an epoch change in this test. let epoch_length = Sassafras::epoch_length() as u64; assert!(epoch_length > 2); progress_to_block(2, &pairs[0]).unwrap(); // Persist tickets as three different segments. let tickets = make_ticket_bodies(submitted_tickets, None); persist_next_epoch_tickets_as_segments(&tickets); let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, submitted_tickets); assert_eq!(meta.tickets_count, [0, 0]); // Progress up to the last epoch slot (do not enact epoch change) progress_to_block(epoch_length, &pairs[0]).unwrap(); // At this point next epoch tickets should have been sorted and ready to be used let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, 0); assert_eq!(meta.tickets_count, [0, epoch_length as u32]); // Compute and sort the tickets ids (aka tickets scores) let mut expected_ids: Vec<_> = tickets.into_iter().map(|(id, _)| id).collect(); expected_ids.sort(); expected_ids.truncate(epoch_length as usize); // Check if we can fetch next epoch tickets ids (outside-in). let slot = Sassafras::current_slot(); assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[1]); assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[3]); assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[5]); assert_eq!(Sassafras::slot_ticket_id(slot + 4).unwrap(), expected_ids[7]); assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[6]); assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[4]); assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[2]); assert_eq!(Sassafras::slot_ticket_id(slot + 10).unwrap(), expected_ids[0]); assert!(Sassafras::slot_ticket_id(slot + 11).is_none()); // Enact epoch change by progressing one more block progress_to_block(epoch_length + 1, &pairs[0]).unwrap(); let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, 0); assert_eq!(meta.tickets_count, [0, 10]); // Check if we can fetch current epoch tickets ids (outside-in). let slot = Sassafras::current_slot(); assert_eq!(Sassafras::slot_ticket_id(slot).unwrap(), expected_ids[1]); assert_eq!(Sassafras::slot_ticket_id(slot + 1).unwrap(), expected_ids[3]); assert_eq!(Sassafras::slot_ticket_id(slot + 2).unwrap(), expected_ids[5]); assert_eq!(Sassafras::slot_ticket_id(slot + 3).unwrap(), expected_ids[7]); assert_eq!(Sassafras::slot_ticket_id(slot + 6).unwrap(), expected_ids[6]); assert_eq!(Sassafras::slot_ticket_id(slot + 7).unwrap(), expected_ids[4]); assert_eq!(Sassafras::slot_ticket_id(slot + 8).unwrap(), expected_ids[2]); assert_eq!(Sassafras::slot_ticket_id(slot + 9).unwrap(), expected_ids[0]); assert!(Sassafras::slot_ticket_id(slot + 10).is_none()); // Enact another epoch change, for which we don't have any ticket progress_to_block(2 * epoch_length + 1, &pairs[0]).unwrap(); let meta = TicketsMeta::::get(); assert_eq!(meta.unsorted_tickets_count, 0); assert_eq!(meta.tickets_count, [0, 0]); }); } #[test] fn block_allowed_to_skip_epochs() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); let pair = &pairs[0]; let start_slot = Slot::from(100); let start_block = 1; ext.execute_with(|| { let epoch_length = Sassafras::epoch_length() as u64; initialize_block(start_block, start_slot, Default::default(), pair); let tickets = make_ticket_bodies(3, Some(pair)); persist_next_epoch_tickets(&tickets); let next_random = Sassafras::next_randomness(); // We want to skip 3 epochs in this test. let offset = 4 * epoch_length; System::set_block_number(start_block + offset - 1); go_to_block(start_block + offset, start_slot + offset, &pairs[0]); // Post-initialization status assert!(SlotRandomness::::exists()); assert_eq!(Sassafras::genesis_slot(), start_slot); assert_eq!(Sassafras::current_slot(), start_slot + offset); assert_eq!(Sassafras::epoch_index(), 4); assert_eq!(Sassafras::current_epoch_start(), start_slot + offset); assert_eq!(Sassafras::current_slot_index(), 0); // Tickets data has been discarded assert_eq!(TicketsMeta::::get(), TicketsMetadata::default()); assert!(tickets.iter().all(|(id, _)| TicketsData::::get(id).is_none())); assert_eq!(SortedCandidates::::get().len(), 0); // We used the last known next epoch randomness as a fallback assert_eq!(next_random, Sassafras::randomness()); }); } #[test] fn obsolete_tickets_are_removed_on_epoch_change() { let (pairs, mut ext) = new_test_ext_with_pairs(4, false); let pair = &pairs[0]; let start_slot = Slot::from(100); let start_block = 1; ext.execute_with(|| { let epoch_length = Sassafras::epoch_length() as u64; initialize_block(start_block, start_slot, Default::default(), pair); let tickets = make_ticket_bodies(10, Some(pair)); let mut epoch1_tickets = tickets[..4].to_vec(); let mut epoch2_tickets = tickets[4..].to_vec(); // Persist some tickets for next epoch (N) persist_next_epoch_tickets(&epoch1_tickets); assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); // Check next epoch tickets presence epoch1_tickets.sort_by_key(|t| t.0); (0..epoch1_tickets.len()).into_iter().for_each(|i| { let id = TicketsIds::::get((1, i as u32)).unwrap(); let body = TicketsData::::get(id).unwrap(); assert_eq!((id, body), epoch1_tickets[i]); }); // Advance one epoch to enact the tickets progress_to_block(start_block + epoch_length, pair).unwrap(); assert_eq!(TicketsMeta::::get().tickets_count, [0, 4]); // Persist some tickets for next epoch (N+1) persist_next_epoch_tickets(&epoch2_tickets); assert_eq!(TicketsMeta::::get().tickets_count, [6, 4]); epoch2_tickets.sort_by_key(|t| t.0); // Check for this epoch and next epoch tickets presence (0..epoch1_tickets.len()).into_iter().for_each(|i| { let id = TicketsIds::::get((1, i as u32)).unwrap(); let body = TicketsData::::get(id).unwrap(); assert_eq!((id, body), epoch1_tickets[i]); }); (0..epoch2_tickets.len()).into_iter().for_each(|i| { let id = TicketsIds::::get((0, i as u32)).unwrap(); let body = TicketsData::::get(id).unwrap(); assert_eq!((id, body), epoch2_tickets[i]); }); // Advance to epoch 2 and check for cleanup progress_to_block(start_block + 2 * epoch_length, pair).unwrap(); assert_eq!(TicketsMeta::::get().tickets_count, [6, 0]); (0..epoch1_tickets.len()).into_iter().for_each(|i| { let id = TicketsIds::::get((1, i as u32)).unwrap(); assert!(TicketsData::::get(id).is_none()); }); (0..epoch2_tickets.len()).into_iter().for_each(|i| { let id = TicketsIds::::get((0, i as u32)).unwrap(); let body = TicketsData::::get(id).unwrap(); assert_eq!((id, body), epoch2_tickets[i]); }); }) } const TICKETS_FILE: &str = "src/data/25_tickets_100_auths.bin"; fn data_read(filename: &str) -> T { use std::{fs::File, io::Read}; let mut file = File::open(filename).unwrap(); let mut buf = Vec::new(); file.read_to_end(&mut buf).unwrap(); T::decode(&mut &buf[..]).unwrap() } fn data_write(filename: &str, data: T) { use std::{fs::File, io::Write}; let mut file = File::create(filename).unwrap(); let buf = data.encode(); file.write_all(&buf).unwrap(); } // We don't want to implement anything secure here. // Just a trivial shuffle for the tests. fn trivial_fisher_yates_shuffle(vector: &mut Vec, random_seed: u64) { let mut rng = random_seed as usize; for i in (1..vector.len()).rev() { let j = rng % (i + 1); vector.swap(i, j); rng = (rng.wrapping_mul(6364793005) + 1) as usize; // Some random number generation } } // For this test we use a set of pre-constructed tickets from a file. // Creating a large set of tickets on the fly takes time, and may be annoying // for test execution. // // A valid ring-context is required for this test since we are passing through the // `submit_ticket` call which tests for ticket validity. #[test] fn submit_tickets_with_ring_proof_check_works() { use pezsp_core::Pair as _; let (authorities, mut tickets): (Vec, Vec) = data_read(TICKETS_FILE); // Also checks that duplicates are discarded tickets.extend(tickets.clone()); trivial_fisher_yates_shuffle(&mut tickets, 321); let (pairs, mut ext) = new_test_ext_with_pairs(authorities.len(), true); let pair = &pairs[0]; // Check if deserialized data has been generated for the correct set of authorities... assert!(authorities.iter().zip(pairs.iter()).all(|(auth, pair)| auth == &pair.public())); ext.execute_with(|| { let start_slot = Slot::from(0); let start_block = 1; // Tweak the config to discard ~half of the tickets. let mut config = EpochConfig::::get(); config.redundancy_factor = 25; EpochConfig::::set(config); initialize_block(start_block, start_slot, Default::default(), pair); NextRandomness::::set([0; 32]); // Check state before tickets submission assert_eq!( TicketsMeta::::get(), TicketsMetadata { unsorted_tickets_count: 0, tickets_count: [0, 0] }, ); // Submit the tickets let max_tickets_per_call = Sassafras::epoch_length() as usize; tickets.chunks(max_tickets_per_call).for_each(|chunk| { let chunk = BoundedVec::truncate_from(chunk.to_vec()); Sassafras::submit_tickets(RuntimeOrigin::none(), chunk).unwrap(); }); // Check state after submission assert_eq!( TicketsMeta::::get(), TicketsMetadata { unsorted_tickets_count: 13, tickets_count: [0, 0] }, ); assert_eq!(UnsortedSegments::::get(0).len(), 13); assert_eq!(UnsortedSegments::::get(1).len(), 0); finalize_block(start_block); }) } #[test] #[ignore = "test tickets data generator"] fn make_tickets_data() { use super::*; use pezsp_core::crypto::Pair; // Number of authorities who produces tickets (for the sake of this test) let tickets_authors_count = 5; // Total number of authorities (the ring) let authorities_count = 100; let (pairs, mut ext) = new_test_ext_with_pairs(authorities_count, true); let authorities: Vec<_> = pairs.iter().map(|sk| sk.public()).collect(); ext.execute_with(|| { let config = EpochConfig::::get(); let tickets_count = tickets_authors_count * config.attempts_number as usize; let mut tickets = Vec::with_capacity(tickets_count); // Construct pre-built tickets with a well known `NextRandomness` value. NextRandomness::::set([0; 32]); println!("Constructing {} tickets", tickets_count); pairs.iter().take(tickets_authors_count).enumerate().for_each(|(i, pair)| { let t = make_tickets(config.attempts_number, pair); tickets.extend(t); println!("{:.2}%", 100f32 * ((i + 1) as f32 / tickets_authors_count as f32)); }); data_write(TICKETS_FILE, (authorities, tickets)); }); }