diff --git a/crates/cardano/src/ewrap/drops.rs b/crates/cardano/src/ewrap/drops.rs index 3fbf3b04..b0da7144 100644 --- a/crates/cardano/src/ewrap/drops.rs +++ b/crates/cardano/src/ewrap/drops.rs @@ -5,7 +5,7 @@ use tracing::{debug, warn}; use crate::{ ewrap::{AccountId, BoundaryWork, DRepId}, - AccountState, CardanoDelta, DRepState, FixedNamespace as _, + AccountState, CardanoDelta, DRepState, FixedNamespace as _, PoolDelegation, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,6 +46,57 @@ impl dolos_core::EntityDelta for DRepExpiration { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PoolDelegatorRetire { + delegator: AccountId, + epoch: Epoch, + + // undo + prev_pool: Option, +} + +impl PoolDelegatorRetire { + pub fn new(delegator: AccountId, epoch: Epoch) -> Self { + Self { + delegator, + epoch, + prev_pool: None, + } + } +} + +impl dolos_core::EntityDelta for PoolDelegatorRetire { + type Entity = AccountState; + + fn key(&self) -> NsKey { + NsKey::from((AccountState::NS, self.delegator.clone())) + } + + fn apply(&mut self, entity: &mut Option) { + let entity = entity.as_mut().expect("account should exist"); + + debug!(delegator=%self.delegator, "retiring pool delegator"); + + // save undo info + self.prev_pool = entity.pool.live().cloned(); + + // apply changes + entity + .pool + .schedule(self.epoch, Some(PoolDelegation::NotDelegated)); + + let Some(PoolDelegation::Pool(pool)) = self.prev_pool else { + unreachable!("account delegated to pool") + }; + entity.retired_pool = Some(pool); + } + + fn undo(&self, _entity: &mut Option) { + // todo!() + // Placeholder undo logic. Ensure this does not panic. + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DRepDelegatorDrop { delegator: AccountId, @@ -107,7 +158,13 @@ impl super::BoundaryVisitor for BoundaryVisitor { ) -> Result<(), ChainError> { let current_epoch = ctx.ending_state.number; - // NOTICE: we're not dropping delegators from retiring pools. We've been back and forth on this decision. The understanding at this point in time is that delegation remains but it just doesn't participate in the rewards. + // Notice that instead of dropping delegators when a pool is retired, we're moving the data to a different field to be able to still track the relationsihp between the pool and the delegators. + + if let Some(pool) = account.delegated_pool_at(current_epoch) { + if ctx.retiring_pools.contains_key(pool) { + self.change(PoolDelegatorRetire::new(id.clone(), current_epoch)); + } + } if let Some(drep) = account.delegated_drep_at(current_epoch) { if ctx.expiring_dreps.contains(drep) { diff --git a/crates/cardano/src/genesis/staking.rs b/crates/cardano/src/genesis/staking.rs index f5782eb1..4e27f4ac 100644 --- a/crates/cardano/src/genesis/staking.rs +++ b/crates/cardano/src/genesis/staking.rs @@ -105,6 +105,7 @@ fn parse_delegation(account: &str, pool: &str, genesis: &Genesis) -> AccountStat registered_at: Some(0), vote_delegated_at: None, deregistered_at: None, + retired_pool: None, stake: EpochValue::with_live(0, stake), } diff --git a/crates/cardano/src/model.rs b/crates/cardano/src/model.rs index d60c0675..440d572f 100644 --- a/crates/cardano/src/model.rs +++ b/crates/cardano/src/model.rs @@ -27,7 +27,7 @@ use crate::{ reset::{AccountTransition, EpochTransition, PoolTransition}, }, ewrap::{ - drops::{DRepDelegatorDrop, DRepExpiration}, + drops::{DRepDelegatorDrop, DRepExpiration, PoolDelegatorRetire}, enactment::{PParamsUpdate, TreasuryWithdrawal}, refunds::{PoolDepositRefund, ProposalDepositRefund}, rewards::AssignRewards, @@ -533,6 +533,10 @@ pub struct AccountState { #[n(6)] pub credential: StakeCredential, + + #[n(7)] + #[cbor(default)] + pub retired_pool: Option, } entity_boilerplate!(AccountState, "accounts"); @@ -547,6 +551,7 @@ impl AccountState { drep: EpochValue::new(epoch), vote_delegated_at: None, deregistered_at: None, + retired_pool: None, } } @@ -1816,6 +1821,7 @@ pub enum CardanoDelta { EpochTransition(EpochTransition), EpochWrapUp(EpochWrapUp), DRepDelegatorDrop(DRepDelegatorDrop), + PoolDelegatorRetire(PoolDelegatorRetire), PoolWrapUp(PoolWrapUp), ProposalDepositRefund(ProposalDepositRefund), TreasuryWithdrawal(TreasuryWithdrawal), @@ -1883,6 +1889,7 @@ delta_from!(PoolDepositRefund); delta_from!(EpochTransition); delta_from!(EpochWrapUp); delta_from!(DRepDelegatorDrop); +delta_from!(PoolDelegatorRetire); delta_from!(PoolWrapUp); delta_from!(ProposalDepositRefund); delta_from!(TreasuryWithdrawal); @@ -1919,6 +1926,7 @@ impl dolos_core::EntityDelta for CardanoDelta { Self::PoolDepositRefund(x) => x.key(), Self::EpochTransition(x) => x.key(), Self::EpochWrapUp(x) => x.key(), + Self::PoolDelegatorRetire(x) => x.key(), Self::DRepDelegatorDrop(x) => x.key(), Self::PoolWrapUp(x) => x.key(), Self::ProposalDepositRefund(x) => x.key(), @@ -1956,6 +1964,7 @@ impl dolos_core::EntityDelta for CardanoDelta { Self::EpochTransition(x) => Self::downcast_apply(x, entity), Self::EpochWrapUp(x) => Self::downcast_apply(x, entity), Self::DRepDelegatorDrop(x) => Self::downcast_apply(x, entity), + Self::PoolDelegatorRetire(x) => Self::downcast_apply(x, entity), Self::PoolWrapUp(x) => Self::downcast_apply(x, entity), Self::ProposalDepositRefund(x) => Self::downcast_apply(x, entity), Self::TreasuryWithdrawal(x) => Self::downcast_apply(x, entity), @@ -1992,6 +2001,7 @@ impl dolos_core::EntityDelta for CardanoDelta { Self::EpochTransition(x) => Self::downcast_undo(x, entity), Self::EpochWrapUp(x) => Self::downcast_undo(x, entity), Self::DRepDelegatorDrop(x) => Self::downcast_undo(x, entity), + Self::PoolDelegatorRetire(x) => Self::downcast_undo(x, entity), Self::PoolWrapUp(x) => Self::downcast_undo(x, entity), Self::ProposalDepositRefund(x) => Self::downcast_undo(x, entity), Self::TreasuryWithdrawal(x) => Self::downcast_undo(x, entity), diff --git a/crates/cardano/src/roll/accounts.rs b/crates/cardano/src/roll/accounts.rs index 71b5d022..b062543d 100644 --- a/crates/cardano/src/roll/accounts.rs +++ b/crates/cardano/src/roll/accounts.rs @@ -15,7 +15,7 @@ use tracing::debug; use crate::model::FixedNamespace as _; use crate::{model::AccountState, pallas_extras, roll::BlockVisitor}; -use crate::{CardanoLogic, DRepDelegation, PParamsSet, PoolDelegation}; +use crate::{CardanoLogic, DRepDelegation, PParamsSet, PoolDelegation, PoolHash}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TrackSeenAddresses { @@ -170,6 +170,7 @@ pub struct StakeDelegation { // undo prev_pool: Option, + prev_retired_pool: Option, } impl StakeDelegation { @@ -179,6 +180,7 @@ impl StakeDelegation { pool, epoch, prev_pool: None, + prev_retired_pool: None, } } } @@ -198,17 +200,20 @@ impl dolos_core::EntityDelta for StakeDelegation { // save undo self.prev_pool = entity.pool.live().cloned(); + self.prev_retired_pool = entity.retired_pool; // apply changes entity .pool .replace(PoolDelegation::Pool(self.pool), self.epoch); + entity.retired_pool = None; } fn undo(&self, entity: &mut Option) { let entity = entity.as_mut().expect("existing account"); entity.pool.reset(self.prev_pool.clone()); + entity.retired_pool = self.prev_retired_pool; } } diff --git a/crates/minibf/src/routes/accounts.rs b/crates/minibf/src/routes/accounts.rs index 0dc2eb2f..cf41da13 100644 --- a/crates/minibf/src/routes/accounts.rs +++ b/crates/minibf/src/routes/accounts.rs @@ -96,6 +96,7 @@ impl<'a> IntoModel for AccountModelBuilder<'a> { let pool_id = self .account_state .delegated_pool_at(current_epoch) + .or(self.account_state.retired_pool.as_ref()) .map(bech32_pool) .transpose()?;