penumbra_sdk_governance/component/
view.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    str::FromStr,
4};
5
6use anyhow::{Context, Result};
7use async_trait::async_trait;
8use cnidarium::{StateRead, StateWrite};
9use futures::StreamExt;
10use ibc_types::core::client::ClientId;
11use penumbra_sdk_asset::{asset, Value, STAKING_TOKEN_DENOM};
12use penumbra_sdk_ibc::component::ClientStateReadExt as _;
13use penumbra_sdk_ibc::component::ClientStateWriteExt as _;
14use penumbra_sdk_num::Amount;
15use penumbra_sdk_proto::{StateReadProto, StateWriteProto};
16use penumbra_sdk_sct::{
17    component::{clock::EpochRead, tree::SctRead},
18    Nullifier,
19};
20use penumbra_sdk_shielded_pool::component::AssetRegistryRead;
21use penumbra_sdk_stake::{
22    component::{validator_handler::ValidatorDataRead, ConsensusIndexRead},
23    DelegationToken, GovernanceKey, IdentityKey,
24};
25use penumbra_sdk_tct as tct;
26use tokio::task::JoinSet;
27use tracing::instrument;
28
29use penumbra_sdk_stake::{rate::RateData, validator};
30
31use crate::{
32    change::ParameterChange,
33    params::GovernanceParameters,
34    proposal::{Proposal, ProposalPayload},
35    proposal_state::State as ProposalState,
36    state_key::persistent_flags,
37    validator_vote::action::ValidatorVoteReason,
38    vote::Vote,
39};
40use crate::{state_key, tally::Tally};
41
42#[async_trait]
43pub trait StateReadExt: StateRead + penumbra_sdk_stake::StateReadExt {
44    /// Returns true if the next height is an upgrade height.
45    /// We look-ahead to the next height because we want to halt the chain immediately after
46    /// committing the block.
47    async fn is_pre_upgrade_height(&self) -> Result<bool> {
48        let Some(next_upgrade_height) = self
49            .nonverifiable_get_raw(state_key::upgrades::next_upgrade().as_bytes())
50            .await?
51        else {
52            return Ok(false);
53        };
54
55        let next_upgrade_height = u64::from_be_bytes(next_upgrade_height.as_slice().try_into()?);
56
57        let current_height = self.get_block_height().await?;
58        Ok(current_height.saturating_add(1) == next_upgrade_height)
59    }
60
61    /// Gets the governance parameters from the JMT.
62    async fn get_governance_params(&self) -> Result<GovernanceParameters> {
63        self.get(state_key::governance_params())
64            .await?
65            .ok_or_else(|| anyhow::anyhow!("Missing GovernanceParameters"))
66    }
67
68    /// Get the id of the next proposal in the sequence of ids.
69    async fn next_proposal_id(&self) -> Result<u64> {
70        Ok(self
71            .get_proto::<u64>(state_key::next_proposal_id())
72            .await?
73            .unwrap_or_default())
74    }
75
76    /// Get the proposal definition for a proposal.
77    async fn proposal_definition(&self, proposal_id: u64) -> Result<Option<Proposal>> {
78        Ok(self
79            .get(&state_key::proposal_definition(proposal_id))
80            .await?)
81    }
82
83    /// Get the proposal payload for a proposal.
84    async fn proposal_payload(&self, proposal_id: u64) -> Result<Option<ProposalPayload>> {
85        Ok(self
86            .get(&state_key::proposal_definition(proposal_id))
87            .await?
88            .map(|p: Proposal| p.payload))
89    }
90
91    /// Get the proposal deposit amount for a proposal.
92    async fn proposal_deposit_amount(&self, proposal_id: u64) -> Result<Option<Amount>> {
93        self.get(&state_key::proposal_deposit_amount(proposal_id))
94            .await
95    }
96
97    /// Get the state of a proposal.
98    async fn proposal_state(&self, proposal_id: u64) -> Result<Option<ProposalState>> {
99        Ok(self
100            .get::<ProposalState>(&state_key::proposal_state(proposal_id))
101            .await?)
102    }
103
104    /// Get all the unfinished proposal ids.
105    async fn unfinished_proposals(&self) -> Result<BTreeSet<u64>> {
106        let prefix = state_key::all_unfinished_proposals();
107        let mut stream = self.prefix_proto(prefix);
108        let mut proposals = BTreeSet::new();
109        while let Some((key, ())) = stream.next().await.transpose()? {
110            let proposal_id = u64::from_str(
111                key.rsplit('/')
112                    .next()
113                    .context("invalid key for unfinished proposal")?,
114            )?;
115            proposals.insert(proposal_id);
116        }
117        Ok(proposals)
118    }
119
120    /// Get the vote of a validator on a particular proposal.
121    async fn validator_vote(
122        &self,
123        proposal_id: u64,
124        identity_key: IdentityKey,
125    ) -> Result<Option<Vote>> {
126        Ok(self
127            .get::<Vote>(&state_key::validator_vote(proposal_id, identity_key))
128            .await?)
129    }
130
131    /// Get the proposal voting start block for a given proposal.
132    async fn proposal_voting_start(&self, proposal_id: u64) -> Result<Option<u64>> {
133        Ok(self
134            .get_proto::<u64>(&state_key::proposal_voting_start(proposal_id))
135            .await?)
136    }
137
138    /// Get the proposal voting end block for a given proposal.
139    async fn proposal_voting_end(&self, proposal_id: u64) -> Result<Option<u64>> {
140        Ok(self
141            .get_proto::<u64>(&state_key::proposal_voting_end(proposal_id))
142            .await?)
143    }
144
145    /// Get the proposal voting start block for a given proposal.
146    async fn proposal_voting_start_position(
147        &self,
148        proposal_id: u64,
149    ) -> Result<Option<tct::Position>> {
150        Ok(self
151            .get_proto::<u64>(&state_key::proposal_voting_start_position(proposal_id))
152            .await?
153            .map(Into::into))
154    }
155
156    /// Get the total voting power across all validators.
157    async fn total_voting_power_at_proposal_start(&self, proposal_id: u64) -> Result<u64> {
158        Ok(self
159            .validator_voting_power_at_proposal_start(proposal_id)
160            .await?
161            .values()
162            .copied()
163            .sum())
164    }
165
166    /// Check whether a nullifier was spent for a given proposal.
167    async fn check_nullifier_unvoted_for_proposal(
168        &self,
169        proposal_id: u64,
170        nullifier: &Nullifier,
171    ) -> Result<()> {
172        if let Some(height) = self
173            .get_proto::<u64>(&state_key::voted_nullifier_lookup_for_proposal(
174                proposal_id,
175                nullifier,
176            ))
177            .await?
178        {
179            // If the nullifier was already voted with, error:
180            anyhow::bail!(
181                "nullifier {nullifier} was already used for voting on proposal {proposal_id} at height {height}",
182            );
183        }
184
185        Ok(())
186    }
187
188    /// Get the [`RateData`] for a validator at the start height of a given proposal.
189    async fn rate_data_at_proposal_start(
190        &self,
191        proposal_id: u64,
192        identity_key: IdentityKey,
193    ) -> Result<Option<RateData>> {
194        self.get(&state_key::rate_data_at_proposal_start(
195            proposal_id,
196            identity_key,
197        ))
198        .await
199    }
200
201    /// Throw an error if the proposal is not votable.
202    async fn check_proposal_votable(&self, proposal_id: u64) -> Result<()> {
203        if let Some(proposal_state) = self.proposal_state(proposal_id).await? {
204            use crate::proposal_state::State::*;
205            match proposal_state {
206                Voting => {
207                    // This is when you can vote on a proposal
208                }
209                Withdrawn { .. } => {
210                    anyhow::bail!("proposal {} has already been withdrawn", proposal_id)
211                }
212                Finished { .. } | Claimed { .. } => {
213                    anyhow::bail!("voting on proposal {} has already concluded", proposal_id)
214                }
215            }
216        } else {
217            anyhow::bail!("proposal {} does not exist", proposal_id);
218        }
219
220        Ok(())
221    }
222
223    /// Throw an error if the proposal was not started at the claimed position.
224    async fn check_proposal_started_at_position(
225        &self,
226        proposal_id: u64,
227        claimed_position: tct::Position,
228    ) -> Result<()> {
229        if let Some(position) = self.proposal_voting_start_position(proposal_id).await? {
230            if position != claimed_position {
231                anyhow::bail!(
232                    "proposal {} was not started at claimed start position of {:?}",
233                    proposal_id,
234                    claimed_position
235                );
236            }
237        } else {
238            anyhow::bail!("proposal {} does not exist", proposal_id);
239        }
240
241        Ok(())
242    }
243
244    /// Throw an error if the nullifier was spent before the proposal started.
245    async fn check_nullifier_unspent_before_start_block_height(
246        &self,
247        proposal_id: u64,
248        nullifier: &Nullifier,
249    ) -> Result<()> {
250        let Some(start_height) = self.proposal_voting_start(proposal_id).await? else {
251            anyhow::bail!("proposal {} does not exist", proposal_id);
252        };
253
254        if let Some(spend_info) = self.spend_info(*nullifier).await? {
255            if spend_info.spend_height < start_height {
256                anyhow::bail!(
257                    "nullifier {} was already spent at block height {} before proposal started at block height {}",
258                    nullifier,
259                    spend_info.spend_height,
260                    start_height
261                );
262            }
263        }
264
265        Ok(())
266    }
267
268    /// Look up the validator for a given asset ID, if it is a delegation token.
269    async fn validator_by_delegation_asset(&self, asset_id: asset::Id) -> Result<IdentityKey> {
270        // Attempt to find the denom for the asset ID of the specified value
271        let Some(denom_metadata) = self.denom_metadata_by_asset(&asset_id).await else {
272            anyhow::bail!("asset ID {} does not correspond to a known denom", asset_id);
273        };
274
275        // Attempt to find the validator identity for the specified denom, failing if it is not a
276        // delegation token
277        let validator_identity = DelegationToken::try_from(denom_metadata)?.validator();
278
279        Ok(validator_identity)
280    }
281
282    /// Throw an error if the exchange between the value and the unbonded amount isn't correct for
283    /// the proposal given.
284    async fn check_unbonded_amount_correct_exchange_for_proposal(
285        &self,
286        proposal_id: u64,
287        value: &Value,
288        unbonded_amount: &Amount,
289    ) -> Result<()> {
290        let validator_identity = self.validator_by_delegation_asset(value.asset_id).await?;
291
292        // Attempt to look up the snapshotted `RateData` for the validator at the start of the proposal
293        let Some(rate_data) = self
294            .rate_data_at_proposal_start(proposal_id, validator_identity)
295            .await?
296        else {
297            anyhow::bail!(
298                "validator {} was not active at the start of proposal {}",
299                validator_identity,
300                proposal_id
301            );
302        };
303
304        // Check that the unbonded amount is correct relative to that exchange rate
305        if rate_data.unbonded_amount(value.amount).value() != unbonded_amount.value() {
306            anyhow::bail!(
307                "unbonded amount {}{} does not correspond to {} staked delegation tokens for validator {} using the exchange rate at the start of proposal {}",
308                unbonded_amount,
309                *STAKING_TOKEN_DENOM,
310                value.amount,
311                validator_identity,
312                proposal_id,
313            );
314        }
315
316        Ok(())
317    }
318
319    async fn check_height_in_future_of_voting_end(&self, height: u64) -> Result<()> {
320        let block_height = self.get_block_height().await?;
321        let voting_blocks = self.get_governance_params().await?.proposal_voting_blocks;
322        let voting_end_height = block_height + voting_blocks;
323
324        if height < voting_end_height {
325            anyhow::bail!(
326                "effective height {} is less than the block height {} for the end of the voting period",
327                height,
328                voting_end_height
329            );
330        }
331        Ok(())
332    }
333
334    /// Check that the validator has not voted on the proposal.
335    async fn check_validator_has_not_voted(
336        &self,
337        proposal_id: u64,
338        identity_key: &IdentityKey,
339    ) -> Result<()> {
340        if let Some(_vote) = self.validator_vote(proposal_id, *identity_key).await? {
341            anyhow::bail!(
342                "validator {} has already voted on proposal {}",
343                identity_key,
344                proposal_id
345            );
346        }
347
348        Ok(())
349    }
350
351    /// Check that the governance key matches the validator's identity key.
352    async fn check_governance_key_matches_validator(
353        &self,
354        identity_key: &IdentityKey,
355        governance_key: &GovernanceKey,
356    ) -> Result<()> {
357        if let Some(validator) = self.get_validator_definition(identity_key).await? {
358            if validator.governance_key != *governance_key {
359                anyhow::bail!(
360                    "governance key {} does not match validator {}",
361                    governance_key,
362                    identity_key
363                );
364            }
365        } else {
366            anyhow::bail!("validator {} does not exist", identity_key);
367        }
368
369        Ok(())
370    }
371
372    /// Check that a deposit claim could be made on the proposal.
373    async fn check_proposal_claimable(&self, proposal_id: u64) -> Result<()> {
374        if let Some(proposal_state) = self.proposal_state(proposal_id).await? {
375            match proposal_state {
376                ProposalState::Voting => {
377                    anyhow::bail!("proposal {} is still voting", proposal_id)
378                }
379                ProposalState::Withdrawn { .. } => {
380                    anyhow::bail!(
381                        "proposal {} has been withdrawn but voting has not concluded",
382                        proposal_id
383                    )
384                }
385                ProposalState::Finished { .. } => {
386                    // This is when you can claim a proposal
387                }
388                ProposalState::Claimed { .. } => {
389                    anyhow::bail!(
390                        "the deposit for proposal {} has already been claimed",
391                        proposal_id
392                    )
393                }
394            }
395        } else {
396            anyhow::bail!("proposal {} does not exist", proposal_id);
397        }
398
399        Ok(())
400    }
401
402    /// Check that the deposit claim amount matches the proposal's deposit amount.
403    async fn check_proposal_claim_valid_deposit(
404        &self,
405        proposal_id: u64,
406        claim_deposit_amount: Amount,
407    ) -> Result<()> {
408        if let Some(proposal_deposit_amount) = self.proposal_deposit_amount(proposal_id).await? {
409            if claim_deposit_amount != proposal_deposit_amount {
410                anyhow::bail!(
411                    "proposal deposit claim for {}{} does not match proposal deposit of {}{}",
412                    claim_deposit_amount,
413                    *STAKING_TOKEN_DENOM,
414                    proposal_deposit_amount,
415                    *STAKING_TOKEN_DENOM,
416                );
417            }
418        } else {
419            anyhow::bail!("proposal {} does not exist", proposal_id);
420        }
421
422        Ok(())
423    }
424
425    /// Get a specific validator's voting power for a proposal.
426    async fn specific_validator_voting_power_at_proposal_start(
427        &self,
428        proposal_id: u64,
429        identity_key: IdentityKey,
430    ) -> Result<u64> {
431        self.get_proto(&state_key::voting_power_at_proposal_start(
432            proposal_id,
433            identity_key,
434        ))
435        .await
436        .map(Option::unwrap_or_default)
437    }
438
439    /// Get all the active validator voting power for the proposal.
440    async fn validator_voting_power_at_proposal_start(
441        &self,
442        proposal_id: u64,
443    ) -> Result<BTreeMap<IdentityKey, u64>> {
444        let mut powers = BTreeMap::new();
445
446        let prefix = state_key::all_voting_power_at_proposal_start(proposal_id);
447        let mut stream = self.prefix_proto(&prefix);
448
449        while let Some((key, power)) = stream.next().await.transpose()? {
450            let identity_key = key
451                .rsplit('/')
452                .next()
453                .ok_or_else(|| {
454                    anyhow::anyhow!(
455                        "incorrect key format for validator voting power at proposal start"
456                    )
457                })?
458                .parse()?;
459            powers.insert(identity_key, power);
460        }
461
462        Ok(powers)
463    }
464
465    /// Check whether a validator was active at the start of a proposal, and fail if not.
466    async fn check_validator_active_at_proposal_start(
467        &self,
468        proposal_id: u64,
469        identity_key: &IdentityKey,
470    ) -> Result<()> {
471        if self
472            .get_proto::<u64>(&state_key::voting_power_at_proposal_start(
473                proposal_id,
474                *identity_key,
475            ))
476            .await?
477            .is_none()
478        {
479            anyhow::bail!(
480                "validator {} was not active at the start of proposal {}",
481                identity_key,
482                proposal_id
483            );
484        }
485
486        Ok(())
487    }
488
489    /// Get all the validator votes for the proposal.
490    async fn validator_votes(&self, proposal_id: u64) -> Result<BTreeMap<IdentityKey, Vote>> {
491        let mut votes = BTreeMap::new();
492
493        let prefix = state_key::all_validator_votes_for_proposal(proposal_id);
494        let mut stream = self.prefix(&prefix);
495
496        while let Some((key, vote)) = stream.next().await.transpose()? {
497            let identity_key = key
498                .rsplit('/')
499                .next()
500                .ok_or_else(|| anyhow::anyhow!("incorrect key format for validator vote"))?
501                .parse()?;
502            votes.insert(identity_key, vote);
503        }
504
505        Ok(votes)
506    }
507
508    /// Get all the *tallied* delegator votes for the proposal (excluding those which have been
509    /// cast but not tallied).
510    async fn tallied_delegator_votes(
511        &self,
512        proposal_id: u64,
513    ) -> Result<BTreeMap<IdentityKey, Tally>> {
514        let mut tallies = BTreeMap::new();
515
516        let prefix = state_key::all_tallied_delegator_votes_for_proposal(proposal_id);
517        let mut stream = self.prefix(&prefix);
518
519        while let Some((key, tally)) = stream.next().await.transpose()? {
520            let identity_key = key
521                .rsplit('/')
522                .next()
523                .ok_or_else(|| anyhow::anyhow!("incorrect key format for delegator vote tally"))?
524                .parse()?;
525            tallies.insert(identity_key, tally);
526        }
527
528        Ok(tallies)
529    }
530
531    /// Add up all the currently tallied votes (without tallying any cast votes that haven't been
532    /// tallied yet).
533    async fn current_tally(&self, proposal_id: u64) -> Result<Tally> {
534        let validator_powers = self
535            .validator_voting_power_at_proposal_start(proposal_id)
536            .await?;
537        let mut validator_votes = self.validator_votes(proposal_id).await?;
538        let mut delegator_tallies = self.tallied_delegator_votes(proposal_id).await?;
539
540        // For each validator, tally their own vote, overriding it with any tallied delegator votes
541        let mut tally = Tally::default();
542        for (validator, power) in validator_powers.into_iter() {
543            let delegator_tally = delegator_tallies.remove(&validator).unwrap_or_default();
544            if let Some(vote) = validator_votes.remove(&validator) {
545                // The effective power of a validator is the voting power of that validator at
546                // proposal start, minus the total voting power used by delegators to that validator
547                // who have voted. Their votes will be added back in below, re-assigning their
548                // voting power to their chosen votes.
549                let effective_power = power.saturating_sub(delegator_tally.total());
550                tally += (vote, effective_power).into();
551            }
552            // Add the delegator votes in, regardless of if the validator has voted.
553            tally += delegator_tally;
554        }
555
556        assert!(
557            validator_votes.is_empty(),
558            "no inactive validator should have voted"
559        );
560        assert!(
561            delegator_tallies.is_empty(),
562            "no delegator should have been able to vote for an inactive validator"
563        );
564
565        Ok(tally)
566    }
567
568    /// Gets the parameter changes scheduled for the given height, if any.
569    async fn param_changes_for_height(&self, height: u64) -> Result<Option<ParameterChange>> {
570        self.get(&state_key::param_changes_for_height(height)).await
571    }
572
573    /// Check if any proposal is started in this block.
574    fn proposal_started(&self) -> bool {
575        self.object_get::<()>(state_key::proposal_started())
576            .is_some()
577    }
578
579    async fn is_chain_halted(&self) -> bool {
580        self.nonverifiable_get_proto(state_key::persistent_flags::halt_bit().as_bytes())
581            .await
582            .expect("no deserialization errors")
583            .unwrap_or_default()
584    }
585}
586
587impl<T: StateRead + penumbra_sdk_stake::StateReadExt + ?Sized> StateReadExt for T {}
588
589#[async_trait]
590pub trait StateWriteExt: StateWrite + penumbra_sdk_ibc::component::ConnectionStateWriteExt {
591    /// Writes the provided governance parameters to the JMT.
592    fn put_governance_params(&mut self, params: GovernanceParameters) {
593        // Change the governance parameters:
594        self.put(state_key::governance_params().into(), params)
595    }
596
597    /// Initialize the proposal counter so that it can always be read.
598    fn init_proposal_counter(&mut self) {
599        self.put_proto(state_key::next_proposal_id().to_string(), 0);
600    }
601
602    /// Store a new proposal with a new proposal id.
603    async fn new_proposal(&mut self, proposal: &Proposal) -> Result<u64> {
604        let proposal_id = self.next_proposal_id().await?;
605        if proposal_id != proposal.id {
606            anyhow::bail!(
607                "proposal id {} does not match next proposal id {}",
608                proposal.id,
609                proposal_id
610            );
611        }
612
613        // Snapshot the rate data and voting power for all active validators at this height
614        let mut js = JoinSet::new();
615        let mut validator_identity_stream = self.consensus_set_stream()?;
616        while let Some(identity_key) = validator_identity_stream.next().await {
617            let identity_key = identity_key?;
618
619            let state = self.get_validator_state(&identity_key);
620            let rate_data = self.get_validator_rate(&identity_key);
621            let power: penumbra_sdk_proto::state::future::DomainFuture<
622                Amount,
623                <Self as StateRead>::GetRawFut,
624            > = self.get_validator_power(&identity_key);
625            js.spawn(async move {
626                let state = state
627                    .await?
628                    .expect("every known validator must have a recorded state");
629                // Compute the rate data, only for active validators, and write it to the state
630                let per_validator = if state == validator::State::Active {
631                    let rate_data = rate_data
632                        .await?
633                        .expect("every known validator must have a recorded current rate");
634                    let power = power
635                        .await?
636                        .expect("every known validator must have a recorded current power");
637                    Some((identity_key, rate_data, power))
638                } else {
639                    None
640                };
641                // Return the pair, to be written to the state
642                anyhow::Ok(per_validator)
643            });
644        }
645        // Iterate over all the futures and insert them into the state (this can be done in
646        // arbitrary order, because they are non-overlapping)
647        while let Some(per_validator) = js.join_next().await.transpose()? {
648            if let Some((identity_key, rate_data, power)) = per_validator? {
649                self.put(
650                    state_key::rate_data_at_proposal_start(proposal_id, identity_key),
651                    rate_data,
652                );
653                self.put(
654                    state_key::voting_power_at_proposal_start(proposal_id, identity_key),
655                    power,
656                )
657            }
658        }
659
660        // Record this proposal id, so we won't re-use it
661        self.put_proto(state_key::next_proposal_id().to_owned(), proposal_id + 1);
662
663        // Store the proposal data
664        self.put(
665            state_key::proposal_definition(proposal_id),
666            proposal.clone(),
667        );
668
669        // Return the new proposal id
670        Ok(proposal_id)
671    }
672
673    /// Mark a nullifier as spent for a given proposal.
674    async fn mark_nullifier_voted(&mut self, proposal_id: u64, nullifier: &Nullifier) {
675        self.put_proto(
676            state_key::voted_nullifier_lookup_for_proposal(proposal_id, nullifier),
677            self.get_block_height()
678                .await
679                .expect("block height should be set"),
680        );
681    }
682
683    /// Record in the object store that some proposal has started.
684    fn mark_proposal_started(&mut self) {
685        self.object_put(state_key::proposal_started(), ());
686    }
687
688    /// Store the proposal deposit amount.
689    fn put_deposit_amount(&mut self, proposal_id: u64, amount: Amount) {
690        self.put(state_key::proposal_deposit_amount(proposal_id), amount);
691    }
692
693    /// Set the state of a proposal.
694    fn put_proposal_state(&mut self, proposal_id: u64, state: ProposalState) {
695        // Set the state of the proposal
696        self.put(state_key::proposal_state(proposal_id), state.clone());
697
698        match &state {
699            ProposalState::Voting | ProposalState::Withdrawn { .. } => {
700                // If we're setting the proposal to a non-finished state, track it in our list of
701                // proposals that are not finished
702                self.put_proto(state_key::unfinished_proposal(proposal_id), ());
703            }
704            ProposalState::Finished { .. } | ProposalState::Claimed { .. } => {
705                // If we're setting the proposal to a finished or claimed state, remove it from our list of
706                // proposals that are not finished
707                self.delete(state_key::unfinished_proposal(proposal_id));
708            }
709        }
710    }
711
712    /// Record a validator vote for a proposal.
713    fn cast_validator_vote(
714        &mut self,
715        proposal_id: u64,
716        identity_key: IdentityKey,
717        vote: Vote,
718        reason: ValidatorVoteReason,
719    ) {
720        // Record the vote
721        self.put(state_key::validator_vote(proposal_id, identity_key), vote);
722        // Record the vote justification
723        self.put(
724            state_key::validator_vote_reason(proposal_id, identity_key),
725            reason,
726        );
727    }
728
729    /// Set the proposal voting start block height for a proposal.
730    fn put_proposal_voting_start(&mut self, proposal_id: u64, end_block: u64) {
731        self.put_proto(state_key::proposal_voting_start(proposal_id), end_block);
732    }
733
734    /// Set the proposal voting end block height for a proposal.
735    fn put_proposal_voting_end(&mut self, proposal_id: u64, end_block: u64) {
736        self.put_proto(state_key::proposal_voting_end(proposal_id), end_block);
737    }
738
739    /// Set the proposal voting start position for a proposal.
740    fn put_proposal_voting_start_position(
741        &mut self,
742        proposal_id: u64,
743        start_position: tct::Position,
744    ) {
745        self.put_proto(
746            state_key::proposal_voting_start_position(proposal_id),
747            u64::from(start_position),
748        );
749    }
750
751    /// Mark a nullifier as having voted on a proposal.
752    async fn mark_nullifier_voted_on_proposal(&mut self, proposal_id: u64, nullifier: &Nullifier) {
753        self.put_proto(
754            state_key::voted_nullifier_lookup_for_proposal(proposal_id, nullifier),
755            self.get_block_height()
756                .await
757                .expect("block height should be set"),
758        );
759    }
760
761    /// Record a delegator vote on a proposal.
762    async fn cast_delegator_vote(
763        &mut self,
764        proposal_id: u64,
765        identity_key: IdentityKey,
766        vote: Vote,
767        nullifier: &Nullifier,
768        unbonded_amount: Amount,
769    ) -> Result<()> {
770        // Convert the unbonded amount into voting power
771        let power = unbonded_amount.value() as u64;
772        let tally: Tally = (vote, power).into();
773
774        // Record the vote
775        self.put(
776            state_key::untallied_delegator_vote(proposal_id, identity_key, nullifier),
777            tally,
778        );
779
780        Ok(())
781    }
782
783    /// Tally delegator votes by sweeping them into the aggregate for each validator, for each proposal.
784    #[instrument(skip(self))]
785    async fn tally_delegator_votes(&mut self, just_for_proposal: Option<u64>) -> Result<()> {
786        // Iterate over all the delegator votes, or just the ones for a specific proposal
787        let prefix = if let Some(proposal_id) = just_for_proposal {
788            state_key::all_untallied_delegator_votes_for_proposal(proposal_id)
789        } else {
790            state_key::all_untallied_delegator_votes().to_string()
791        };
792        let mut prefix_stream = self.prefix(&prefix);
793
794        // We need to keep track of modifications and then apply them after iteration, because
795        // `self.prefix(..)` borrows `self` immutably, so we can't mutate `self` during iteration
796        let mut keys_to_delete = vec![];
797        let mut new_tallies: BTreeMap<u64, BTreeMap<IdentityKey, Tally>> = BTreeMap::new();
798
799        while let Some((key, tally)) = prefix_stream.next().await.transpose()? {
800            // Extract the validator identity key from the key string
801            let mut reverse_path_elements = key.rsplit('/');
802            reverse_path_elements.next(); // skip the nullifier element of the key
803            let identity_key = reverse_path_elements
804                .next()
805                .ok_or_else(|| {
806                    anyhow::anyhow!("unexpected key format for untallied delegator vote")
807                })?
808                .parse()?;
809            let proposal_id = reverse_path_elements
810                .next()
811                .ok_or_else(|| {
812                    anyhow::anyhow!("unexpected key format for untallied delegator vote")
813                })?
814                .parse()?;
815
816            // Get the current tally for this validator
817            let mut current_tally = self
818                .get::<Tally>(&state_key::tallied_delegator_votes(
819                    proposal_id,
820                    identity_key,
821                ))
822                .await?
823                .unwrap_or_default();
824
825            // Add the new tally to the current tally
826            current_tally += tally;
827
828            // Remember the new tally
829            new_tallies
830                .entry(proposal_id)
831                .or_default()
832                .insert(identity_key, current_tally);
833
834            // Remember to delete this key
835            keys_to_delete.push(key);
836        }
837
838        // Explicit drop because we need to borrow self mutably again below
839        drop(prefix_stream);
840
841        // Actually record the key deletions in the state
842        for key in keys_to_delete {
843            self.delete(key);
844        }
845
846        // Actually record the new tallies in the state
847        for (proposal_id, new_tallies_for_proposal) in new_tallies {
848            for (identity_key, tally) in new_tallies_for_proposal {
849                tracing::debug!(
850                    proposal_id,
851                    identity_key = %identity_key,
852                    yes = %tally.yes(),
853                    no = %tally.no(),
854                    abstain = %tally.abstain(),
855                    "tallying delegator votes"
856                );
857                self.put(
858                    state_key::tallied_delegator_votes(proposal_id, identity_key),
859                    tally,
860                );
861            }
862        }
863
864        Ok(())
865    }
866
867    #[instrument(skip(self))]
868    async fn enact_proposal(
869        &mut self,
870        proposal_id: u64,
871        payload: &ProposalPayload,
872    ) -> Result<Result<()>> // inner error from proposal execution
873    {
874        match payload {
875            ProposalPayload::Signaling { .. } => {
876                // Nothing to do for signaling proposals
877                tracing::info!("signaling proposal passed, nothing to do");
878            }
879            ProposalPayload::Emergency { halt_chain } => {
880                // If the proposal calls to halt the chain...
881                if *halt_chain {
882                    // Print an informational message and signal to the consensus worker to halt the
883                    // process after the state is committed
884                    tracing::info!("emergency proposal passed calling for immediate chain halt");
885                    self.signal_halt();
886                }
887            }
888            ProposalPayload::ParameterChange(change) => {
889                let current_height = self.get_block_height().await?;
890                // The parameter change should take effect in the next block.
891                let change_height = current_height + 1;
892                tracing::info!(
893                    change_height,
894                    ?change,
895                    "parameter change proposal passed, scheduling for next block"
896                );
897                // Note: if two parameter changes are scheduled for the same height by two
898                // passing proposals that were submitted in exactly the same block, one will
899                // clobber the other. This is undesirable but seems unlikely to happen in practice.
900                self.put(
901                    state_key::param_changes_for_height(change_height),
902                    change.clone(),
903                );
904            }
905            ProposalPayload::CommunityPoolSpend {
906                transaction_plan: _,
907            } => {
908                // All we need to do here is signal to the `App` that we'd like this transaction to
909                // be slotted in at the end of the block:
910                self.deliver_community_pool_transaction(proposal_id).await?;
911            }
912            ProposalPayload::UpgradePlan { height } => {
913                tracing::info!(target_height = height, "upgrade plan proposal passed");
914                self.signal_upgrade(*height).await?;
915            }
916            ProposalPayload::FreezeIbcClient { client_id } => {
917                let client_id = &ClientId::from_str(client_id)
918                    .map_err(|e| tonic::Status::aborted(format!("invalid client id: {e}")))?;
919                let client_state = self.get_client_state(client_id).await?;
920
921                let frozen_client =
922                    client_state.with_frozen_height(ibc_types::core::client::Height {
923                        revision_number: 0,
924                        revision_height: 1,
925                    });
926                self.put_client(client_id, frozen_client);
927            }
928            ProposalPayload::UnfreezeIbcClient { client_id } => {
929                let client_id = &ClientId::from_str(client_id)
930                    .map_err(|e| tonic::Status::aborted(format!("invalid client id: {e}")))?;
931                let client_state = self.get_client_state(client_id).await?;
932
933                let unfrozen_client = client_state.unfrozen();
934                self.put_client(client_id, unfrozen_client);
935            }
936        }
937        Ok(Ok(()))
938    }
939
940    async fn deliver_community_pool_transaction(&mut self, proposal: u64) -> Result<()> {
941        // Schedule for beginning of next block
942        let delivery_height = self.get_block_height().await? + 1;
943
944        tracing::info!(%proposal, %delivery_height, "scheduling Community Pool transaction for delivery at next block");
945
946        self.put_proto(
947            state_key::deliver_single_community_pool_transaction_at_height(
948                delivery_height,
949                proposal,
950            ),
951            proposal,
952        );
953        Ok(())
954    }
955
956    /// Records the next upgrade height.
957    /// After commititng the height, the chain should halt and wait for an upgrade.
958    /// It re-uses the same mechanism as emergency halting that prevents the chain from
959    /// restarting, without setting `halt_bit`.
960    async fn signal_upgrade(&mut self, height: u64) -> Result<()> {
961        self.nonverifiable_put_raw(
962            state_key::upgrades::next_upgrade().into(),
963            height.to_be_bytes().to_vec(),
964        );
965        Ok(())
966    }
967
968    /// Sets the application `halt_bit` to `true`, signaling that
969    /// the chain should be halted, and preventing restarts until
970    /// a migration is ran.
971    fn signal_halt(&mut self) {
972        self.nonverifiable_put_proto(persistent_flags::halt_bit().as_bytes().to_vec(), true);
973    }
974
975    /// Sets the application `halt_bit` to `false`, signaling that
976    /// the chain can resume, and the application is ready to start.
977    fn ready_to_start(&mut self) {
978        self.nonverifiable_put_proto(persistent_flags::halt_bit().as_bytes().to_vec(), false);
979    }
980}
981
982impl<T: StateWrite + StateReadExt + ?Sized> StateWriteExt for T {}