#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
pub mod mock;
#[cfg(test)]
mod tests;
pub mod types;
pub mod utils;
use dkg_runtime_primitives::{
handlers::decode_proposals::{decode_proposal_header, decode_proposal_identifier},
traits::OnAuthoritySetChangeHandler,
ProposalHandlerTrait, ProposalNonce, ResourceId, TypedChainId,
};
use frame_support::{
pallet_prelude::{ensure, DispatchResultWithPostInfo},
traits::{EnsureOrigin, EstimateNextSessionRotation, Get},
BoundedVec,
};
use frame_system::pallet_prelude::BlockNumberFor;
use core::fmt::Debug;
use sp_runtime::{traits::Convert, RuntimeAppPublic};
use sp_std::prelude::*;
use types::{ProposalStatus, ProposalVotes};
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
mod weights;
pub use weights::WebbWeight;
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use crate::{
types::{ProposalVotes, DKG_DEFAULT_PROPOSER_THRESHOLD},
weights::WeightInfo,
};
use dkg_runtime_primitives::{
proposal::{Proposal, ProposalKind},
ProposalNonce,
};
use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*};
use frame_system::pallet_prelude::*;
pub type ProposalOf<T> = Proposal<<T as Config>::MaxProposalLength>;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type NextSessionRotation: EstimateNextSessionRotation<BlockNumberFor<Self>>;
type DKGId: Member + Parameter + RuntimeAppPublic + MaybeSerializeDeserialize;
type DKGAuthorityToMerkleLeaf: Convert<Self::DKGId, Vec<u8>>;
type ProposalHandler: ProposalHandlerTrait<MaxProposalLength = Self::MaxProposalLength>;
#[pallet::constant]
type ChainIdentifier: Get<TypedChainId>;
#[pallet::constant]
type ProposalLifetime: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type Period: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type MaxVotes: Get<u32> + TypeInfo + Clone;
#[pallet::constant]
type MaxResources: Get<u32> + TypeInfo;
#[pallet::constant]
type MaxProposers: Get<u32> + TypeInfo;
#[pallet::constant]
type VotingKeySize: Get<u32> + Debug + Clone + Eq + PartialEq + PartialOrd + Ord + TypeInfo;
#[pallet::constant]
type MaxProposalLength: Get<u32>
+ Debug
+ Clone
+ Eq
+ PartialEq
+ PartialOrd
+ Ord
+ TypeInfo;
type WeightInfo: WeightInfo;
}
#[pallet::storage]
#[pallet::getter(fn chains)]
pub type ChainNonces<T: Config> = StorageMap<_, Blake2_256, TypedChainId, ProposalNonce>;
#[pallet::type_value]
pub fn DefaultForProposerThreshold() -> u32 {
DKG_DEFAULT_PROPOSER_THRESHOLD
}
#[pallet::storage]
#[pallet::getter(fn proposer_threshold)]
pub type ProposerThreshold<T: Config> =
StorageValue<_, u32, ValueQuery, DefaultForProposerThreshold>;
#[pallet::storage]
#[pallet::getter(fn proposers)]
pub type Proposers<T: Config> =
StorageValue<_, BoundedVec<T::AccountId, T::MaxProposers>, ValueQuery>;
pub type VotingKey<T> = BoundedVec<u8, <T as Config>::VotingKeySize>;
pub type VotingKeyTuple<T> = (<T as frame_system::Config>::AccountId, VotingKey<T>);
pub type VoterList<T> = BoundedVec<VotingKeyTuple<T>, <T as Config>::MaxProposers>;
#[pallet::storage]
#[pallet::getter(fn external_proposer_accounts)]
pub type VotingKeys<T: Config> =
StorageValue<_, BoundedVec<(T::AccountId, VotingKey<T>), T::MaxProposers>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn proposer_count)]
pub type ProposerCount<T: Config> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn votes)]
pub type Votes<T: Config> = StorageDoubleMap<
_,
Blake2_256,
TypedChainId,
Blake2_256,
(ProposalNonce, ProposalOf<T>),
ProposalVotes<T::AccountId, BlockNumberFor<T>, T::MaxVotes>,
>;
#[pallet::storage]
#[pallet::getter(fn resources)]
pub type Resources<T: Config> =
StorageMap<_, Blake2_256, ResourceId, BoundedVec<u8, T::MaxResources>>;
#[pallet::event]
#[pallet::generate_deposit(pub fn deposit_event)]
pub enum Event<T: Config> {
ProposerThresholdChanged { new_threshold: u32 },
ChainWhitelisted { chain_id: TypedChainId },
VoteFor {
kind: ProposalKind,
src_chain_id: TypedChainId,
proposal_nonce: ProposalNonce,
who: T::AccountId,
},
VoteAgainst {
kind: ProposalKind,
src_chain_id: TypedChainId,
proposal_nonce: ProposalNonce,
who: T::AccountId,
},
ProposalApproved {
kind: ProposalKind,
src_chain_id: TypedChainId,
proposal_nonce: ProposalNonce,
},
ProposalRejected {
kind: ProposalKind,
src_chain_id: TypedChainId,
proposal_nonce: ProposalNonce,
},
ProposalSucceeded {
kind: ProposalKind,
src_chain_id: TypedChainId,
proposal_nonce: ProposalNonce,
},
ProposalFailed {
kind: ProposalKind,
src_chain_id: TypedChainId,
proposal_nonce: ProposalNonce,
},
ProposersReset { proposers: Vec<T::AccountId> },
}
#[pallet::error]
pub enum Error<T> {
InvalidPermissions,
ThresholdNotSet,
InvalidChainId,
InvalidThreshold,
ChainNotWhitelisted,
ChainAlreadyWhitelisted,
ResourceDoesNotExist,
ProposerAlreadyExists,
ProposerInvalid,
MustBeProposer,
ProposerAlreadyVoted,
ProposalAlreadyExists,
ProposalDoesNotExist,
ProposalNotComplete,
ProposalAlreadyComplete,
ProposalExpired,
ProposerCountIsZero,
OutOfBounds,
InvalidProposal,
}
#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
pub initial_chain_ids: Vec<[u8; 6]>,
pub initial_r_ids: Vec<(ResourceId, Vec<u8>)>,
pub initial_proposers: Vec<T::AccountId>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
for bytes in self.initial_chain_ids.iter() {
let mut chain_id_bytes = [0u8; TypedChainId::LENGTH];
let f = 0;
let t = f + TypedChainId::LENGTH;
chain_id_bytes.copy_from_slice(&bytes[f..t]);
let chain_id = TypedChainId::from(chain_id_bytes);
ChainNonces::<T>::insert(chain_id, ProposalNonce::from(0));
}
for (r_id, r_data) in self.initial_r_ids.iter() {
let bounded_input: BoundedVec<_, _> =
r_data.clone().try_into().expect("Genesis resources is too large");
Resources::<T>::insert(*r_id, bounded_input);
}
let bounded_proposers: BoundedVec<T::AccountId, T::MaxProposers> = self
.initial_proposers
.clone()
.try_into()
.expect("Genesis proposers is too large");
Proposers::<T>::put(bounded_proposers);
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(<T as Config>::WeightInfo::set_threshold())]
#[pallet::call_index(0)]
pub fn set_threshold(origin: OriginFor<T>, threshold: u32) -> DispatchResultWithPostInfo {
Self::ensure_admin(origin)?;
Self::set_proposer_threshold(threshold)
}
#[pallet::weight(<T as Config>::WeightInfo::set_resource())]
#[pallet::call_index(1)]
pub fn set_resource(
origin: OriginFor<T>,
id: ResourceId,
method: Vec<u8>,
) -> DispatchResultWithPostInfo {
Self::ensure_admin(origin)?;
Self::register_resource(id, method)
}
#[pallet::weight(<T as Config>::WeightInfo::remove_resource())]
#[pallet::call_index(2)]
pub fn remove_resource(origin: OriginFor<T>, id: ResourceId) -> DispatchResultWithPostInfo {
Self::ensure_admin(origin)?;
Self::unregister_resource(id)
}
#[pallet::weight(<T as Config>::WeightInfo::whitelist_chain())]
#[pallet::call_index(3)]
pub fn whitelist_chain(
origin: OriginFor<T>,
chain_id: TypedChainId,
) -> DispatchResultWithPostInfo {
Self::ensure_admin(origin)?;
Self::whitelist(chain_id)
}
#[pallet::weight(<T as Config>::WeightInfo::acknowledge_proposal())]
#[pallet::call_index(4)]
pub fn acknowledge_proposal(
origin: OriginFor<T>,
prop: ProposalOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let proposal_ident =
decode_proposal_identifier(&prop).map_err(|_| Error::<T>::InvalidProposal)?;
let proposal_header =
decode_proposal_header(prop.data()).map_err(|_| Error::<T>::InvalidProposal)?;
ensure!(Self::is_proposer(&who), Error::<T>::MustBeProposer);
ensure!(
Self::chain_whitelisted(proposal_ident.typed_chain_id),
Error::<T>::ChainNotWhitelisted
);
ensure!(
Self::resource_exists(proposal_header.resource_id),
Error::<T>::ResourceDoesNotExist
);
Self::vote_for(who, proposal_header.nonce, proposal_ident.typed_chain_id, &prop)
}
#[pallet::weight(<T as Config>::WeightInfo::reject_proposal())]
#[pallet::call_index(5)]
pub fn reject_proposal(
origin: OriginFor<T>,
prop: ProposalOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let proposal_ident =
decode_proposal_identifier(&prop).map_err(|_| Error::<T>::InvalidProposal)?;
let proposal_header =
decode_proposal_header(prop.data()).map_err(|_| Error::<T>::InvalidProposal)?;
ensure!(Self::is_proposer(&who), Error::<T>::MustBeProposer);
ensure!(
Self::chain_whitelisted(proposal_ident.typed_chain_id),
Error::<T>::ChainNotWhitelisted
);
ensure!(
Self::resource_exists(proposal_header.resource_id),
Error::<T>::ResourceDoesNotExist
);
Self::vote_against(who, proposal_header.nonce, proposal_ident.typed_chain_id, &prop)
}
#[pallet::weight(<T as Config>::WeightInfo::eval_vote_state(prop.data().len() as u32))]
#[pallet::call_index(6)]
pub fn eval_vote_state(
origin: OriginFor<T>,
nonce: ProposalNonce,
src_chain_id: TypedChainId,
prop: ProposalOf<T>,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
Self::try_resolve_proposal(nonce, src_chain_id, &prop)
}
}
}
impl<T: Config> Pallet<T> {
pub fn ensure_admin(o: T::RuntimeOrigin) -> DispatchResultWithPostInfo {
T::AdminOrigin::ensure_origin(o)?;
Ok(().into())
}
pub fn is_proposer(who: &T::AccountId) -> bool {
Self::proposers().contains(who)
}
pub fn resource_exists(id: ResourceId) -> bool {
Resources::<T>::contains_key(id)
}
pub fn chain_whitelisted(chain_id: TypedChainId) -> bool {
ChainNonces::<T>::contains_key(chain_id)
}
pub fn set_proposer_threshold(threshold: u32) -> DispatchResultWithPostInfo {
ensure!(threshold > 0, Error::<T>::InvalidThreshold);
ensure!(threshold <= Proposers::<T>::get().len() as u32, Error::<T>::InvalidThreshold);
ProposerThreshold::<T>::put(threshold);
Self::deposit_event(Event::ProposerThresholdChanged { new_threshold: threshold });
Ok(().into())
}
pub fn register_resource(id: ResourceId, method: Vec<u8>) -> DispatchResultWithPostInfo {
let bounded_method: BoundedVec<_, _> =
method.try_into().map_err(|_| Error::<T>::OutOfBounds)?;
Resources::<T>::insert(id, bounded_method);
Ok(().into())
}
pub fn unregister_resource(id: ResourceId) -> DispatchResultWithPostInfo {
Resources::<T>::remove(id);
Ok(().into())
}
pub fn whitelist(chain_id: TypedChainId) -> DispatchResultWithPostInfo {
ensure!(chain_id != T::ChainIdentifier::get(), Error::<T>::InvalidChainId);
ensure!(!Self::chain_whitelisted(chain_id), Error::<T>::ChainAlreadyWhitelisted);
ChainNonces::<T>::insert(chain_id, ProposalNonce::from(0));
Self::deposit_event(Event::ChainWhitelisted { chain_id });
Ok(().into())
}
fn commit_vote(
who: T::AccountId,
nonce: ProposalNonce,
src_chain_id: TypedChainId,
prop: &ProposalOf<T>,
in_favour: bool,
) -> DispatchResultWithPostInfo {
let now = <frame_system::Pallet<T>>::block_number();
let mut votes = match Votes::<T>::get(src_chain_id, (nonce, prop.clone())) {
Some(v) => v,
None => ProposalVotes::<
<T as frame_system::Config>::AccountId,
BlockNumberFor<T>,
<T as Config>::MaxVotes,
> {
expiry: now + T::ProposalLifetime::get(),
..Default::default()
},
};
ensure!(!votes.is_complete(), Error::<T>::ProposalAlreadyComplete);
ensure!(!votes.is_expired(now), Error::<T>::ProposalExpired);
ensure!(!votes.has_voted(&who), Error::<T>::ProposerAlreadyVoted);
if in_favour {
votes.votes_for.try_push(who.clone()).map_err(|_| Error::<T>::OutOfBounds)?;
Self::deposit_event(Event::VoteFor {
src_chain_id,
proposal_nonce: nonce,
kind: prop.kind(),
who,
});
} else {
votes.votes_against.try_push(who.clone()).map_err(|_| Error::<T>::OutOfBounds)?;
Self::deposit_event(Event::VoteAgainst {
src_chain_id,
proposal_nonce: nonce,
kind: prop.kind(),
who,
});
}
Votes::<T>::insert(src_chain_id, (nonce, prop.clone()), votes.clone());
Ok(().into())
}
fn try_resolve_proposal(
nonce: ProposalNonce,
src_chain_id: TypedChainId,
prop: &ProposalOf<T>,
) -> DispatchResultWithPostInfo {
if let Some(mut votes) = Votes::<T>::get(src_chain_id, (nonce, prop.clone())) {
let now = <frame_system::Pallet<T>>::block_number();
ensure!(!votes.is_complete(), Error::<T>::ProposalAlreadyComplete);
ensure!(!votes.is_expired(now), Error::<T>::ProposalExpired);
let status =
votes.try_to_complete(ProposerThreshold::<T>::get(), ProposerCount::<T>::get());
Votes::<T>::insert(src_chain_id, (nonce, prop.clone()), votes.clone());
match status {
ProposalStatus::Approved => Self::finalize_execution(src_chain_id, nonce, prop),
ProposalStatus::Rejected => Self::cancel_execution(src_chain_id, nonce, prop),
_ => Ok(().into()),
}
} else {
Err(Error::<T>::ProposalDoesNotExist.into())
}
}
fn vote_for(
who: T::AccountId,
nonce: ProposalNonce,
src_chain_id: TypedChainId,
prop: &ProposalOf<T>,
) -> DispatchResultWithPostInfo {
Self::commit_vote(who, nonce, src_chain_id, prop, true)?;
Self::try_resolve_proposal(nonce, src_chain_id, prop)
}
fn vote_against(
who: T::AccountId,
nonce: ProposalNonce,
src_chain_id: TypedChainId,
prop: &ProposalOf<T>,
) -> DispatchResultWithPostInfo {
Self::commit_vote(who, nonce, src_chain_id, prop, false)?;
Self::try_resolve_proposal(nonce, src_chain_id, prop)
}
fn finalize_execution(
src_chain_id: TypedChainId,
nonce: ProposalNonce,
prop: &ProposalOf<T>,
) -> DispatchResultWithPostInfo {
Self::deposit_event(Event::ProposalApproved {
src_chain_id,
proposal_nonce: nonce,
kind: prop.kind(),
});
T::ProposalHandler::handle_unsigned_proposal(prop.clone())?;
Self::deposit_event(Event::ProposalSucceeded {
src_chain_id,
proposal_nonce: nonce,
kind: prop.kind(),
});
Ok(().into())
}
fn cancel_execution(
src_chain_id: TypedChainId,
nonce: ProposalNonce,
prop: &ProposalOf<T>,
) -> DispatchResultWithPostInfo {
Self::deposit_event(Event::ProposalRejected {
src_chain_id,
proposal_nonce: nonce,
kind: prop.kind(),
});
Ok(().into())
}
}
impl<T: Config>
OnAuthoritySetChangeHandler<T::AccountId, dkg_runtime_primitives::AuthoritySetId, T::DKGId>
for Pallet<T>
{
fn on_authority_set_changed(authorities: &[T::AccountId], authority_ids: &[T::DKGId]) {
let new_external_accounts = authority_ids
.iter()
.map(|id| T::DKGAuthorityToMerkleLeaf::convert(id.clone()))
.map(|id| {
let bounded_external_account: VotingKey<T> =
id.try_into().expect("External account outside limits!");
bounded_external_account
})
.collect::<Vec<_>>();
ProposerCount::<T>::put(authorities.len() as u32);
let bounded_proposers: BoundedVec<T::AccountId, T::MaxProposers> =
authorities.to_vec().try_into().expect("Too many authorities!");
Proposers::<T>::put(bounded_proposers);
let bounded_external_accounts: VoterList<T> = authorities
.iter()
.cloned()
.zip(new_external_accounts)
.collect::<Vec<_>>()
.try_into()
.expect("Too many external proposer accounts!");
VotingKeys::<T>::put(bounded_external_accounts);
Self::deposit_event(Event::<T>::ProposersReset { proposers: authorities.to_vec() });
}
}
pub struct DKGEcdsaToEthereumAddress;
impl Convert<dkg_runtime_primitives::crypto::AuthorityId, Vec<u8>> for DKGEcdsaToEthereumAddress {
fn convert(a: dkg_runtime_primitives::crypto::AuthorityId) -> Vec<u8> {
use k256::{ecdsa::VerifyingKey, elliptic_curve::sec1::ToEncodedPoint};
let _x = VerifyingKey::from_sec1_bytes(sp_core::crypto::ByteArray::as_slice(&a));
VerifyingKey::from_sec1_bytes(sp_core::crypto::ByteArray::as_slice(&a))
.map(|pub_key| {
let uncompressed = pub_key.to_encoded_point(false);
sp_io::hashing::keccak_256(&uncompressed.as_bytes()[1..])[12..].to_vec()
})
.map_err(|_| {
log::error!(target: "runtime::dkg_proposals", "Invalid DKG PublicKey format!");
})
.unwrap_or_default()
}
}