use crate::{Accrete, LogFormatter, Logging, Portable};
use codec::{Decode, Encode};
use core::fmt::Debug;
use frame_system::{pallet_prelude::BlockNumberFor, Config, Pallet};
use sp_core::blake2_256;
use sp_std::vec;
use sp_runtime::{
offchain::storage::{MutateStorageError, StorageValueRef},
traits::{One, Saturating, Zero},
DispatchError, Vec
};
pub const HEAD_BLOCK: &'static [u8] = b"LOCAL_HEAD_BLOCK";
#[derive(Clone, Debug, Encode, Decode)]
pub struct Branch<T: Config, S: ForkScopes> {
pub parent: Option<[u8; 32]>,
pub head: BlockNumberFor<T>,
pub scope: S,
pub genesis: [u8; 32],
pub counter: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ForkAction {
MoveToParentBranchBack(u32),
MoveToParentBranch,
MoveToChildBranch(u32),
MoveToNextChildBranch,
MoveToSiblingBranch(u32),
MoveToNextSiblingBranch,
MoveToPreviousSiblingBranch,
MoveToRootBranch,
}
pub trait ForkScopes: Portable + Default + Debug + Accrete {}
impl<T> ForkScopes for T where T: Portable + Default + Debug + Accrete {}
pub trait ForksHandler<T: Config, S: ForkScopes>:
Logging<BlockNumberFor<T>, Logger = DispatchError> + Sized
{
const TAG: &[u8];
const MAX_FORKS: u32;
const MAX_RECOVER_TRAVERSAL: u32;
fn start<F: FnOnce()>(
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
ocw: F,
) {
let block = Pallet::<T>::block_number();
let actual_parent_block = block.saturating_sub(One::one());
let actual_grandparent_block = block.saturating_sub(2u8.into());
if actual_parent_block == actual_grandparent_block {
return;
}
let block = block.saturating_sub(One::one());
let _ = loop {
let current = Pallet::<T>::block_hash(block);
let parent = Pallet::<T>::block_hash(block.saturating_sub(One::one()));
let head = match Self::get_head() {
Some(v) => v,
None => {
let initial_head = block.saturating_sub(One::one());
store_encoded([Self::TAG, HEAD_BLOCK].concat(), &initial_head);
initial_head
}
};
let divider_hash = Self::get_divider(parent);
if divider_hash.is_none() {
match Self::parent_divider_unavailable(block, target, fmt) {
Ok(_) => continue,
Err(e) => break e,
}
}
let branch_hash = Self::get_branch_hash(parent);
if block <= head {
let prev_branch = match branch_hash {
Some(h) => match Self::get_branch(&h) {
Some(b) => b,
None => match Self::parent_branch_unavailable(block, target, fmt) {
Ok(_) => continue,
Err(e) => break e,
},
},
None => match Self::parent_branch_hash_unavailable(block, target, fmt) {
Ok(_) => continue,
Err(e) => break e,
},
};
let scope = prev_branch.scope.accrete();
let genesis = prev_branch.genesis;
let mut counter = prev_branch.counter.clone();
let mut i = 0u32;
let k = loop {
let mut try_counter = counter.clone();
try_counter.push(i);
let try_branch_hash = branch_key(Self::TAG, genesis, &try_counter);
if load_value::<Branch<T, S>>(&try_branch_hash).is_none() {
break Some(i);
}
i += 1;
if i > Self::MAX_FORKS {
break None;
}
};
let Some(new_counter) = k else {
match Self::max_forks(block, target, fmt) {
Ok(_) => continue,
Err(e) => break e,
}
};
counter.push(new_counter);
let new_branch_hash = branch_key(Self::TAG, genesis, &counter);
let new_branch = Branch::<T, S> {
parent: branch_hash,
head: block,
scope,
genesis,
counter,
};
store_encoded(&new_branch_hash, &new_branch);
let new_divider_hash = divider_key(Self::TAG, parent, new_branch_hash);
store_encoded(&new_divider_hash, &new_branch_hash);
let block_hash = block_key(Self::TAG, current);
store_encoded(&block_hash, &new_divider_hash);
ocw();
return;
}
let Some(located_branch_hash) = branch_hash else {
match Self::parent_branch_hash_unavailable(block, target, fmt) {
Ok(_) => continue,
Err(e) => break e,
}
};
let storage_ref = StorageValueRef::persistent(&located_branch_hash);
let result = storage_ref.mutate(|result: Result<Option<Branch<T, S>>, _>| {
let Ok(maybe) = result else {
match Self::inherited_branch_decode_error(block, target, fmt) {
Ok(_) => return Err(None),
Err(e) => return Err(Some(e)),
}
};
let mut branch = match maybe {
Some(v) => v,
None => match Self::parent_branch_unavailable(block, target, fmt) {
Ok(_) => return Err(None),
Err(e) => return Err(Some(e)),
},
};
branch.head = block;
Ok(branch)
});
match result {
Ok(_) => {}
Err(e) => match e {
MutateStorageError::ConcurrentModification(_) => {
match Self::inherited_branch_mutation_conflict(block, target, fmt) {
Ok(_) => {
ocw();
return;
}
Err(e) => break e,
}
}
MutateStorageError::ValueFunctionFailed(e) => match e {
Some(e) => break e,
None => continue,
},
},
}
let block_hash = block_key(Self::TAG, current);
let Some(divider) = divider_hash else {
unreachable!()
};
store_encoded(&block_hash, ÷r);
store_encoded([Self::TAG, HEAD_BLOCK].concat(), &block);
ocw();
return;
};
return;
}
fn get_head() -> Option<BlockNumberFor<T>> {
load_value::<BlockNumberFor<T>>(&[Self::TAG, HEAD_BLOCK].concat())
}
fn get_branch_hash(hash: T::Hash) -> Option<[u8; 32]> {
let divider_hash = Self::get_divider(hash)?;
load_value::<[u8; 32]>(÷r_hash)
}
fn get_block_branch(hash: T::Hash) -> Option<Branch<T, S>> {
let branch_hash = Self::get_branch_hash(hash)?;
Self::get_branch(&branch_hash)
}
fn get_branch(branch_hash: &[u8]) -> Option<Branch<T, S>> {
load_value::<Branch<T, S>>(branch_hash)
}
fn get_prev_branch(hash: T::Hash) -> Option<Branch<T, S>> {
let branch = Self::get_block_branch(hash)?;
let parent = branch.parent?;
Self::get_branch(&parent)
}
fn get_prev_block_branch() -> Option<Branch<T, S>> {
let block = Pallet::<T>::block_number();
let hash = Pallet::<T>::block_hash(block.saturating_sub(One::one()));
Self::get_block_branch(hash)
}
fn get_divider(hash: T::Hash) -> Option<[u8; 32]> {
let hash = block_key(Self::TAG, hash);
load_value::<[u8; 32]>(&hash)
}
fn transition(
branch: &Branch<T, S>,
action: ForkAction,
) -> Option<Branch<T, S>> {
match action {
ForkAction::MoveToParentBranch => {
let parent_key = branch.parent?;
let parent_branch = load_value::<Branch<T, S>>(&parent_key)?;
Some(parent_branch)
},
ForkAction::MoveToParentBranchBack(n) => {
if n.is_zero() {
return Some(branch.clone());
}
let mut current_branch = branch.clone();
for _ in 0..n {
let next =
Self::transition(¤t_branch, ForkAction::MoveToParentBranch)?;
current_branch = next;
}
Some(current_branch)
},
ForkAction::MoveToChildBranch(index) => {
let mut child_counter = branch.counter.clone();
child_counter.push(index);
let child_key = branch_key(Self::TAG, branch.genesis, &child_counter);
let child_branch = load_value::<Branch<T, S>>(&child_key)?;
Some(child_branch)
},
ForkAction::MoveToNextChildBranch => {
Self::transition(branch, ForkAction::MoveToChildBranch(0))
},
ForkAction::MoveToSiblingBranch(index) => {
let sibling_counter = if branch.counter.is_empty() {
vec![index]
} else {
let mut c = branch.counter.clone();
*c.last_mut()? = index;
c
};
if sibling_counter == branch.counter {
return None;
}
let sibling_key = branch_key(Self::TAG, branch.genesis, &sibling_counter);
let sibling_branch = load_value::<Branch<T, S>>(&sibling_key)?;
Some(sibling_branch)
},
ForkAction::MoveToNextSiblingBranch => {
let next_index = branch.counter.last().map(|k| k.saturating_add(One::one())).unwrap_or(0);
Self::transition(branch, ForkAction::MoveToSiblingBranch(next_index))
},
ForkAction::MoveToPreviousSiblingBranch => {
let last = *branch.counter.last()?;
if last.is_zero() {
return None;
}
Self::transition(branch, ForkAction::MoveToSiblingBranch(last.saturating_sub(One::one())))
},
ForkAction::MoveToRootBranch => {
let mut current_branch = branch.clone();
loop {
if current_branch.parent.is_none() {
return Some(current_branch);
}
match Self::transition(¤t_branch, ForkAction::MoveToParentBranch) {
Some(parent) => {
current_branch = parent;
},
None => return Some(current_branch),
}
}
},
}
}
fn gen_scope_item_key(
item: &S::Item,
) -> [u8; 32] {
S::make_key(item)
}
fn scope_item_exists(
key: &[u8; 32],
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<bool, Self::Logger> {
let Some(branch) = Self::get_prev_block_branch() else {
return Err(<Self as Logging<BlockNumberFor<T>>>::error(
&Self::forks_not_enabled(),
Pallet::<T>::block_number(),
target,
fmt,
));
};
if branch.scope.exists_in_local(&key) {
return Ok(true);
}
if branch.scope.exists_in_inherited(&key) {
return Ok(true);
}
Ok(false)
}
fn add_to_scope(
item: S::Item,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<[u8; 32], Self::Logger> {
let block = Pallet::<T>::block_number()
.saturating_sub(One::one());
let hash = Pallet::<T>::block_hash(block);
let Some(branch_hash) = Self::get_branch_hash(hash) else {
return Err(<Self as Logging<BlockNumberFor<T>>>::error(
&Self::forks_not_enabled(),
Pallet::<T>::block_number(),
target,
fmt,
));
};
let Some(mut branch) = Self::get_branch(&branch_hash) else {
return Err(<Self as Logging<BlockNumberFor<T>>>::error(
&Self::inconsistent_forks(),
Pallet::<T>::block_number(),
target,
fmt,
));
};
let key = branch.scope.add_to_local(item);
store_encoded(&branch_hash, &branch);
Ok(key)
}
fn remove_from_scope(
key: &[u8; 32],
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
let block = Pallet::<T>::block_number()
.saturating_sub(One::one());
let hash = Pallet::<T>::block_hash(block);
let Some(branch_hash) = Self::get_branch_hash(hash) else {
return Err(<Self as Logging<BlockNumberFor<T>>>::error(
&Self::forks_not_enabled(),
Pallet::<T>::block_number(),
target,
fmt,
));
};
let Some(mut branch) = Self::get_prev_block_branch() else {
return Err(<Self as Logging<BlockNumberFor<T>>>::error(
&Self::inconsistent_forks(),
Pallet::<T>::block_number(),
target,
fmt,
));
};
if branch.scope.exists_in_local(&key) {
branch.scope.remove_from_local(&key);
store_encoded(&branch_hash, &branch);
return Ok(());
}
if !branch.scope.exists_in_inherited(&key) {
return Ok(());
}
branch.scope.remove_from_inherited(&key);
store_encoded(&branch_hash, &branch);
Ok(())
}
fn forks_not_enabled() -> DispatchError;
fn inconsistent_forks() -> DispatchError;
fn parent_branch_hash_unavailable(
block: BlockNumberFor<T>,
_target: Option<&str>,
_fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
let mut recoverer = block.saturating_sub(2u8.into());
let mut recovered = None;
let mut attempts = 0u32;
loop {
if attempts >= Self::MAX_RECOVER_TRAVERSAL {
break;
}
if recoverer.is_zero() {
break;
}
let recover_via = Pallet::<T>::block_hash(recoverer.saturating_sub(One::one()));
if let Some(branch) = Self::get_block_branch(recover_via) {
recovered = Some(branch);
break;
}
recoverer = recoverer.saturating_sub(One::one());
attempts += 1;
}
let scope = match recovered {
Some(branch) => branch.scope.accrete(),
None => {
let parent_block = block.saturating_sub(One::one());
let parent = Pallet::<T>::block_hash(parent_block);
let grand_parent = Pallet::<T>::block_hash(block.saturating_sub(2u8.into()));
let (scope, genesis) = (
S::default(),
grand_parent.encode().using_encoded(blake2_256),
);
let branch = Branch::<T, S> {
parent: None,
head: parent_block,
scope,
genesis,
counter: Vec::new(),
};
let branch_hash = branch_key(Self::TAG, genesis, &[]);
store_encoded(&branch_hash, &branch);
let divider_hash = divider_key(Self::TAG, parent, branch_hash);
store_encoded(÷r_hash, &branch_hash);
let block_hash = block_key(Self::TAG, parent);
store_encoded(&block_hash, ÷r_hash);
return Ok(());
}
};
let mut target = recoverer.saturating_add(One::one());
let mut branchkey = None;
while target < block {
let current = Pallet::<T>::block_hash(target);
let parent = Pallet::<T>::block_hash(target.saturating_sub(One::one()));
let branch_hash = match branchkey {
Some(h) => h,
None => {
let genesis = parent.encode().using_encoded(blake2_256);
let branch = Branch::<T, S> {
parent: Self::get_branch_hash(Pallet::<T>::block_hash(recoverer.saturating_sub(One::one()))),
head: target,
scope: scope.clone(),
genesis,
counter: Vec::new(),
};
let key = branch_key(Self::TAG, genesis, &[]);
store_encoded(&key, &branch);
branchkey = Some(key);
key
}
};
let divider_hash = divider_key(Self::TAG, parent, branch_hash);
store_encoded(÷r_hash, &branch_hash);
let block_hash = block_key(Self::TAG, current);
store_encoded(&block_hash, ÷r_hash);
target = target.saturating_add(One::one());
}
Ok(())
}
fn parent_branch_unavailable(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
let parent = Pallet::<T>::block_hash(block.saturating_sub(1u8.into()));
let divider_hash = match Self::get_divider(parent) {
Some(v) => v,
None => {
return Self::parent_branch_hash_unavailable(block, target, fmt);
}
};
let mut divider_ref = StorageValueRef::persistent(÷r_hash);
divider_ref.clear();
Self::parent_branch_hash_unavailable(block, target, fmt)
}
fn inherited_branch_mutation_conflict(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
let current = Pallet::<T>::block_hash(block);
let parent = Pallet::<T>::block_hash(block.saturating_sub(One::one()));
let branch_hash = match Self::get_branch_hash(parent) {
Some(v) => v,
None => {
return Self::parent_branch_hash_unavailable(block, target, fmt);
}
};
let prev_branch = match Self::get_branch(&branch_hash) {
Some(v) => v,
None => {
return Self::parent_branch_unavailable(block, target, fmt);
}
};
let scope = prev_branch.scope.accrete();
let genesis = prev_branch.genesis;
let mut counter = prev_branch.counter.clone();
let mut i = 0u32;
let next_counter = loop {
let mut try_counter = counter.clone();
try_counter.push(i);
let try_branch_hash = branch_key(Self::TAG, genesis, &try_counter);
if load_value::<Branch<T, S>>(&try_branch_hash).is_none() {
break Some(i);
}
i += 1;
if i > Self::MAX_FORKS {
break None;
}
};
let Some(new_counter) = next_counter else {
return Self::max_forks(block, target, fmt);
};
counter.push(new_counter);
let new_branch_hash = branch_key(Self::TAG, genesis, &counter);
let new_branch = Branch::<T, S> {
parent: Some(branch_hash),
head: block,
scope,
genesis,
counter,
};
store_encoded(&new_branch_hash, &new_branch);
let divider_hash = divider_key(Self::TAG, parent, new_branch_hash);
store_encoded(÷r_hash, &new_branch_hash);
let block_hash = block_key(Self::TAG, current);
store_encoded(&block_hash, ÷r_hash);
store_encoded([Self::TAG, HEAD_BLOCK].concat(), &block);
Ok(())
}
fn parent_divider_unavailable(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
Self::parent_branch_hash_unavailable(block, target, fmt)
}
fn inherited_branch_decode_error(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
Self::parent_branch_unavailable(block, target, fmt)
}
fn max_forks(
block: BlockNumberFor<T>,
target: Option<&str>,
fmt: Option<LogFormatter<BlockNumberFor<T>, Self::Level>>,
) -> Result<(), Self::Logger> {
Err(<Self as Logging<BlockNumberFor<T>>>::error(
&Self::max_forks_error(),
block,
target,
fmt,
))
}
fn max_forks_error() -> DispatchError;
}
fn make_hash(tag: &[u8], input: impl Encode, suffix: &[u8]) -> [u8; 32] {
let mut source = tag.encode();
source.extend_from_slice(&input.encode());
source.extend_from_slice(suffix);
blake2_256(&source)
}
fn store_encoded<K: AsRef<[u8]>, V: Encode>(key: K, value: &V) {
let storage_ref = StorageValueRef::persistent(key.as_ref());
storage_ref.set(&value);
}
fn load_value<V: codec::Decode>(key: &[u8]) -> Option<V> {
let storage_ref = StorageValueRef::persistent(key);
let Ok(result) = storage_ref.get::<V>() else {
return None;
};
result
}
fn branch_key(tag: &[u8], genesis: [u8; 32], counter: &[u32]) -> [u8; 32] {
let mut identity = genesis.encode();
for c in counter {
identity.extend_from_slice(&c.encode());
}
make_hash(tag, &identity, b"branch")
}
fn divider_key(tag: &[u8], hash: impl Encode, branch_key: [u8; 32]) -> [u8; 32] {
let mut identity = hash.encode();
identity.extend_from_slice(&branch_key.encode());
make_hash(tag, &identity, b"divider")
}
fn block_key(tag: &[u8], hash: impl Encode) -> [u8; 32] {
let identity = hash.encode();
make_hash(tag, &identity, b"block")
}
#[cfg(test)]
mod tests {
use super::*;
use frame_support::derive_impl;
use frame_system::pallet_prelude::BlockNumberFor;
use sp_core::offchain::{
testing::{TestOffchainExt, TestTransactionPoolExt},
OffchainDbExt, OffchainWorkerExt, TransactionPoolExt,
};
use sp_io::TestExternalities;
use sp_runtime::{
offchain::storage::StorageValueRef,
traits::{BlakeTwo256, Hash},
AccountId32, BuildStorage, DispatchError,
};
pub type Block = frame_system::mocking::MockBlock<Test>;
#[frame_support::runtime]
pub mod runtime {
#[runtime::runtime]
#[runtime::derive(
RuntimeCall,
RuntimeEvent,
RuntimeError,
RuntimeOrigin,
RuntimeFreezeReason,
RuntimeHoldReason,
RuntimeSlashReason,
RuntimeLockId,
RuntimeTask,
RuntimeViewFunction
)]
pub struct Test;
#[runtime::pallet_index(0)]
pub type System = frame_system::Pallet<Test>;
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
impl frame_system::Config for Test {
type Block = Block;
type AccountId = AccountId32;
type Lookup = sp_runtime::traits::IdentityLookup<Self::AccountId>;
}
fn new_ocw_ext() -> TestExternalities {
let storage = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
let mut ext = TestExternalities::new(storage);
ext.execute_with(|| System::set_block_number(1u64));
let (offchain, _state) = TestOffchainExt::new();
ext.register_extension(OffchainWorkerExt::new(offchain.clone()));
ext.register_extension(OffchainDbExt::new(offchain));
let (pool, _) = TestTransactionPoolExt::new();
ext.register_extension(TransactionPoolExt::new(pool));
ext
}
#[derive(Clone, Debug, Default, Encode, Decode)]
struct TestScope {
local: std::collections::BTreeSet<[u8; 32]>,
inherited: std::collections::BTreeSet<[u8; 32]>,
}
impl Accrete for TestScope {
type Item = Vec<u8>;
fn accrete(&self) -> Self {
let mut inh = self.inherited.clone();
inh.extend(self.local.iter().copied());
Self { local: std::collections::BTreeSet::new(), inherited: inh }
}
fn inherited(&self) -> Vec<[u8; 32]> { self.inherited.iter().copied().collect() }
fn local(&self) -> Vec<[u8; 32]> { self.local.iter().copied().collect() }
fn add_to_local(&mut self, item: Self::Item) -> [u8; 32] {
let key = Self::make_key(&item);
self.local.insert(key);
key
}
fn exists_in_local(&self, key: &[u8; 32]) -> bool { self.local.contains(key) }
fn exists_in_inherited(&self, key: &[u8; 32]) -> bool { self.inherited.contains(key) }
fn remove_from_local(&mut self, key: &[u8; 32]) { self.local.remove(key); }
fn remove_from_inherited(&mut self, key: &[u8; 32]) { self.inherited.remove(key); }
}
const TAG: &[u8] = b"test_forks";
struct TestForks;
impl ForksHandler<Test, TestScope> for TestForks {
const TAG: &[u8] = b"test_forks";
const MAX_FORKS: u32 = 3;
const MAX_RECOVER_TRAVERSAL: u32 = 10;
fn max_forks_error() -> DispatchError {
DispatchError::Other("max_forks_exceeded")
}
fn forks_not_enabled() -> DispatchError {
DispatchError::Other("forks-not-enabled")
}
fn inconsistent_forks() -> DispatchError {
DispatchError::Other("inconsistent-forks")
}
}
fn mock_hash(n: u64) -> <Test as frame_system::Config>::Hash {
BlakeTwo256::hash(&n.to_le_bytes())
}
fn register_block_hashes(start: u64, end: u64) {
for n in start..=end {
frame_system::BlockHash::<Test>::insert(n, mock_hash(n));
}
}
fn set_block(n: u64) { System::set_block_number(n) }
fn read_head() -> Option<BlockNumberFor<Test>> {
load_value::<BlockNumberFor<Test>>(&[TAG, HEAD_BLOCK].concat())
}
fn resolve_branch(n: u64) -> Option<Branch<Test, TestScope>> {
TestForks::get_block_branch(mock_hash(n))
}
fn branch_by_lineage(
genesis: [u8; 32],
counter: &[u32],
) -> Option<Branch<Test, TestScope>> {
load_value::<Branch<Test, TestScope>>(&branch_key(TAG, genesis, counter))
}
fn recovery_genesis(gp_block: u64) -> [u8; 32] {
mock_hash(gp_block).encode().using_encoded(blake2_256)
}
#[test]
fn key_builders_block_key_is_deterministic_and_tag_namespaced() {
let h1 = mock_hash(1);
let h2 = mock_hash(2);
let tag_a = b"pallet_a".as_ref();
let tag_b = b"pallet_b".as_ref();
assert_eq!(block_key(tag_a, h1), block_key(tag_a, h1));
assert_ne!(block_key(tag_a, h1), block_key(tag_b, h1));
assert_ne!(block_key(tag_a, h1), block_key(tag_a, h2));
}
#[test]
fn key_builders_divider_key_differentiates_siblings_from_same_parent() {
let tag = TAG;
let parent = mock_hash(5);
let branch_a = [0xAAu8; 32];
let branch_b = [0xBBu8; 32];
let d_a = divider_key(tag, parent, branch_a);
let d_b = divider_key(tag, parent, branch_b);
assert_ne!(d_a, d_b);
assert_eq!(d_a, divider_key(tag, parent, branch_a));
}
#[test]
fn key_builders_branch_key_encodes_full_counter_lineage() {
let tag = TAG;
let genesis = recovery_genesis(0);
let k_root = branch_key(tag, genesis, &[]);
let k_fork0 = branch_key(tag, genesis, &[0]);
let k_fork1 = branch_key(tag, genesis, &[1]);
let k_nested = branch_key(tag, genesis, &[0, 0]);
assert_ne!(k_root, k_fork0);
assert_ne!(k_fork0, k_fork1);
assert_ne!(k_fork0, k_nested);
assert_eq!(k_fork0, branch_key(tag, genesis, &[0]));
let genesis2 = recovery_genesis(1);
assert_ne!(branch_key(tag, genesis, &[0]), branch_key(tag, genesis2, &[0]));
}
#[test]
fn bootstrap_guard_skips_block_zero() {
new_ocw_ext().execute_with(|| {
set_block(0);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(!ran, "ocw must not run at block 0");
assert!(read_head().is_none());
});
}
#[test]
fn bootstrap_guard_skips_block_one() {
new_ocw_ext().execute_with(|| {
set_block(1);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(!ran, "ocw must not run at block 1");
assert!(read_head().is_none());
});
}
#[test]
fn bootstrap_guard_exact_boundary_block_two_passes() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 2);
set_block(1);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(!ran, "block 1 must be skipped by the bootstrap guard");
set_block(2);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "block 2 must pass the bootstrap guard and call ocw");
});
}
#[test]
fn fresh_graph_initialisation_at_block_two() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 2);
set_block(2);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run after fresh graph initialisation");
assert_eq!(read_head(), Some(1));
let root = resolve_branch(1)
.expect("block 1 routing must resolve");
assert!(root.parent.is_none());
assert!(root.counter.is_empty());
assert_eq!(root.head, 1);
let b0 = resolve_branch(0)
.expect("block 0 routing exists as the synthetic root anchor");
assert_eq!(b0.genesis, root.genesis);
assert_eq!(b0.head, 1);
assert!(
branch_by_lineage(root.genesis, &[0]).is_none(),
"no sibling branch may exist after fresh init"
);
});
}
#[test]
fn sequential_extension_advances_root_branch_head() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run at block 3");
assert_eq!(read_head(), Some(2));
let b2 = resolve_branch(2).expect("block 2 routing must resolve");
assert_eq!(b2.head, 2);
assert!(b2.counter.is_empty());
let b1 = resolve_branch(1).expect("block 1 routing must still resolve");
assert_eq!(b1.genesis, b2.genesis);
assert!(
branch_by_lineage(b2.genesis, &[0]).is_none(),
"no sibling may exist after sequential extension"
);
set_block(4);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run at block 4");
assert_eq!(read_head(), Some(3));
let b3 = resolve_branch(3).expect("block 3 routing must resolve");
assert_eq!(b3.head, 3);
assert_eq!(b3.genesis, b2.genesis, "genesis must be stable across extensions");
});
}
#[test]
fn sibling_fork_created_when_block_at_or_below_head() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
set_block(4);
TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(3));
let canonical = resolve_branch(3).expect("canonical block 3 must resolve");
let fork_hash_3 = BlakeTwo256::hash(b"fork_block_3");
let fork_hash_2 = BlakeTwo256::hash(b"fork_block_2");
frame_system::BlockHash::<Test>::insert(3, fork_hash_3);
frame_system::BlockHash::<Test>::insert(2, fork_hash_2);
set_block(4);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run on the sibling fork path");
assert_eq!(read_head(), Some(3), "HEAD must stay at 3");
let sibling = TestForks::get_block_branch(fork_hash_3)
.expect("fork_hash_3 must resolve to sibling branch");
assert_eq!(sibling.head, 3);
assert_eq!(sibling.counter, vec![0u32]);
assert!(sibling.parent.is_some());
let fork_root_key = sibling.parent.unwrap();
let fork_root = TestForks::get_branch(&fork_root_key)
.expect("fork recovery root must be loadable via sibling.parent");
assert_eq!(fork_root.head, 2);
assert!(fork_root.counter.is_empty());
frame_system::BlockHash::<Test>::insert(3, mock_hash(3));
let canonical_after = resolve_branch(3)
.expect("canonical routing must survive fork creation");
assert_eq!(canonical_after.genesis, canonical.genesis);
});
}
#[test]
fn ocw_closure_runs_exactly_once_per_successful_call() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
let mut count = 0u32;
set_block(0);
TestForks::start(None, None, || { count += 1; });
set_block(1);
TestForks::start(None, None, || { count += 1; });
assert_eq!(count, 0, "ocw must never run at blocks 0 or 1");
set_block(2);
TestForks::start(None, None, || { count += 1; });
assert_eq!(count, 1);
set_block(3);
TestForks::start(None, None, || { count += 1; });
assert_eq!(count, 2);
set_block(4);
TestForks::start(None, None, || { count += 1; });
assert_eq!(count, 3);
});
}
#[test]
fn recovery_missing_divider_rebuilds_routing_and_advances_head() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 6);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
set_block(4);
TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(3));
StorageValueRef::persistent(&block_key(TAG, mock_hash(3))).clear();
assert!(resolve_branch(3).is_none(), "block 3 routing must be broken after wipe");
set_block(5);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run after missing-divider recovery");
assert_eq!(read_head(), Some(4), "HEAD must advance to 4 after recovery");
let b4 = resolve_branch(4).expect("block 4 routing must resolve after recovery");
assert_eq!(b4.head, 4);
let b3 = resolve_branch(3).expect("block 3 routing must be rebuilt by forward recovery");
assert_eq!(b3.head, 4, "rebuilt block 3 branch shares payload with block 4 (head advanced to 4 by mutate)");
assert_eq!(
b3.genesis, b4.genesis,
"block 3 and block 4 must share the same synthetic branch lineage"
);
});
}
#[test]
fn recovery_stale_divider_is_cleared_before_fallback_recovery() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2); TestForks::start(None, None, || {});
set_block(3); TestForks::start(None, None, || {});
set_block(4); TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(3));
let divider_hash = TestForks::get_divider(mock_hash(2))
.expect("divider for mock_hash(2) must exist before corruption");
assert!(resolve_branch(3).is_some(),
"block 3 routing must be intact before corruption");
let b3 = resolve_branch(3).expect("block 3 must resolve");
let root_key = branch_key(TAG, b3.genesis, &[]);
StorageValueRef::persistent(&root_key).clear();
assert!(resolve_branch(3).is_none(),
"routing must return None when branch payload is missing");
set_block(4);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(
load_value::<[u8; 32]>(÷r_hash).is_none(),
"stale divider must be cleared before recovery"
);
assert!(ran, "ocw must run after stale-divider recovery");
assert_eq!(read_head(), Some(3));
});
}
#[test]
fn recovery_rebuilds_multi_block_gap_with_shared_synthetic_branch() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 7);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
set_block(4);
TestForks::start(None, None, || {});
set_block(5);
TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(4));
StorageValueRef::persistent(&block_key(TAG, mock_hash(3))).clear();
StorageValueRef::persistent(&block_key(TAG, mock_hash(4))).clear();
assert!(resolve_branch(3).is_none(), "block 3 routing must be broken");
assert!(resolve_branch(4).is_none(), "block 4 routing must be broken");
set_block(6);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run after multi-block gap recovery");
assert_eq!(read_head(), Some(5));
let b5 = resolve_branch(5).expect("block 5 routing must resolve");
assert_eq!(b5.head, 5);
let b4 = resolve_branch(4).expect("block 4 routing must be rebuilt by forward recovery");
let parent_key = b4.parent.expect("synthetic branch must carry parent linkage");
let parent_branch = TestForks::get_branch(&parent_key)
.expect("synthetic branch parent must be loadable");
let b2 = resolve_branch(2).expect("block 2 must be intact");
assert_eq!(parent_branch.genesis, b2.genesis,
"synthetic branch parent must link to the last intact ancestor (block 2)");
assert!(resolve_branch(3).is_none(),
"block 3 routing is not restored by forward rebuild");
});
}
#[test]
fn decode_error_in_mutate_triggers_recovery_and_ocw_still_runs() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(2));
let b2 = resolve_branch(2).expect("block 2 must resolve before corruption");
let root_key = branch_key(TAG, b2.genesis, &[]);
StorageValueRef::persistent(&root_key).set(&[0xDE, 0xAD, 0xBE, 0xEF]);
assert!(resolve_branch(2).is_none(),
"routing must return None when payload is corrupt");
set_block(4);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(ran, "ocw must run after decode-error recovery");
assert_eq!(read_head(), Some(3), "HEAD must advance to 3");
let b3 = resolve_branch(3).expect("block 3 routing must resolve after recovery");
assert_eq!(b3.head, 3);
});
}
#[test]
fn concurrent_modification_promotes_conflict_writer_to_sibling_branch() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2); TestForks::start(None, None, || {});
set_block(3); TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(2));
let root_branch_hash = TestForks::get_branch_hash(mock_hash(1))
.expect("root branch hash must exist");
let root = TestForks::get_branch(&root_branch_hash)
.expect("root branch must be loadable");
assert!(root.counter.is_empty());
assert_eq!(root.head, 2);
let result = TestForks::inherited_branch_mutation_conflict(2, None, None);
assert!(result.is_ok(), "conflict handler must succeed");
assert_eq!(read_head(), Some(2), "HEAD must be 2 after conflict resolution");
let sibling = TestForks::get_block_branch(mock_hash(2))
.expect("block 2 routing must resolve to the sibling branch");
assert_eq!(sibling.counter, vec![0u32]);
assert_eq!(sibling.head, 2);
let sibling_parent_key = sibling.parent
.expect("sibling must carry a parent key");
assert_eq!(sibling_parent_key, root_branch_hash,
"sibling parent must be the root branch (writer 1's branch)");
let root_after = TestForks::get_branch(&root_branch_hash)
.expect("root branch must still exist");
assert_eq!(root_after.counter, root.counter);
assert_eq!(root_after.head, root.head);
assert_eq!(root_after.genesis, sibling.genesis);
});
}
#[test]
fn max_forks_exhaustion_prevents_sibling_creation_and_ocw() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
set_block(4);
TestForks::start(None, None, || {});
assert_eq!(read_head(), Some(3));
let genesis_fork = recovery_genesis(1);
let root_key = branch_key(TAG, genesis_fork, &[]);
store_encoded(&root_key, &Branch::<Test, TestScope> {
parent: None,
head: 2,
scope: TestScope::default(),
genesis: genesis_fork,
counter: vec![],
});
for i in 0u32..=TestForks::MAX_FORKS {
let sibling_key = branch_key(TAG, genesis_fork, &[i]);
store_encoded(&sibling_key, &Branch::<Test, TestScope> {
parent: Some(root_key),
head: 2,
scope: TestScope::default(),
genesis: genesis_fork,
counter: vec![i],
});
}
let fork_hash_2 = BlakeTwo256::hash(b"max_fork_block_2");
let fork_hash_3 = BlakeTwo256::hash(b"max_fork_block_3");
frame_system::BlockHash::<Test>::insert(2, fork_hash_2);
frame_system::BlockHash::<Test>::insert(3, fork_hash_3);
let dh = divider_key(TAG, fork_hash_2, root_key);
store_encoded(&dh, &root_key);
store_encoded(&block_key(TAG, fork_hash_2), &dh);
set_block(4);
let mut ran = false;
TestForks::start(None, None, || { ran = true; });
assert!(!ran, "ocw must NOT run when MAX_FORKS is exhausted");
assert_eq!(read_head(), Some(3), "HEAD must stay at 3");
assert!(
branch_by_lineage(genesis_fork, &[TestForks::MAX_FORKS + 1]).is_none(),
"no branch beyond MAX_FORKS must be created"
);
});
}
#[test]
fn recovery_synthetic_branch_preserves_parent_linkage() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
set_block(4);
TestForks::start(None, None, || {});
let b2_branch_hash = TestForks::get_branch_hash(mock_hash(2))
.expect("block 2 branch hash must exist");
StorageValueRef::persistent(&block_key(TAG, mock_hash(3))).clear();
set_block(5);
TestForks::start(None, None, || {});
let b3 = resolve_branch(3).expect("block 3 routing must be rebuilt");
let parent_key = b3.parent
.expect("rebuilt synthetic branch must carry a parent key");
assert_eq!(parent_key, b2_branch_hash,
"synthetic branch parent must point to block 2's branch");
let parent_branch = TestForks::get_branch(&parent_key)
.expect("parent branch must be loadable");
let b2 = resolve_branch(2).expect("block 2 must still be intact");
assert_eq!(parent_branch.genesis, b2.genesis);
});
}
#[test]
fn repeated_start_at_same_block_is_idempotent() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let head_first = read_head();
let b2_first = resolve_branch(2).expect("block 2 must resolve");
set_block(3);
TestForks::start(None, None, || {});
assert_eq!(read_head(), head_first, "HEAD must not change after repeated start");
let b2_second = resolve_branch(2).expect("block 2 must still resolve");
assert_eq!(b2_first.genesis, b2_second.genesis,
"repeated start must not alter branch genesis");
assert_eq!(b2_first.head, b2_second.head,
"repeated start must not alter branch head");
});
}
#[test]
fn fork_action_handler_navigation_after_graph_built() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 6);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
set_block(4);
TestForks::start(None, None, || {});
let fork_hash_3 = BlakeTwo256::hash(b"nav_fork_3");
let fork_hash_2 = BlakeTwo256::hash(b"nav_fork_2");
frame_system::BlockHash::<Test>::insert(3, fork_hash_3);
frame_system::BlockHash::<Test>::insert(2, fork_hash_2);
set_block(4);
TestForks::start(None, None, || {});
frame_system::BlockHash::<Test>::insert(3, mock_hash(3));
frame_system::BlockHash::<Test>::insert(2, mock_hash(2));
let root = resolve_branch(3)
.expect("canonical root must resolve via block 3 routing");
let sibling = TestForks::get_block_branch(fork_hash_3)
.expect("sibling must resolve via fork_hash_3 routing chain");
let fork_root_key = sibling.parent
.expect("sibling must carry a parent key");
let fork_root = TestForks::get_branch(&fork_root_key)
.expect("fork_root must be loadable via sibling.parent");
assert_eq!(root.head, 3);
assert!(root.parent.is_none());
assert_eq!(fork_root.head, 2);
assert!(fork_root.counter.is_empty());
assert_eq!(sibling.head, 3);
assert_eq!(sibling.counter, vec![0u32]);
let p = TestForks::transition(&sibling, ForkAction::MoveToParentBranch)
.expect("MoveToParentBranch from sibling must succeed");
assert!(p.counter.is_empty(), );
assert_eq!(p.head, fork_root.head);
let fork_root_parent = TestForks::transition(&fork_root, ForkAction::MoveToParentBranch)
.expect("MoveToParentBranch on fork_root must succeed");
assert_eq!(fork_root_parent.genesis, root.genesis);
assert!(fork_root_parent.counter.is_empty());
assert_eq!(fork_root_parent.head, root.head);
let b = TestForks::transition(&sibling, ForkAction::MoveToParentBranchBack(0))
.expect("MoveToParentBranchBack(0) must return self");
assert_eq!(b.counter, sibling.counter);
assert_eq!(b.head, sibling.head);
let b = TestForks::transition(&sibling, ForkAction::MoveToParentBranchBack(1))
.expect("MoveToParentBranchBack(1) must succeed");
assert!(b.counter.is_empty());
let b = TestForks::transition(&sibling, ForkAction::MoveToParentBranchBack(2))
.expect("MoveToParentBranchBack(2) must succeed - sibling has 2 levels of ancestry");
assert_eq!(
b.genesis, root.genesis,
"Back(2) from sibling must reach the canonical root"
);
assert!(
TestForks::transition(&sibling, ForkAction::MoveToParentBranchBack(3)).is_none(),
"MoveToParentBranchBack(3) exceeds ancestry depth -> None"
);
let child = TestForks::transition(&fork_root, ForkAction::MoveToChildBranch(0))
.expect("MoveToChildBranch(0) from fork_root must succeed");
assert_eq!(child.counter, vec![0u32]);
assert!(
TestForks::transition(&fork_root, ForkAction::MoveToChildBranch(1)).is_none(),
"MoveToChildBranch(1) where no child exists must return None"
);
assert!(
TestForks::transition(&sibling, ForkAction::MoveToChildBranch(0)).is_none(),
"MoveToChildBranch(0) from a leaf branch must return None"
);
let child = TestForks::transition(&fork_root, ForkAction::MoveToNextChildBranch)
.expect("MoveToNextChildBranch from fork_root must succeed");
assert_eq!(child.counter, vec![0u32]);
assert!(
TestForks::transition(&sibling, ForkAction::MoveToNextChildBranch).is_none(),
"MoveToNextChildBranch from leaf must return None"
);
let sib = TestForks::transition(&fork_root, ForkAction::MoveToSiblingBranch(0))
.expect("MoveToSiblingBranch(0) from root must reach sibling[0]");
assert_eq!(sib.counter, vec![0u32]);
assert!(
TestForks::transition(&sibling, ForkAction::MoveToSiblingBranch(0)).is_none(),
"MoveToSiblingBranch(same index) must return None"
);
assert!(
TestForks::transition(&fork_root, ForkAction::MoveToSiblingBranch(1)).is_none(),
"MoveToSiblingBranch to a non-existent slot must return None"
);
let arrived = TestForks::transition(&fork_root, ForkAction::MoveToNextSiblingBranch)
.expect("MoveToNextSiblingBranch from fork_root must succeed");
assert_eq!(arrived.counter, vec![0u32]);
assert!(
TestForks::transition(&sibling, ForkAction::MoveToNextSiblingBranch).is_none(),
"MoveToNextSiblingBranch when next slot is empty must return None"
);
assert!(
TestForks::transition(&sibling, ForkAction::MoveToPreviousSiblingBranch).is_none(),
"MoveToPreviousSiblingBranch at index 0 must return None"
);
assert!(
TestForks::transition(&fork_root, ForkAction::MoveToPreviousSiblingBranch).is_none(),
"MoveToPreviousSiblingBranch on a root (counter=[]) must return None"
);
let root_b = TestForks::transition(&sibling, ForkAction::MoveToRootBranch)
.expect("MoveToRootBranch from sibling must succeed");
assert!(root_b.parent.is_none());
assert_eq!(root_b.genesis, root.genesis);
let root_from_fork = TestForks::transition(&fork_root, ForkAction::MoveToRootBranch)
.expect("MoveToRootBranch from fork_root must succeed");
assert!(root_from_fork.parent.is_none());
assert_eq!(root_from_fork.genesis, root.genesis);
});
}
#[test]
fn scope_handler_gen_scope_item_key_is_deterministic_and_distinct() {
let item_a: Vec<u8> = b"key_a".to_vec();
let item_b: Vec<u8> = b"key_b".to_vec();
let k1 = TestForks::gen_scope_item_key(&item_a);
let k2 = TestForks::gen_scope_item_key(&item_a);
let k3 = TestForks::gen_scope_item_key(&item_b);
assert_eq!(k1, k2);
assert_ne!(k1, k3);
}
#[test]
fn scope_handler_scope_item_exists_returns_err_when_fork_graph_not_initialized() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2);
let key = TestForks::gen_scope_item_key(&b"test_item".to_vec());
let result = TestForks::scope_item_exists(&key, None, None);
assert_eq!(result, Err(DispatchError::Other("forks-not-enabled")));
});
}
#[test]
fn scope_handler_scope_item_exists_returns_false_for_absent_key() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let key = TestForks::gen_scope_item_key(&b"absent".to_vec());
let result = TestForks::scope_item_exists(&key, None, None);
assert_eq!(result, Ok(false));
});
}
#[test]
fn scope_handler_add_to_scope_makes_key_visible_in_local_scope() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let item = b"active_key".to_vec();
let key = TestForks::add_to_scope(item.clone(), None, None).unwrap();
let exists = TestForks::scope_item_exists(&key, None, None).unwrap();
assert!(exists);
});
}
#[test]
fn scope_handler_add_to_scope_returns_err_when_fork_graph_not_initialized() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2);
let result = TestForks::add_to_scope(b"item".to_vec(), None, None);
assert_eq!(result, Err(DispatchError::Other("forks-not-enabled")));
});
}
#[test]
fn scope_handler_local_key_propagates_to_inherited_on_next_branch() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 6);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let item = b"propagated_key".to_vec();
let key = TestForks::add_to_scope(item.clone(), None, None).unwrap();
set_block(4);
TestForks::start(None, None, || {});
let exists = TestForks::scope_item_exists(&key, None, None).unwrap();
assert!(exists);
});
}
#[test]
fn scope_handler_remove_from_scope_removes_key_from_local_scope() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 5);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let item = b"removable_key".to_vec();
let key = TestForks::add_to_scope(item.clone(), None, None).unwrap();
assert!(TestForks::scope_item_exists(&key, None, None).unwrap());
TestForks::remove_from_scope(&key, None, None).unwrap();
let exists = TestForks::scope_item_exists(&key, None, None).unwrap();
assert!(!exists);
});
}
#[test]
fn scope_handler_remove_from_scope_removes_key_from_inherited_scope() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 6);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let item = b"inherited_key".to_vec();
let key = TestForks::add_to_scope(item.clone(), None, None).unwrap();
set_block(4);
TestForks::start(None, None, || {});
assert!(TestForks::scope_item_exists(&key, None, None).unwrap());
TestForks::remove_from_scope(&key, None, None).unwrap();
let exists = TestForks::scope_item_exists(&key, None, None).unwrap();
assert!(!exists);
});
}
#[test]
fn scope_handler_remove_from_scope_is_noop_for_absent_key() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2);
TestForks::start(None, None, || {});
set_block(3);
TestForks::start(None, None, || {});
let key = TestForks::gen_scope_item_key(&b"never_added".to_vec());
let result = TestForks::remove_from_scope(&key, None, None);
assert_eq!(result, Ok(()));
});
}
#[test]
fn scope_handler_remove_from_scope_returns_err_when_fork_graph_not_initialized() {
new_ocw_ext().execute_with(|| {
register_block_hashes(0, 4);
set_block(2);
let key = TestForks::gen_scope_item_key(&b"item".to_vec());
let result = TestForks::remove_from_scope(&key, None, None);
assert_eq!(result, Err(DispatchError::Other("forks-not-enabled")));
});
}
}