diff --git a/.changelog/unreleased/bug-fixes/4833-fix-active-total-stake.md b/.changelog/unreleased/bug-fixes/4833-fix-active-total-stake.md new file mode 100644 index 00000000000..7d799361882 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/4833-fix-active-total-stake.md @@ -0,0 +1,3 @@ +- Fixed the total active stake accounting in PoS when validators move + between active and inactive states. ([\#4833](https://github.com/namada- + net/namada/pull/4833)) \ No newline at end of file diff --git a/crates/node/src/shell/mod.rs b/crates/node/src/shell/mod.rs index 84b6f7b0155..caaac959ca7 100644 --- a/crates/node/src/shell/mod.rs +++ b/crates/node/src/shell/mod.rs @@ -818,6 +818,23 @@ where _ => None, }; + // Temporary migration code to fix the total active stake value + if let Some(height) = + std::env::var("NAMADA_MIGRATION_HEIGHT").ok().map(|height| { + ::from_str(height.trim()) + .expect( + "Invalid block height set in \ + `NAMADA_MIGRATION_HEIGHT` env var", + ) + }) + { + if height == height_to_commit { + proof_of_stake::fix_total_active_stake::<_, governance::Store<_>>( + &mut self.state, + ).expect("Must be able to fix total active stake") + } + } + self.state .commit_block() .expect("Encountered a storage error while committing a block"); diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index a9b0aa84300..03f2e17cbe1 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -63,7 +63,7 @@ pub use namada_state::{ pub use namada_systems::proof_of_stake::*; use namada_systems::{governance, trans_token}; pub use parameters::{OwnedPosParams, PosParams}; -use storage::write_validator_name; +use storage::{update_total_active_deltas, write_validator_name}; pub use types::GenesisValidator; use types::{DelegationEpochs, into_tm_voting_power}; @@ -2181,7 +2181,8 @@ where } // Re-insert the validator into the validator set and update its state - let pipeline_epoch = checked!(current_epoch + params.pipeline_len)?; + let offset = params.pipeline_len; + let pipeline_epoch = checked!(current_epoch + offset)?; let stake = read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; @@ -2191,8 +2192,20 @@ where validator, stake, current_epoch, - params.pipeline_len, + offset, )?; + + // Add the validator's stake to active total + let stake = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; + update_total_active_deltas::( + storage, + ¶ms, + stake.change(), + current_epoch, + Some(offset), + )?; + Ok(()) } @@ -2545,6 +2558,22 @@ where params.pipeline_len, )?; + // Remove the validator's stake from active total + let stake = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; + if stake.is_positive() { + update_total_active_deltas::( + storage, + ¶ms, + stake + .change() + .negate() + .expect("Negative stake cannot overflow"), + current_epoch, + Some(params.pipeline_len), + )?; + } + Ok(()) } @@ -2609,6 +2638,17 @@ where params.pipeline_len, )?; + // Add the validator's stake to active total + let stake = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; + update_total_active_deltas::( + storage, + ¶ms, + stake.change(), + current_epoch, + Some(params.pipeline_len), + )?; + Ok(()) } @@ -3009,6 +3049,23 @@ where offset, )?; } + + // Remove the validator's stake from active total + let offset_epoch = checked!(current_epoch + start_offset)?; + let stake = read_validator_stake(storage, params, validator, offset_epoch)?; + if stake.is_positive() { + update_total_active_deltas::( + storage, + params, + stake + .change() + .negate() + .expect("Negative stake cannot overflow"), + current_epoch, + Some(start_offset), + )?; + } + Ok(()) } @@ -3220,6 +3277,47 @@ fn prune_old_delegations( Ok(()) } +/// Temporary migration code to fix the total active stake value +pub fn fix_total_active_stake(storage: &mut S) -> Result<()> +where + S: StorageRead + StorageWrite, + Gov: governance::Read, +{ + let epoch = storage.get_block_epoch()?; + let validators = storage::read_all_validator_addresses(storage, epoch)?; + let mut total_stake = token::Amount::zero(); + let params = read_pos_params::<_, Gov>(storage)?; + for validator in validators { + let state = storage::read_validator_state::<_, Gov>( + storage, &validator, epoch, + )?; + let is_active = matches!( + state, + Some( + ValidatorState::Consensus + | ValidatorState::BelowCapacity + | ValidatorState::BelowThreshold, + ) + ); + if is_active { + let stake = + read_validator_stake(storage, ¶ms, &validator, epoch)?; + total_stake = + total_stake.checked_add(stake).expect("Must not overflow"); + } + } + + let total_active_deltas = storage::total_active_deltas_handle(); + total_active_deltas.set::( + storage, + total_stake.change(), + epoch, + 0, + )?; + + Ok(()) +} + #[cfg(any(test, feature = "testing"))] /// PoS related utility functions to help set up tests. pub mod test_utils { diff --git a/crates/proof_of_stake/src/storage.rs b/crates/proof_of_stake/src/storage.rs index ccb33dc09e8..d1684118286 100644 --- a/crates/proof_of_stake/src/storage.rs +++ b/crates/proof_of_stake/src/storage.rs @@ -726,7 +726,6 @@ where { let offset = offset_opt.unwrap_or(params.pipeline_len); let total_deltas = total_deltas_handle(); - let total_active_deltas = total_active_deltas_handle(); let offset_epoch = checked!(current_epoch + offset)?; // Update total deltas @@ -744,22 +743,48 @@ where // Update total active voting power if update_active_voting_power { - let active_delta = total_active_deltas - .get_delta_val(storage, offset_epoch)? - .unwrap_or_default(); - total_active_deltas.set::( + update_total_active_deltas::( storage, - active_delta.checked_add(delta).expect( - "Total active voting power updated amount should not overflow", - ), + params, + delta, current_epoch, - offset, + offset_opt, )?; } Ok(()) } +/// Update PoS total active deltas. +pub fn update_total_active_deltas( + storage: &mut S, + params: &OwnedPosParams, + delta: token::Change, + current_epoch: namada_core::chain::Epoch, + offset_opt: Option, +) -> Result<()> +where + S: StorageRead + StorageWrite, + Gov: governance::Read, +{ + let offset = offset_opt.unwrap_or(params.pipeline_len); + + let total_active_deltas = total_active_deltas_handle(); + let offset_epoch = checked!(current_epoch + offset)?; + + let active_delta = total_active_deltas + .get_delta_val(storage, offset_epoch)? + .unwrap_or_default(); + total_active_deltas.set::( + storage, + active_delta.checked_add(delta).expect( + "Total active voting power updated amount should not overflow", + ), + current_epoch, + offset, + ) +} + /// Read PoS validator's email. pub fn read_validator_email( storage: &S,