use super::{gas_provider::auxiliary::*, *};
use crate::storage::MapStorage;
use core::iter::FromIterator;
use enum_iterator::all;
use frame_support::{assert_err, assert_ok};
use gear_utils::{NonEmpty, RingGet};
use primitive_types::H256;
use proptest::prelude::*;
use std::{
cell::{Ref, RefCell, RefMut},
collections::{BTreeSet, HashMap},
thread::LocalKey,
};
use strategies::GasTreeAction;
mod assertions;
mod strategies;
mod utils;
type PropertyTestGasProvider = AuxiliaryGasProvider<
TotalIssuanceStorageImpl,
TotalIssuanceProviderImpl,
GasNodesStorageImpl,
GasNodesProviderImpl,
>;
type Gas = <PropertyTestGasProvider as Provider>::GasTree;
std::thread_local! {
pub(crate) static TOTAL_ISSUANCE: RefCell<Option<Balance>> = Default::default();
pub(crate) static GAS_NODES: RefCell<BTreeMap<NodeId, Node>> = Default::default();
}
pub struct TotalIssuanceStorageImpl;
pub type TotalIssuanceProviderImpl = RefCell<Option<Balance>>;
impl TotalIssuanceStorage<TotalIssuanceProviderImpl> for TotalIssuanceStorageImpl {
fn storage() -> &'static LocalKey<TotalIssuanceProviderImpl> {
&TOTAL_ISSUANCE
}
}
impl TotalIssuanceProvider for TotalIssuanceProviderImpl {
fn data(&self) -> Ref<'_, Option<Balance>> {
self.borrow()
}
fn data_mut(&self) -> RefMut<'_, Option<Balance>> {
self.borrow_mut()
}
}
pub struct GasNodesStorageImpl;
pub type GasNodesProviderImpl = RefCell<BTreeMap<NodeId, Node>>;
impl GasNodesStorage<GasNodesProviderImpl> for GasNodesStorageImpl {
fn storage() -> &'static LocalKey<GasNodesProviderImpl> {
&GAS_NODES
}
}
impl GasNodesProvider for GasNodesProviderImpl {
fn data(&self) -> Ref<'_, BTreeMap<NodeId, Node>> {
self.borrow()
}
fn data_mut(&self) -> RefMut<'_, BTreeMap<NodeId, Node>> {
self.borrow_mut()
}
}
impl<U> From<H256> for GasNodeId<PlainNodeId, U> {
fn from(raw_id: H256) -> Self {
Self::Node(PlainNodeId(raw_id))
}
}
impl ReservationNodeId {
fn random() -> Self {
Self(H256::random())
}
}
fn gas_tree_node_clone() -> BTreeMap<NodeId, Node> {
GAS_NODES.with(|tree| {
tree.data()
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<BTreeMap<_, _>>()
})
}
#[derive(Debug, Default)]
struct TestTree {
expected_balance: u64,
spent: u64,
caught: u64,
system_reserve: u64,
locked: u64,
}
impl TestTree {
fn new(balance: u64) -> Self {
Self {
expected_balance: balance,
..Default::default()
}
}
fn total_expenses(&self) -> u64 {
let balance = self.spent + self.caught + self.system_reserve + self.locked;
assert!(
balance <= self.expected_balance,
"tree has too many expenses"
);
balance
}
}
#[derive(Debug)]
struct TestForest {
trees: HashMap<NodeId, TestTree>,
}
impl TestForest {
fn create(root: H256, balance: u64) -> Self {
Self {
trees: [(root.into(), TestTree::new(balance))].into(),
}
}
fn register_tree(&mut self, root: impl Into<NodeId>, balance: u64) {
let root = root.into();
self.trees
.entry(root)
.and_modify(|_| unreachable!("duplicated tree: {:?}", root))
.or_insert_with(|| TestTree::new(balance));
}
#[track_caller]
fn tree_by_origin_mut(&mut self, origin: impl Into<NodeId>) -> &mut TestTree {
self.trees
.get_mut(&origin.into())
.expect("tree root not found")
}
#[track_caller]
fn tree_mut(&mut self, node: impl Into<NodeId>) -> &mut TestTree {
let origin = Gas::get_origin_key(node).expect("child node not found");
self.tree_by_origin_mut(origin)
}
fn total_expenses(&self) -> u64 {
self.trees.values().map(|tree| tree.total_expenses()).sum()
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(600))]
#[test]
fn test_tree_properties((max_balance, actions) in strategies::gas_tree_props_test_strategy())
{
TotalIssuanceWrap::<TotalIssuanceStorageImpl, TotalIssuanceProviderImpl>::kill();
<GasNodesWrap<GasNodesStorageImpl, GasNodesProviderImpl> as storage::MapStorage>::clear();
let external = ExternalOrigin(H256::random());
let mut node_ids = Vec::with_capacity(actions.len() + 1);
let root_node = H256::random();
let mut forest = TestForest::create(root_node, max_balance);
node_ids.push(root_node.into());
let lock_ids = all::<LockId>().collect::<Vec<_>>();
Gas::create(external, GasMultiplier::ValuePerGas(100), root_node, max_balance).expect("Failed to create gas tree");
assert_eq!(Gas::total_supply(), max_balance);
let mut marked_consumed = BTreeSet::new();
let mut unspec_ref_nodes = BTreeSet::new();
let mut spec_ref_nodes = BTreeSet::new();
let mut reserved_nodes = BTreeSet::new();
let mut locked_nodes = BTreeSet::new();
let mut system_reserve_nodes = BTreeSet::new();
for action in actions {
match action {
GasTreeAction::SplitWithValue(parent_idx, amount) => {
let &parent = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(parent_idx);
let child = H256::random();
if let Err(e) = Gas::split_with_value(parent, child, amount) {
assertions::assert_not_invariant_error(e);
} else {
spec_ref_nodes.insert(child);
node_ids.push(child.into());
}
}
GasTreeAction::Split(parent_idx) => {
let &parent = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(parent_idx);
let child = H256::random();
if let Err(e) = Gas::split(parent, child) {
assertions::assert_not_invariant_error(e);
} else {
unspec_ref_nodes.insert(child);
node_ids.push(child.into());
}
}
GasTreeAction::Spend(from, amount) => {
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
if let GasNodeId::Node(from) = from {
let res = Gas::spend(from, amount);
if let Err(e) = &res {
assertions::assert_not_invariant_error(*e);
assert_err!(res, GasTreeError::InsufficientBalance);
} else {
assert_ok!(res);
forest.tree_mut(from).spent += amount;
}
}
}
GasTreeAction::Consume(id) => {
let &consuming = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(id);
let origin = Gas::get_origin_key(consuming).expect("node exists");
match utils::consume_node(consuming) {
Ok((maybe_caught, remaining_nodes, removed_nodes)) => {
marked_consumed.insert(consuming);
node_ids.retain(|id| !removed_nodes.contains_key(id));
{
let mut expected_remaining_ids = remaining_nodes.keys().copied().collect::<Vec<_>>();
expected_remaining_ids.sort();
let mut actual_remaining_ids = node_ids.clone();
actual_remaining_ids.sort();
assert_eq!(
expected_remaining_ids,
actual_remaining_ids
);
}
assertions::assert_removed_nodes_props(
consuming,
removed_nodes,
&remaining_nodes,
&marked_consumed,
);
if origin == consuming {
assertions::assert_root_children_removed(origin, &remaining_nodes);
}
forest.tree_by_origin_mut(origin).caught += maybe_caught.unwrap_or_default();
}
Err(e) => {
match e {
GasTreeError::NodeWasConsumed => {
assert!(marked_consumed.contains(&consuming));
assertions::assert_not_invariant_error(e);
}
GasTreeError::ConsumedWithLock => {
assert!(locked_nodes.contains(&consuming));
assertions::assert_not_invariant_error(e);
}
GasTreeError::ConsumedWithSystemReservation if matches!(consuming, GasNodeId::Node(_)) => {
assert!(system_reserve_nodes.contains(&consuming.to_node_id().unwrap()));
assertions::assert_not_invariant_error(e);
}
_ => panic!("consumed with unknown error: {e:?}")
}
}
}
}
GasTreeAction::Cut(from, amount) => {
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
let child = H256::random();
if let Err(e) = Gas::cut(from, child, amount) {
assertions::assert_not_invariant_error(e)
} else {
node_ids.push(child.into());
forest.register_tree(child, amount);
}
}
GasTreeAction::Reserve(from, amount) => {
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
let child = ReservationNodeId::random();
if let GasNodeId::Node(from) = from {
if let Err(e) = Gas::reserve(from, child, amount) {
assertions::assert_not_invariant_error(e)
} else {
node_ids.push(child.into());
reserved_nodes.insert(child);
forest.register_tree(child, amount);
}
}
}
GasTreeAction::Lock(from, amount) => {
let &lock_id = NonEmpty::from_slice(&lock_ids).expect("non-empty vector; qed").ring_get(from);
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
if let Err(e) = Gas::lock(from, lock_id, amount) {
assertions::assert_not_invariant_error(e)
} else {
forest.tree_mut(from).locked += amount;
locked_nodes.insert(from);
}
}
GasTreeAction::Unlock(from, amount) => {
let &lock_id = NonEmpty::from_slice(&lock_ids).expect("non-empty vector; qed").ring_get(from);
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
if let Err(e) = Gas::unlock(from, lock_id, amount) {
assertions::assert_not_invariant_error(e)
} else {
forest.tree_mut(from).locked -= amount;
locked_nodes.insert(from);
}
}
GasTreeAction::SystemReserve(from, amount) => {
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
if let GasNodeId::Node(from) = from {
if let Err(e) = Gas::system_reserve(from, amount) {
assertions::assert_not_invariant_error(e)
} else {
forest.tree_mut(from).system_reserve += amount;
system_reserve_nodes.insert(from);
}
}
}
GasTreeAction::SystemUnreserve(from) => {
let &from = NonEmpty::from_slice(&node_ids).expect("always has a tree root").ring_get(from);
if let GasNodeId::Node(from) = from {
match Gas::system_unreserve(from) {
Ok(amount) => {
forest.tree_mut(from).system_reserve -= amount;
system_reserve_nodes.remove(&from);
},
Err(e) => {
assertions::assert_not_invariant_error(e);
}
}
}
}
}
if node_ids.is_empty() {
break;
}
}
let gas_tree_ids = BTreeSet::from_iter(gas_tree_node_clone().keys().copied());
assert_eq!(gas_tree_ids, BTreeSet::from_iter(node_ids));
let mut rest_value = 0;
for (node_id, node) in gas_tree_node_clone() {
assert_ok!(Gas::get_external(node_id), external);
if let Some(value) = node.value() {
rest_value += value;
}
if let GasNode::SpecifiedLocal { parent, .. } | GasNode::UnspecifiedLocal { parent, .. } = node {
assert!(gas_tree_ids.contains(&parent));
let parent_node = GasNodesWrap::<GasNodesStorageImpl, GasNodesProviderImpl>::get(&parent).expect("checked");
assert!(parent_node.value().is_some());
}
if node.is_specified_local() {
assert!(spec_ref_nodes.contains(&node_id.to_node_id().unwrap().into_origin()));
} else if node.is_unspecified_local() {
assert!(unspec_ref_nodes.contains(&node_id.to_node_id().unwrap().into_origin()));
}
if node.system_reserve().map(|x| x != 0).unwrap_or(false) {
assert!(!node.is_consumed());
assert!(system_reserve_nodes.contains(&node_id.to_node_id().unwrap()));
assert!(node.is_external() || node.is_specified_local() || node.is_unspecified_local());
}
if !node.lock().is_zero() {
assert!(!node.is_consumed());
assert!(locked_nodes.contains(&node_id));
}
if node.is_reserved() {
let node_id = node_id.to_reservation_id().unwrap();
assert!(reserved_nodes.contains(&node_id));
}
if node.is_consumed() {
assert!(node.lock().is_zero());
assert!(matches!(node.system_reserve(), Some(0) | None));
assert!(node.refs() != 0);
assert!(marked_consumed.contains(&node_id));
assert!(node.is_external() || node.is_specified_local() || node.is_reserved());
if node.unspec_refs() == 0 {
let value = node.value().expect("node with value, checked");
assert!(value == 0);
}
} else {
assert!(!marked_consumed.contains(&node_id));
}
if let Some(value) = node.value() && (value != 0 && !node.is_cut()) {
assert!(node.is_patron());
}
let (ancestor_with_value, ancestor_id) = Gas::node_with_value(node.clone()).expect("tree is invalidated");
if ancestor_with_value != node {
assert_eq!(node.parent(), ancestor_id);
}
assert!(ancestor_with_value.value().is_some());
}
if !gas_tree_ids.is_empty() {
assert_eq!(max_balance, rest_value + forest.total_expenses());
}
}
#[test]
fn test_empty_tree(actions in strategies::gas_tree_action_strategy(100)) {
TotalIssuanceWrap::<TotalIssuanceStorageImpl, TotalIssuanceProviderImpl>::kill();
GasNodesWrap::<GasNodesStorageImpl, GasNodesProviderImpl>::clear();
let mut nodes = Vec::with_capacity(actions.len());
for node in &mut nodes {
*node = H256::random();
}
for action in actions {
match action {
GasTreeAction::SplitWithValue(parent_idx, amount) => {
if let Some(non_empty_nodes) = NonEmpty::from_slice(&nodes) {
let &parent = non_empty_nodes.ring_get(parent_idx);
let child = H256::random();
Gas::split_with_value(parent, child, amount).expect("Failed to split with value");
}
}
GasTreeAction::Split(parent_idx) => {
if let Some(non_empty_nodes) = NonEmpty::from_slice(&nodes) {
let &parent = non_empty_nodes.ring_get(parent_idx);
let child = H256::random();
Gas::split(parent, child).expect("Failed to split without value");
}
}
GasTreeAction::Reserve(parent_idx, amount) => {
if let Some(non_empty_nodes) = NonEmpty::from_slice(&nodes) {
let &parent = non_empty_nodes.ring_get(parent_idx);
let child = ReservationNodeId::random();
Gas::reserve(parent, child, amount).expect("Failed to create reservation");
}
}
_ => {}
}
}
assert!(GAS_NODES.with(|tree| tree.data().iter().count()) == 0);
}
}