feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,516 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! An opt-in utility for tracking historical sessions in FRAME-session.
|
||||
//!
|
||||
//! This is generally useful when implementing blockchains that require accountable
|
||||
//! safety where validators from some amount f prior sessions must remain slashable.
|
||||
//!
|
||||
//! Rather than store the full session data for any given session, we instead commit
|
||||
//! to the roots of merkle tries containing the session data.
|
||||
//!
|
||||
//! These roots and proofs of inclusion can be generated at any time during the current session.
|
||||
//! Afterwards, the proofs can be fed to a consensus module when reporting misbehavior.
|
||||
|
||||
pub mod offchain;
|
||||
pub mod onchain;
|
||||
mod shared;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use core::fmt::Debug;
|
||||
use pezsp_runtime::{
|
||||
traits::{Convert, OpaqueKeys},
|
||||
KeyTypeId,
|
||||
};
|
||||
use pezsp_session::{MembershipProof, ValidatorCount};
|
||||
use pezsp_staking::SessionIndex;
|
||||
use pezsp_trie::{
|
||||
trie_types::{TrieDBBuilder, TrieDBMutBuilderV0},
|
||||
LayoutV0, MemoryDB, RandomState, Recorder, StorageProof, Trie, TrieMut, TrieRecorder,
|
||||
};
|
||||
|
||||
use pezframe_support::{
|
||||
print,
|
||||
traits::{KeyOwnerProofSystem, ValidatorSet, ValidatorSetWithIdentification},
|
||||
Parameter,
|
||||
};
|
||||
|
||||
const LOG_TARGET: &'static str = "runtime::historical";
|
||||
|
||||
use crate::{self as pezpallet_session, Pallet as Session};
|
||||
|
||||
pub use pallet::*;
|
||||
use pezsp_trie::{accessed_nodes_tracker::AccessedNodesTracker, recorder_ext::RecorderExt};
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
|
||||
/// The in-code storage version.
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// Config necessary for the historical pallet.
|
||||
#[pallet::config]
|
||||
pub trait Config: pezpallet_session::Config + pezframe_system::Config {
|
||||
/// The overarching event type.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Full identification of the validator.
|
||||
type FullIdentification: Parameter;
|
||||
|
||||
/// A conversion from validator ID to full identification.
|
||||
///
|
||||
/// This should contain any references to economic actors associated with the
|
||||
/// validator, since they may be outdated by the time this is queried from a
|
||||
/// historical trie.
|
||||
///
|
||||
/// It must return the identification for the current session index.
|
||||
type FullIdentificationOf: Convert<Self::ValidatorId, Option<Self::FullIdentification>>;
|
||||
}
|
||||
|
||||
/// Mapping from historical session indices to session-data root hash and validator count.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn historical_root)]
|
||||
pub type HistoricalSessions<T: Config> =
|
||||
StorageMap<_, Twox64Concat, SessionIndex, (T::Hash, ValidatorCount), OptionQuery>;
|
||||
|
||||
/// The range of historical sessions we store. [first, last)
|
||||
#[pallet::storage]
|
||||
pub type StoredRange<T> = StorageValue<_, (SessionIndex, SessionIndex), OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T> {
|
||||
/// The merkle root of the validators of the said session were stored
|
||||
RootStored { index: SessionIndex },
|
||||
/// The merkle roots of up to this session index were pruned
|
||||
RootsPruned { up_to: SessionIndex },
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Prune historical stored session roots up to (but not including)
|
||||
/// `up_to`.
|
||||
pub fn prune_up_to(up_to: SessionIndex) {
|
||||
StoredRange::<T>::mutate(|range| {
|
||||
let (start, end) = match *range {
|
||||
Some(range) => range,
|
||||
None => return, // nothing to prune.
|
||||
};
|
||||
|
||||
let up_to = core::cmp::min(up_to, end);
|
||||
|
||||
if up_to < start {
|
||||
return; // out of bounds. harmless.
|
||||
}
|
||||
|
||||
(start..up_to).for_each(HistoricalSessions::<T>::remove);
|
||||
|
||||
let new_start = up_to;
|
||||
*range = if new_start == end {
|
||||
None // nothing is stored.
|
||||
} else {
|
||||
Some((new_start, end))
|
||||
}
|
||||
});
|
||||
|
||||
Self::deposit_event(Event::<T>::RootsPruned { up_to });
|
||||
}
|
||||
|
||||
fn full_id_validators() -> Vec<(T::ValidatorId, T::FullIdentification)> {
|
||||
<Session<T>>::validators()
|
||||
.into_iter()
|
||||
.filter_map(|validator| {
|
||||
T::FullIdentificationOf::convert(validator.clone())
|
||||
.map(|full_id| (validator, full_id))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ValidatorSet<T::AccountId> for Pallet<T> {
|
||||
type ValidatorId = T::ValidatorId;
|
||||
type ValidatorIdOf = T::ValidatorIdOf;
|
||||
|
||||
fn session_index() -> pezsp_staking::SessionIndex {
|
||||
super::Pallet::<T>::current_index()
|
||||
}
|
||||
|
||||
fn validators() -> Vec<Self::ValidatorId> {
|
||||
super::Pallet::<T>::validators()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ValidatorSetWithIdentification<T::AccountId> for Pallet<T> {
|
||||
type Identification = T::FullIdentification;
|
||||
type IdentificationOf = T::FullIdentificationOf;
|
||||
}
|
||||
|
||||
/// Specialization of the crate-level `SessionManager` which returns the set of full identification
|
||||
/// when creating a new session.
|
||||
pub trait SessionManager<ValidatorId, FullIdentification>:
|
||||
pezpallet_session::SessionManager<ValidatorId>
|
||||
{
|
||||
/// If there was a validator set change, its returns the set of new validators along with their
|
||||
/// full identifications.
|
||||
fn new_session(new_index: SessionIndex) -> Option<Vec<(ValidatorId, FullIdentification)>>;
|
||||
fn new_session_genesis(
|
||||
new_index: SessionIndex,
|
||||
) -> Option<Vec<(ValidatorId, FullIdentification)>> {
|
||||
<Self as SessionManager<_, _>>::new_session(new_index)
|
||||
}
|
||||
fn start_session(start_index: SessionIndex);
|
||||
fn end_session(end_index: SessionIndex);
|
||||
}
|
||||
|
||||
/// An `SessionManager` implementation that wraps an inner `I` and also
|
||||
/// sets the historical trie root of the ending session.
|
||||
pub struct NoteHistoricalRoot<T, I>(core::marker::PhantomData<(T, I)>);
|
||||
|
||||
impl<T: Config, I: SessionManager<T::ValidatorId, T::FullIdentification>> NoteHistoricalRoot<T, I> {
|
||||
fn do_new_session(new_index: SessionIndex, is_genesis: bool) -> Option<Vec<T::ValidatorId>> {
|
||||
<StoredRange<T>>::mutate(|range| {
|
||||
range.get_or_insert_with(|| (new_index, new_index)).1 = new_index + 1;
|
||||
});
|
||||
|
||||
let new_validators_and_id = if is_genesis {
|
||||
<I as SessionManager<_, _>>::new_session_genesis(new_index)
|
||||
} else {
|
||||
<I as SessionManager<_, _>>::new_session(new_index)
|
||||
};
|
||||
let new_validators_opt = new_validators_and_id
|
||||
.as_ref()
|
||||
.map(|new_validators| new_validators.iter().map(|(v, _id)| v.clone()).collect());
|
||||
|
||||
if let Some(new_validators) = new_validators_and_id {
|
||||
let count = new_validators.len() as ValidatorCount;
|
||||
match ProvingTrie::<T>::generate_for(new_validators) {
|
||||
Ok(trie) => {
|
||||
<HistoricalSessions<T>>::insert(new_index, &(trie.root, count));
|
||||
Pallet::<T>::deposit_event(Event::RootStored { index: new_index });
|
||||
},
|
||||
Err(reason) => {
|
||||
print("Failed to generate historical ancestry-inclusion proof.");
|
||||
print(reason);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
let previous_index = new_index.saturating_sub(1);
|
||||
if let Some(previous_session) = <HistoricalSessions<T>>::get(previous_index) {
|
||||
<HistoricalSessions<T>>::insert(new_index, previous_session);
|
||||
Pallet::<T>::deposit_event(Event::RootStored { index: new_index });
|
||||
}
|
||||
}
|
||||
|
||||
new_validators_opt
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, I> pezpallet_session::SessionManager<T::ValidatorId> for NoteHistoricalRoot<T, I>
|
||||
where
|
||||
I: SessionManager<T::ValidatorId, T::FullIdentification>,
|
||||
{
|
||||
fn new_session(new_index: SessionIndex) -> Option<Vec<T::ValidatorId>> {
|
||||
Self::do_new_session(new_index, false)
|
||||
}
|
||||
|
||||
fn new_session_genesis(new_index: SessionIndex) -> Option<Vec<T::ValidatorId>> {
|
||||
Self::do_new_session(new_index, true)
|
||||
}
|
||||
|
||||
fn start_session(start_index: SessionIndex) {
|
||||
<I as SessionManager<_, _>>::start_session(start_index)
|
||||
}
|
||||
|
||||
fn end_session(end_index: SessionIndex) {
|
||||
onchain::store_session_validator_set_to_offchain::<T>(end_index);
|
||||
<I as SessionManager<_, _>>::end_session(end_index)
|
||||
}
|
||||
}
|
||||
|
||||
/// A tuple of the validator's ID and their full identification.
|
||||
pub type IdentificationTuple<T> =
|
||||
(<T as pezpallet_session::Config>::ValidatorId, <T as Config>::FullIdentification);
|
||||
|
||||
/// A trie instance for checking and generating proofs.
|
||||
pub struct ProvingTrie<T: Config> {
|
||||
db: MemoryDB<T::Hashing>,
|
||||
root: T::Hash,
|
||||
}
|
||||
|
||||
impl<T: Config> ProvingTrie<T> {
|
||||
fn generate_for<I>(validators: I) -> Result<Self, &'static str>
|
||||
where
|
||||
I: IntoIterator<Item = (T::ValidatorId, T::FullIdentification)>,
|
||||
{
|
||||
let mut db = MemoryDB::with_hasher(RandomState::default());
|
||||
let mut root = Default::default();
|
||||
|
||||
{
|
||||
let mut trie = TrieDBMutBuilderV0::new(&mut db, &mut root).build();
|
||||
for (i, (validator, full_id)) in validators.into_iter().enumerate() {
|
||||
let i = i as u32;
|
||||
let keys = match <Session<T>>::load_keys(&validator) {
|
||||
None => continue,
|
||||
Some(k) => k,
|
||||
};
|
||||
|
||||
let id_tuple = (validator, full_id);
|
||||
|
||||
// map each key to the owner index.
|
||||
for key_id in T::Keys::key_ids() {
|
||||
let key = keys.get_raw(*key_id);
|
||||
let res =
|
||||
(key_id, key).using_encoded(|k| i.using_encoded(|v| trie.insert(k, v)));
|
||||
|
||||
res.map_err(|_| "failed to insert into trie")?;
|
||||
}
|
||||
|
||||
// map each owner index to the full identification.
|
||||
i.using_encoded(|k| id_tuple.using_encoded(|v| trie.insert(k, v)))
|
||||
.map_err(|_| "failed to insert into trie")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ProvingTrie { db, root })
|
||||
}
|
||||
|
||||
fn from_proof(root: T::Hash, proof: StorageProof) -> Self {
|
||||
ProvingTrie { db: proof.into_memory_db(), root }
|
||||
}
|
||||
|
||||
/// Prove the full verification data for a given key and key ID.
|
||||
pub fn prove(&self, key_id: KeyTypeId, key_data: &[u8]) -> Option<Vec<Vec<u8>>> {
|
||||
let mut recorder = Recorder::<LayoutV0<T::Hashing>>::new();
|
||||
self.query(key_id, key_data, Some(&mut recorder));
|
||||
|
||||
Some(recorder.into_raw_storage_proof())
|
||||
}
|
||||
|
||||
/// Access the underlying trie root.
|
||||
pub fn root(&self) -> &T::Hash {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// Search for a key inside the proof.
|
||||
fn query(
|
||||
&self,
|
||||
key_id: KeyTypeId,
|
||||
key_data: &[u8],
|
||||
recorder: Option<&mut dyn TrieRecorder<T::Hash>>,
|
||||
) -> Option<IdentificationTuple<T>> {
|
||||
let trie = TrieDBBuilder::new(&self.db, &self.root)
|
||||
.with_optional_recorder(recorder)
|
||||
.build();
|
||||
|
||||
let val_idx = (key_id, key_data)
|
||||
.using_encoded(|s| trie.get(s))
|
||||
.ok()?
|
||||
.and_then(|raw| u32::decode(&mut &*raw).ok())?;
|
||||
|
||||
val_idx
|
||||
.using_encoded(|s| trie.get(s))
|
||||
.ok()?
|
||||
.and_then(|raw| <IdentificationTuple<T>>::decode(&mut &*raw).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config, D: AsRef<[u8]>> KeyOwnerProofSystem<(KeyTypeId, D)> for Pallet<T> {
|
||||
type Proof = MembershipProof;
|
||||
type IdentificationTuple = IdentificationTuple<T>;
|
||||
|
||||
fn prove(key: (KeyTypeId, D)) -> Option<Self::Proof> {
|
||||
let session = <Session<T>>::current_index();
|
||||
let validators = Self::full_id_validators();
|
||||
|
||||
let count = validators.len() as ValidatorCount;
|
||||
|
||||
let trie = ProvingTrie::<T>::generate_for(validators).ok()?;
|
||||
|
||||
let (id, data) = key;
|
||||
trie.prove(id, data.as_ref()).map(|trie_nodes| MembershipProof {
|
||||
session,
|
||||
trie_nodes,
|
||||
validator_count: count,
|
||||
})
|
||||
}
|
||||
|
||||
fn check_proof(key: (KeyTypeId, D), proof: Self::Proof) -> Option<IdentificationTuple<T>> {
|
||||
fn print_error<E: Debug>(e: E) {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"Rejecting equivocation report because of key ownership proof error: {:?}", e
|
||||
);
|
||||
}
|
||||
|
||||
let (id, data) = key;
|
||||
let (root, count) = if proof.session == <Session<T>>::current_index() {
|
||||
let validators = Self::full_id_validators();
|
||||
let count = validators.len() as ValidatorCount;
|
||||
let trie = ProvingTrie::<T>::generate_for(validators).map_err(print_error).ok()?;
|
||||
(trie.root, count)
|
||||
} else {
|
||||
<HistoricalSessions<T>>::get(&proof.session)?
|
||||
};
|
||||
|
||||
if count != proof.validator_count {
|
||||
print_error("InvalidCount");
|
||||
return None;
|
||||
}
|
||||
|
||||
let proof = StorageProof::new_with_duplicate_nodes_check(proof.trie_nodes)
|
||||
.map_err(print_error)
|
||||
.ok()?;
|
||||
let mut accessed_nodes_tracker = AccessedNodesTracker::<T::Hash>::new(proof.len());
|
||||
let trie = ProvingTrie::<T>::from_proof(root, proof);
|
||||
let res = trie.query(id, data.as_ref(), Some(&mut accessed_nodes_tracker))?;
|
||||
accessed_nodes_tracker.ensure_no_unused_nodes().map_err(print_error).ok()?;
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{
|
||||
force_new_session, set_next_validators, NextValidators, Session, System, Test,
|
||||
};
|
||||
use alloc::vec;
|
||||
|
||||
use pezsp_runtime::{key_types::DUMMY, testing::UintAuthorityId, BuildStorage};
|
||||
use pezsp_state_machine::BasicExternalities;
|
||||
|
||||
use pezframe_support::traits::{KeyOwnerProofSystem, OnInitialize};
|
||||
|
||||
type Historical = Pallet<Test>;
|
||||
|
||||
pub(crate) fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let keys: Vec<_> = NextValidators::get()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|i| (i, i, UintAuthorityId(i).into()))
|
||||
.collect();
|
||||
BasicExternalities::execute_with_storage(&mut t, || {
|
||||
for (ref k, ..) in &keys {
|
||||
pezframe_system::Pallet::<Test>::inc_providers(k);
|
||||
}
|
||||
});
|
||||
pezpallet_session::GenesisConfig::<Test> { keys, ..Default::default() }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
pezsp_io::TestExternalities::new(t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_proof_is_good() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_next_validators(vec![1, 2]);
|
||||
force_new_session();
|
||||
|
||||
System::set_block_number(1);
|
||||
Session::on_initialize(1);
|
||||
|
||||
let encoded_key_1 = UintAuthorityId(1).encode();
|
||||
let proof = Historical::prove((DUMMY, &encoded_key_1[..])).unwrap();
|
||||
|
||||
// proof-checking in the same session is OK.
|
||||
assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some());
|
||||
|
||||
set_next_validators(vec![1, 2, 4]);
|
||||
force_new_session();
|
||||
|
||||
System::set_block_number(2);
|
||||
Session::on_initialize(2);
|
||||
|
||||
assert!(Historical::historical_root(proof.session).is_some());
|
||||
assert!(Session::current_index() > proof.session);
|
||||
|
||||
// proof-checking in the next session is also OK.
|
||||
assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some());
|
||||
|
||||
set_next_validators(vec![1, 2, 5]);
|
||||
|
||||
force_new_session();
|
||||
System::set_block_number(3);
|
||||
Session::on_initialize(3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_up_to_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
for i in 1..99u64 {
|
||||
set_next_validators(vec![i]);
|
||||
force_new_session();
|
||||
|
||||
System::set_block_number(i);
|
||||
Session::on_initialize(i);
|
||||
}
|
||||
|
||||
assert_eq!(<StoredRange<Test>>::get(), Some((0, 100)));
|
||||
|
||||
for i in 0..100 {
|
||||
assert!(Historical::historical_root(i).is_some())
|
||||
}
|
||||
|
||||
Historical::prune_up_to(10);
|
||||
assert_eq!(<StoredRange<Test>>::get(), Some((10, 100)));
|
||||
|
||||
Historical::prune_up_to(9);
|
||||
assert_eq!(<StoredRange<Test>>::get(), Some((10, 100)));
|
||||
|
||||
for i in 10..100 {
|
||||
assert!(Historical::historical_root(i).is_some())
|
||||
}
|
||||
|
||||
Historical::prune_up_to(99);
|
||||
assert_eq!(<StoredRange<Test>>::get(), Some((99, 100)));
|
||||
|
||||
Historical::prune_up_to(100);
|
||||
assert_eq!(<StoredRange<Test>>::get(), None);
|
||||
|
||||
for i in 99..199u64 {
|
||||
set_next_validators(vec![i]);
|
||||
force_new_session();
|
||||
|
||||
System::set_block_number(i);
|
||||
Session::on_initialize(i);
|
||||
}
|
||||
|
||||
assert_eq!(<StoredRange<Test>>::get(), Some((100, 200)));
|
||||
|
||||
for i in 100..200 {
|
||||
assert!(Historical::historical_root(i).is_some())
|
||||
}
|
||||
|
||||
Historical::prune_up_to(9999);
|
||||
assert_eq!(<StoredRange<Test>>::get(), None);
|
||||
|
||||
for i in 100..200 {
|
||||
assert!(Historical::historical_root(i).is_none())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! Off-chain logic for creating a proof based data provided by on-chain logic.
|
||||
//!
|
||||
//! Validator-set extracting an iterator from an off-chain worker stored list containing historical
|
||||
//! validator-sets. Based on the logic of historical slashing, but the validation is done off-chain.
|
||||
//! Use [`fn store_current_session_validator_set_to_offchain()`](super::onchain) to store the
|
||||
//! required data to the offchain validator set. This is used in conjunction with [`ProvingTrie`]
|
||||
//! and the off-chain indexing API.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use pezsp_runtime::{
|
||||
offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
|
||||
KeyTypeId,
|
||||
};
|
||||
use pezsp_session::MembershipProof;
|
||||
|
||||
use super::{shared, Config, IdentificationTuple, ProvingTrie};
|
||||
use crate::{Pallet as SessionModule, SessionIndex};
|
||||
|
||||
/// A set of validators, which was used for a fixed session index.
|
||||
struct ValidatorSet<T: Config> {
|
||||
validator_set: Vec<IdentificationTuple<T>>,
|
||||
}
|
||||
|
||||
impl<T: Config> ValidatorSet<T> {
|
||||
/// Load the set of validators for a particular session index from the off-chain storage.
|
||||
///
|
||||
/// If none is found or decodable given `prefix` and `session`, it will return `None`.
|
||||
/// Empty validator sets should only ever exist for genesis blocks.
|
||||
pub fn load_from_offchain_db(session_index: SessionIndex) -> Option<Self> {
|
||||
let derived_key = shared::derive_key(shared::PREFIX, session_index);
|
||||
StorageValueRef::persistent(derived_key.as_ref())
|
||||
.get::<Vec<(T::ValidatorId, T::FullIdentification)>>()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|validator_set| Self { validator_set })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.validator_set.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement conversion into iterator for usage
|
||||
/// with [ProvingTrie](super::ProvingTrie::generate_for).
|
||||
impl<T: Config> core::iter::IntoIterator for ValidatorSet<T> {
|
||||
type Item = (T::ValidatorId, T::FullIdentification);
|
||||
type IntoIter = alloc::vec::IntoIter<Self::Item>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.validator_set.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a proof based on the data available in the off-chain database.
|
||||
///
|
||||
/// Based on the yielded `MembershipProof` the implementer may decide what
|
||||
/// to do, i.e. in case of a failed proof, enqueue a transaction back on
|
||||
/// chain reflecting that, with all its consequences such as i.e. slashing.
|
||||
pub fn prove_session_membership<T: Config, D: AsRef<[u8]>>(
|
||||
session_index: SessionIndex,
|
||||
session_key: (KeyTypeId, D),
|
||||
) -> Option<MembershipProof> {
|
||||
let validators = ValidatorSet::<T>::load_from_offchain_db(session_index)?;
|
||||
let count = validators.len() as u32;
|
||||
let trie = ProvingTrie::<T>::generate_for(validators.into_iter()).ok()?;
|
||||
|
||||
let (id, data) = session_key;
|
||||
trie.prove(id, data.as_ref()).map(|trie_nodes| MembershipProof {
|
||||
session: session_index,
|
||||
trie_nodes,
|
||||
validator_count: count,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to prune anything that is older than `first_to_keep` session index.
|
||||
///
|
||||
/// Due to re-organisation it could be that the `first_to_keep` might be less
|
||||
/// than the stored one, in which case the conservative choice is made to keep records
|
||||
/// up to the one that is the lesser.
|
||||
pub fn prune_older_than<T: Config>(first_to_keep: SessionIndex) {
|
||||
let derived_key = shared::LAST_PRUNE.to_vec();
|
||||
let entry = StorageValueRef::persistent(derived_key.as_ref());
|
||||
match entry.mutate(
|
||||
|current: Result<Option<SessionIndex>, StorageRetrievalError>| -> Result<_, ()> {
|
||||
match current {
|
||||
Ok(Some(current)) if current < first_to_keep => Ok(first_to_keep),
|
||||
// do not move the cursor, if the new one would be behind ours
|
||||
Ok(Some(current)) => Ok(current),
|
||||
Ok(None) => Ok(first_to_keep),
|
||||
// if the storage contains undecodable data, overwrite with current anyways
|
||||
// which might leak some entries being never purged, but that is acceptable
|
||||
// in this context
|
||||
Err(_) => Ok(first_to_keep),
|
||||
}
|
||||
},
|
||||
) {
|
||||
Ok(new_value) => {
|
||||
// on a re-org this is not necessarily true, with the above they might be equal
|
||||
if new_value < first_to_keep {
|
||||
for session_index in new_value..first_to_keep {
|
||||
let derived_key = shared::derive_key(shared::PREFIX, session_index);
|
||||
let _ = StorageValueRef::persistent(derived_key.as_ref()).clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(MutateStorageError::ConcurrentModification(_)) => {},
|
||||
Err(MutateStorageError::ValueFunctionFailed(_)) => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the newest `n` items, and prune all items older than that.
|
||||
pub fn keep_newest<T: Config>(n_to_keep: usize) {
|
||||
let session_index = <SessionModule<T>>::current_index();
|
||||
let n_to_keep = n_to_keep as SessionIndex;
|
||||
if n_to_keep < session_index {
|
||||
prune_older_than::<T>(session_index - n_to_keep)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
historical::{onchain, Pallet},
|
||||
mock::{force_new_session, set_next_validators, NextValidators, Session, System, Test},
|
||||
};
|
||||
|
||||
use codec::Encode;
|
||||
use pezsp_core::{
|
||||
crypto::key_types::DUMMY,
|
||||
offchain::{testing::TestOffchainExt, OffchainDbExt, OffchainWorkerExt, StorageKind},
|
||||
};
|
||||
use pezsp_runtime::{testing::UintAuthorityId, BuildStorage};
|
||||
use pezsp_state_machine::BasicExternalities;
|
||||
|
||||
use pezframe_support::traits::{KeyOwnerProofSystem, OnInitialize};
|
||||
|
||||
type Historical = Pallet<Test>;
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default()
|
||||
.build_storage()
|
||||
.expect("Failed to create test externalities.");
|
||||
|
||||
let keys: Vec<_> = NextValidators::get()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|i| (i, i, UintAuthorityId(i).into()))
|
||||
.collect();
|
||||
|
||||
BasicExternalities::execute_with_storage(&mut t, || {
|
||||
for (ref k, ..) in &keys {
|
||||
pezframe_system::Pallet::<Test>::inc_providers(k);
|
||||
}
|
||||
});
|
||||
|
||||
crate::GenesisConfig::<Test> { keys, ..Default::default() }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
|
||||
let (offchain, offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db());
|
||||
|
||||
const ITERATIONS: u32 = 5u32;
|
||||
let mut seed = [0u8; 32];
|
||||
seed[0..4].copy_from_slice(&ITERATIONS.to_le_bytes());
|
||||
offchain_state.write().seed = seed;
|
||||
|
||||
ext.register_extension(OffchainDbExt::new(offchain.clone()));
|
||||
ext.register_extension(OffchainWorkerExt::new(offchain));
|
||||
ext
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode_roundtrip() {
|
||||
use super::super::{super::Config as SessionConfig, Config as HistoricalConfig};
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
let sample = (
|
||||
22u32 as <Test as SessionConfig>::ValidatorId,
|
||||
7_777_777 as <Test as HistoricalConfig>::FullIdentification,
|
||||
);
|
||||
|
||||
let encoded = sample.encode();
|
||||
let decoded = Decode::decode(&mut encoded.as_slice()).expect("Must decode");
|
||||
assert_eq!(sample, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onchain_to_offchain() {
|
||||
let mut ext = new_test_ext();
|
||||
|
||||
const DATA: &[u8] = &[7, 8, 9, 10, 11];
|
||||
ext.execute_with(|| {
|
||||
b"alphaomega"[..].using_encoded(|key| pezsp_io::offchain_index::set(key, DATA));
|
||||
});
|
||||
|
||||
ext.persist_offchain_overlay();
|
||||
|
||||
ext.execute_with(|| {
|
||||
let data = b"alphaomega"[..].using_encoded(|key| {
|
||||
pezsp_io::offchain::local_storage_get(StorageKind::PERSISTENT, key)
|
||||
});
|
||||
assert_eq!(data, Some(DATA.to_vec()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_proof_offchain() {
|
||||
let mut ext = new_test_ext();
|
||||
let encoded_key_1 = UintAuthorityId(1).encode();
|
||||
|
||||
ext.execute_with(|| {
|
||||
set_next_validators(vec![1, 2]);
|
||||
force_new_session();
|
||||
|
||||
System::set_block_number(1);
|
||||
Session::on_initialize(1);
|
||||
|
||||
// "on-chain"
|
||||
onchain::store_current_session_validator_set_to_offchain::<Test>();
|
||||
assert_eq!(<SessionModule<Test>>::current_index(), 1);
|
||||
|
||||
set_next_validators(vec![7, 8]);
|
||||
|
||||
force_new_session();
|
||||
});
|
||||
|
||||
ext.persist_offchain_overlay();
|
||||
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(2);
|
||||
Session::on_initialize(2);
|
||||
assert_eq!(<SessionModule<Test>>::current_index(), 2);
|
||||
|
||||
// "off-chain"
|
||||
let proof = prove_session_membership::<Test, _>(1, (DUMMY, &encoded_key_1));
|
||||
assert!(proof.is_some());
|
||||
let proof = proof.expect("Must be Some(Proof)");
|
||||
|
||||
assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! On-chain logic to store a validator-set for deferred validation using an off-chain worker.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use codec::Encode;
|
||||
use pezsp_runtime::traits::Convert;
|
||||
|
||||
use super::{shared, Config as HistoricalConfig};
|
||||
use crate::{Config as SessionConfig, Pallet as SessionModule, SessionIndex};
|
||||
|
||||
/// Store the validator-set associated to the `session_index` to the off-chain database.
|
||||
///
|
||||
/// Further processing is then done [`off-chain side`](super::offchain).
|
||||
///
|
||||
/// **Must** be called from on-chain, i.e. a call that originates from
|
||||
/// `on_initialize(..)` or `on_finalization(..)`.
|
||||
/// **Must** be called during the session, which validator-set is to be stored for further
|
||||
/// off-chain processing. Otherwise the `FullIdentification` might not be available.
|
||||
pub fn store_session_validator_set_to_offchain<T: HistoricalConfig + SessionConfig>(
|
||||
session_index: SessionIndex,
|
||||
) {
|
||||
let encoded_validator_list = <SessionModule<T>>::validators()
|
||||
.into_iter()
|
||||
.filter_map(|validator_id: <T as SessionConfig>::ValidatorId| {
|
||||
let full_identification =
|
||||
<<T as HistoricalConfig>::FullIdentificationOf>::convert(validator_id.clone());
|
||||
full_identification.map(|full_identification| (validator_id, full_identification))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
encoded_validator_list.using_encoded(|encoded_validator_list| {
|
||||
let derived_key = shared::derive_key(shared::PREFIX, session_index);
|
||||
pezsp_io::offchain_index::set(derived_key.as_slice(), encoded_validator_list);
|
||||
});
|
||||
}
|
||||
|
||||
/// Store the validator set associated to the _current_ session index to the off-chain database.
|
||||
///
|
||||
/// See [`store_session_validator_set_to_offchain`]
|
||||
/// for further information and restrictions.
|
||||
pub fn store_current_session_validator_set_to_offchain<T: HistoricalConfig + SessionConfig>() {
|
||||
store_session_validator_set_to_offchain::<T>(<SessionModule<T>>::current_index());
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// 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.
|
||||
|
||||
//! Shared logic between on-chain and off-chain components used for slashing using an off-chain
|
||||
//! worker.
|
||||
|
||||
use alloc::{borrow::ToOwned, vec::Vec};
|
||||
use codec::Encode;
|
||||
use pezsp_staking::SessionIndex;
|
||||
|
||||
pub(super) const PREFIX: &[u8] = b"session_historical";
|
||||
pub(super) const LAST_PRUNE: &[u8] = b"session_historical_last_prune";
|
||||
|
||||
/// Derive the key used to store the list of validators
|
||||
pub(super) fn derive_key<P: AsRef<[u8]>>(prefix: P, session_index: SessionIndex) -> Vec<u8> {
|
||||
session_index.using_encoded(|encoded_session_index| {
|
||||
let mut key = prefix.as_ref().to_owned();
|
||||
key.push(b'/');
|
||||
key.extend_from_slice(encoded_session_index);
|
||||
key
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user