From 285e26b1b711833feba0c1286e254d6966a2ff95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Sep 2025 16:43:53 +0100 Subject: [PATCH 1/6] PoS: rm validator's stake from active total when jailing --- crates/proof_of_stake/src/lib.rs | 19 +++++++++++- crates/proof_of_stake/src/storage.rs | 43 ++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index a9b0aa84300..4c4111f42f3 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}; @@ -3009,6 +3009,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(()) } 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, From 6442b8815a6be458c8315abc8f151edfe3797d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Sep 2025 16:46:10 +0100 Subject: [PATCH 2/6] PoS: add validator's stake to active total when unjailing --- crates/proof_of_stake/src/lib.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index 4c4111f42f3..90ed058bce9 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -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(()) } From 902bce89c1a7ff239cc06309650f9e2db60f0718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Sep 2025 16:48:19 +0100 Subject: [PATCH 3/6] PoS: rm validator's stake from active total when deactivating --- crates/proof_of_stake/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index 90ed058bce9..1fd57a735fb 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -2558,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(()) } From 0e8878ab2380d9d521f04ee137c5c0ef4dcd9730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Sep 2025 16:49:06 +0100 Subject: [PATCH 4/6] PoS: add validator's stake to active total when reactivating --- crates/proof_of_stake/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/proof_of_stake/src/lib.rs b/crates/proof_of_stake/src/lib.rs index 1fd57a735fb..05889085282 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -2638,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(()) } From edda731d46eedda8a6d9d19f7244e16ee8575df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Tue, 23 Sep 2025 21:48:43 +0100 Subject: [PATCH 5/6] node: add PoS migration to fix total active stake --- crates/node/src/shell/mod.rs | 17 +++++++++++++ crates/proof_of_stake/src/lib.rs | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) 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 05889085282..03f2e17cbe1 100644 --- a/crates/proof_of_stake/src/lib.rs +++ b/crates/proof_of_stake/src/lib.rs @@ -3277,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 { From 8612ac321c4de84f3071a3f71a299407a8093d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zemanovi=C4=8D?= Date: Wed, 24 Sep 2025 11:12:39 +0100 Subject: [PATCH 6/6] changelog: add #4833 --- .changelog/unreleased/bug-fixes/4833-fix-active-total-stake.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/4833-fix-active-total-stake.md 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