Files
pezkuwi-subxt/substrate/frame/sassafras/src/tests.rs
T
Davide Galassi 095f4bd9ae Sassafras Consensus Pallet (#1577)
This PR introduces the pallet for Sassafras consensus.

## Non Goals

The pallet delivers only the bare-bones and doesn't deliver support for
auxiliary functionalities such as equivocation report and support for
epoch change via session pallet.

These functionalities were drafted in the [main
PR](https://github.com/paritytech/polkadot-sdk/pull/1336), but IMO is
better to introduce this auxiliary stuff in a follow up PR and after
client code.

## Potential follow ups

https://github.com/paritytech/polkadot-sdk/issues/2364

---------

Co-authored-by: Sebastian Kunert <skunert49@gmail.com>
Co-authored-by: Koute <koute@users.noreply.github.com>
Co-authored-by: Michal Kucharczyk <1728078+michalkucharczyk@users.noreply.github.com>
Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>
Co-authored-by: Bastian Köcher <git@kchr.de>
2023-12-01 16:39:07 +01:00

875 lines
31 KiB
Rust

// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// 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 pallet.
use crate::*;
use mock::*;
use sp_consensus_sassafras::Slot;
fn h2b<const N: usize>(hex: &str) -> [u8; N] {
array_bytes::hex2array_unchecked(hex)
}
fn b2h<const N: usize>(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
NextRandomness::<Test>::set([0; 32]);
RandomnessAccumulator::<Test>::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
NextRandomness::<Test>::set([0; 32]);
RandomnessAccumulator::<Test>::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<TicketId> = (0..tickets_count).map(|i| i as TicketId).collect();
// Next epoch tickets
let next_tickets: Vec<TicketId> =
(0..tickets_count - 1).map(|i| (i + tickets_count) as TicketId).collect();
new_test_ext(0).execute_with(|| {
// Some corner cases
TicketsIds::<Test>::insert((0, 0_u32), 1_u128);
// Cleanup
(0..3).for_each(|i| TicketsIds::<Test>::remove((0, i as u32)));
curr_tickets
.iter()
.enumerate()
.for_each(|(i, id)| TicketsIds::<Test>::insert((0, i as u32), id));
next_tickets
.iter()
.enumerate()
.for_each(|(i, id)| TicketsIds::<Test>::insert((1, i as u32), id));
TicketsMeta::<Test>::set(TicketsMetadata {
tickets_count: [curr_tickets.len() as u32, next_tickets.len() as u32],
unsorted_tickets_count: 0,
});
// Before importing the first block the pallet always return `None`
// This is a kind of special hardcoded case that should never happen in practice
// as the first thing the pallet 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::<Test>::set(genesis_slot);
frame_system::Pallet::<Test>::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(|| {
frame_system::Pallet::<Test>::set_block_number(One::one());
let mut meta = TicketsMetadata { tickets_count: [0, 0], unsorted_tickets_count: 0 };
let curr_epoch_idx = EpochIndex::<Test>::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::<Test>::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::<Test>::set(meta);
TicketsIds::<Test>::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::<Test>::set(meta);
TicketsIds::<Test>::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::<Test>::set(meta);
TicketsIds::<Test>::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!(ClaimTemporaryData::<Test>::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!(!ClaimTemporaryData::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"),
);
// 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 = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData(
sp_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!(ClaimTemporaryData::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"),
);
let header = finalize_block(end_block);
// Post-finalization status
assert!(!ClaimTemporaryData::<Test>::exists());
common_assertions();
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("be9261adb9686dfd3f23f8a276b7acc7f4beb3137070beb64c282ac22d84cbf0"),
);
// 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!(ClaimTemporaryData::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
assert_eq!(
Sassafras::next_randomness(),
h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"),
);
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("bf0f1228f4ff953c8c1bda2cceb668bf86ea05d7ae93e26d021c9690995d5279"),
);
let header = finalize_block(end_block);
// Post-finalization status
assert!(!ClaimTemporaryData::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
assert_eq!(
Sassafras::next_randomness(),
h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"),
);
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("8a1ceb346036c386d021264b10912c8b656799668004c4a487222462b394cd89"),
);
// 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 = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData(
sp_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 pallet 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 = sp_consensus_sassafras::digests::ConsensusLog::NextEpochData(
sp_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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::get((1, i as u32)).unwrap();
let body = TicketsData::<Test>::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::<Test>::get((1, i as u32)).is_none());
assert!(TicketsData::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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;
go_to_block(start_block + offset, start_slot + offset, &pairs[0]);
// Post-initialization status
assert!(ClaimTemporaryData::<Test>::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::<Test>::get(), TicketsMetadata::default());
assert!(tickets.iter().all(|(id, _)| TicketsData::<Test>::get(id).is_none()));
assert_eq!(SortedCandidates::<Test>::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::<Test>::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::<Test>::get((1, i as u32)).unwrap();
let body = TicketsData::<Test>::get(id).unwrap();
assert_eq!((id, body), epoch1_tickets[i]);
});
// Advance one epoch to enact the tickets
go_to_block(start_block + epoch_length, start_slot + epoch_length, pair);
assert_eq!(TicketsMeta::<Test>::get().tickets_count, [0, 4]);
// Persist some tickets for next epoch (N+1)
persist_next_epoch_tickets(&epoch2_tickets);
assert_eq!(TicketsMeta::<Test>::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::<Test>::get((1, i as u32)).unwrap();
let body = TicketsData::<Test>::get(id).unwrap();
assert_eq!((id, body), epoch1_tickets[i]);
});
(0..epoch2_tickets.len()).into_iter().for_each(|i| {
let id = TicketsIds::<Test>::get((0, i as u32)).unwrap();
let body = TicketsData::<Test>::get(id).unwrap();
assert_eq!((id, body), epoch2_tickets[i]);
});
// Advance to epoch 2 and check for cleanup
go_to_block(start_block + 2 * epoch_length, start_slot + 2 * epoch_length, pair);
assert_eq!(TicketsMeta::<Test>::get().tickets_count, [6, 0]);
(0..epoch1_tickets.len()).into_iter().for_each(|i| {
let id = TicketsIds::<Test>::get((1, i as u32)).unwrap();
assert!(TicketsData::<Test>::get(id).is_none());
});
(0..epoch2_tickets.len()).into_iter().for_each(|i| {
let id = TicketsIds::<Test>::get((0, i as u32)).unwrap();
let body = TicketsData::<Test>::get(id).unwrap();
assert_eq!((id, body), epoch2_tickets[i]);
});
})
}
const TICKETS_FILE: &str = "src/data/25_tickets_100_auths.bin";
fn data_read<T: Decode>(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<T: Encode>(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<T>(vector: &mut Vec<T>, 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 sp_core::Pair as _;
// env_logger::init();
let (authorities, mut tickets): (Vec<AuthorityId>, Vec<TicketEnvelope>) =
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::<Test>::get();
config.redundancy_factor = 25;
EpochConfig::<Test>::set(config);
initialize_block(start_block, start_slot, Default::default(), pair);
NextRandomness::<Test>::set([0; 32]);
// Check state before tickets submission
assert_eq!(
TicketsMeta::<Test>::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::<Test>::get(),
TicketsMetadata { unsorted_tickets_count: 16, tickets_count: [0, 0] },
);
assert_eq!(UnsortedSegments::<Test>::get(0).len(), 16);
assert_eq!(UnsortedSegments::<Test>::get(1).len(), 0);
finalize_block(start_block);
})
}
#[test]
#[ignore = "test tickets data generator"]
fn make_tickets_data() {
use super::*;
use sp_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::<Test>::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::<Test>::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));
});
}