#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::all)]
use frame_support::{
pallet_prelude::RuntimeDebug,
traits::{Currency, Get},
};
pub use pallet::*;
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_std::marker::PhantomData;
pub use weights::*;
pub mod weights;
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
mod common;
pub struct RootHashSize<T>(PhantomData<T>);
impl<T: Config> Get<u32> for RootHashSize<T> {
fn get() -> u32 {
<T::Hashing as sp_core::Hasher>::LENGTH as u32
}
}
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub const PRE_COMMIT_EXPIRATION_DURATION_BLOCKS: u32 = 800;
const MAX_LOOP_IN_TX: u64 = 100;
const STORAGE_MAX_DAYS: u32 = 376200;
const ANCHOR_PREFIX: &[u8; 6] = b"anchor";
const EVICT_PRE_COMMIT_LIST_SIZE: u32 = 100;
#[derive(Encode, Decode, Default, Clone, PartialEq, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct PreCommitData<Hash, AccountId, BlockNumber, Balance> {
signing_root: Hash,
identity: AccountId,
expiration_block: BlockNumber,
deposit: Balance,
}
#[derive(Encode, Decode, Default, Clone, PartialEq, RuntimeDebug, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct AnchorData<Hash, BlockNumber> {
id: Hash,
pub doc_root: Hash,
anchored_block: BlockNumber,
}
#[frame_support::pallet]
pub mod pallet {
use cfg_traits::fees::{Fee, Fees};
use frame_support::{pallet_prelude::*, storage::child, traits::ReservableCurrency};
use frame_system::pallet_prelude::*;
use sp_runtime::{
traits::{CheckedAdd, CheckedMul, Hash},
ArithmeticError, StateVersion,
};
use sp_std::vec::Vec;
use super::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + pallet_timestamp::Config {
type Fees: Fees<AccountId = Self::AccountId, Balance = BalanceOf<Self>>;
type CommitAnchorFeeKey: Get<<Self::Fees as Fees>::FeeKey>;
type PreCommitDepositFeeKey: Get<<Self::Fees as Fees>::FeeKey>;
type WeightInfo: WeightInfo;
type Currency: ReservableCurrency<Self::AccountId>;
}
#[pallet::storage]
#[pallet::getter(fn get_pre_commit)]
pub(super) type PreCommits<T: Config> = StorageMap<
_,
Blake2_256,
T::Hash,
PreCommitData<T::Hash, T::AccountId, BlockNumberFor<T>, BalanceOf<T>>,
>;
#[pallet::storage]
#[pallet::getter(fn get_anchor_evict_date)]
pub(super) type AnchorEvictDates<T: Config> = StorageMap<_, Blake2_256, T::Hash, u32>;
#[pallet::storage]
#[pallet::getter(fn get_anchor_id_by_index)]
pub(super) type AnchorIndexes<T: Config> = StorageMap<_, Blake2_256, u64, T::Hash>;
#[pallet::storage]
#[pallet::getter(fn get_latest_anchor_index)]
pub(super) type LatestAnchorIndex<T: Config> = StorageValue<_, u64>;
#[pallet::storage]
#[pallet::getter(fn get_latest_evicted_anchor_index)]
pub(super) type LatestEvictedAnchorIndex<T: Config> = StorageValue<_, u64>;
#[pallet::storage]
#[pallet::getter(fn get_latest_evicted_date)]
pub(super) type LatestEvictedDate<T: Config> = StorageValue<_, u32>;
#[pallet::storage]
#[pallet::getter(fn get_evicted_anchor_root_by_day)]
pub(super) type EvictedAnchorRoots<T: Config> =
StorageMap<_, Blake2_256, u32, BoundedVec<u8, RootHashSize<T>>>;
#[pallet::error]
pub enum Error<T> {
AnchorAlreadyExists,
AnchorStoreDateInPast,
AnchorStoreDateAboveMaxLimit,
PreCommitAlreadyExists,
NotOwnerOfPreCommit,
InvalidPreCommitProof,
EvictionDateTooBig,
FailedToConvertEpochToDays,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(<T as pallet::Config>::WeightInfo::pre_commit())]
#[pallet::call_index(0)]
pub fn pre_commit(
origin: OriginFor<T>,
anchor_id: T::Hash,
signing_root: T::Hash,
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(
Self::get_anchor_by_id(anchor_id).is_none(),
Error::<T>::AnchorAlreadyExists
);
ensure!(
Self::get_valid_pre_commit(anchor_id).is_none(),
Error::<T>::PreCommitAlreadyExists
);
let expiration_block = <frame_system::Pallet<T>>::block_number()
.checked_add(&BlockNumberFor::<T>::from(
PRE_COMMIT_EXPIRATION_DURATION_BLOCKS,
))
.ok_or(ArithmeticError::Overflow)?;
let deposit = T::Fees::fee_value(T::PreCommitDepositFeeKey::get());
T::Currency::reserve(&who, deposit)?;
<PreCommits<T>>::insert(
anchor_id,
PreCommitData {
signing_root,
identity: who.clone(),
expiration_block,
deposit,
},
);
Ok(())
}
#[pallet::weight(<T as pallet::Config>::WeightInfo::commit())]
#[pallet::call_index(1)]
pub fn commit(
origin: OriginFor<T>,
anchor_id_preimage: T::Hash,
doc_root: T::Hash,
proof: T::Hash,
stored_until_date: T::Moment,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let eviction_date_u64 = TryInto::<u64>::try_into(stored_until_date)
.or(Err(Error::<T>::EvictionDateTooBig))?;
let nowt = <pallet_timestamp::Pallet<T>>::get();
let now: u64 =
TryInto::<u64>::try_into(nowt).or(Err(Error::<T>::EvictionDateTooBig))?;
ensure!(
now + common::MILLISECS_PER_DAY < eviction_date_u64,
Error::<T>::AnchorStoreDateInPast
);
let stored_until_date_from_epoch = common::get_days_since_epoch(eviction_date_u64)
.ok_or(Error::<T>::EvictionDateTooBig)?;
ensure!(
stored_until_date_from_epoch <= STORAGE_MAX_DAYS,
Error::<T>::AnchorStoreDateAboveMaxLimit
);
let anchor_id =
(anchor_id_preimage).using_encoded(<T as frame_system::Config>::Hashing::hash);
ensure!(
Self::get_anchor_by_id(anchor_id).is_none(),
Error::<T>::AnchorAlreadyExists
);
if let Some(pre_commit) = Self::get_valid_pre_commit(anchor_id) {
ensure!(
pre_commit.identity == who.clone(),
Error::<T>::NotOwnerOfPreCommit
);
ensure!(
Self::has_valid_pre_commit_proof(anchor_id, doc_root, proof),
Error::<T>::InvalidPreCommitProof
);
}
let now_u64 = TryInto::<u64>::try_into(<pallet_timestamp::Pallet<T>>::get())
.or(Err(ArithmeticError::Overflow))?;
let today_in_days_from_epoch = common::get_days_since_epoch(now_u64)
.ok_or(Error::<T>::FailedToConvertEpochToDays)?;
let multiplier = stored_until_date_from_epoch
.checked_sub(today_in_days_from_epoch)
.ok_or(ArithmeticError::Underflow)?;
let fee = T::Fees::fee_value(T::CommitAnchorFeeKey::get())
.checked_mul(&multiplier.into())
.ok_or(ArithmeticError::Overflow)?;
T::Fees::fee_to_author(&who, Fee::Balance(fee))?;
let anchored_block = <frame_system::Pallet<T>>::block_number();
let anchor_data = AnchorData {
id: anchor_id,
doc_root,
anchored_block,
};
let prefixed_key = Self::anchor_storage_key(&stored_until_date_from_epoch.encode());
Self::store_anchor(
anchor_id,
&prefixed_key,
stored_until_date_from_epoch,
&anchor_data.encode(),
)?;
Self::evict_pre_commit(anchor_id, false);
Ok(())
}
#[pallet::weight(<T as pallet::Config>::WeightInfo::evict_pre_commits())]
#[pallet::call_index(2)]
pub fn evict_pre_commits(
origin: OriginFor<T>,
anchor_ids: BoundedVec<T::Hash, ConstU32<EVICT_PRE_COMMIT_LIST_SIZE>>,
) -> DispatchResult {
ensure_signed(origin)?;
for anchor_id in anchor_ids {
Self::evict_pre_commit(anchor_id, true);
}
Ok(())
}
#[pallet::weight(<T as pallet::Config>::WeightInfo::evict_anchors())]
#[pallet::call_index(3)]
pub fn evict_anchors(origin: OriginFor<T>) -> DispatchResult {
ensure_signed(origin)?;
let now_u64 = TryInto::<u64>::try_into(<pallet_timestamp::Pallet<T>>::get())
.or(Err(ArithmeticError::Overflow))?;
let today_in_days_from_epoch = common::get_days_since_epoch(now_u64)
.ok_or(Error::<T>::FailedToConvertEpochToDays)?;
let evict_date = <LatestEvictedDate<T>>::get()
.unwrap_or_default()
.checked_add(1)
.ok_or(ArithmeticError::Overflow)?;
let mut yesterday = today_in_days_from_epoch
.checked_sub(1)
.ok_or(ArithmeticError::Underflow)?;
if yesterday > evict_date + MAX_LOOP_IN_TX as u32 {
yesterday = evict_date + MAX_LOOP_IN_TX as u32 - 1;
}
let _evicted_trie_count =
Self::evict_anchor_child_tries(evict_date, today_in_days_from_epoch);
let _evicted_anchor_indexes_count = Self::remove_anchor_indexes(yesterday)?;
<LatestEvictedDate<T>>::put(yesterday);
Ok(())
}
}
impl<T: Config> Pallet<T> {
fn get_valid_pre_commit(
anchor_id: T::Hash,
) -> Option<PreCommitData<T::Hash, T::AccountId, BlockNumberFor<T>, BalanceOf<T>>> {
<PreCommits<T>>::get(anchor_id).filter(|pre_commit| {
pre_commit.expiration_block > <frame_system::Pallet<T>>::block_number()
})
}
fn evict_pre_commit(anchor_id: T::Hash, only_if_expired: bool) {
if let Some(pre_commit) = PreCommits::<T>::get(anchor_id) {
if !only_if_expired
|| pre_commit.expiration_block <= <frame_system::Pallet<T>>::block_number()
{
PreCommits::<T>::remove(anchor_id);
T::Currency::unreserve(&pre_commit.identity, pre_commit.deposit);
}
}
}
fn has_valid_pre_commit_proof(
anchor_id: T::Hash,
doc_root: T::Hash,
proof: T::Hash,
) -> bool {
let signing_root = match <PreCommits<T>>::get(anchor_id) {
Some(pre_commit) => pre_commit.signing_root,
None => return false,
};
let mut concatenated_bytes = signing_root.as_ref().to_vec();
let proof_bytes = proof.as_ref().to_vec();
concatenated_bytes.extend(proof_bytes);
let calculated_root = <T as frame_system::Config>::Hashing::hash(&concatenated_bytes);
return doc_root == calculated_root;
}
fn evict_anchor_child_tries(from: u32, until: u32) -> usize {
(from..until)
.map(|day| {
(
day,
common::generate_child_storage_key(&Self::anchor_storage_key(
&day.encode(),
)),
)
})
.map(|(day, key)| {
if !<EvictedAnchorRoots<T>>::contains_key(day) {
let root: BoundedVec<_, _> = child::root(&key, StateVersion::V0)
.try_into()
.expect("The output hash must use the block hasher");
<EvictedAnchorRoots<T>>::insert(day, root);
}
key
})
.map(|key| child::clear_storage(&key, None, None))
.count()
}
pub(crate) fn remove_anchor_indexes(yesterday: u32) -> Result<usize, DispatchError> {
let evicted_index = <LatestEvictedAnchorIndex<T>>::get()
.unwrap_or_default()
.checked_add(1)
.ok_or(ArithmeticError::Overflow)?;
let anchor_index = <LatestAnchorIndex<T>>::get()
.unwrap_or_default()
.checked_add(1)
.ok_or(ArithmeticError::Overflow)?;
let count = (evicted_index..anchor_index)
.take(MAX_LOOP_IN_TX as usize)
.filter_map(|idx| {
let anchor_id = <AnchorIndexes<T>>::get(idx)?;
let eviction_date = <AnchorEvictDates<T>>::get(anchor_id).unwrap_or_default();
Some((idx, anchor_id, eviction_date))
})
.filter(|(_, _, anchor_evict_date)| anchor_evict_date <= &yesterday)
.map(|(idx, anchor_id, _)| {
<AnchorEvictDates<T>>::remove(anchor_id);
<AnchorIndexes<T>>::remove(idx);
<LatestEvictedAnchorIndex<T>>::put(idx);
})
.count();
Ok(count)
}
pub fn get_anchor_by_id(
anchor_id: T::Hash,
) -> Option<AnchorData<T::Hash, BlockNumberFor<T>>> {
let anchor_evict_date = <AnchorEvictDates<T>>::get(anchor_id)?;
let anchor_evict_date_enc: &[u8] = &anchor_evict_date.encode();
let prefixed_key = Self::anchor_storage_key(anchor_evict_date_enc);
let child_info = common::generate_child_storage_key(&prefixed_key);
child::get_raw(&child_info, anchor_id.as_ref())
.and_then(|data| AnchorData::decode(&mut &*data).ok())
}
pub fn anchor_storage_key(storage_key: &[u8]) -> Vec<u8> {
let mut prefixed_key = Vec::with_capacity(ANCHOR_PREFIX.len() + storage_key.len());
prefixed_key.extend_from_slice(ANCHOR_PREFIX);
prefixed_key.extend_from_slice(storage_key);
prefixed_key
}
fn store_anchor(
anchor_id: T::Hash,
prefixed_key: &Vec<u8>,
stored_until_date_from_epoch: u32,
anchor_data_encoded: &[u8],
) -> DispatchResult {
let idx = <LatestAnchorIndex<T>>::get()
.unwrap_or_default()
.checked_add(1)
.ok_or(ArithmeticError::Overflow)?;
let child_info = common::generate_child_storage_key(prefixed_key);
child::put_raw(&child_info, anchor_id.as_ref(), &anchor_data_encoded);
<AnchorEvictDates<T>>::insert(&anchor_id, &stored_until_date_from_epoch);
<AnchorIndexes<T>>::insert(idx, &anchor_id);
<LatestAnchorIndex<T>>::put(idx);
Ok(())
}
}
}