Skip to main content

tendermint_machine/slash_reason/
mod.rs

1use core::fmt;
2
3use crate::{
4  BlockNumber, RoundNumber, Validator, ValidatorSet, Signature, AggregateSignature,
5  SignatureScheme, BlockHash, Block, Blockchain, ValidRound, Data, Message, MessageError,
6};
7
8#[cfg(test)]
9mod tests;
10
11/// A pair of datas which equivocate with each other.
12///
13/// These represent two separate [`Data`]s which were published with the same block number and
14/// round number, by the same signer, when an honest validator will always only publish one such
15/// message.
16#[derive(Clone)]
17#[cfg_attr(test, derive(PartialEq, Eq))]
18#[cfg_attr(feature = "alloc", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
19pub(crate) enum EquivocatingData<S: Signature, A: AggregateSignature, H: BlockHash> {
20  Proposal {
21    first_valid_round: Option<ValidRound<A>>,
22    first_proposal: H,
23    second_valid_round: Option<ValidRound<A>>,
24    second_proposal: H,
25  },
26  Prevote {
27    first_block: Option<H>,
28    second_block: Option<H>,
29  },
30  Precommit {
31    first_block_and_precommit_signature: Option<(H, S)>,
32    second_block_and_precommit_signature: Option<(H, S)>,
33  },
34}
35
36impl<S: Signature, A: AggregateSignature, H: fmt::Debug + BlockHash> fmt::Debug
37  for EquivocatingData<S, A, H>
38{
39  fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40    match self {
41      Self::Proposal { first_valid_round, first_proposal, second_valid_round, second_proposal } => {
42        formatter
43          .debug_struct("EquivocatingData::Proposal")
44          .field("first_valid_round", first_valid_round)
45          .field("first_proposal", first_proposal)
46          .field("second_valid_round", second_valid_round)
47          .field("second_proposal", second_proposal)
48          .finish_non_exhaustive()
49      }
50      Self::Prevote { first_block, second_block } => formatter
51        .debug_struct("EquivocatingData::Prevote")
52        .field("first_block", first_block)
53        .field("second_block", second_block)
54        .finish_non_exhaustive(),
55      Self::Precommit {
56        first_block_and_precommit_signature,
57        second_block_and_precommit_signature,
58      } => formatter
59        .debug_struct("EquivocatingData::Precommit")
60        .field(
61          "first_block",
62          &first_block_and_precommit_signature.as_ref().map(|(block, _precommit_signature)| block),
63        )
64        .field(
65          "second_block",
66          &second_block_and_precommit_signature.as_ref().map(|(block, _precommit_signature)| block),
67        )
68        .finish_non_exhaustive(),
69    }
70  }
71}
72
73/// A block hash, yet wrapped as to be opaque.
74///
75/// We use this to ensure that this type only implements the traits we explicitly want implemented.
76/// The main thing we want to avoid is an inadvertent implementation of [`BorshSerialize`] or
77/// [`BorshDeserialize`], as this isn't guaranteed to have an equivalent [`borsh`] representation
78/// to the original type this fills in for.
79#[derive(Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(test, derive(Debug))]
81pub(crate) struct OpaqueBlockHash<'hash>(&'hash [u8]);
82impl AsRef<[u8]> for OpaqueBlockHash<'_> {
83  fn as_ref(&self) -> &[u8] {
84    self.0
85  }
86}
87
88/// A stub block which satisfies the `Block` trait from only a hash.
89///
90/// We use this to build a `Data` (expecting `B: Block`) from solely a hash, as for the purposes of
91/// verifying its signature.
92#[derive(Clone)]
93#[cfg_attr(test, derive(PartialEq, Eq))]
94#[repr(transparent)]
95pub(crate) struct StubBlock<'hash>(&'hash [u8]);
96
97#[cfg(test)]
98impl<'hash> StubBlock<'hash> {
99  pub(crate) fn from(hash: &'hash [u8]) -> Self {
100    Self(hash)
101  }
102}
103impl<'hash> Block for StubBlock<'hash> {
104  type Hash = OpaqueBlockHash<'hash>;
105  fn hash(&self) -> Self::Hash {
106    OpaqueBlockHash(self.0)
107  }
108}
109
110impl<S: Signature, A: AggregateSignature, H: BlockHash> EquivocatingData<S, A, H> {
111  fn split(&self) -> (Data<S, A, StubBlock<'_>>, Data<S, A, StubBlock<'_>>) {
112    match self {
113      EquivocatingData::Proposal {
114        first_valid_round,
115        first_proposal,
116        second_valid_round,
117        second_proposal,
118      } => (
119        Data::Proposal {
120          valid_round: first_valid_round.clone(),
121          proposal: StubBlock(first_proposal.as_ref()),
122        },
123        Data::Proposal {
124          valid_round: second_valid_round.clone(),
125          proposal: StubBlock(second_proposal.as_ref()),
126        },
127      ),
128      EquivocatingData::Prevote { first_block, second_block } => (
129        Data::Prevote { block: first_block.as_ref().map(|hash| OpaqueBlockHash(hash.as_ref())) },
130        Data::Prevote { block: second_block.as_ref().map(|hash| OpaqueBlockHash(hash.as_ref())) },
131      ),
132      EquivocatingData::Precommit {
133        first_block_and_precommit_signature,
134        second_block_and_precommit_signature,
135      } => (
136        Data::Precommit {
137          block_and_precommit_signature: first_block_and_precommit_signature
138            .as_ref()
139            .map(|(block, signature)| (OpaqueBlockHash(block.as_ref()), signature.clone())),
140        },
141        Data::Precommit {
142          block_and_precommit_signature: second_block_and_precommit_signature
143            .as_ref()
144            .map(|(block, signature)| (OpaqueBlockHash(block.as_ref()), signature.clone())),
145        },
146      ),
147    }
148  }
149}
150
151/// Evidence for a slash.
152#[derive(Clone)]
153#[cfg_attr(test, derive(PartialEq, Eq))]
154#[cfg_attr(feature = "alloc", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
155pub(crate) enum Evidence<S: Signature, A: AggregateSignature, H: BlockHash> {
156  /// The validator equivocated, sending two distinct messages when they should have only sent one.
157  Equivocation { data: EquivocatingData<S, A, H>, first_signature: S, second_signature: S },
158  /// The validator created an invalid proposal.
159  InvalidProposal { valid_round: Option<ValidRound<A>>, proposal: H, signature: S },
160  /// The validator created an invalid precommit.
161  InvalidPrecommit { block: H, precommit_signature: S, signature: S },
162}
163
164impl<S: Signature, A: AggregateSignature, H: fmt::Debug + BlockHash> fmt::Debug
165  for Evidence<S, A, H>
166{
167  fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168    match self {
169      Self::Equivocation { data, first_signature: _, second_signature: _ } => {
170        formatter.debug_struct("Evidence::Equivocation").field("data", data).finish_non_exhaustive()
171      }
172      Self::InvalidProposal { valid_round, proposal, signature: _ } => formatter
173        .debug_struct("Evidence::InvalidProposal")
174        .field("valid_round", valid_round)
175        .field("proposal", proposal)
176        .finish_non_exhaustive(),
177      Self::InvalidPrecommit { block, precommit_signature: _, signature: _ } => formatter
178        .debug_struct("Evidence::InvalidPrecommit")
179        .field("block", block)
180        .finish_non_exhaustive(),
181    }
182  }
183}
184
185/// A reason to slash a validator.
186///
187/// This contains the necessary evidence to convince other validators of this slash.
188#[derive(Clone)]
189#[cfg_attr(test, derive(PartialEq, Eq))]
190#[cfg_attr(feature = "alloc", derive(borsh::BorshSerialize, borsh::BorshDeserialize))]
191pub struct SlashReason<S: Signature, A: AggregateSignature, H: BlockHash> {
192  pub(crate) block_number: BlockNumber,
193  pub(crate) round_number: RoundNumber,
194  pub(crate) evidence: Evidence<S, A, H>,
195}
196
197/// The [`SlashReason`] type for a [`Blockchain`].
198pub type SlashReasonFor<B> = SlashReason<
199  <<B as Blockchain>::SignatureScheme as SignatureScheme>::Signature,
200  <<B as Blockchain>::SignatureScheme as SignatureScheme>::AggregateSignature,
201  <<B as Blockchain>::Block as Block>::Hash,
202>;
203
204impl<S: Signature, A: AggregateSignature, H: fmt::Debug + BlockHash> fmt::Debug
205  for SlashReason<S, A, H>
206{
207  fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208    let Self { block_number, round_number, evidence } = self;
209    formatter
210      .debug_struct("SlashReason")
211      .field("block_number", block_number)
212      .field("round_number", round_number)
213      .field("evidence", evidence)
214      .finish()
215  }
216}
217
218/// An invalid reason for a slash was provided.
219#[derive(Clone, Debug)]
220pub struct InvalidReason;
221
222impl<S: Signature, A: AggregateSignature, H: BlockHash> SlashReason<S, A, H> {
223  /// Form a `SlashReason` from a pair of equivocating messages.
224  ///
225  /// This does not guarantee the messages actually form an equivocation. This will return `None`
226  /// if the messages are not of the same type and therefore fundamentally cannot form an
227  /// equivocation.
228  pub(crate) fn equivocation<V: Validator, B: Block<Hash = H>>(
229    first_message: Message<V, S, A, B>,
230    second_message: Message<V, S, A, B>,
231  ) -> Option<Self> {
232    Some(Self {
233      block_number: first_message.block_number,
234      round_number: first_message.round_number,
235      evidence: Evidence::Equivocation {
236        data: match (first_message.data, second_message.data) {
237          (
238            Data::Proposal { valid_round: first_valid_round, proposal: first_proposal },
239            Data::Proposal { valid_round: second_valid_round, proposal: second_proposal },
240          ) => EquivocatingData::Proposal {
241            first_valid_round,
242            first_proposal: first_proposal.hash(),
243            second_valid_round,
244            second_proposal: second_proposal.hash(),
245          },
246          (Data::Prevote { block: first_block }, Data::Prevote { block: second_block }) => {
247            EquivocatingData::Prevote { first_block, second_block }
248          }
249          (
250            Data::Precommit { block_and_precommit_signature: first_block_and_precommit_signature },
251            Data::Precommit { block_and_precommit_signature: second_block_and_precommit_signature },
252          ) => EquivocatingData::Precommit {
253            first_block_and_precommit_signature,
254            second_block_and_precommit_signature,
255          },
256          _ => None?,
257        },
258        first_signature: first_message.signature,
259        second_signature: second_message.signature,
260      },
261    })
262  }
263
264  /// Verify the reasoning for this slash.
265  pub fn verify<V: Validator>(
266    &self,
267    genesis: impl AsRef<[u8]>,
268    validator_set: &(impl ?Sized + ValidatorSet<Validator = V>),
269    signature_scheme: &(impl ?Sized
270        + SignatureScheme<Validator = V, Signature = S, AggregateSignature = A>),
271    validator: V,
272  ) -> Result<(), InvalidReason> {
273    match &self.evidence {
274      Evidence::Equivocation { data, first_signature, second_signature } => {
275        let (data1, data2) = data.split();
276
277        // If these aren't distinct, this isn't an equivocation
278        if data1 == data2 {
279          Err(InvalidReason)?;
280        }
281
282        // Check these were both signed by this validator
283        for (data, signature) in [(data1, first_signature), (data2, second_signature)] {
284          let verify_message = |data, signature| {
285            if !signature_scheme.verify(
286              &validator,
287              Message::<V, S, A, _>::signature_message(
288                genesis.as_ref(),
289                self.block_number,
290                self.round_number,
291                &data,
292              ),
293              signature,
294            ) {
295              /*
296                If this message's signature was invalid, this wasn't actually a message from the
297                accused validator, and this is an invalid reason to slash the accused validator.
298              */
299              Err(InvalidReason)?;
300            }
301
302            Ok(())
303          };
304
305          verify_message(data, signature)?;
306        }
307      }
308      Evidence::InvalidProposal { valid_round, proposal, signature } => {
309        if !matches!(
310          (Message {
311            validator,
312            block_number: self.block_number,
313            round_number: self.round_number,
314            data: Data::Proposal {
315              valid_round: valid_round.clone(),
316              proposal: StubBlock(proposal.as_ref()),
317            },
318            signature: signature.clone(),
319          })
320          .static_verificiation(genesis, validator_set, signature_scheme),
321          Err(MessageError::Invalid(SlashReason { .. }))
322        ) {
323          Err(InvalidReason)?;
324        }
325      }
326      Evidence::InvalidPrecommit { block, precommit_signature, signature } => {
327        if !matches!(
328          (Message::<_, _, _, StubBlock<'_>> {
329            validator,
330            block_number: self.block_number,
331            round_number: self.round_number,
332            data: Data::Precommit {
333              block_and_precommit_signature: Some((
334                StubBlock(block.as_ref()).hash(),
335                precommit_signature.clone()
336              )),
337            },
338            signature: signature.clone(),
339          })
340          .static_verificiation(genesis, validator_set, signature_scheme),
341          Err(MessageError::Invalid(SlashReason { .. }))
342        ) {
343          Err(InvalidReason)?;
344        }
345      }
346    }
347
348    Ok(())
349  }
350}