From cb3e324f7ab4350d046a4a32ee146241e2587670 Mon Sep 17 00:00:00 2001 From: brentstone Date: Fri, 10 Feb 2023 13:35:18 -0500 Subject: [PATCH] WIP --- proof_of_stake/src/lib.rs | 303 ++++++++++++++++++++++++---------- proof_of_stake/src/storage.rs | 8 + proof_of_stake/src/types.rs | 31 +++- 3 files changed, 255 insertions(+), 87 deletions(-) diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 283d5c521b5..b9fb529861d 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -49,13 +49,15 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; use storage::{ bonds_for_source_prefix, bonds_prefix, current_block_proposer_key, - get_validator_address_from_bond, into_tm_voting_power, is_bond_key, - is_unbond_key, is_validator_slashes_key, last_block_proposer_key, - mult_amount, mult_change_to_amount, num_consensus_validators_key, - params_key, slashes_prefix, unbonds_for_source_prefix, unbonds_prefix, - validator_address_raw_hash_key, validator_max_commission_rate_change_key, - BondDetails, BondsAndUnbondsDetail, BondsAndUnbondsDetails, - ReverseOrdTokenAmount, RewardsAccumulator, UnbondDetails, + decimal_mult_amount, get_validator_address_from_bond, into_tm_voting_power, + is_bond_key, is_unbond_key, is_validator_slashes_key, + last_block_proposer_key, mult_change_to_amount, + num_consensus_validators_key, params_key, slashes_prefix, + unbonds_for_source_prefix, unbonds_prefix, validator_address_raw_hash_key, + validator_max_commission_rate_change_key, BondDetails, + BondsAndUnbondsDetail, BondsAndUnbondsDetails, ReverseOrdTokenAmount, + RewardsAccumulator, SlashedAmount, UnbondDetails, UnbondRecord, + ValidatorTotalUnbonded, }; use thiserror::Error; use types::{ @@ -133,6 +135,8 @@ pub enum UnbondError { ValidatorHasNoVotingPower(Address), #[error("Voting power overflow: {0}")] VotingPowerOverflow(TryFromIntError), + #[error("Trying to unbond from a jailed validator: {0}")] + ValidatorIsJailed(Address), } #[allow(missing_docs)] @@ -286,6 +290,12 @@ pub fn unbond_handle(source: &Address, validator: &Address) -> Unbonds { Unbonds::open(key) } +/// Get the storage handle to a validator's total-unbonded map +pub fn total_unbonded_handle(validator: &Address) -> ValidatorTotalUnbonded { + let key = storage::validator_total_unbonded_key(validator); + ValidatorTotalUnbonded::open(key) +} + /// Get the storage handle to a PoS validator's deltas pub fn validator_set_positions_handle() -> ValidatorSetPositions { let key = storage::validator_set_positions_key(); @@ -1385,7 +1395,8 @@ where Ok(()) } -/// NEW: Unbond. +/// Unbond tokens that are bonded between a validator and a source (self or +/// delegator) pub fn unbond_tokens( storage: &mut S, source: Option<&Address>, @@ -1418,9 +1429,17 @@ where if !is_validator(storage, validator, ¶ms, pipeline_epoch)? { return Err(BondError::NotAValidator(validator.clone()).into()); } + // TODO: current or pipeline epoch? + if validator_state_handle(validator).get( + storage, + pipeline_epoch, + ¶ms, + )? == Some(ValidatorState::Jailed) + { + return Err(UnbondError::ValidatorIsJailed(validator.clone()).into()); + } - // Should be able to unbond inactive validators, but we'll need to prevent - // jailed unbonding with slashing + // Should be able to unbond inactive validators // Check that validator is not inactive at anywhere between the current // epoch and pipeline offset @@ -1450,17 +1469,16 @@ where .into()); } - // Iterate thru this, find non-zero delta entries starting from most recent, - // then just start decrementing those values For every delta val that - // gets decremented down to 0, need a unique unbond object to have a clear - // start epoch - - // TODO: do we want to apply slashing here? (It is done here previously) + // Iterate thru bonds, find non-zero delta entries starting from most + // recent, then start decrementing those values. For every val that gets + // decremented down to 0, need a unique unbond object - let unbond_handle = unbond_handle(source, validator); + let unbonds = unbond_handle(source, validator); let withdrawable_epoch = current_epoch + params.pipeline_len + params.unbonding_len; - let mut to_decrement = token::Amount::from_change(amount); + let mut remaining = token::Amount::from_change(amount); + // let mut amount_after_slashing = amount; + let mut slashed_amount = token::Change::default(); // We read all matched bonds into memory to do reverse iteration #[allow(clippy::needless_collect)] @@ -1484,7 +1502,7 @@ where let mut new_bond_values_map = HashMap::::new(); - while to_decrement > token::Amount::default() { + while remaining > token::Amount::default() { let bond = bond_iter.next().transpose()?; if bond.is_none() { continue; @@ -1492,18 +1510,49 @@ where let (bond_epoch, bond_amnt) = bond.unwrap(); let bond_amnt = token::Amount::from_change(bond_amnt); - if to_decrement < bond_amnt { + if remaining < bond_amnt { // Decrement the amount in this bond and create the unbond object - // with amount `to_decrement` and starting epoch `bond_epoch` - let new_bond_amnt = bond_amnt - to_decrement; - new_bond_values_map - .insert(bond_epoch, (new_bond_amnt, to_decrement)); - to_decrement = token::Amount::default(); + // with amount `remaining` and starting epoch `bond_epoch` + let new_bond_amnt = bond_amnt - remaining; + new_bond_values_map.insert(bond_epoch, (new_bond_amnt, remaining)); + let to_slash = get_slashed_amount_of_bond( + storage, validator, ¶ms, remaining, bond_epoch, + )?; + // amount_after_slashing -= to_slash; + slashed_amount += to_slash; + + // TODO: need to handle a corner case wherein an unbond is submitted + // with the same amount and epoch as a previous one before. Perhaps + // use the Position type as well? + let record = UnbondRecord { + amount: remaining, + start: bond_epoch, + }; + total_unbonded_handle(validator).insert( + storage, + pipeline_epoch, + record, + )?; + remaining = token::Amount::default(); } else { // Set the bond remaining delta to 0 then continue decrementing new_bond_values_map .insert(bond_epoch, (token::Amount::default(), bond_amnt)); - to_decrement -= bond_amnt; + let record = UnbondRecord { + amount: bond_amnt, + start: bond_epoch, + }; + total_unbonded_handle(validator).insert( + storage, + pipeline_epoch, + record, + )?; + let to_slash = get_slashed_amount_of_bond( + storage, validator, ¶ms, bond_amnt, bond_epoch, + )?; + // amount_after_slashing -= to_slash; + slashed_amount += to_slash; + remaining -= bond_amnt; } } drop(bond_iter); @@ -1514,7 +1563,7 @@ where { bond_remain_handle.set(storage, new_bond_amnt.into(), bond_epoch, 0)?; update_unbond( - &unbond_handle, + &unbonds, storage, &withdrawable_epoch, &bond_epoch, @@ -1535,21 +1584,65 @@ where println!("Updating validator set for unbonding"); // Update the validator set at the pipeline offset - update_validator_set(storage, ¶ms, validator, -amount, current_epoch)?; + update_validator_set( + storage, + ¶ms, + validator, + -slashed_amount, + current_epoch, + )?; // Update the validator and total deltas at the pipeline offset update_validator_deltas( storage, ¶ms, validator, - -amount, + -slashed_amount, current_epoch, )?; - update_total_deltas(storage, ¶ms, -amount, current_epoch)?; + update_total_deltas(storage, ¶ms, -slashed_amount, current_epoch)?; Ok(()) } +fn get_slashed_amount_of_bond( + storage: &mut S, + validator: &Address, + params: &PosParams, + amount: token::Amount, + bond_epoch: Epoch, +) -> storage_api::Result +where + S: StorageRead, +{ + // TODO: + // 1. consider if cubic slashing window width extends below the bond_epoch + // 2. do we want to optimize this calc? + + let mut total_rate = Decimal::default(); + let slashes = validator_slashes_handle(validator); + for slash in slashes.iter(storage)? { + let Slash { + epoch: infraction_epoch, + block_height: _, + r#type: slash_type, + } = slash?; + if infraction_epoch < bond_epoch { + continue; + } + let rate = get_final_cubic_slash_rate( + storage, + params, + infraction_epoch, + slash_type, + )?; + // TODO: do I want to simply add these as described in + // InformalSystems `unbond` pseudocode? + total_rate += rate; + } + Ok(decimal_mult_i128(total_rate, amount.change())) +} + fn update_unbond( handle: &Unbonds, storage: &mut S, @@ -1629,7 +1722,7 @@ where Ok(()) } -/// NEW: Withdraw. +/// Withdraw tokens from that have been unbonded from proof-of-stake pub fn withdraw_tokens( storage: &mut S, source: Option<&Address>, @@ -1648,12 +1741,12 @@ where // A handle to an empty location is valid - we just won't see any data let unbond_handle = unbond_handle(source, validator); - let mut slashed = token::Amount::default(); + // let mut slashed = token::Amount::default(); let mut withdrawable_amount = token::Amount::default(); let mut unbonds_to_remove: Vec<(Epoch, Epoch)> = Vec::new(); + // TODO: use `find_unbonds` - let unbond_iter = unbond_handle.iter(storage)?; - for unbond in unbond_iter { + for unbond in unbond_handle.iter(storage)? { // println!("\nUNBOND ITER\n"); let ( NestedSubKey::Data { @@ -1663,35 +1756,59 @@ where amount, ) = unbond?; - // dbg!(&end_epoch, &start_epoch, amount); - - // TODO: - // 1. cubic slashing - // 2. adding slash rates in same epoch, applying cumulatively in dif + // TODO: adding slash rates in same epoch, applying cumulatively in dif // epochs if withdraw_epoch > current_epoch { continue; } + let mut updated_amount = amount; + let mut computed_amounts: HashSet = HashSet::new(); for slash in validator_slashes.iter(storage)? { let Slash { epoch, block_height: _, r#type: slash_type, } = slash?; - if epoch > start_epoch && epoch < withdraw_epoch { - let slash_rate = - get_cubic_slash_rate(storage, ¶ms, epoch, slash_type)?; - let to_slash = token::Amount::from(decimal_mult_u64( - slash_rate, - u64::from(amount), - )); - slashed += to_slash; + + if epoch < start_epoch + || epoch >= withdraw_epoch - params.unbonding_len + { + continue; + } + // TODO: review this here to make sure consistent with specs from + // Informal + let mut to_remove = HashSet::::new(); + for computed in &computed_amounts { + if computed.epoch + params.unbonding_len < epoch { + updated_amount -= computed.amount; + to_remove.insert(computed.clone()); + } } + for slashed_amount_obj in to_remove { + computed_amounts.remove(&slashed_amount_obj); + } + + let slash_rate = get_final_cubic_slash_rate( + storage, ¶ms, epoch, slash_type, + )?; + let to_slash = decimal_mult_amount(slash_rate, updated_amount); + + // TODO: still needed?? + // slashed += to_slash; + computed_amounts.insert(SlashedAmount { + amount: to_slash, + epoch, + }); } - withdrawable_amount += amount; + let amount_after_slashing = updated_amount + - computed_amounts + .iter() + .fold(token::Amount::default(), |sum, val| sum + val.amount); + + withdrawable_amount += amount_after_slashing; unbonds_to_remove.push((withdraw_epoch, start_epoch)); } - withdrawable_amount -= slashed; + // withdrawable_amount -= slashed; // Remove the unbond data from storage for (withdraw_epoch, start_epoch) in unbonds_to_remove { @@ -1768,7 +1885,7 @@ where } /// NEW: apply a slash and write it to storage -pub fn slash( +pub fn slash_old( storage: &mut S, params: &PosParams, current_epoch: Epoch, @@ -2471,7 +2588,7 @@ fn make_unbond_details( } return Some( acc.unwrap_or_default() - + mult_amount( + + decimal_mult_amount( slash.r#type.get_slash_rate(params), amount, ), @@ -2651,12 +2768,12 @@ pub fn slashes_handle() -> EpochedSlashes { EpochedSlashes::open(key) } -/// Calculate cubic slashing rate -pub fn get_cubic_slash_rate( +/// Calculate the cubic slashing rate using all slashes within a window around +/// the given infraction epoch +pub fn compute_cubic_slash_rate( storage: &S, params: &PosParams, infraction_epoch: Epoch, - current_slash_type: SlashType, ) -> storage_api::Result where S: StorageRead, @@ -2668,41 +2785,54 @@ where let slashes = slashes_handle().at(&epoch); let infracting_stake = slashes.iter(storage)?.fold(Decimal::ZERO, |sum, res| { - let (key, _slash) = res.unwrap(); - match key { + let ( NestedSubKey::Data { - key, + key: validator, nested_sub_key: _, - } => { - let validator_stake = - read_validator_stake(storage, params, &key, epoch) - .unwrap() - .unwrap_or_default(); - sum + Decimal::from(validator_stake) - // TODO: does something more complex need to be done - // here in the event some of these slashes correspond to - // the same validator? - } - } + }, + _slash, + ) = res.unwrap(); + + let validator_stake = + read_validator_stake(storage, params, &validator, epoch) + .unwrap() + .unwrap_or_default(); + sum + Decimal::from(validator_stake) + // TODO: does something more complex need to be done + // here in the event some of these slashes correspond to + // the same validator? }); let total_stake = Decimal::from(read_total_stake(storage, params, epoch)?); sum_vp_fraction += infracting_stake / total_stake; } + Ok(dec!(9) * sum_vp_fraction * sum_vp_fraction) +} + +/// Get final cubic slashing rate that is bound from below by some minimum value +/// and capped at 100% +pub fn get_final_cubic_slash_rate( + storage: &S, + params: &PosParams, + infraction_epoch: Epoch, + current_slash_type: SlashType, +) -> storage_api::Result +where + S: StorageRead, +{ + let cubic_rate = + compute_cubic_slash_rate(storage, params, infraction_epoch)?; // Need some truncation right now to max the rate at 100% let rate = cmp::min( Decimal::ONE, - cmp::max( - current_slash_type.get_slash_rate(params), - dec!(9) * sum_vp_fraction * sum_vp_fraction, - ), + cmp::max(current_slash_type.get_slash_rate(params), cubic_rate), ); Ok(rate) } /// Apply and record a slash for a misbehavior that has been received from /// Tendermint -pub fn slash_cubic( +pub fn slash( storage: &mut S, params: &PosParams, current_epoch: Epoch, @@ -2715,7 +2845,7 @@ where S: StorageRead + StorageWrite, { // Upon slash detection, write the slash to the validator storage, write it - // to EpochedSlashes at the processing epoch, jail the validator, and + // to EpochedSlashes at the processing epoch, jail the validator, and then // immediately remove it from the validator set // Write the slash data to storage @@ -2822,20 +2952,19 @@ pub fn process_slashes( where S: StorageRead + StorageWrite, { - // TODO: can perhaps simplify this by calculating the cubic slash rate only - // once, since all slashes iterated in this processing should correspond to - // the same infraction epoch - let params = read_pos_params(storage)?; let infraction_epoch = current_epoch - params.unbonding_len; - let slashes = slashes_handle().at(¤t_epoch); + let enqueued_slashes = slashes_handle().at(¤t_epoch); let mut validator_slash_rates: HashMap = HashMap::new(); - for slash in slashes.iter(storage)? { + let cubic_slash_rate = + compute_cubic_slash_rate(storage, ¶ms, infraction_epoch)?; + + for slash in enqueued_slashes.iter(storage)? { let ( NestedSubKey::Data { - key: address, + key: validator, nested_sub_key: _, }, slash, @@ -2848,14 +2977,18 @@ where // TODO: consider if something more elaborate needs to be done here (if // there are multiple slashes from same validator for example) - let slash_rate = - get_cubic_slash_rate(storage, ¶ms, slash.epoch, slash.r#type)?; + let slash_rate = cmp::min( + Decimal::ONE, + cmp::max(slash.r#type.get_slash_rate(¶ms), cubic_slash_rate), + ); + // Accumulate additively the slash rates for each validator let cur_validator_rate = validator_slash_rates - .get(&address) + .get(&validator) .cloned() .unwrap_or_default(); - validator_slash_rates.insert(address, slash_rate + cur_validator_rate); + validator_slash_rates + .insert(validator, slash_rate + cur_validator_rate); } if validator_slash_rates.is_empty() { diff --git a/proof_of_stake/src/storage.rs b/proof_of_stake/src/storage.rs index ebd93ceea3b..8734340a91e 100644 --- a/proof_of_stake/src/storage.rs +++ b/proof_of_stake/src/storage.rs @@ -26,6 +26,7 @@ const SLASHES_PREFIX: &str = "slash"; const ALL_SLASHES_KEY: &str = "all_slashes"; const BOND_STORAGE_KEY: &str = "bond"; const UNBOND_STORAGE_KEY: &str = "unbond"; +const VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY: &str = "total_unbonded"; const VALIDATOR_SETS_STORAGE_PREFIX: &str = "validator_sets"; const CONSENSUS_VALIDATOR_SET_STORAGE_KEY: &str = "consensus"; const NUM_CONSENSUS_VALIDATORS_STORAGE_KEY: &str = "num_consensus"; @@ -458,6 +459,13 @@ pub fn is_unbond_key(key: &Key) -> Option<(BondId, Epoch, Epoch)> { } } +/// Storage key for validator's total-unbonded amount to track for slashing +pub fn validator_total_unbonded_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_TOTAL_UNBONDED_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + /// Storage prefix for validator sets. pub fn validator_sets_prefix() -> Key { Key::from(ADDRESS.to_db_key()) diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index cd8234fc201..1a78cfcc7e2 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -140,9 +140,33 @@ pub type EpochedSlashes = crate::epoched::NestedEpoched< 23, >; -/// Epochs validator's unbonds +/// Epoched validator's unbonds pub type Unbonds = NestedMap>; +/// Total unbonded for validators needed for slashing +/// TODO: (CHECK IF CORRECT BOUNDS) +pub type ValidatorTotalUnbonded = LazyMap; + +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +/// TODO: an unbond record +pub struct UnbondRecord { + /// Dangus + pub amount: token::Amount, + /// Bangus + pub start: Epoch, +} + +#[derive( + Debug, Clone, BorshSerialize, BorshDeserialize, Eq, Hash, PartialEq, +)] +/// TODO: slashed amount for thing +pub struct SlashedAmount { + /// Perlangus + pub amount: token::Amount, + /// Churms + pub epoch: Epoch, +} + #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] /// Commission rate and max commission rate change per epoch for a validator pub struct CommissionPair { @@ -500,7 +524,10 @@ pub fn mult_change_to_amount( /// Multiply a value of type Decimal with one of type Amount and then return the /// truncated Amount -pub fn mult_amount(dec: Decimal, amount: token::Amount) -> token::Amount { +pub fn decimal_mult_amount( + dec: Decimal, + amount: token::Amount, +) -> token::Amount { let prod = dec * Decimal::from(amount); // truncate the number to the floor token::Amount::from(prod.to_u64().expect("Product is out of bounds"))