use crate::Process;
use console::{
account::{Address, PrivateKey},
network::{MainnetV0, prelude::*},
};
use rand::seq::IteratorRandom;
use snarkvm_ledger_committee::{MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE, MIN_VALIDATOR_STAKE};
#[cfg(feature = "rocks")]
use snarkvm_ledger_store::{FinalizeMode, FinalizeStorage, FinalizeStore, atomic_finalize};
#[cfg(not(feature = "rocks"))]
use snarkvm_ledger_store::{
FinalizeMode,
FinalizeStorage,
FinalizeStore,
atomic_finalize,
helpers::memory::FinalizeMemory,
};
use indexmap::{IndexMap, IndexSet};
use super::test_utils::*;
fn random_below_with_blowup(value: u64, rng: &mut impl Rng) -> u64 {
const BLOWUP: u64 = 20;
let upper_bound = value + value / BLOWUP;
match rng.random_range(0..100) {
0..=79 => rng.random_range(0..=upper_bound),
_ => value,
}
}
fn random_with_default<'a, T>(
values: impl Iterator<Item = &'a T>,
default: Option<&'a T>,
rng: &mut impl Rng,
) -> &'a T {
match rng.random_range(0..100) {
0..=79 => default.unwrap_or(values.choose(rng).unwrap()),
_ => values.choose(rng).unwrap(),
}
}
#[derive(Clone)]
enum Operation {
BondValidator {
private_key: PrivateKey<MainnetV0>,
withdrawal_address: Address<MainnetV0>,
amount: u64,
commission: u8,
},
BondPublic {
private_key: PrivateKey<MainnetV0>,
validator_address: Address<MainnetV0>,
withdrawal_address: Address<MainnetV0>,
amount: u64,
},
UnbondPublic { withdrawal_private_key: PrivateKey<MainnetV0>, staker_address: Address<MainnetV0>, amount: u64 },
}
impl Debug for Operation {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Operation::BondValidator { private_key, amount, .. } => {
let validator_address = Address::<MainnetV0>::try_from(private_key).unwrap().to_string();
write!(f, "BondValidator {{ {}, {amount} }}", &validator_address[..12])
}
Operation::BondPublic { private_key, validator_address, amount, .. } => {
let delegator_address = Address::<MainnetV0>::try_from(private_key).unwrap().to_string();
let validator_address = validator_address.to_string();
write!(f, "BondPublic {{ {}, {}, {amount} }}", &delegator_address[..12], &validator_address[..12])
}
Operation::UnbondPublic { staker_address, amount, .. } => {
let staker_address = staker_address.to_string();
write!(f, "UnbondPublic {{ {}, {amount} }}", &staker_address[..12])
}
}
}
}
impl Operation {
pub fn random(state: &State, rng: &mut impl Rng) -> Self {
match rng.random_range(0..3) {
0 => Operation::random_bond_validator(state, rng),
1 => Operation::random_bond_public(state, rng),
_ => Operation::random_unbond_public(state, rng),
}
}
fn random_bond_validator(state: &State, rng: &mut impl Rng) -> Self {
let validator = state.stakers().choose(rng).unwrap();
let account_balance = state.account_balance(validator.address());
Operation::BondValidator {
private_key: *validator.private_key(),
withdrawal_address: *random_with_default(state.stakers(), Some(validator), rng).withdrawal_address(),
amount: random_below_with_blowup(account_balance, rng),
commission: random_below_with_blowup(100, rng) as u8,
}
}
fn random_bond_public(state: &State, rng: &mut impl Rng) -> Self {
let delegator = state.stakers().choose(rng).unwrap();
let validator_addresses = state.stakers().map(|staker| staker.address());
let default_address = state.bonded_to(delegator.address());
let account_balance = state.account_balance(delegator.address());
Operation::BondPublic {
private_key: *delegator.private_key(),
validator_address: *random_with_default(validator_addresses, default_address.as_ref(), rng),
withdrawal_address: *random_with_default(state.stakers(), Some(delegator), rng).withdrawal_address(),
amount: random_below_with_blowup(account_balance, rng),
}
}
fn random_unbond_public(state: &State, rng: &mut impl Rng) -> Self {
let staker = state.stakers().choose(rng).unwrap();
let withdrawal_private_key = match state.bonded_to(staker.address()) {
Some(validator_address) => {
let validator = state.staker_with_address(&validator_address).unwrap();
[*staker.withdrawal_private_key(), *validator.withdrawal_private_key()].into_iter().choose(rng).unwrap()
}
None => *state.stakers().choose(rng).unwrap().withdrawal_private_key(),
};
let account_balance = state.account_balance(staker.address());
Operation::UnbondPublic {
withdrawal_private_key,
staker_address: *staker.address(),
amount: random_below_with_blowup(account_balance, rng),
}
}
}
#[derive(Copy, Clone, Debug)]
struct Staker(PrivateKey<MainnetV0>, Address<MainnetV0>, PrivateKey<MainnetV0>, Address<MainnetV0>, u64);
impl Staker {
fn private_key(&self) -> &PrivateKey<MainnetV0> {
&self.0
}
fn address(&self) -> &Address<MainnetV0> {
&self.1
}
fn withdrawal_private_key(&self) -> &PrivateKey<MainnetV0> {
&self.2
}
fn withdrawal_address(&self) -> &Address<MainnetV0> {
&self.3
}
fn initial_balance(&self) -> u64 {
self.4
}
}
#[derive(Default)]
struct State {
stakers: IndexMap<Address<MainnetV0>, Staker>,
commissions: IndexMap<Address<MainnetV0>, u8>,
bonded_to: IndexMap<Address<MainnetV0>, Address<MainnetV0>>,
bonded_amounts: IndexMap<Address<MainnetV0>, u64>,
account_balances: IndexMap<Address<MainnetV0>, u64>,
unbonding_amounts: IndexMap<Address<MainnetV0>, u64>,
has_delegated_state: IndexSet<Address<MainnetV0>>,
withdrawal_addresses: IndexMap<Address<MainnetV0>, Address<MainnetV0>>,
}
impl State {
pub fn new<F: FinalizeStorage<MainnetV0>>(
num_stakers: u32,
store: &FinalizeStore<MainnetV0, F>,
rng: &mut TestRng,
) -> Self {
let (validators, _) = initialize_stakers(store, num_stakers, 0, rng).unwrap();
let stakers: IndexMap<Address<MainnetV0>, Staker> = validators
.into_iter()
.map(|(private_key, (address, initial_balance, withdrawal_private_key, withdrawal_address))| {
(address, Staker(private_key, address, withdrawal_private_key, withdrawal_address, initial_balance))
})
.collect();
let account_balances = stakers.iter().map(|(address, staker)| (*address, staker.initial_balance())).collect();
Self { stakers, account_balances, ..Default::default() }
}
fn stakers(&self) -> impl Iterator<Item = &Staker> {
self.stakers.iter().map(|(_, staker)| staker)
}
fn in_committee(&self, address: &Address<MainnetV0>) -> bool {
self.delegated_amount(address) > MIN_VALIDATOR_STAKE
&& self.bonded_amount(address) > MIN_VALIDATOR_SELF_STAKE
&& self.bonded_to(address) == Some(*address)
}
fn is_bonded(&self, address: &Address<MainnetV0>) -> bool {
self.bonded_to.contains_key(address)
}
fn is_unbonding(&self, address: &Address<MainnetV0>) -> bool {
self.unbonding_amounts.contains_key(address)
}
fn bonded_to(&self, address: &Address<MainnetV0>) -> Option<Address<MainnetV0>> {
self.bonded_to.get(address).copied()
}
fn bonded_amount(&self, address: &Address<MainnetV0>) -> u64 {
self.bonded_amounts.get(address).copied().unwrap_or_default()
}
fn bond_state(&self, address: &Address<MainnetV0>) -> Option<(Address<MainnetV0>, u64)> {
self.bonded_to(address).zip(Some(self.bonded_amount(address)))
}
fn account_balance(&self, address: &Address<MainnetV0>) -> u64 {
self.account_balances.get(address).copied().unwrap_or_default()
}
fn unbond_state(&self, address: &Address<MainnetV0>) -> Option<(u64, u32)> {
self.unbonding_amounts.get(address).copied().zip(Some(10360))
}
fn delegated_amount(&self, validator_address: &Address<MainnetV0>) -> u64 {
self.stakers()
.filter_map(|staker| {
if self.bonded_to(staker.address()) == Some(*validator_address) {
Some(self.bonded_amount(staker.address()))
} else {
None
}
})
.sum()
}
fn delegated_state(&self, validator_address: &Address<MainnetV0>) -> Option<u64> {
if self.has_delegated_state.contains(validator_address) {
Some(self.delegated_amount(validator_address))
} else {
None
}
}
fn staker_with_address(&self, address: &Address<MainnetV0>) -> Option<Staker> {
self.stakers.get(address).copied()
}
fn staker_with_private_key(&self, private_key: &PrivateKey<MainnetV0>) -> Option<Staker> {
Address::try_from(private_key).ok().and_then(|address| self.stakers.get(&address)).copied()
}
fn withdrawal_address(&self, address: &Address<MainnetV0>) -> Option<Address<MainnetV0>> {
self.withdrawal_addresses.get(address).copied()
}
fn commision(&self, address: &Address<MainnetV0>) -> Option<u8> {
self.commissions.get(address).copied()
}
fn check_bond_validator(
&self,
private_key: &PrivateKey<MainnetV0>,
withdrawal_address: &Address<MainnetV0>,
amount: u64,
commission: u8,
) -> Result<()> {
let validator = self.staker_with_private_key(private_key).unwrap();
ensure!(amount >= 1_000_000u64);
ensure!(commission <= 100u8);
ensure!(amount <= self.account_balance(validator.address()));
if self.is_bonded(validator.address()) {
ensure!(self.bonded_to(validator.address()) == Some(*validator.address()));
ensure!(self.withdrawal_address(validator.address()) == Some(*withdrawal_address));
ensure!(self.commision(validator.address()) == Some(commission));
} else {
ensure!(amount >= MIN_VALIDATOR_SELF_STAKE);
ensure!(self.delegated_amount(validator.address()) + amount >= MIN_VALIDATOR_STAKE);
ensure!(!self.is_unbonding(validator.address()));
ensure!(
self.withdrawal_address(validator.address()).is_none()
|| self.withdrawal_address(validator.address()) == Some(*withdrawal_address)
);
}
Ok(())
}
fn check_bond_public(
&self,
private_key: &PrivateKey<MainnetV0>,
validator_address: &Address<MainnetV0>,
withdrawal_address: &Address<MainnetV0>,
amount: u64,
) -> Result<()> {
let delegator = self.staker_with_private_key(private_key).unwrap();
ensure!(amount >= 1_000_000u64);
ensure!(amount <= self.account_balance(delegator.address()));
ensure!(delegator.address() != validator_address);
ensure!(!self.is_unbonding(validator_address));
if self.is_bonded(delegator.address()) {
ensure!(self.bonded_to(delegator.address()) == Some(*validator_address));
ensure!(self.withdrawal_address(delegator.address()) == Some(*withdrawal_address));
} else {
ensure!(amount >= MIN_DELEGATOR_STAKE);
ensure!(
self.withdrawal_address(delegator.address()).is_none()
|| self.withdrawal_address(delegator.address()) == Some(*withdrawal_address)
);
}
Ok(())
}
fn check_unbond_public(
&self,
withdrawal_private_key: &PrivateKey<MainnetV0>,
staker_address: &Address<MainnetV0>,
amount: u64,
) -> Result<()> {
let staker = self.staker_with_address(staker_address).unwrap();
let validator_address = self.bonded_to(staker_address).ok_or(anyhow!("staker is not bonded"))?;
let validator = self.staker_with_address(&validator_address).unwrap();
let withdrawal_address = Address::try_from(withdrawal_private_key).unwrap();
ensure!(
self.withdrawal_address(staker.address()) == Some(withdrawal_address)
|| self.withdrawal_address(validator.address()) == Some(withdrawal_address)
);
ensure!(amount <= self.bonded_amount(staker_address));
Ok(())
}
fn bond_validator(
&mut self,
private_key: &PrivateKey<MainnetV0>,
withdrawal_address: &Address<MainnetV0>,
amount: u64,
commission: u8,
) {
let validator = self.staker_with_private_key(private_key).unwrap();
self.commissions.insert(*validator.address(), commission);
self.withdrawal_addresses.insert(*validator.address(), *withdrawal_address);
self.bonded_to.insert(*validator.address(), *validator.address());
*self.bonded_amounts.entry(*validator.address()).or_default() += amount;
self.account_balances[validator.address()] -= amount;
self.has_delegated_state.insert(*validator.address());
}
fn bond_public(
&mut self,
private_key: &PrivateKey<MainnetV0>,
validator_address: &Address<MainnetV0>,
withdrawal_address: &Address<MainnetV0>,
amount: u64,
) {
let delegator = self.staker_with_private_key(private_key).unwrap();
self.bonded_to.insert(*delegator.address(), *validator_address);
*self.bonded_amounts.entry(*delegator.address()).or_default() += amount;
self.account_balances[delegator.address()] -= amount;
self.withdrawal_addresses.insert(*delegator.address(), *withdrawal_address);
self.has_delegated_state.insert(*validator_address);
}
fn unbond_public(
&mut self,
withdrawal_private_key: &PrivateKey<MainnetV0>,
staker_address: &Address<MainnetV0>,
amount: u64,
_height: u64,
) {
let staker = self.staker_with_address(staker_address).unwrap();
let validator_address = self.bonded_to(staker_address).unwrap();
let validator = self.staker_with_address(&validator_address).unwrap();
let withdrawal_address = Address::try_from(withdrawal_private_key).unwrap();
let validator_in_committee = self.in_committee(&validator_address);
self.bonded_amounts[staker.address()] -= amount;
*self.unbonding_amounts.entry(*staker.address()).or_default() += amount;
if *staker_address != validator_address {
if self.bonded_amount(staker.address()) < MIN_DELEGATOR_STAKE
|| self.withdrawal_address(&validator_address) == Some(withdrawal_address)
{
*self.unbonding_amounts.entry(*staker.address()).or_default() +=
self.bonded_amounts.swap_remove(staker.address()).unwrap();
self.bonded_to.swap_remove(staker.address());
}
if validator_in_committee && self.delegated_amount(validator.address()) < MIN_VALIDATOR_STAKE {
*self.unbonding_amounts.entry(*validator.address()).or_default() +=
self.bonded_amounts.swap_remove(validator.address()).unwrap();
self.bonded_to.swap_remove(validator.address());
self.commissions.swap_remove(validator.address());
}
} else {
if self.bonded_amount(staker.address()) < MIN_VALIDATOR_SELF_STAKE
|| self.delegated_amount(staker.address()) < MIN_VALIDATOR_STAKE
{
*self.unbonding_amounts.entry(*staker.address()).or_default() +=
self.bonded_amounts.swap_remove(staker.address()).unwrap();
self.bonded_to.swap_remove(staker.address());
self.commissions.swap_remove(staker.address());
}
}
}
pub fn execute_operation(&mut self, op: &Operation) -> Result<()> {
match op {
Operation::BondValidator { private_key, withdrawal_address, amount, commission } => {
self.check_bond_validator(private_key, withdrawal_address, *amount, *commission)?;
self.bond_validator(private_key, withdrawal_address, *amount, *commission);
}
Operation::BondPublic { private_key, validator_address, withdrawal_address, amount } => {
self.check_bond_public(private_key, validator_address, withdrawal_address, *amount)?;
self.bond_public(private_key, validator_address, withdrawal_address, *amount);
}
Operation::UnbondPublic { withdrawal_private_key, staker_address, amount } => {
self.check_unbond_public(withdrawal_private_key, staker_address, *amount)?;
self.unbond_public(withdrawal_private_key, staker_address, *amount, 10_000);
}
}
Ok(())
}
}
fn execute_operation<F: FinalizeStorage<MainnetV0>>(
process: &Process<MainnetV0>,
store: &FinalizeStore<MainnetV0, F>,
op: &Operation,
rng: &mut TestRng,
) -> Result<()> {
match op {
Operation::BondValidator { private_key, withdrawal_address, amount, commission } => {
bond_validator(process, store, private_key, withdrawal_address, *amount, *commission, rng)?;
}
Operation::BondPublic { private_key, validator_address, withdrawal_address, amount } => {
bond_public(process, store, private_key, validator_address, withdrawal_address, *amount, rng)?;
}
Operation::UnbondPublic { withdrawal_private_key, staker_address, amount } => {
unbond_public(process, store, withdrawal_private_key, staker_address, *amount, 10_000, rng)?;
}
}
Ok(())
}
fn validate_state<F: FinalizeStorage<MainnetV0>>(state: &State, store: &FinalizeStore<MainnetV0, F>) -> Result<()> {
for staker in state.stakers() {
ensure!(account_balance(store, staker.address()).unwrap() == state.account_balance(staker.address()));
ensure!(bond_state(store, staker.address()).unwrap() == state.bond_state(staker.address()));
ensure!(unbond_state(store, staker.address()).unwrap() == state.unbond_state(staker.address()));
ensure!(delegated_state(store, staker.address()).unwrap() == state.delegated_state(staker.address()));
}
Ok(())
}
fn print_operations(ops: &[Operation]) {
eprintln!("[💸]: Operations:");
for (i, op) in ops.iter().enumerate() {
eprintln!(" [{i}]: {op:?}");
}
}
#[ignore] #[test]
fn test_random_operations() {
loop {
let mut rng = TestRng::default();
let process = Process::<MainnetV0>::load().unwrap();
let (store, _) = sample_finalize_store!();
let mut state = State::new(8, &store, &mut rng);
let mut executed_ops = Vec::new();
test_atomic_finalize!(store, FinalizeMode::RealRun, {
while executed_ops.len() < 64 {
let op = Operation::random(&state, &mut rng);
let expected_result = state.execute_operation(&op);
let actual_result = execute_operation(&process, &store, &op, &mut rng);
if actual_result.is_ok() != expected_result.is_ok() {
eprintln!("[💥]: State divergence detected.");
eprintln!(" Expected result: {expected_result:?}");
eprintln!(" Actual result: {actual_result:?}");
executed_ops.push(op);
print_operations(&executed_ops);
panic!();
}
if let Err(error) = validate_state(&state, &store) {
eprintln!("[💥]: State divergence detected.");
eprintln!(" Error: {error:?}");
executed_ops.push(op);
print_operations(&executed_ops);
panic!();
}
if actual_result.is_ok() {
executed_ops.push(op);
}
}
Ok(())
})
.unwrap();
}
}