penumbra_sdk_stake/
validator.rs

1//! Penumbra validators and related structures.
2
3use penumbra_sdk_keys::Address;
4use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
5use serde::{Deserialize, Serialize};
6use serde_unit_struct::{Deserialize_unit_struct, Serialize_unit_struct};
7use serde_with::{serde_as, DisplayFromStr};
8
9use crate::{DelegationToken, FundingStream, FundingStreams, GovernanceKey, IdentityKey};
10
11mod bonding;
12mod definition;
13mod info;
14mod state;
15mod status;
16
17pub use bonding::State as BondingState;
18pub use definition::Definition;
19pub use info::Info;
20pub use state::State;
21pub use status::Status;
22
23/// Describes a Penumbra validator's configuration data.
24///
25/// This data is unauthenticated; the [`Definition`] action includes
26/// a signature over the transaction with the validator's identity key.
27#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
28#[serde(try_from = "pb::Validator", into = "pb::Validator")]
29pub struct Validator {
30    /// The validator's identity verification key.
31    pub identity_key: IdentityKey,
32
33    /// The validator's governance verification key.
34    pub governance_key: GovernanceKey,
35
36    /// The validator's consensus key, used by Tendermint for signing blocks and
37    /// other consensus operations.
38    pub consensus_key: tendermint::PublicKey,
39
40    /// The validator's (human-readable) name.
41    /// Length: <= 140 characters.
42    pub name: String,
43
44    /// The validator's website URL.
45    /// Length: <= 70 characters.
46    pub website: String,
47
48    /// The validator's description.
49    /// Length: <= 280 characters.
50    pub description: String,
51
52    /// Whether the validator is enabled or not.
53    ///
54    /// Disabled validators cannot be delegated to, and immediately begin unbonding.
55    pub enabled: bool,
56
57    /// The destinations for the validator's staking reward. The commission is implicitly defined
58    /// by the configuration of funding_streams, the sum of FundingStream.rate_bps.
59    ///
60    // NOTE: unclaimed rewards are tracked by inserting reward notes for the last epoch into the
61    // SCT at the beginning of each epoch
62    pub funding_streams: FundingStreams,
63
64    /// The sequence number determines which validator data takes priority, and
65    /// prevents replay attacks.  The chain only accepts new
66    /// [`Definition`]s with increasing sequence numbers, preventing a
67    /// third party from replaying previously valid but stale configuration data
68    /// as an update.
69    pub sequence_number: u32,
70}
71
72impl Validator {
73    pub fn token(&self) -> DelegationToken {
74        DelegationToken::new(self.identity_key.clone())
75    }
76}
77
78#[serde_as]
79#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
80pub struct ValidatorToml {
81    /// The sequence number determines which validator data takes priority, and
82    /// prevents replay attacks.  The chain only accepts new [`Definition`]s with
83    /// with increasing sequence numbers, preventing a third-party from replaying
84    /// previously valid but stale configuration data as an update.
85    pub sequence_number: u32,
86
87    /// Whether the validator is enabled or not.
88    ///
89    /// Disabled validators cannot be delegated to, and immediately begin unbonding.
90    pub enabled: bool,
91
92    /// The validator's (human-readable) name.
93    pub name: String,
94
95    /// The validator's website URL.
96    pub website: String,
97
98    /// The validator's description.
99    pub description: String,
100
101    /// The validator's identity verification key.
102    #[serde_as(as = "DisplayFromStr")]
103    pub identity_key: IdentityKey,
104
105    /// The validator's governance verification key.
106    #[serde_as(as = "DisplayFromStr")]
107    pub governance_key: GovernanceKey,
108
109    /// The validator's consensus key, used by Tendermint for signing blocks and
110    /// other consensus operations.
111    pub consensus_key: tendermint::PublicKey,
112
113    /// The destinations for the validator's staking reward. The commission is implicitly defined
114    /// by the configuration of funding_streams, the sum of FundingStream.rate_bps.
115    ///
116    // NOTE: unclaimed rewards are tracked by inserting reward notes for the last epoch into the
117    // SCT at the beginning of each epoch
118    #[serde(rename = "funding_stream")]
119    pub funding_streams: Vec<FundingStreamToml>,
120}
121
122impl From<Validator> for ValidatorToml {
123    fn from(v: Validator) -> Self {
124        ValidatorToml {
125            identity_key: v.identity_key,
126            governance_key: v.governance_key,
127            consensus_key: v.consensus_key,
128            name: v.name,
129            website: v.website,
130            description: v.description,
131            enabled: v.enabled,
132            funding_streams: v.funding_streams.into_iter().map(Into::into).collect(),
133            sequence_number: v.sequence_number,
134        }
135    }
136}
137
138impl TryFrom<ValidatorToml> for Validator {
139    type Error = anyhow::Error;
140
141    fn try_from(v: ValidatorToml) -> anyhow::Result<Self> {
142        // Validation:
143        // - Website has a max length of 70 bytes
144        if v.website.len() > 70 {
145            anyhow::bail!("validator website field must be less than 70 bytes");
146        }
147
148        // - Name has a max length of 140 bytes
149        if v.name.len() > 140 {
150            anyhow::bail!("validator name must be less than 140 bytes");
151        }
152
153        // - Description has a max length of 280 bytes
154        if v.description.len() > 280 {
155            anyhow::bail!("validator description must be less than 280 bytes");
156        }
157
158        Ok(Validator {
159            identity_key: v.identity_key,
160            governance_key: v.governance_key,
161            consensus_key: v.consensus_key,
162            name: v.name,
163            website: v.website,
164            description: v.description,
165            enabled: v.enabled,
166            funding_streams: FundingStreams::try_from(
167                v.funding_streams
168                    .into_iter()
169                    .map(Into::into)
170                    .collect::<Vec<_>>(),
171            )?,
172            sequence_number: v.sequence_number,
173        })
174    }
175}
176
177/// Human-readable TOML-optimized version of a [`FundingStream`].
178#[allow(clippy::large_enum_variant)]
179#[serde_as]
180#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
181#[serde(untagged)]
182pub enum FundingStreamToml {
183    Address {
184        #[serde(rename = "recipient")]
185        #[serde_as(as = "DisplayFromStr")]
186        address: Address,
187        rate_bps: u16,
188    },
189    CommunityPool {
190        recipient: CommunityPool,
191        rate_bps: u16,
192    },
193}
194
195// Unit struct solely to add a `recipient = "CommunityPool"` field to the TOML representation
196#[derive(Debug, PartialEq, Eq, Clone, Deserialize_unit_struct, Serialize_unit_struct)]
197pub struct CommunityPool;
198
199impl From<FundingStream> for FundingStreamToml {
200    fn from(f: FundingStream) -> Self {
201        match f {
202            FundingStream::ToAddress { address, rate_bps } => {
203                FundingStreamToml::Address { address, rate_bps }
204            }
205            FundingStream::ToCommunityPool { rate_bps } => FundingStreamToml::CommunityPool {
206                rate_bps,
207                recipient: CommunityPool,
208            },
209        }
210    }
211}
212
213impl From<FundingStreamToml> for FundingStream {
214    fn from(f: FundingStreamToml) -> Self {
215        match f {
216            FundingStreamToml::Address { address, rate_bps } => {
217                FundingStream::ToAddress { address, rate_bps }
218            }
219            FundingStreamToml::CommunityPool { rate_bps, .. } => {
220                FundingStream::ToCommunityPool { rate_bps }
221            }
222        }
223    }
224}
225
226impl DomainType for Validator {
227    type Proto = pb::Validator;
228}
229
230impl From<Validator> for pb::Validator {
231    fn from(v: Validator) -> Self {
232        pb::Validator {
233            identity_key: Some(v.identity_key.into()),
234            governance_key: Some(v.governance_key.into()),
235            consensus_key: v.consensus_key.to_bytes(),
236            name: v.name,
237            website: v.website,
238            description: v.description,
239            enabled: v.enabled,
240            funding_streams: v.funding_streams.into_iter().map(Into::into).collect(),
241            sequence_number: v.sequence_number,
242        }
243    }
244}
245
246impl TryFrom<pb::Validator> for Validator {
247    type Error = anyhow::Error;
248    fn try_from(v: pb::Validator) -> Result<Self, Self::Error> {
249        // Validation:
250        // - Website has a max length of 70 bytes
251        if v.website.len() > 70 {
252            anyhow::bail!("validator website field must be less than 70 bytes");
253        }
254
255        // - Name has a max length of 140 bytes
256        if v.name.len() > 140 {
257            anyhow::bail!("validator name must be less than 140 bytes");
258        }
259
260        // - Description has a max length of 280 bytes
261        if v.description.len() > 280 {
262            anyhow::bail!("validator description must be less than 280 bytes");
263        }
264
265        Ok(Validator {
266            identity_key: v
267                .identity_key
268                .ok_or_else(|| anyhow::anyhow!("missing identity key"))?
269                .try_into()?,
270            governance_key: v
271                .governance_key
272                .ok_or_else(|| anyhow::anyhow!("missing governance key"))?
273                .try_into()?,
274            consensus_key: tendermint::PublicKey::from_raw_ed25519(&v.consensus_key)
275                .ok_or_else(|| anyhow::anyhow!("invalid ed25519 consensus pubkey"))?,
276            name: v.name,
277            website: v.website,
278            description: v.description,
279            enabled: v.enabled,
280            funding_streams: v
281                .funding_streams
282                .into_iter()
283                .map(TryInto::try_into)
284                .collect::<Result<Vec<FundingStream>, _>>()?
285                .try_into()?,
286            sequence_number: v.sequence_number,
287        })
288    }
289}