// 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 . #![forbid(unused_crate_dependencies)] #![forbid(unused_extern_crates)] //! A set of primitive constructors, to aid in crafting meaningful testcase while reducing //! repetition. //! //! Note that `dummy_` prefixed values are meant to be fillers, that should not matter, and will //! contain randomness based data. use codec::{Decode, Encode}; use pezkuwi_primitives::{ AppVerify, CandidateCommitments, CandidateDescriptorV2, CandidateHash, CandidateReceiptV2, CollatorId, CollatorSignature, CommittedCandidateReceiptV2, CoreIndex, Hash, HashT, HeadData, Id, Id as ParaId, InternalVersion, MutateDescriptorV2, PersistedValidationData, SessionIndex, ValidationCode, ValidationCodeHash, ValidatorId, }; use pezsp_application_crypto::{sr25519, ByteArray}; use pezsp_keyring::Sr25519Keyring; use pezsp_runtime::{generic::Digest, traits::BlakeTwo256}; pub use rand; use scale_info::TypeInfo; const MAX_POV_SIZE: u32 = 1_000_000; /// The legacy descriptor of a legacy candidate receipt. #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo)] pub struct CandidateDescriptor { /// The ID of the para this is a candidate for. pub para_id: Id, /// The hash of the relay-chain block this is executed in the context of. pub relay_parent: H, /// The collator's sr25519 public key. pub collator: CollatorId, /// The blake2-256 hash of the persisted validation data. This is extra data derived from /// relay-chain state which may vary based on bitfields included before the candidate. /// Thus it cannot be derived entirely from the relay-parent. pub persisted_validation_data_hash: Hash, /// The blake2-256 hash of the PoV. pub pov_hash: Hash, /// The root of a block's erasure encoding Merkle tree. pub erasure_root: Hash, /// Signature on blake2-256 of components of this receipt: /// The teyrchain index, the relay parent, the validation data hash, and the `pov_hash`. pub signature: CollatorSignature, /// Hash of the para header that is being generated by this candidate. pub para_head: Hash, /// The blake2-256 hash of the validation code bytes. pub validation_code_hash: ValidationCodeHash, } impl> CandidateDescriptor { /// Check the signature of the collator within this descriptor. pub fn check_collator_signature(&self) -> Result<(), ()> { check_collator_signature( &self.relay_parent, &self.para_id, &self.persisted_validation_data_hash, &self.pov_hash, &self.validation_code_hash, &self.collator, &self.signature, ) } } /// A legacy candidate-receipt. #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo)] pub struct CandidateReceipt { /// The descriptor of the candidate. pub descriptor: CandidateDescriptor, /// The hash of the encoded commitments made as a result of candidate execution. pub commitments_hash: Hash, } impl CandidateReceipt { /// Get a reference to the candidate descriptor. pub fn descriptor(&self) -> &CandidateDescriptor { &self.descriptor } /// Computes the blake2-256 hash of the receipt. pub fn hash(&self) -> CandidateHash where H: Encode, { CandidateHash(BlakeTwo256::hash_of(self)) } } impl From> for CandidateReceipt { fn from(value: CandidateReceiptV2) -> Self { Self { descriptor: value.descriptor.into(), commitments_hash: value.commitments_hash } } } impl> From> for CandidateReceiptV2 { fn from(value: CandidateReceipt) -> Self { Self { descriptor: value.descriptor.into(), commitments_hash: value.commitments_hash } } } /// A legacy candidate-receipt with commitments directly included. #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo)] pub struct CommittedCandidateReceipt { /// The descriptor of the candidate. pub descriptor: CandidateDescriptor, /// The commitments of the candidate receipt. pub commitments: CandidateCommitments, } impl CommittedCandidateReceipt { /// Get a reference to the candidate descriptor. pub fn descriptor(&self) -> &CandidateDescriptor { &self.descriptor } } impl CommittedCandidateReceipt { /// Transforms this into a plain `CandidateReceipt`. pub fn to_plain(&self) -> CandidateReceipt { CandidateReceipt { descriptor: self.descriptor.clone(), commitments_hash: self.commitments.hash(), } } /// Computes the hash of the committed candidate receipt. /// /// This computes the canonical hash, not the hash of the directly encoded data. /// Thus this is a shortcut for `candidate.to_plain().hash()`. pub fn hash(&self) -> CandidateHash where H: Encode, { self.to_plain().hash() } /// Does this committed candidate receipt corresponds to the given [`CandidateReceipt`]? pub fn corresponds_to(&self, receipt: &CandidateReceipt) -> bool where H: PartialEq, { receipt.descriptor == self.descriptor && receipt.commitments_hash == self.commitments.hash() } } impl PartialOrd for CommittedCandidateReceipt { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for CommittedCandidateReceipt { fn cmp(&self, other: &Self) -> core::cmp::Ordering { // TODO: compare signatures or something more sane // https://github.com/pezkuwichain/pezkuwi-sdk/issues/132 self.descriptor() .para_id .cmp(&other.descriptor().para_id) .then_with(|| self.commitments.head_data.cmp(&other.commitments.head_data)) } } impl From> for CommittedCandidateReceipt { fn from(value: CommittedCandidateReceiptV2) -> Self { Self { descriptor: value.descriptor.into(), commitments: value.commitments } } } impl From> for CandidateDescriptor { fn from(value: CandidateDescriptorV2) -> Self { Self { para_id: value.para_id(), relay_parent: value.relay_parent(), collator: value.rebuild_collator_field_for_tests(), persisted_validation_data_hash: value.persisted_validation_data_hash(), pov_hash: value.pov_hash(), erasure_root: value.erasure_root(), signature: value.rebuild_signature_field_for_tests(), para_head: value.para_head(), validation_code_hash: value.validation_code_hash(), } } } fn clone_into_array(slice: &[T]) -> A where A: Default + AsMut<[T]>, T: Clone, { let mut a = A::default(); >::as_mut(&mut a).clone_from_slice(slice); a } impl> From> for CandidateDescriptorV2 { fn from(value: CandidateDescriptor) -> Self { let collator = value.collator.as_slice(); CandidateDescriptorV2::new_from_raw( value.para_id, value.relay_parent, InternalVersion(collator[0]), u16::from_ne_bytes(clone_into_array(&collator[1..=2])), SessionIndex::from_ne_bytes(clone_into_array(&collator[3..=6])), clone_into_array(&collator[7..]), value.persisted_validation_data_hash, value.pov_hash, value.erasure_root, value.signature.into_inner().0, value.para_head, value.validation_code_hash, ) } } impl> From> for CommittedCandidateReceiptV2 { fn from(value: CommittedCandidateReceipt) -> Self { Self { descriptor: value.descriptor.into(), commitments: value.commitments } } } /// Get a collator signature payload on a relay-parent, block-data combo. pub fn collator_signature_payload>( relay_parent: &H, para_id: &Id, persisted_validation_data_hash: &Hash, pov_hash: &Hash, validation_code_hash: &ValidationCodeHash, ) -> [u8; 132] { // 32-byte hash length is protected in a test below. let mut payload = [0u8; 132]; payload[0..32].copy_from_slice(relay_parent.as_ref()); u32::from(*para_id).using_encoded(|s| payload[32..32 + s.len()].copy_from_slice(s)); payload[36..68].copy_from_slice(persisted_validation_data_hash.as_ref()); payload[68..100].copy_from_slice(pov_hash.as_ref()); payload[100..132].copy_from_slice(validation_code_hash.as_ref()); payload } pub(crate) fn check_collator_signature>( relay_parent: &H, para_id: &Id, persisted_validation_data_hash: &Hash, pov_hash: &Hash, validation_code_hash: &ValidationCodeHash, collator: &CollatorId, signature: &CollatorSignature, ) -> Result<(), ()> { let payload = collator_signature_payload( relay_parent, para_id, persisted_validation_data_hash, pov_hash, validation_code_hash, ); if signature.verify(&payload[..], collator) { Ok(()) } else { Err(()) } } /// Creates a candidate receipt with filler data. pub fn dummy_candidate_receipt>(relay_parent: H) -> CandidateReceipt { CandidateReceipt:: { commitments_hash: dummy_candidate_commitments(dummy_head_data()).hash(), descriptor: dummy_candidate_descriptor(relay_parent), } } /// Creates a v2 candidate receipt with filler data. pub fn dummy_candidate_receipt_v2 + Copy>(relay_parent: H) -> CandidateReceiptV2 { CandidateReceiptV2:: { commitments_hash: dummy_candidate_commitments(dummy_head_data()).hash(), descriptor: dummy_candidate_descriptor_v2(relay_parent), } } /// Creates a committed candidate receipt with filler data. pub fn dummy_committed_candidate_receipt>( relay_parent: H, ) -> CommittedCandidateReceipt { CommittedCandidateReceipt:: { descriptor: dummy_candidate_descriptor::(relay_parent), commitments: dummy_candidate_commitments(dummy_head_data()), } } /// Creates a v2 committed candidate receipt with filler data. pub fn dummy_committed_candidate_receipt_v2 + Copy>( relay_parent: H, ) -> CommittedCandidateReceiptV2 { CommittedCandidateReceiptV2 { descriptor: dummy_candidate_descriptor_v2::(relay_parent), commitments: dummy_candidate_commitments(dummy_head_data()), } } /// Create a candidate receipt with a bogus signature and filler data. Optionally set the commitment /// hash with the `commitments` arg. pub fn dummy_candidate_receipt_bad_sig( relay_parent: Hash, commitments: impl Into>, ) -> CandidateReceipt { let commitments_hash = if let Some(commitments) = commitments.into() { commitments } else { dummy_candidate_commitments(dummy_head_data()).hash() }; CandidateReceipt:: { commitments_hash, descriptor: dummy_candidate_descriptor_bad_sig(relay_parent), } } /// Create a candidate receipt with a bogus signature and filler data. Optionally set the commitment /// hash with the `commitments` arg. pub fn dummy_candidate_receipt_v2_bad_sig( relay_parent: Hash, commitments: impl Into>, ) -> CandidateReceiptV2 { let commitments_hash = if let Some(commitments) = commitments.into() { commitments } else { dummy_candidate_commitments(dummy_head_data()).hash() }; CandidateReceiptV2:: { commitments_hash, descriptor: dummy_candidate_descriptor_bad_sig(relay_parent).into(), } } /// Create candidate commitments with filler data. pub fn dummy_candidate_commitments(head_data: impl Into>) -> CandidateCommitments { CandidateCommitments { head_data: head_data.into().unwrap_or(dummy_head_data()), upward_messages: vec![].try_into().expect("empty vec fits within bounds"), new_validation_code: None, horizontal_messages: vec![].try_into().expect("empty vec fits within bounds"), processed_downward_messages: 0, hrmp_watermark: 0_u32, } } /// Create meaningless dummy hash. pub fn dummy_hash() -> Hash { Hash::zero() } /// Create meaningless dummy digest. pub fn dummy_digest() -> Digest { Digest::default() } /// Create a candidate descriptor with a bogus signature and filler data. pub fn dummy_candidate_descriptor_bad_sig(relay_parent: Hash) -> CandidateDescriptor { let zeros = Hash::zero(); CandidateDescriptor:: { para_id: 0.into(), relay_parent, collator: dummy_collator(), persisted_validation_data_hash: zeros, pov_hash: zeros, erasure_root: zeros, signature: dummy_collator_signature(), para_head: zeros, validation_code_hash: dummy_validation_code().hash(), } } /// Create a candidate descriptor with filler data. pub fn dummy_candidate_descriptor>(relay_parent: H) -> CandidateDescriptor { let collator = pezsp_keyring::Sr25519Keyring::Ferdie; let invalid = Hash::zero(); let descriptor = make_valid_candidate_descriptor( 1.into(), relay_parent, invalid, invalid, invalid, invalid, invalid, collator, ); descriptor } /// Create a v2 candidate descriptor with filler data. pub fn dummy_candidate_descriptor_v2 + Copy>( relay_parent: H, ) -> CandidateDescriptorV2 { let invalid = Hash::zero(); let descriptor = make_valid_candidate_descriptor_v2( 1.into(), relay_parent, CoreIndex(1), 1, invalid, invalid, invalid, invalid, invalid, ); descriptor } /// Create meaningless validation code. pub fn dummy_validation_code() -> ValidationCode { ValidationCode(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]) } /// Create meaningless head data. pub fn dummy_head_data() -> HeadData { HeadData(vec![]) } /// Create a meaningless validator id. pub fn dummy_validator() -> ValidatorId { ValidatorId::from(sr25519::Public::default()) } /// Create a meaningless collator id. pub fn dummy_collator() -> CollatorId { CollatorId::from(sr25519::Public::default()) } /// Create a meaningless collator signature. It is important to not be 0, as we'd confuse /// v1 and v2 descriptors. pub fn dummy_collator_signature() -> CollatorSignature { CollatorSignature::from_slice(&mut (0..64).into_iter().collect::>().as_slice()) .expect("64 bytes; qed") } /// Create a zeroed collator signature. pub fn zero_collator_signature() -> CollatorSignature { CollatorSignature::from(sr25519::Signature::default()) } /// Create a meaningless persisted validation data. pub fn dummy_pvd(parent_head: HeadData, relay_parent_number: u32) -> PersistedValidationData { PersistedValidationData { parent_head, relay_parent_number, max_pov_size: MAX_POV_SIZE, relay_parent_storage_root: dummy_hash(), } } /// Creates a meaningless signature pub fn dummy_signature() -> pezkuwi_primitives::ValidatorSignature { pezsp_core::crypto::UncheckedFrom::unchecked_from([1u8; 64]) } /// Create a meaningless candidate, returning its receipt and PVD. pub fn make_candidate( relay_parent_hash: Hash, relay_parent_number: u32, para_id: ParaId, parent_head: HeadData, head_data: HeadData, validation_code_hash: ValidationCodeHash, ) -> (CommittedCandidateReceiptV2, PersistedValidationData) { let pvd = dummy_pvd(parent_head, relay_parent_number); let commitments = CandidateCommitments { head_data, horizontal_messages: Default::default(), upward_messages: Default::default(), new_validation_code: None, processed_downward_messages: 0, hrmp_watermark: relay_parent_number, }; let mut candidate = dummy_candidate_receipt_bad_sig(relay_parent_hash, Some(Default::default())); candidate.commitments_hash = commitments.hash(); candidate.descriptor.para_id = para_id; candidate.descriptor.persisted_validation_data_hash = pvd.hash(); candidate.descriptor.validation_code_hash = validation_code_hash; let candidate = CommittedCandidateReceiptV2 { descriptor: candidate.descriptor.into(), commitments }; (candidate, pvd) } /// Create a meaningless v2 candidate, returning its receipt and PVD. pub fn make_candidate_v2( relay_parent_hash: Hash, relay_parent_number: u32, para_id: ParaId, parent_head: HeadData, head_data: HeadData, validation_code_hash: ValidationCodeHash, ) -> (CommittedCandidateReceiptV2, PersistedValidationData) { let pvd = dummy_pvd(parent_head, relay_parent_number); let commitments = CandidateCommitments { head_data, horizontal_messages: Default::default(), upward_messages: Default::default(), new_validation_code: None, processed_downward_messages: 0, hrmp_watermark: relay_parent_number, }; let mut descriptor = dummy_candidate_descriptor_v2(relay_parent_hash); descriptor.set_para_id(para_id); descriptor.set_persisted_validation_data_hash(pvd.hash()); descriptor.set_validation_code_hash(validation_code_hash); let candidate = CommittedCandidateReceiptV2 { descriptor, commitments }; (candidate, pvd) } /// Create a new candidate descriptor, and apply a valid signature /// using the provided `collator` key. pub fn make_valid_candidate_descriptor>( para_id: ParaId, relay_parent: H, persisted_validation_data_hash: Hash, pov_hash: Hash, validation_code_hash: impl Into, para_head: Hash, erasure_root: Hash, collator: Sr25519Keyring, ) -> CandidateDescriptor { let validation_code_hash = validation_code_hash.into(); let payload = collator_signature_payload::( &relay_parent, ¶_id, &persisted_validation_data_hash, &pov_hash, &validation_code_hash, ); let signature = collator.sign(&payload).into(); let descriptor = CandidateDescriptor { para_id, relay_parent, collator: collator.public().into(), persisted_validation_data_hash, pov_hash, erasure_root, signature, para_head, validation_code_hash, }; assert!(descriptor.check_collator_signature().is_ok()); descriptor } /// Create a v2 candidate descriptor. pub fn make_valid_candidate_descriptor_v2 + Copy>( para_id: ParaId, relay_parent: H, core_index: CoreIndex, session_index: SessionIndex, persisted_validation_data_hash: Hash, pov_hash: Hash, validation_code_hash: impl Into, para_head: Hash, erasure_root: Hash, ) -> CandidateDescriptorV2 { let validation_code_hash = validation_code_hash.into(); let descriptor = CandidateDescriptorV2::new( para_id, relay_parent, core_index, session_index, persisted_validation_data_hash, pov_hash, erasure_root, para_head, validation_code_hash, ); descriptor } /// After manually modifying the candidate descriptor, resign with a defined collator key. pub fn resign_candidate_descriptor_with_collator>( descriptor: &mut CandidateDescriptor, collator: Sr25519Keyring, ) { descriptor.collator = collator.public().into(); let payload = collator_signature_payload::( &descriptor.relay_parent, &descriptor.para_id, &descriptor.persisted_validation_data_hash, &descriptor.pov_hash, &descriptor.validation_code_hash, ); let signature = collator.sign(&payload).into(); descriptor.signature = signature; } /// Extracts validators's public keys (`ValidatorId`) from `Sr25519Keyring` pub fn validator_pubkeys(val_ids: &[Sr25519Keyring]) -> Vec { val_ids.iter().map(|v| v.public().into()).collect() } /// Builder for `CandidateReceipt`. pub struct TestCandidateBuilder { pub para_id: ParaId, pub pov_hash: Hash, pub relay_parent: Hash, pub commitments_hash: Hash, pub core_index: CoreIndex, } impl std::default::Default for TestCandidateBuilder { fn default() -> Self { let zeros = Hash::zero(); Self { para_id: 0.into(), pov_hash: zeros, relay_parent: zeros, commitments_hash: zeros, core_index: CoreIndex(0), } } } impl TestCandidateBuilder { /// Build a `CandidateReceipt`. pub fn build(self) -> CandidateReceiptV2 { let mut descriptor = dummy_candidate_descriptor_v2(self.relay_parent); descriptor.set_para_id(self.para_id); descriptor.set_pov_hash(self.pov_hash); descriptor.set_core_index(self.core_index); CandidateReceiptV2 { descriptor, commitments_hash: self.commitments_hash } } } /// A special `Rng` that always returns zero for testing something that implied /// to be random but should not be random in the tests pub struct AlwaysZeroRng; impl Default for AlwaysZeroRng { fn default() -> Self { Self {} } } impl rand::RngCore for AlwaysZeroRng { fn next_u32(&mut self) -> u32 { 0_u32 } fn next_u64(&mut self) -> u64 { 0_u64 } fn fill_bytes(&mut self, dest: &mut [u8]) { for element in dest.iter_mut() { *element = 0_u8; } } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> { self.fill_bytes(dest); Ok(()) } } #[cfg(test)] mod candidate_receipt_tests { use super::*; use bitvec::prelude::*; use pezkuwi_primitives::{ transpose_claim_queue, v9::CandidateUMPSignals, BackedCandidate, CandidateDescriptorVersion, ClaimQueueOffset, CommittedCandidateReceiptError, CoreSelector, InternalVersion, UMPSignal, UMP_SEPARATOR, }; use std::collections::BTreeMap; #[test] fn collator_signature_payload_is_valid() { // if this fails, collator signature verification code has to be updated. let h = Hash::default(); assert_eq!(h.as_ref().len(), 32); let _payload = collator_signature_payload( &Hash::repeat_byte(1), &5u32.into(), &Hash::repeat_byte(2), &Hash::repeat_byte(3), &Hash::repeat_byte(4).into(), ); } #[test] fn is_binary_compatibile() { let old_ccr = dummy_committed_candidate_receipt(Hash::default()); let new_ccr = dummy_committed_candidate_receipt_v2(Hash::default()); assert_eq!(old_ccr.encoded_size(), new_ccr.encoded_size()); let encoded_old = old_ccr.encode(); // Deserialize from old candidate receipt. let new_ccr: CommittedCandidateReceiptV2 = Decode::decode(&mut encoded_old.as_slice()).unwrap(); // We get same candidate hash. assert_eq!(old_ccr.hash(), new_ccr.hash()); } #[test] fn test_from_v1_descriptor() { let mut old_ccr = dummy_committed_candidate_receipt(Hash::default()).to_plain(); old_ccr.descriptor.collator = dummy_collator(); old_ccr.descriptor.signature = dummy_collator_signature(); let mut new_ccr = dummy_committed_candidate_receipt_v2(Hash::default()).to_plain(); // Override descriptor from old candidate receipt. new_ccr.descriptor = old_ccr.descriptor.clone().into(); // We get same candidate hash. assert_eq!(old_ccr.hash(), new_ccr.hash()); assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::V1); assert_eq!(old_ccr.descriptor.collator, new_ccr.descriptor.collator().unwrap()); assert_eq!(old_ccr.descriptor.signature, new_ccr.descriptor.signature().unwrap()); } #[test] fn invalid_version_descriptor() { let mut new_ccr = dummy_committed_candidate_receipt_v2(Hash::default()); assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::V2); // Put some unknown version. new_ccr.descriptor.set_version(InternalVersion(100)); // Deserialize as V1. let new_ccr: CommittedCandidateReceiptV2 = Decode::decode(&mut new_ccr.encode().as_slice()).unwrap(); assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::Unknown); assert_eq!( new_ccr.parse_ump_signals(&std::collections::BTreeMap::new()), Err(CommittedCandidateReceiptError::UnknownVersion(InternalVersion(100))) ); } #[test] fn test_version2_receipts_decoded_as_v1() { let mut new_ccr = dummy_committed_candidate_receipt_v2(Hash::default()); new_ccr.descriptor.set_core_index(CoreIndex(123)); new_ccr.descriptor.set_para_id(ParaId::new(1000)); // dummy XCM messages new_ccr.commitments.upward_messages.force_push(vec![0u8; 256]); new_ccr.commitments.upward_messages.force_push(vec![0xff; 256]); // separator new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); // CoreIndex commitment new_ccr .commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode()); let encoded_ccr = new_ccr.encode(); let decoded_ccr: CommittedCandidateReceipt = Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); assert_eq!(decoded_ccr.descriptor.relay_parent, new_ccr.descriptor.relay_parent()); assert_eq!(decoded_ccr.descriptor.para_id, new_ccr.descriptor.para_id()); assert_eq!(new_ccr.hash(), decoded_ccr.hash()); // Encode v1 and decode as V2 let encoded_ccr = new_ccr.encode(); let v2_ccr: CommittedCandidateReceiptV2 = Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); assert_eq!(v2_ccr.descriptor.core_index(), Some(CoreIndex(123))); let mut cq = BTreeMap::new(); cq.insert( CoreIndex(123), vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), ); assert!(new_ccr.parse_ump_signals(&transpose_claim_queue(cq)).is_ok()); assert_eq!(new_ccr.hash(), v2_ccr.hash()); } // V1 descriptors are forbidden once the teyrchain runtime started sending UMP signals. #[test] fn test_v1_descriptors_with_ump_signal() { let mut ccr = dummy_committed_candidate_receipt(Hash::default()); ccr.descriptor.para_id = ParaId::new(1024); // Adding collator signature should make it decode as v1. ccr.descriptor.signature = dummy_collator_signature(); ccr.descriptor.collator = dummy_collator(); ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); ccr.commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(1), ClaimQueueOffset(1)).encode()); ccr.commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); let encoded_ccr: Vec = ccr.encode(); let v1_ccr: CommittedCandidateReceiptV2 = Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); assert_eq!(v1_ccr.descriptor.version(), CandidateDescriptorVersion::V1); assert!(!v1_ccr.commitments.ump_signals().unwrap().is_empty()); let mut cq = BTreeMap::new(); cq.insert(CoreIndex(0), vec![v1_ccr.descriptor.para_id()].into()); cq.insert(CoreIndex(1), vec![v1_ccr.descriptor.para_id()].into()); assert_eq!(v1_ccr.descriptor.core_index(), None); assert_eq!( v1_ccr.parse_ump_signals(&transpose_claim_queue(cq)), Err(CommittedCandidateReceiptError::UMPSignalWithV1Decriptor) ); } #[test] fn test_core_select_is_optional() { // Testing edge case when collators provide zeroed signature and collator id. let mut old_ccr = dummy_committed_candidate_receipt(Hash::default()); old_ccr.descriptor.para_id = ParaId::new(1000); let encoded_ccr: Vec = old_ccr.encode(); let new_ccr: CommittedCandidateReceiptV2 = Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); let mut cq = BTreeMap::new(); cq.insert(CoreIndex(0), vec![new_ccr.descriptor.para_id()].into()); // Since collator sig and id are zeroed, it means that the descriptor uses format // version 2. Should still pass checks without core selector. assert!(new_ccr.parse_ump_signals(&transpose_claim_queue(cq)).is_ok()); let mut cq = BTreeMap::new(); cq.insert(CoreIndex(0), vec![new_ccr.descriptor.para_id()].into()); cq.insert(CoreIndex(1), vec![new_ccr.descriptor.para_id()].into()); // Passes even if 2 cores are assigned, because elastic scaling MVP could still inject the // core index in the `BackedCandidate`. assert!(new_ccr.parse_ump_signals(&transpose_claim_queue(cq)).is_ok()); // Adding collator signature should make it decode as v1. old_ccr.descriptor.signature = dummy_collator_signature(); old_ccr.descriptor.collator = dummy_collator(); let old_ccr_hash = old_ccr.hash(); let encoded_ccr: Vec = old_ccr.encode(); let new_ccr: CommittedCandidateReceiptV2 = Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); assert_eq!(new_ccr.descriptor.signature(), Some(old_ccr.descriptor.signature)); assert_eq!(new_ccr.descriptor.collator(), Some(old_ccr.descriptor.collator)); assert_eq!(new_ccr.descriptor.core_index(), None); assert_eq!(new_ccr.descriptor.para_id(), ParaId::new(1000)); assert_eq!(old_ccr_hash, new_ccr.hash()); } #[test] // Test valid scenarios for parse_ump_signals(): // - no signals // - only selected core signal // - only approved peer signal // - both signals in any order fn test_ump_commitments() { let mut new_ccr = dummy_committed_candidate_receipt_v2(Hash::default()); new_ccr.descriptor.set_core_index(CoreIndex(123)); new_ccr.descriptor.set_para_id(ParaId::new(1000)); let mut cq = BTreeMap::new(); cq.insert( CoreIndex(123), vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), ); let cq = transpose_claim_queue(cq); // No commitments // dummy XCM messages new_ccr.commitments.upward_messages.force_push(vec![0u8; 256]); new_ccr.commitments.upward_messages.force_push(vec![0xff; 256]); assert_eq!(new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy(None, None))); // separator new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); assert_eq!(new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy(None, None))); // CoreIndex commitment { let mut new_ccr = new_ccr.clone(); new_ccr .commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode()); assert_eq!( new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy(Some((CoreSelector(0), ClaimQueueOffset(1))), None)) ); } { let mut new_ccr = new_ccr.clone(); // Test having only an approved peer. new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); assert_eq!( new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy(None, Some(vec![1, 2, 3].try_into().unwrap()))) ); // Test having an approved peer and a core selector. new_ccr .commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode()); assert_eq!( new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy( Some((CoreSelector(0), ClaimQueueOffset(1))), Some(vec![1, 2, 3].try_into().unwrap()) )) ); } // Test having a core selector and an approved peer. new_ccr .commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode()); new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); assert_eq!( new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy( Some((CoreSelector(0), ClaimQueueOffset(1))), Some(vec![1, 2, 3].try_into().unwrap()) )) ); } #[test] fn test_invalid_ump_commitments() { let mut new_ccr = dummy_committed_candidate_receipt_v2(Hash::default()); new_ccr.descriptor.set_core_index(CoreIndex(0)); new_ccr.descriptor.set_para_id(ParaId::new(1000)); new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); let mut cq = BTreeMap::new(); cq.insert(CoreIndex(0), vec![new_ccr.descriptor.para_id()].into()); let cq = transpose_claim_queue(cq); // Add an approved peer message. new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); // Garbage message. new_ccr.commitments.upward_messages.force_push(vec![0, 13, 200].encode()); // No signals can be decoded. assert_eq!( new_ccr.parse_ump_signals(&cq), Err(CommittedCandidateReceiptError::UmpSignalDecode) ); assert_eq!( new_ccr.commitments.ump_signals(), Err(CommittedCandidateReceiptError::UmpSignalDecode) ); // Verify core index checks. { // Has two cores assigned but no core commitment. Will pass the check if the descriptor // core index is indeed assigned to the para. new_ccr.commitments.upward_messages.clear(); new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); let mut cq = BTreeMap::new(); cq.insert( CoreIndex(0), vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), ); cq.insert( CoreIndex(100), vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), ); let cq = transpose_claim_queue(cq); assert_eq!( new_ccr.parse_ump_signals(&cq), Ok(CandidateUMPSignals::dummy(None, Some(vec![1, 2, 3].try_into().unwrap()))) ); new_ccr.descriptor.set_core_index(CoreIndex(1)); assert_eq!( new_ccr.parse_ump_signals(&cq), Err(CommittedCandidateReceiptError::InvalidCoreIndex) ); new_ccr.descriptor.set_core_index(CoreIndex(0)); new_ccr .commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode()); // No assignments. assert_eq!( new_ccr.parse_ump_signals(&transpose_claim_queue(Default::default())), Err(CommittedCandidateReceiptError::NoAssignment) ); // Mismatch between descriptor index and commitment. new_ccr.descriptor.set_core_index(CoreIndex(1)); assert_eq!( new_ccr.parse_ump_signals(&cq), Err(CommittedCandidateReceiptError::CoreIndexMismatch { descriptor: CoreIndex(1), commitments: CoreIndex(0), }) ); } new_ccr.descriptor.set_core_index(CoreIndex(0)); // Add two ApprovedPeer messages new_ccr.commitments.upward_messages.clear(); new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![4, 5].try_into().unwrap()).encode()); assert_eq!( new_ccr.parse_ump_signals(&cq), Err(CommittedCandidateReceiptError::DuplicateUMPSignal) ); // Too many new_ccr.commitments.upward_messages.clear(); new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); new_ccr .commitments .upward_messages .force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(0)).encode()); new_ccr .commitments .upward_messages .force_push(UMPSignal::ApprovedPeer(vec![1, 2, 3].try_into().unwrap()).encode()); assert_eq!( new_ccr.parse_ump_signals(&cq), Err(CommittedCandidateReceiptError::TooManyUMPSignals) ); } #[test] fn test_backed_candidate_injected_core_index() { let initial_validator_indices = bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1]; let mut candidate = BackedCandidate::new( dummy_committed_candidate_receipt_v2(Hash::default()), vec![], initial_validator_indices.clone(), CoreIndex(10), ); // No core index supplied. candidate .set_validator_indices_and_core_index(initial_validator_indices.clone().into(), None); let (validator_indices, core_index) = candidate.validator_indices_and_core_index(); assert_eq!(validator_indices, initial_validator_indices.as_bitslice()); assert!(core_index.is_none()); // No core index supplied. Decoding is corrupted if backing group // size larger than 8. candidate.set_validator_indices_and_core_index( bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1, 0, 1, 0, 1, 0].into(), None, ); let (validator_indices, core_index) = candidate.validator_indices_and_core_index(); assert_eq!(validator_indices, bitvec![u8, bitvec::order::Lsb0; 0].as_bitslice()); assert!(core_index.is_some()); // Core index supplied. let mut candidate = BackedCandidate::new( dummy_committed_candidate_receipt_v2(Hash::default()), vec![], bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1], CoreIndex(10), ); let (validator_indices, core_index) = candidate.validator_indices_and_core_index(); assert_eq!(validator_indices, bitvec![u8, bitvec::order::Lsb0; 0, 1, 0, 1]); assert_eq!(core_index, Some(CoreIndex(10))); let encoded_validator_indices = candidate.raw_validator_indices(); candidate.set_validator_indices_and_core_index(validator_indices.into(), core_index); assert_eq!(candidate.raw_validator_indices(), encoded_validator_indices); } }