celestia_tendermint/
evidence.rs

1//! Evidence of malfeasance by validators (i.e. signing conflicting votes).
2
3use core::{
4    convert::{TryFrom, TryInto},
5    slice,
6};
7
8use celestia_tendermint_proto::google::protobuf::Duration as RawDuration;
9use celestia_tendermint_proto::Protobuf;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    block::{signed_header::SignedHeader, Height},
14    error::Error,
15    prelude::*,
16    serializers, validator,
17    vote::Power,
18    Time, Vote,
19};
20
21/// Evidence of malfeasance by validators (i.e. signing conflicting votes or light client attack).
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum Evidence {
24    /// Duplicate vote evidence
25    DuplicateVote(Box<DuplicateVoteEvidence>),
26
27    /// LightClient attack evidence
28    LightClientAttack(Box<LightClientAttackEvidence>),
29}
30
31impl From<LightClientAttackEvidence> for Evidence {
32    fn from(ev: LightClientAttackEvidence) -> Self {
33        Self::LightClientAttack(Box::new(ev))
34    }
35}
36
37impl From<DuplicateVoteEvidence> for Evidence {
38    fn from(ev: DuplicateVoteEvidence) -> Self {
39        Self::DuplicateVote(Box::new(ev))
40    }
41}
42
43/// Duplicate vote evidence
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct DuplicateVoteEvidence {
46    pub vote_a: Vote,
47    pub vote_b: Vote,
48    pub total_voting_power: Power,
49    pub validator_power: Power,
50    pub timestamp: Time,
51}
52
53impl DuplicateVoteEvidence {
54    /// constructor
55    pub fn new(vote_a: Vote, vote_b: Vote) -> Result<Self, Error> {
56        if vote_a.height != vote_b.height {
57            return Err(Error::invalid_evidence());
58        }
59
60        // Todo: make more assumptions about what is considered a valid evidence for duplicate vote
61        Ok(Self {
62            vote_a,
63            vote_b,
64            total_voting_power: Default::default(),
65            validator_power: Default::default(),
66            timestamp: Time::unix_epoch(),
67        })
68    }
69
70    /// Get votes
71    pub fn votes(&self) -> (&Vote, &Vote) {
72        (&self.vote_a, &self.vote_b)
73    }
74}
75
76/// Conflicting block detected in light client attack
77#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
78pub struct ConflictingBlock {
79    pub signed_header: SignedHeader,
80    pub validator_set: validator::Set,
81}
82
83/// Light client attack evidence
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub struct LightClientAttackEvidence {
86    pub conflicting_block: ConflictingBlock,
87    pub common_height: Height,
88    pub byzantine_validators: Vec<validator::Info>,
89    pub total_voting_power: Power,
90    pub timestamp: Time,
91}
92
93/// A list of `Evidence`.
94///
95/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#evidencedata>
96#[derive(Clone, Debug, Default, PartialEq, Eq)]
97pub struct List(Vec<Evidence>);
98
99impl List {
100    /// Create a new evidence data collection
101    pub fn new<I>(into_evidence: I) -> List
102    where
103        I: Into<Vec<Evidence>>,
104    {
105        List(into_evidence.into())
106    }
107
108    /// Convert this evidence data into a vector
109    pub fn into_vec(self) -> Vec<Evidence> {
110        self.0
111    }
112
113    /// Iterate over the evidence data
114    pub fn iter(&self) -> slice::Iter<'_, Evidence> {
115        self.0.iter()
116    }
117}
118
119impl AsRef<[Evidence]> for List {
120    fn as_ref(&self) -> &[Evidence] {
121        &self.0
122    }
123}
124
125/// EvidenceParams determine how we handle evidence of malfeasance.
126///
127/// [Tendermint documentation](https://docs.tendermint.com/master/spec/core/data_structures.html#evidenceparams)
128#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
129pub struct Params {
130    /// Max age of evidence, in blocks.
131    #[serde(with = "serializers::from_str")]
132    pub max_age_num_blocks: u64,
133
134    /// Max age of evidence, in time.
135    ///
136    /// It should correspond with an app's "unbonding period" or other similar
137    /// mechanism for handling [Nothing-At-Stake attacks][nas].
138    ///
139    /// [nas]: https://github.com/ethereum/wiki/wiki/Proof-of-Stake-FAQ#what-is-the-nothing-at-stake-problem-and-how-can-it-be-fixed
140    pub max_age_duration: Duration,
141
142    /// This sets the maximum size of total evidence in bytes that can be
143    /// committed in a single block, and should fall comfortably under the max
144    /// block bytes. The default is 1048576 or 1MB.
145    #[serde(with = "serializers::from_str", default)]
146    pub max_bytes: i64,
147}
148
149// =============================================================================
150// Protobuf conversions
151// =============================================================================
152
153tendermint_pb_modules! {
154    use pb::types as raw;
155
156    use super::{List, LightClientAttackEvidence, DuplicateVoteEvidence, ConflictingBlock, Evidence, Params};
157    use crate::{error::Error, prelude::*};
158
159    impl Protobuf<raw::Evidence> for Evidence {}
160
161    impl TryFrom<raw::Evidence> for Evidence {
162        type Error = Error;
163
164        fn try_from(value: raw::Evidence) -> Result<Self, Self::Error> {
165            match value.sum.ok_or_else(Error::invalid_evidence)? {
166                raw::evidence::Sum::DuplicateVoteEvidence(ev) => {
167                    Ok(Evidence::DuplicateVote(Box::new(ev.try_into()?)))
168                },
169                raw::evidence::Sum::LightClientAttackEvidence(ev) => {
170                    Ok(Evidence::LightClientAttack(Box::new(ev.try_into()?)))
171                },
172            }
173        }
174    }
175
176    impl From<Evidence> for raw::Evidence {
177        fn from(value: Evidence) -> Self {
178            match value {
179                Evidence::DuplicateVote(ev) => raw::Evidence {
180                    sum: Some(raw::evidence::Sum::DuplicateVoteEvidence((*ev).into())),
181                },
182                Evidence::LightClientAttack(ev) => raw::Evidence {
183                    sum: Some(raw::evidence::Sum::LightClientAttackEvidence((*ev).into())),
184                },
185            }
186        }
187    }
188
189    impl Protobuf<raw::DuplicateVoteEvidence> for DuplicateVoteEvidence {}
190
191    impl TryFrom<raw::DuplicateVoteEvidence> for DuplicateVoteEvidence {
192        type Error = Error;
193
194        fn try_from(value: raw::DuplicateVoteEvidence) -> Result<Self, Self::Error> {
195            Ok(Self {
196                vote_a: value
197                    .vote_a
198                    .ok_or_else(Error::missing_evidence)?
199                    .try_into()?,
200                vote_b: value
201                    .vote_b
202                    .ok_or_else(Error::missing_evidence)?
203                    .try_into()?,
204                total_voting_power: value.total_voting_power.try_into()?,
205                validator_power: value.validator_power.try_into()?,
206                timestamp: value
207                    .timestamp
208                    .ok_or_else(Error::missing_timestamp)?
209                    .try_into()?,
210            })
211        }
212    }
213
214    impl From<DuplicateVoteEvidence> for raw::DuplicateVoteEvidence {
215        fn from(value: DuplicateVoteEvidence) -> Self {
216            raw::DuplicateVoteEvidence {
217                vote_a: Some(value.vote_a.into()),
218                vote_b: Some(value.vote_b.into()),
219                total_voting_power: value.total_voting_power.into(),
220                validator_power: value.total_voting_power.into(),
221                timestamp: Some(value.timestamp.into()),
222            }
223        }
224    }
225
226    impl Protobuf<raw::LightBlock> for ConflictingBlock {}
227
228    impl TryFrom<raw::LightBlock> for ConflictingBlock {
229        type Error = Error;
230
231        fn try_from(value: raw::LightBlock) -> Result<Self, Self::Error> {
232            Ok(ConflictingBlock {
233                signed_header: value
234                    .signed_header
235                    .ok_or_else(Error::missing_evidence)?
236                    .try_into()?,
237                validator_set: value
238                    .validator_set
239                    .ok_or_else(Error::missing_evidence)?
240                    .try_into()?,
241            })
242        }
243    }
244
245    impl From<ConflictingBlock> for raw::LightBlock {
246        fn from(value: ConflictingBlock) -> Self {
247            raw::LightBlock {
248                signed_header: Some(value.signed_header.into()),
249                validator_set: Some(value.validator_set.into()),
250            }
251        }
252    }
253
254    impl Protobuf<raw::LightClientAttackEvidence> for LightClientAttackEvidence {}
255
256    impl TryFrom<raw::LightClientAttackEvidence> for LightClientAttackEvidence {
257        type Error = Error;
258
259        fn try_from(ev: raw::LightClientAttackEvidence) -> Result<Self, Self::Error> {
260            Ok(LightClientAttackEvidence {
261                conflicting_block: ev
262                    .conflicting_block
263                    .ok_or_else(Error::missing_evidence)?
264                    .try_into()?,
265                common_height: ev.common_height.try_into()?,
266                byzantine_validators: ev
267                    .byzantine_validators
268                    .into_iter()
269                    .map(TryInto::try_into)
270                    .collect::<Result<Vec<_>, _>>()?,
271                total_voting_power: ev.total_voting_power.try_into()?,
272                timestamp: ev
273                    .timestamp
274                    .ok_or_else(Error::missing_timestamp)?
275                    .try_into()?,
276            })
277        }
278    }
279
280    impl From<LightClientAttackEvidence> for raw::LightClientAttackEvidence {
281        fn from(ev: LightClientAttackEvidence) -> Self {
282            raw::LightClientAttackEvidence {
283                conflicting_block: Some(ev.conflicting_block.into()),
284                common_height: ev.common_height.into(),
285                byzantine_validators: ev
286                    .byzantine_validators
287                    .into_iter()
288                    .map(Into::into)
289                    .collect(),
290                total_voting_power: ev.total_voting_power.into(),
291                timestamp: Some(ev.timestamp.into()),
292            }
293        }
294    }
295
296    impl Protobuf<raw::EvidenceList> for List {}
297
298    impl TryFrom<raw::EvidenceList> for List {
299        type Error = Error;
300        fn try_from(value: raw::EvidenceList) -> Result<Self, Self::Error> {
301            let evidence = value
302                .evidence
303                .into_iter()
304                .map(TryInto::try_into)
305                .collect::<Result<Vec<_>, _>>()?;
306            Ok(Self(evidence))
307        }
308    }
309
310    impl From<List> for raw::EvidenceList {
311        fn from(value: List) -> Self {
312            raw::EvidenceList {
313                evidence: value.0.into_iter().map(Into::into).collect(),
314            }
315        }
316    }
317
318    impl Protobuf<raw::EvidenceParams> for Params {}
319
320    impl TryFrom<raw::EvidenceParams> for Params {
321        type Error = Error;
322
323        fn try_from(value: raw::EvidenceParams) -> Result<Self, Self::Error> {
324            Ok(Self {
325                max_age_num_blocks: value
326                    .max_age_num_blocks
327                    .try_into()
328                    .map_err(Error::negative_max_age_num)?,
329                max_age_duration: value
330                    .max_age_duration
331                    .ok_or_else(Error::missing_max_age_duration)?
332                    .try_into()?,
333                max_bytes: value.max_bytes,
334            })
335        }
336    }
337
338    impl From<Params> for raw::EvidenceParams {
339        fn from(value: Params) -> Self {
340            Self {
341                // Todo: Implement proper domain types so this becomes infallible
342                max_age_num_blocks: value.max_age_num_blocks.try_into().unwrap(),
343                max_age_duration: Some(value.max_age_duration.into()),
344                max_bytes: value.max_bytes,
345            }
346        }
347    }
348}
349
350/// Duration is a wrapper around core::time::Duration
351/// essentially, to keep the usages look cleaner
352/// i.e. you can avoid using serde annotations everywhere
353/// Todo: harmonize google::protobuf::Duration, core::time::Duration and this. Too many structs.
354/// <https://github.com/informalsystems/tendermint-rs/issues/741>
355#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
356pub struct Duration(#[serde(with = "serializers::time_duration")] pub core::time::Duration);
357
358impl From<Duration> for core::time::Duration {
359    fn from(d: Duration) -> core::time::Duration {
360        d.0
361    }
362}
363
364impl Protobuf<RawDuration> for Duration {}
365
366impl TryFrom<RawDuration> for Duration {
367    type Error = Error;
368
369    fn try_from(value: RawDuration) -> Result<Self, Self::Error> {
370        Ok(Self(core::time::Duration::new(
371            value.seconds.try_into().map_err(Error::integer_overflow)?,
372            value.nanos.try_into().map_err(Error::integer_overflow)?,
373        )))
374    }
375}
376
377impl From<Duration> for RawDuration {
378    fn from(value: Duration) -> Self {
379        // Todo: make the struct into a proper domaintype so this becomes infallible.
380        Self {
381            seconds: value.0.as_secs() as i64,
382            nanos: value.0.subsec_nanos() as i32,
383        }
384    }
385}