feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
@@ -0,0 +1,226 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use crate::configuration::TestAuthorities;
use itertools::Itertools;
use pezkuwi_node_network_protocol::{
grid_topology::{SessionGridTopology, TopologyPeerInfo},
View,
};
use pezkuwi_node_primitives::approval::time::{Clock, SystemClock, Tick};
use pezkuwi_node_subsystem::messages::{
ApprovalDistributionMessage, ApprovalVotingParallelMessage,
};
use pezkuwi_node_subsystem_types::messages::{
network_bridge_event::NewGossipTopology, NetworkBridgeEvent,
};
use pezkuwi_overseer::AllMessages;
use pezkuwi_primitives::{
BlockNumber, CandidateEvent, CandidateReceiptV2, CoreIndex, GroupIndex, Hash, Header,
Id as ParaId, MutateDescriptorV2, Slot, ValidatorIndex,
};
use pezkuwi_primitives_test_helpers::dummy_candidate_receipt_v2_bad_sig;
use rand::{seq::SliceRandom, SeedableRng};
use rand_chacha::ChaCha20Rng;
use sc_network_types::PeerId;
use sp_consensus_babe::{
digests::{CompatibleDigestItem, PreDigest, SecondaryVRFPreDigest},
AllowedSlots, BabeEpochConfiguration, Epoch as BabeEpoch, VrfSignature, VrfTranscript,
};
use sp_core::crypto::VrfSecret;
use sp_keyring::sr25519::Keyring as Sr25519Keyring;
use sp_runtime::{Digest, DigestItem};
use std::sync::{atomic::AtomicU64, Arc};
/// A fake system clock used for driving the approval voting and make
/// it process blocks, assignments and approvals from the past.
#[derive(Clone)]
pub struct PastSystemClock {
/// The real system clock
real_system_clock: SystemClock,
/// The difference in ticks between the real system clock and the current clock.
delta_ticks: Arc<AtomicU64>,
}
impl PastSystemClock {
/// Creates a new fake system clock with `delta_ticks` between the real time and the fake one.
pub fn new(real_system_clock: SystemClock, delta_ticks: Arc<AtomicU64>) -> Self {
PastSystemClock { real_system_clock, delta_ticks }
}
}
impl Clock for PastSystemClock {
fn tick_now(&self) -> Tick {
self.real_system_clock.tick_now() -
self.delta_ticks.load(std::sync::atomic::Ordering::SeqCst)
}
fn wait(
&self,
tick: Tick,
) -> std::pin::Pin<Box<dyn futures::prelude::Future<Output = ()> + Send + 'static>> {
self.real_system_clock
.wait(tick + self.delta_ticks.load(std::sync::atomic::Ordering::SeqCst))
}
}
/// Helper function to generate a babe epoch for this benchmark.
/// It does not change for the duration of the test.
pub fn generate_babe_epoch(current_slot: Slot, authorities: TestAuthorities) -> BabeEpoch {
let authorities = authorities
.validator_babe_id
.into_iter()
.enumerate()
.map(|(index, public)| (public, index as u64))
.collect_vec();
BabeEpoch {
epoch_index: 1,
start_slot: current_slot.saturating_sub(1u64),
duration: 200,
authorities,
randomness: [0xde; 32],
config: BabeEpochConfiguration { c: (1, 4), allowed_slots: AllowedSlots::PrimarySlots },
}
}
/// Generates a topology to be used for this benchmark.
pub fn generate_topology(test_authorities: &TestAuthorities) -> SessionGridTopology {
let keyrings = test_authorities
.validator_authority_id
.clone()
.into_iter()
.zip(test_authorities.peer_ids.clone())
.collect_vec();
let topology = keyrings
.clone()
.into_iter()
.enumerate()
.map(|(index, (discovery_id, peer_id))| TopologyPeerInfo {
peer_ids: vec![peer_id],
validator_index: ValidatorIndex(index as u32),
discovery_id,
})
.collect_vec();
let shuffled = (0..keyrings.len()).collect_vec();
SessionGridTopology::new(shuffled, topology)
}
/// Generates new session topology message.
pub fn generate_new_session_topology(
test_authorities: &TestAuthorities,
test_node: ValidatorIndex,
approval_voting_parallel_enabled: bool,
) -> Vec<AllMessages> {
let topology = generate_topology(test_authorities);
let event = NetworkBridgeEvent::NewGossipTopology(NewGossipTopology {
session: 1,
topology,
local_index: Some(test_node),
});
vec![if approval_voting_parallel_enabled {
AllMessages::ApprovalVotingParallel(ApprovalVotingParallelMessage::NetworkBridgeUpdate(
event,
))
} else {
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::NetworkBridgeUpdate(event))
}]
}
/// Generates a peer view change for the passed `block_hash`
pub fn generate_peer_view_change_for(
block_hash: Hash,
peer_id: PeerId,
approval_voting_parallel_enabled: bool,
) -> AllMessages {
let network = NetworkBridgeEvent::PeerViewChange(peer_id, View::new([block_hash], 0));
if approval_voting_parallel_enabled {
AllMessages::ApprovalVotingParallel(ApprovalVotingParallelMessage::NetworkBridgeUpdate(
network,
))
} else {
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::NetworkBridgeUpdate(network))
}
}
/// Helper function to create a a signature for the block header.
fn garbage_vrf_signature() -> VrfSignature {
let transcript = VrfTranscript::new(b"test-garbage", &[]);
Sr25519Keyring::Alice.pair().vrf_sign(&transcript.into())
}
/// Helper function to create a block header.
pub fn make_header(parent_hash: Hash, slot: Slot, number: u32) -> Header {
let digest =
{
let mut digest = Digest::default();
let vrf_signature = garbage_vrf_signature();
digest.push(DigestItem::babe_pre_digest(PreDigest::SecondaryVRF(
SecondaryVRFPreDigest { authority_index: 0, slot, vrf_signature },
)));
digest
};
Header {
digest,
extrinsics_root: Default::default(),
number,
state_root: Default::default(),
parent_hash,
}
}
/// Helper function to create a candidate receipt.
fn make_candidate(para_id: ParaId, hash: &Hash) -> CandidateReceiptV2 {
let mut r = dummy_candidate_receipt_v2_bad_sig(*hash, Some(Default::default()));
r.descriptor.set_para_id(para_id);
r
}
/// Helper function to create a list of candidates that are included in the block
pub fn make_candidates(
block_hash: Hash,
block_number: BlockNumber,
num_cores: u32,
num_candidates: u32,
) -> Vec<CandidateEvent> {
let seed = [block_number as u8; 32];
let mut rand_chacha = ChaCha20Rng::from_seed(seed);
let mut candidates = (0..num_cores)
.map(|core| {
CandidateEvent::CandidateIncluded(
make_candidate(ParaId::from(core), &block_hash),
Vec::new().into(),
CoreIndex(core),
GroupIndex(core),
)
})
.collect_vec();
let (candidates, _) = candidates.partial_shuffle(&mut rand_chacha, num_candidates as usize);
candidates
.iter_mut()
.map(|val| val.clone())
.sorted_by(|a, b| match (a, b) {
(
CandidateEvent::CandidateIncluded(_, _, core_a, _),
CandidateEvent::CandidateIncluded(_, _, core_b, _),
) => core_a.0.cmp(&core_b.0),
(_, _) => todo!("Should not happen"),
})
.collect_vec()
}
@@ -0,0 +1,676 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use crate::{
approval::{
helpers::{generate_babe_epoch, generate_topology},
test_message::{MessagesBundle, TestMessageInfo},
ApprovalTestState, ApprovalsOptions, BlockTestData, GeneratedState,
BUFFER_FOR_GENERATION_MILLIS, LOG_TARGET, SLOT_DURATION_MILLIS,
},
configuration::{TestAuthorities, TestConfiguration},
mock::runtime_api::session_info_for_peers,
NODE_UNDER_TEST,
};
use codec::Encode;
use futures::SinkExt;
use itertools::Itertools;
use pezkuwi_node_core_approval_voting::criteria::{compute_assignments, Config};
use pezkuwi_node_network_protocol::{
grid_topology::{GridNeighbors, RandomRouting, RequiredRouting, SessionGridTopology},
v3 as protocol_v3,
};
use pezkuwi_node_primitives::approval::{
self,
time::tranche_to_tick,
v2::{CoreBitfield, IndirectAssignmentCertV2, IndirectSignedApprovalVoteV2},
};
use pezkuwi_primitives::{
ApprovalVoteMultipleCandidates, CandidateEvent, CandidateHash, CandidateIndex, CoreIndex, Hash,
SessionInfo, Slot, ValidatorId, ValidatorIndex, ASSIGNMENT_KEY_TYPE_ID,
};
use rand::{seq::SliceRandom, RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
use rand_distr::{Distribution, Normal};
use sc_keystore::LocalKeystore;
use sc_network_types::PeerId;
use sc_service::SpawnTaskHandle;
use sha1::Digest;
use sp_application_crypto::AppCrypto;
use sp_consensus_babe::SlotDuration;
use sp_keystore::Keystore;
use sp_timestamp::Timestamp;
use std::{
cmp::max,
collections::{BTreeMap, HashSet},
fs,
io::Write,
path::{Path, PathBuf},
time::Duration,
};
/// A generator of messages coming from a given Peer/Validator
pub struct PeerMessagesGenerator {
/// The grid neighbors of the node under test.
pub topology_node_under_test: GridNeighbors,
/// The topology of the network for the epoch under test.
pub topology: SessionGridTopology,
/// The validator index for this object generates the messages.
pub validator_index: ValidatorIndex,
/// An array of pre-generated random samplings, that is used to determine, which nodes would
/// send a given assignment, to the node under test because of the random samplings.
/// As an optimization we generate this sampling at the beginning of the test and just pick
/// one randomly, because always taking the samples would be too expensive for benchmark.
pub random_samplings: Vec<Vec<ValidatorIndex>>,
/// Channel for sending the generated messages to the aggregator
pub tx_messages: futures::channel::mpsc::UnboundedSender<(Hash, Vec<MessagesBundle>)>,
/// The list of test authorities
pub test_authorities: TestAuthorities,
//// The session info used for the test.
pub session_info: SessionInfo,
/// The blocks used for testing
pub blocks: Vec<BlockTestData>,
/// Approval options params.
pub options: ApprovalsOptions,
}
impl PeerMessagesGenerator {
/// Generates messages by spawning a blocking task in the background which begins creating
/// the assignments/approvals and peer view changes at the beginning of each block.
pub fn generate_messages(mut self, spawn_task_handle: &SpawnTaskHandle) {
spawn_task_handle.spawn("generate-messages", "generate-messages", async move {
for block_info in &self.blocks {
let assignments = self.generate_assignments(block_info);
let bytes = self.validator_index.0.to_be_bytes();
let seed = [
bytes[0], bytes[1], bytes[2], bytes[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let mut rand_chacha = ChaCha20Rng::from_seed(seed);
let approvals = issue_approvals(
assignments,
block_info.hash,
&self.test_authorities.validator_public,
block_info.candidates.clone(),
&self.options,
&mut rand_chacha,
self.test_authorities.keyring.keystore_ref(),
);
self.tx_messages
.send((block_info.hash, approvals))
.await
.expect("Should not fail");
}
})
}
// Builds the messages finger print corresponding to this configuration.
// When the finger print exists already on disk the messages are not re-generated.
fn messages_fingerprint(
configuration: &TestConfiguration,
options: &ApprovalsOptions,
) -> String {
let mut fingerprint = options.fingerprint();
let configuration_bytes = bincode::serialize(&configuration).unwrap();
fingerprint.extend(configuration_bytes);
let mut sha1 = sha1::Sha1::new();
sha1.update(fingerprint);
let result = sha1.finalize();
hex::encode(result)
}
/// Generate all messages(Assignments & Approvals) needed for approving `blocks``.
pub fn generate_messages_if_needed(
configuration: &TestConfiguration,
test_authorities: &TestAuthorities,
options: &ApprovalsOptions,
spawn_task_handle: &SpawnTaskHandle,
) -> PathBuf {
let path_name = format!(
"{}/{}",
options.workdir_prefix,
Self::messages_fingerprint(configuration, options)
);
let path = Path::new(&path_name);
if path.exists() {
return path.to_path_buf();
}
gum::info!("Generate message because file does not exist");
let delta_to_first_slot_under_test = Timestamp::new(BUFFER_FOR_GENERATION_MILLIS);
let initial_slot = Slot::from_timestamp(
(*Timestamp::current() - *delta_to_first_slot_under_test).into(),
SlotDuration::from_millis(SLOT_DURATION_MILLIS),
);
let babe_epoch = generate_babe_epoch(initial_slot, test_authorities.clone());
let session_info = session_info_for_peers(configuration, test_authorities);
let blocks = ApprovalTestState::generate_blocks_information(
configuration,
&babe_epoch,
initial_slot,
);
gum::info!(target: LOG_TARGET, "Generate messages");
let topology = generate_topology(test_authorities);
let random_samplings = random_samplings_to_node(
ValidatorIndex(NODE_UNDER_TEST),
test_authorities.validator_public.len(),
test_authorities.validator_public.len() * 2,
);
let topology_node_under_test =
topology.compute_grid_neighbors_for(ValidatorIndex(NODE_UNDER_TEST)).unwrap();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
// Spawn a thread to generate the messages for each validator, so that we speed up the
// generation.
for current_validator_index in 1..test_authorities.validator_public.len() {
let peer_message_source = PeerMessagesGenerator {
topology_node_under_test: topology_node_under_test.clone(),
topology: topology.clone(),
validator_index: ValidatorIndex(current_validator_index as u32),
test_authorities: test_authorities.clone(),
session_info: session_info.clone(),
blocks: blocks.clone(),
tx_messages: tx.clone(),
random_samplings: random_samplings.clone(),
options: options.clone(),
};
peer_message_source.generate_messages(spawn_task_handle);
}
std::mem::drop(tx);
let seed = [0x32; 32];
let mut rand_chacha = ChaCha20Rng::from_seed(seed);
let mut all_messages: BTreeMap<u64, Vec<MessagesBundle>> = BTreeMap::new();
// Receive all messages and sort them by Tick they have to be sent.
loop {
match rx.try_next() {
Ok(Some((block_hash, messages))) =>
for message in messages {
let block_info = blocks
.iter()
.find(|val| val.hash == block_hash)
.expect("Should find blocks");
let tick_to_send = tranche_to_tick(
SLOT_DURATION_MILLIS,
block_info.slot,
message.tranche_to_send(),
);
let to_add = all_messages.entry(tick_to_send).or_default();
to_add.push(message);
},
Ok(None) => break,
Err(_) => {
std::thread::sleep(Duration::from_millis(50));
},
}
}
let all_messages = all_messages
.into_iter()
.flat_map(|(_, mut messages)| {
// Shuffle the messages inside the same tick, so that we don't priorities messages
// for older nodes. we try to simulate the same behaviour as in real world.
messages.shuffle(&mut rand_chacha);
messages
})
.collect_vec();
gum::info!("Generated a number of {:} unique messages", all_messages.len());
let generated_state = GeneratedState { all_messages: Some(all_messages), initial_slot };
let mut messages_file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.unwrap();
messages_file
.write_all(&generated_state.encode())
.expect("Could not update message file");
path.to_path_buf()
}
/// Generates assignments for the given `current_validator_index`
/// Returns a list of assignments to be sent sorted by tranche.
fn generate_assignments(&self, block_info: &BlockTestData) -> Vec<TestMessageInfo> {
let config = Config::from(&self.session_info);
let leaving_cores = block_info
.candidates
.clone()
.into_iter()
.map(|candidate_event| {
if let CandidateEvent::CandidateIncluded(candidate, _, core_index, group_index) =
candidate_event
{
(candidate.hash(), core_index, group_index)
} else {
todo!("Variant is never created in this benchmark")
}
})
.collect_vec();
let mut assignments_by_tranche = BTreeMap::new();
let bytes = self.validator_index.0.to_be_bytes();
let seed = [
bytes[0], bytes[1], bytes[2], bytes[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let mut rand_chacha = ChaCha20Rng::from_seed(seed);
let to_be_sent_by = neighbours_that_would_sent_message(
&self.test_authorities.peer_ids,
self.validator_index.0,
&self.topology_node_under_test,
&self.topology,
);
let leaving_cores = leaving_cores
.clone()
.into_iter()
.filter(|(_, core_index, _group_index)| core_index.0 != self.validator_index.0)
.collect_vec();
let store = LocalKeystore::in_memory();
let _public = store
.sr25519_generate_new(
ASSIGNMENT_KEY_TYPE_ID,
Some(self.test_authorities.key_seeds[self.validator_index.0 as usize].as_str()),
)
.expect("should not fail");
let assignments = compute_assignments(
&store,
block_info.relay_vrf_story.clone(),
&config,
leaving_cores.clone(),
self.options.enable_assignments_v2,
);
let random_sending_nodes = self
.random_samplings
.get(rand_chacha.next_u32() as usize % self.random_samplings.len())
.unwrap();
let random_sending_peer_ids = random_sending_nodes
.iter()
.map(|validator| (*validator, self.test_authorities.peer_ids[validator.0 as usize]))
.collect_vec();
let mut unique_assignments = HashSet::new();
for (core_index, assignment) in assignments {
let assigned_cores = match &assignment.cert().kind {
approval::v2::AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } =>
core_bitfield.iter_ones().map(|val| CoreIndex::from(val as u32)).collect_vec(),
approval::v2::AssignmentCertKindV2::RelayVRFDelay { core_index } => {
vec![*core_index]
},
approval::v2::AssignmentCertKindV2::RelayVRFModulo { sample: _ } => {
vec![core_index]
},
};
let bitfiled: CoreBitfield = assigned_cores.clone().try_into().unwrap();
// For the cases where tranch0 assignments are in a single certificate we need to make
// sure we create a single message.
if unique_assignments.insert(bitfiled) {
let this_tranche_assignments =
assignments_by_tranche.entry(assignment.tranche()).or_insert_with(Vec::new);
this_tranche_assignments.push((
IndirectAssignmentCertV2 {
block_hash: block_info.hash,
validator: self.validator_index,
cert: assignment.cert().clone(),
},
block_info
.candidates
.iter()
.enumerate()
.filter(|(_index, candidate)| {
if let CandidateEvent::CandidateIncluded(_, _, core, _) = candidate {
assigned_cores.contains(core)
} else {
panic!("Should not happen");
}
})
.map(|(index, _)| index as u32)
.collect_vec()
.try_into()
.unwrap(),
to_be_sent_by
.iter()
.chain(random_sending_peer_ids.iter())
.copied()
.collect::<HashSet<(ValidatorIndex, PeerId)>>(),
assignment.tranche(),
));
}
}
assignments_by_tranche
.into_values()
.flat_map(|assignments| assignments.into_iter())
.map(|assignment| {
let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![(
assignment.0,
assignment.1,
)]);
TestMessageInfo {
msg,
sent_by: assignment
.2
.into_iter()
.map(|(validator_index, _)| validator_index)
.collect_vec(),
tranche: assignment.3,
block_hash: block_info.hash,
}
})
.collect_vec()
}
}
/// A list of random samplings that we use to determine which nodes should send a given message to
/// the node under test.
/// We can not sample every time for all the messages because that would be too expensive to
/// perform, so pre-generate a list of samples for a given network size.
/// - result[i] give us as a list of random nodes that would send a given message to the node under
/// test.
fn random_samplings_to_node(
node_under_test: ValidatorIndex,
num_validators: usize,
num_samplings: usize,
) -> Vec<Vec<ValidatorIndex>> {
let seed = [7u8; 32];
let mut rand_chacha = ChaCha20Rng::from_seed(seed);
(0..num_samplings)
.map(|_| {
(0..num_validators)
.filter(|sending_validator_index| {
*sending_validator_index != NODE_UNDER_TEST as usize
})
.flat_map(|sending_validator_index| {
let mut validators = (0..num_validators).collect_vec();
validators.shuffle(&mut rand_chacha);
let mut random_routing = RandomRouting::default();
validators
.into_iter()
.flat_map(|validator_to_send| {
if random_routing.sample(num_validators, &mut rand_chacha) {
random_routing.inc_sent();
if validator_to_send == node_under_test.0 as usize {
Some(ValidatorIndex(sending_validator_index as u32))
} else {
None
}
} else {
None
}
})
.collect_vec()
})
.collect_vec()
})
.collect_vec()
}
/// Helper function to randomly determine how many approvals we coalesce together in a single
/// message.
fn coalesce_approvals_len(
coalesce_mean: f32,
coalesce_std_dev: f32,
rand_chacha: &mut ChaCha20Rng,
) -> usize {
max(
1,
Normal::new(coalesce_mean, coalesce_std_dev)
.expect("normal distribution parameters are good")
.sample(rand_chacha)
.round() as i32,
) as usize
}
/// Helper function to create approvals signatures for all assignments passed as arguments.
/// Returns a list of Approvals messages that need to be sent.
fn issue_approvals(
assignments: Vec<TestMessageInfo>,
block_hash: Hash,
validator_ids: &[ValidatorId],
candidates: Vec<CandidateEvent>,
options: &ApprovalsOptions,
rand_chacha: &mut ChaCha20Rng,
store: &LocalKeystore,
) -> Vec<MessagesBundle> {
let mut queued_to_sign: Vec<TestSignInfo> = Vec::new();
let mut num_coalesce =
coalesce_approvals_len(options.coalesce_mean, options.coalesce_std_dev, rand_chacha);
let result = assignments
.iter()
.map(|message| match &message.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(assignments) => {
let mut approvals_to_create = Vec::new();
let current_validator_index = queued_to_sign
.first()
.map(|msg| msg.validator_index)
.unwrap_or(ValidatorIndex(99999));
// Invariant for this benchmark.
assert_eq!(assignments.len(), 1);
let assignment = assignments.first().unwrap();
let earliest_tranche = queued_to_sign
.first()
.map(|val| val.assignment.tranche)
.unwrap_or(message.tranche);
if queued_to_sign.len() >= num_coalesce ||
(!queued_to_sign.is_empty() &&
current_validator_index != assignment.0.validator) ||
message.tranche - earliest_tranche >= options.coalesce_tranche_diff
{
approvals_to_create.push(TestSignInfo::sign_candidates(
&mut queued_to_sign,
validator_ids,
block_hash,
num_coalesce,
store,
));
num_coalesce = coalesce_approvals_len(
options.coalesce_mean,
options.coalesce_std_dev,
rand_chacha,
);
}
// If more that one candidate was in the assignment queue all of them for issuing
// approvals
for candidate_index in assignment.1.iter_ones() {
let candidate = candidates.get(candidate_index).unwrap();
if let CandidateEvent::CandidateIncluded(candidate, _, _, _) = candidate {
queued_to_sign.push(TestSignInfo {
candidate_hash: candidate.hash(),
candidate_index: candidate_index as CandidateIndex,
validator_index: assignment.0.validator,
assignment: message.clone(),
});
} else {
todo!("Other enum variants are not used in this benchmark");
}
}
approvals_to_create
},
_ => {
todo!("Other enum variants are not used in this benchmark");
},
})
.collect_vec();
let mut messages = result.into_iter().flatten().collect_vec();
if !queued_to_sign.is_empty() {
messages.push(TestSignInfo::sign_candidates(
&mut queued_to_sign,
validator_ids,
block_hash,
num_coalesce,
store,
));
}
messages
}
/// Helper struct to gather information about more than one candidate an sign it in a single
/// approval message.
struct TestSignInfo {
/// The candidate hash
candidate_hash: CandidateHash,
/// The candidate index
candidate_index: CandidateIndex,
/// The validator sending the assignments
validator_index: ValidatorIndex,
/// The assignments covering this candidate
assignment: TestMessageInfo,
}
impl TestSignInfo {
/// Helper function to create a signature for all candidates in `to_sign` parameter.
/// Returns a TestMessage
fn sign_candidates(
to_sign: &mut Vec<TestSignInfo>,
validator_ids: &[ValidatorId],
block_hash: Hash,
num_coalesce: usize,
store: &LocalKeystore,
) -> MessagesBundle {
let current_validator_index = to_sign.first().map(|val| val.validator_index).unwrap();
let tranche_approval_can_be_sent =
to_sign.iter().map(|val| val.assignment.tranche).max().unwrap();
let validator_id = validator_ids.get(current_validator_index.0 as usize).unwrap().clone();
let unique_assignments: HashSet<TestMessageInfo> =
to_sign.iter().map(|info| info.assignment.clone()).collect();
let mut to_sign = to_sign
.drain(..)
.sorted_by(|val1, val2| val1.candidate_index.cmp(&val2.candidate_index))
.peekable();
let mut bundle = MessagesBundle {
assignments: unique_assignments.into_iter().collect_vec(),
approvals: Vec::new(),
};
while to_sign.peek().is_some() {
let to_sign = to_sign.by_ref().take(num_coalesce).collect_vec();
let hashes = to_sign.iter().map(|val| val.candidate_hash).collect_vec();
let candidate_indices = to_sign.iter().map(|val| val.candidate_index).collect_vec();
let sent_by = to_sign
.iter()
.flat_map(|val| val.assignment.sent_by.iter())
.copied()
.collect::<HashSet<ValidatorIndex>>();
let payload = ApprovalVoteMultipleCandidates(&hashes).signing_payload(1);
let signature = store
.sr25519_sign(ValidatorId::ID, &validator_id.clone().into(), &payload[..])
.unwrap()
.unwrap()
.into();
let indirect = IndirectSignedApprovalVoteV2 {
block_hash,
candidate_indices: candidate_indices.try_into().unwrap(),
validator: current_validator_index,
signature,
};
let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![indirect]);
bundle.approvals.push(TestMessageInfo {
msg,
sent_by: sent_by.into_iter().collect_vec(),
tranche: tranche_approval_can_be_sent,
block_hash,
});
}
bundle
}
}
/// Determine what neighbours would send a given message to the node under test.
fn neighbours_that_would_sent_message(
peer_ids: &[PeerId],
current_validator_index: u32,
topology_node_under_test: &GridNeighbors,
topology: &SessionGridTopology,
) -> Vec<(ValidatorIndex, PeerId)> {
let topology_originator = topology
.compute_grid_neighbors_for(ValidatorIndex(current_validator_index))
.unwrap();
let originator_y = topology_originator.validator_indices_y.iter().find(|validator| {
topology_node_under_test.required_routing_by_index(**validator, false) ==
RequiredRouting::GridY
});
assert!(originator_y != Some(&ValidatorIndex(NODE_UNDER_TEST)));
let originator_x = topology_originator.validator_indices_x.iter().find(|validator| {
topology_node_under_test.required_routing_by_index(**validator, false) ==
RequiredRouting::GridX
});
assert!(originator_x != Some(&ValidatorIndex(NODE_UNDER_TEST)));
let is_neighbour = topology_originator
.validator_indices_x
.contains(&ValidatorIndex(NODE_UNDER_TEST)) ||
topology_originator
.validator_indices_y
.contains(&ValidatorIndex(NODE_UNDER_TEST));
let mut to_be_sent_by = originator_y
.into_iter()
.chain(originator_x)
.map(|val| (*val, peer_ids[val.0 as usize]))
.collect_vec();
if is_neighbour {
to_be_sent_by.push((ValidatorIndex(current_validator_index), peer_ids[0]));
}
to_be_sent_by
}
@@ -0,0 +1,64 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use crate::approval::{ApprovalTestState, PastSystemClock, LOG_TARGET, SLOT_DURATION_MILLIS};
use futures::FutureExt;
use pezkuwi_node_primitives::approval::time::{slot_number_to_tick, Clock, TICK_DURATION_MILLIS};
use pezkuwi_node_subsystem::{overseer, SpawnedSubsystem, SubsystemError};
use pezkuwi_node_subsystem_types::messages::ChainSelectionMessage;
/// Mock ChainSelection subsystem used to answer request made by the approval-voting subsystem,
/// during benchmark. All the necessary information to answer the requests is stored in the `state`
pub struct MockChainSelection {
pub state: ApprovalTestState,
pub clock: PastSystemClock,
}
#[overseer::subsystem(ChainSelection, error=SubsystemError, prefix=self::overseer)]
impl<Context> MockChainSelection {
fn start(self, ctx: Context) -> SpawnedSubsystem {
let future = self.run(ctx).map(|_| Ok(())).boxed();
SpawnedSubsystem { name: "mock-chain-subsystem", future }
}
}
#[overseer::contextbounds(ChainSelection, prefix = self::overseer)]
impl MockChainSelection {
async fn run<Context>(self, mut ctx: Context) {
loop {
let msg = ctx.recv().await.expect("Should not fail");
match msg {
orchestra::FromOrchestra::Signal(_) => {},
orchestra::FromOrchestra::Communication { msg } =>
if let ChainSelectionMessage::Approved(hash) = msg {
let block_info = self.state.get_info_by_hash(hash);
let approved_number = block_info.block_number;
block_info.approved.store(true, std::sync::atomic::Ordering::SeqCst);
self.state
.last_approved_block
.store(approved_number, std::sync::atomic::Ordering::SeqCst);
let approved_in_tick = self.clock.tick_now() -
slot_number_to_tick(SLOT_DURATION_MILLIS, block_info.slot);
gum::info!(target: LOG_TARGET, ?hash, "Chain selection approved after {:} ms", approved_in_tick * TICK_DURATION_MILLIS);
},
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Pezkuwi.
// Pezkuwi 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.
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
use crate::{
approval::{ApprovalsOptions, BlockTestData, CandidateTestData},
configuration::TestAuthorities,
};
use codec::{Decode, Encode};
use itertools::Itertools;
use pezkuwi_node_network_protocol::v3 as protocol_v3;
use pezkuwi_primitives::{CandidateIndex, Hash, ValidatorIndex};
use sc_network_types::PeerId;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct TestMessageInfo {
/// The actual message
pub msg: protocol_v3::ApprovalDistributionMessage,
/// The list of peers that would sends this message in a real topology.
/// It includes both the peers that would send the message because of the topology
/// or because of randomly choosing so.
pub sent_by: Vec<ValidatorIndex>,
/// The tranche at which this message should be sent.
pub tranche: u32,
/// The block hash this message refers to.
pub block_hash: Hash,
}
impl std::hash::Hash for TestMessageInfo {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match &self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(assignments) => {
for (assignment, candidates) in assignments {
(assignment.block_hash, assignment.validator).hash(state);
candidates.hash(state);
}
},
protocol_v3::ApprovalDistributionMessage::Approvals(approvals) => {
for approval in approvals {
(approval.block_hash, approval.validator).hash(state);
approval.candidate_indices.hash(state);
}
},
};
}
}
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
/// A list of messages that depend of each-other, approvals cover one of the assignments and
/// vice-versa.
pub struct MessagesBundle {
pub assignments: Vec<TestMessageInfo>,
pub approvals: Vec<TestMessageInfo>,
}
impl MessagesBundle {
/// The tranche when this bundle can be sent correctly, so no assignments or approvals will be
/// from the future.
pub fn tranche_to_send(&self) -> u32 {
self.assignments
.iter()
.chain(self.approvals.iter())
.max_by(|a, b| a.tranche.cmp(&b.tranche))
.unwrap()
.tranche
}
/// The min tranche in the bundle.
pub fn min_tranche(&self) -> u32 {
self.assignments
.iter()
.chain(self.approvals.iter())
.min_by(|a, b| a.tranche.cmp(&b.tranche))
.unwrap()
.tranche
}
/// Tells if the bundle is needed for sending.
/// We either send it because we need more assignments and approvals to approve the candidates
/// or because we configured the test to send messages until a given tranche.
pub fn should_send(
&self,
candidates_test_data: &HashMap<(Hash, CandidateIndex), CandidateTestData>,
options: &ApprovalsOptions,
) -> bool {
self.needed_for_approval(candidates_test_data) ||
(!options.stop_when_approved &&
self.min_tranche() <= options.last_considered_tranche)
}
/// Tells if the bundle is needed because we need more messages to approve the candidates.
pub fn needed_for_approval(
&self,
candidates_test_data: &HashMap<(Hash, CandidateIndex), CandidateTestData>,
) -> bool {
self.assignments
.iter()
.any(|message| message.needed_for_approval(candidates_test_data))
}
/// Mark the assignments in the bundle as sent.
pub fn record_sent_assignment(
&self,
candidates_test_data: &mut HashMap<(Hash, CandidateIndex), CandidateTestData>,
) {
self.assignments
.iter()
.for_each(|assignment| assignment.record_sent_assignment(candidates_test_data));
}
}
impl TestMessageInfo {
/// Tells if the message is an approval.
fn is_approval(&self) -> bool {
match self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(_) => false,
protocol_v3::ApprovalDistributionMessage::Approvals(_) => true,
}
}
/// Records an approval.
/// We use this to check after all messages have been processed that we didn't loose any
/// message.
pub fn record_vote(&self, state: &BlockTestData) {
if self.is_approval() {
match &self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(_) => todo!(),
protocol_v3::ApprovalDistributionMessage::Approvals(approvals) => {
for approval in approvals {
for candidate_index in approval.candidate_indices.iter_ones() {
state
.votes
.get(approval.validator.0 as usize)
.unwrap()
.get(candidate_index)
.unwrap()
.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
},
}
}
}
/// Mark the assignments in the message as sent.
pub fn record_sent_assignment(
&self,
candidates_test_data: &mut HashMap<(Hash, CandidateIndex), CandidateTestData>,
) {
match &self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(assignments) => {
for (assignment, candidate_indices) in assignments {
for candidate_index in candidate_indices.iter_ones() {
let candidate_test_data = candidates_test_data
.get_mut(&(assignment.block_hash, candidate_index as CandidateIndex))
.unwrap();
candidate_test_data.mark_sent_assignment(self.tranche)
}
}
},
protocol_v3::ApprovalDistributionMessage::Approvals(_approvals) => todo!(),
}
}
/// Returns a list of candidates indices in this message
pub fn candidate_indices(&self) -> HashSet<usize> {
let mut unique_candidate_indices = HashSet::new();
match &self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(assignments) => {
for (_assignment, candidate_indices) in assignments {
for candidate_index in candidate_indices.iter_ones() {
unique_candidate_indices.insert(candidate_index);
}
}
},
protocol_v3::ApprovalDistributionMessage::Approvals(approvals) => {
for approval in approvals {
for candidate_index in approval.candidate_indices.iter_ones() {
unique_candidate_indices.insert(candidate_index);
}
}
},
}
unique_candidate_indices
}
/// Marks this message as no-shows if the number of configured no-shows is above the registered
/// no-shows.
/// Returns true if the message is a no-show.
pub fn no_show_if_required(
&self,
assignments: &[TestMessageInfo],
candidates_test_data: &mut HashMap<(Hash, CandidateIndex), CandidateTestData>,
) -> bool {
let mut should_no_show = false;
if self.is_approval() {
let covered_candidates = assignments
.iter()
.map(|assignment| (assignment, assignment.candidate_indices()))
.collect_vec();
match &self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(_) => todo!(),
protocol_v3::ApprovalDistributionMessage::Approvals(approvals) => {
assert_eq!(approvals.len(), 1);
for approval in approvals {
should_no_show = should_no_show ||
approval.candidate_indices.iter_ones().all(|candidate_index| {
let candidate_test_data = candidates_test_data
.get_mut(&(
approval.block_hash,
candidate_index as CandidateIndex,
))
.unwrap();
let assignment = covered_candidates
.iter()
.find(|(_assignment, candidates)| {
candidates.contains(&candidate_index)
})
.unwrap();
candidate_test_data.should_no_show(assignment.0.tranche)
});
if should_no_show {
for candidate_index in approval.candidate_indices.iter_ones() {
let candidate_test_data = candidates_test_data
.get_mut(&(
approval.block_hash,
candidate_index as CandidateIndex,
))
.unwrap();
let assignment = covered_candidates
.iter()
.find(|(_assignment, candidates)| {
candidates.contains(&candidate_index)
})
.unwrap();
candidate_test_data.record_no_show(assignment.0.tranche)
}
}
}
},
}
}
should_no_show
}
/// Tells if a message is needed for approval
pub fn needed_for_approval(
&self,
candidates_test_data: &HashMap<(Hash, CandidateIndex), CandidateTestData>,
) -> bool {
match &self.msg {
protocol_v3::ApprovalDistributionMessage::Assignments(assignments) =>
assignments.iter().any(|(assignment, candidate_indices)| {
candidate_indices.iter_ones().any(|candidate_index| {
candidates_test_data
.get(&(assignment.block_hash, candidate_index as CandidateIndex))
.map(|data| data.should_send_tranche(self.tranche))
.unwrap_or_default()
})
}),
protocol_v3::ApprovalDistributionMessage::Approvals(approvals) =>
approvals.iter().any(|approval| {
approval.candidate_indices.iter_ones().any(|candidate_index| {
candidates_test_data
.get(&(approval.block_hash, candidate_index as CandidateIndex))
.map(|data| data.should_send_tranche(self.tranche))
.unwrap_or_default()
})
}),
}
}
/// Splits a message into multiple messages based on what peers should send this message.
/// It build a HashMap of messages that should be sent by each peer.
pub fn split_by_peer_id(
self,
authorities: &TestAuthorities,
) -> HashMap<(ValidatorIndex, PeerId), Vec<TestMessageInfo>> {
let mut result: HashMap<(ValidatorIndex, PeerId), Vec<TestMessageInfo>> = HashMap::new();
for validator_index in &self.sent_by {
let peer = authorities.peer_ids.get(validator_index.0 as usize).unwrap();
result.entry((*validator_index, *peer)).or_default().push(TestMessageInfo {
msg: self.msg.clone(),
sent_by: Default::default(),
tranche: self.tranche,
block_hash: self.block_hash,
});
}
result
}
}