penumbra_sdk_stake/component/action_handler/
validator_definition.rs

1use crate::{
2    component::{
3        action_handler::ActionHandler, validator_handler::ValidatorDataRead,
4        validator_handler::ValidatorManager,
5    },
6    rate::RateData,
7    validator,
8};
9use anyhow::{ensure, Context, Result};
10use async_trait::async_trait;
11use cnidarium::StateWrite;
12use decaf377_rdsa::VerificationKey;
13use penumbra_sdk_proto::DomainType;
14
15#[async_trait]
16impl ActionHandler for validator::Definition {
17    type CheckStatelessContext = ();
18    async fn check_stateless(&self, _context: ()) -> Result<()> {
19        // First, we check that the validator website/name/description does not
20        // exceed 70, 140, and 280 characters respectively. We use guard statements
21        // so that clients can display actionable error messages.
22        if self.validator.website.len() > 70 {
23            anyhow::bail!("validator website field must be less than 70 characters")
24        }
25
26        if self.validator.name.len() > 140 {
27            anyhow::bail!("validator name must be less than 140 characters")
28        }
29
30        if self.validator.description.len() > 280 {
31            anyhow::bail!("validator description must be less than 280 characters")
32        }
33
34        if self.validator.funding_streams.len() > 8 {
35            anyhow::bail!("validators can declare at most 8 funding streams")
36        }
37
38        // This prevents an attacker who compromises a validator identity signing key from locking
39        // the validator in an enabled state permanently, instead making it so that the original
40        // operator always has the option of disabling the validator permanently, regardless of what
41        // the attacker does. This reduces the incentive to steal compromise validator signing keys,
42        // because it reduces the expected payoff of such a compromise.
43        if self.validator.sequence_number == u32::MAX && self.validator.enabled {
44            anyhow::bail!("validators must be disabled when their lifetime is over")
45        }
46
47        // Then, we check the signature:
48        let definition_bytes = self.validator.encode_to_vec();
49        VerificationKey::try_from(self.validator.identity_key.0)
50            .and_then(|vk| vk.verify(&definition_bytes, &self.auth_sig))
51            .context("validator definition signature failed to verify")?;
52
53        let total_funding_bps = self
54            .validator
55            .funding_streams
56            .iter()
57            .map(|fs| fs.rate_bps() as u64)
58            .sum::<u64>();
59
60        if total_funding_bps > 10_000 {
61            anyhow::bail!(
62                "validator defined {} bps of funding streams, greater than 10000bps (= 100%)",
63                total_funding_bps
64            );
65        }
66
67        Ok(())
68    }
69
70    async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
71        // These checks all formerly happened in the `check_stateful` method,
72        // if profiling shows that they cause a bottleneck we could (CAREFULLY)
73        // move some of them back.
74        let new_validator = &self.validator;
75
76        // Check that the sequence numbers of the updated validators is correct...
77        // Check whether we are redefining an existing validator.
78        let prev_definition = state
79            .get_validator_definition(&new_validator.identity_key)
80            .await?;
81
82        if let Some(prev_validator) = &prev_definition {
83            // Ensure that the highest existing sequence number is less than
84            // the new sequence number.
85            // Ensure that the sequence number keeps increasing.
86            let old_seq = prev_validator.sequence_number;
87            let new_seq = new_validator.sequence_number;
88            ensure!(
89                new_seq > old_seq,
90                "definition sequence number must increase (given {}, but previous definition sequence number is {})",
91                new_seq,
92                old_seq,
93            );
94        }
95
96        // Check if the consensus key is known, and if so, that it is by the
97        // validator that declares it in this definition.
98        if let Some(ck_owner) = state
99            .get_validator_definition_by_consensus_key(&new_validator.consensus_key)
100            .await?
101        {
102            // If we detect that the new definition tries to squat someone else's
103            // consensus key, we MUST reject this definition:
104            //
105            // 1. It prevents someone from declaring an (app-level) validator that
106            // "piggybacks" on the actual behavior of someone else's validator.
107            //
108            // 2. If we submit a validator update to CometBFT that
109            // includes duplicate consensus keys, CometBFT gets confused
110            // and hangs.
111            ensure!(
112                ck_owner.identity_key == new_validator.identity_key,
113                "consensus key {:?} is already in use by validator {}",
114                new_validator.consensus_key,
115                ck_owner.identity_key,
116            );
117        }
118
119        /* ------------ execution ----------- */
120        // If the validator is already defined, we update the definition.
121        // Otherwise, we add the new validator and "prime" its state.
122        if prev_definition.is_some() {
123            state
124                .update_validator_definition(new_validator.clone())
125                .await
126                .context(
127                    "should be able to update validator during validator definition execution",
128                )?;
129        } else {
130            let validator_key = new_validator.identity_key;
131
132            // The validator starts with a reward rate of 0 and an exchange rate
133            // of 1, expressed in bps^2 (i.e. 1_0000_0000 is 1.0).
134            let initial_rate_data = RateData {
135                identity_key: validator_key,
136                validator_reward_rate: 0u128.into(),
137                validator_exchange_rate: 1_0000_0000u128.into(),
138            };
139
140            state
141                .add_validator(new_validator.clone(), initial_rate_data)
142                .await
143                .context("should be able to add validator during validator definition execution")?;
144        }
145
146        Ok(())
147    }
148}