Skip to main content

celestia_types/
byzantine.rs

1use celestia_proto::share::eds::byzantine::pb::BadEncoding as RawBadEncodingFraudProof;
2use celestia_proto::share::eds::byzantine::pb::Share as RawShareWithProof;
3use tendermint::block::Height;
4use tendermint_proto::Protobuf;
5
6use crate::bail_validation;
7use crate::consts::appconsts;
8use crate::eds::AxisType;
9use crate::fraud_proof::FraudProof;
10use crate::hash::Hash;
11use crate::nmt::{NS_SIZE, Namespace, NamespaceProof, Nmt, NmtExt};
12use crate::{Error, ExtendedHeader, Result};
13
14/// A proof that the block producer incorrectly encoded [`ExtendedDataSquare`].
15///
16/// A malicious actor may incorrectly extend the original data with the
17/// recovery data, thus making it impossible to reconstruct the original
18/// information.
19///
20/// Light node cannot detect such behaviour with [`Data Availability Sampling`].
21/// When the full node collects all the [`Share`]s of a row or column of the [`ExtendedDataSquare`]
22/// and detects that it was incorrectly encoded, it should create and announce
23/// [`BadEncodingFraudProof`] so that light nodes can reject this block.
24///
25/// [`Data Availability Sampling`]: https://docs.celestia.org/learn/how-celestia-works/data-availability-layer
26/// [`ExtendedDataSquare`]: crate::ExtendedDataSquare
27/// [`Share`]: crate::share::Share
28#[derive(Debug, Clone, PartialEq)]
29pub struct BadEncodingFraudProof {
30    header_hash: Hash,
31    block_height: u64,
32    // ShareWithProof contains all shares from row or col.
33    // Shares that did not pass verification in rsmt2d will be nil (None).
34    // For non-nil shares MerkleProofs are computed.
35    shares: Vec<Option<ShareWithProof>>,
36    // Index represents the row/col index where ErrByzantineRow/ErrByzantineColl occurred.
37    index: u16,
38    // Axis represents the axis that verification failed on.
39    axis: AxisType,
40}
41
42impl FraudProof for BadEncodingFraudProof {
43    const TYPE: &'static str = "badencoding";
44
45    fn header_hash(&self) -> Hash {
46        self.header_hash
47    }
48
49    fn height(&self) -> u64 {
50        self.block_height
51    }
52
53    fn validate(&self, header: &ExtendedHeader) -> Result<()> {
54        if header.height() != self.height() {
55            bail_validation!(
56                "header height ({}) != fraud proof height ({})",
57                header.height(),
58                self.height()
59            );
60        }
61
62        // NOTE: shouldn't ever happen as header should be validated before
63        if header.dah.row_roots().len() != header.dah.column_roots().len() {
64            bail_validation!(
65                "dah rows len ({}) != dah columns len ({})",
66                header.dah.row_roots().len(),
67                header.dah.column_roots().len(),
68            );
69        }
70
71        let square_width = usize::from(header.dah.square_width());
72        let ods_width = square_width / 2;
73
74        if usize::from(self.index) >= square_width {
75            bail_validation!(
76                "fraud proof index ({}) >= dah square width ({})",
77                self.index,
78                square_width,
79            );
80        }
81
82        if self.shares.len() != square_width {
83            bail_validation!(
84                "fraud proof shares len ({}) != dah square width ({})",
85                self.shares.len(),
86                square_width,
87            );
88        }
89
90        let shares_count = self.shares.iter().filter(|s| s.is_some()).count();
91        if shares_count < ods_width {
92            bail_validation!(
93                "fraud proof present shares count ({shares_count}) < dah ods width ({ods_width})"
94            );
95        }
96
97        // verify that Merkle proofs correspond to particular shares.
98        for (share_idx, maybe_share) in self.shares.iter().enumerate() {
99            let Some(share_with_proof) = maybe_share else {
100                continue;
101            };
102
103            let ShareWithProof {
104                leaf: NmtLeaf { namespace, share },
105                proof,
106                proof_axis,
107            } = share_with_proof;
108
109            // unwraps are safe because we validated that index is in range
110            let root = match (self.axis, proof_axis) {
111                (AxisType::Row, AxisType::Row) => header.dah.row_root(self.index).unwrap(),
112                (AxisType::Row, AxisType::Col) => header.dah.column_root(share_idx as u16).unwrap(),
113                (AxisType::Col, AxisType::Row) => header.dah.row_root(share_idx as u16).unwrap(),
114                (AxisType::Col, AxisType::Col) => header.dah.column_root(self.index).unwrap(),
115            };
116
117            proof
118                .verify_range(&root, &[&share], **namespace)
119                .map_err(Error::RangeProofError)?;
120        }
121
122        // rebuild the whole axis
123        let mut rebuilt_shares: Vec<_> = self
124            .shares
125            .iter()
126            .map(|maybe_share| {
127                maybe_share
128                    .clone()
129                    .map(|share_with_proof| share_with_proof.leaf.share)
130                    .unwrap_or_default()
131            })
132            .collect();
133        if leopard_codec::reconstruct(&mut rebuilt_shares, ods_width).is_err() {
134            // we couldn't reconstruct the data even tho we had enough *proven* shares
135            // befp is legit
136            return Ok(());
137        }
138
139        // re-encode the parity data
140        if leopard_codec::encode(&mut rebuilt_shares, ods_width).is_err() {
141            // NOTE: this is unreachable for current implementation of encode, esp. since
142            // reconstruct succeeded, however leaving that as a future-proofing
143            //
144            // we couldn't encode parity data after reconstruction from proven shares
145            // befp is legit
146            return Ok(());
147        }
148
149        let mut nmt = Nmt::default();
150
151        for (n, share) in rebuilt_shares.iter().enumerate() {
152            let ns = if n < ods_width {
153                // safety: length must be correct
154                Namespace::from_raw(&share[..NS_SIZE]).unwrap()
155            } else {
156                Namespace::PARITY_SHARE
157            };
158            if nmt.push_leaf(share, *ns).map_err(Error::Nmt).is_err() {
159                // we couldn't rebuild the nmt from reconstructed data
160                // befp is legit
161                return Ok(());
162            }
163        }
164
165        // unwraps are safe because we validated that index is in range
166        let expected_root = match self.axis {
167            AxisType::Row => header.dah.row_root(self.index).unwrap(),
168            AxisType::Col => header.dah.column_root(self.index).unwrap(),
169        };
170        let root = nmt.root();
171
172        if root == expected_root {
173            bail_validation!("recomputed root hash matches the one in header");
174        }
175
176        Ok(())
177    }
178}
179
180#[derive(Debug, Clone, PartialEq)]
181struct ShareWithProof {
182    leaf: NmtLeaf,
183    proof: NamespaceProof,
184    proof_axis: AxisType,
185}
186
187#[derive(Debug, Clone, PartialEq)]
188struct NmtLeaf {
189    namespace: Namespace,
190    share: Vec<u8>,
191}
192
193impl NmtLeaf {
194    fn new(mut bytes: Vec<u8>) -> Result<Self> {
195        if bytes.len() != appconsts::SHARE_SIZE + NS_SIZE {
196            return Err(Error::InvalidNmtLeafSize(bytes.len()));
197        }
198
199        let share = bytes.split_off(NS_SIZE);
200
201        Ok(Self {
202            namespace: Namespace::from_raw(&bytes)?,
203            share,
204        })
205    }
206
207    fn to_vec(&self) -> Vec<u8> {
208        let mut bytes = self.namespace.as_bytes().to_vec();
209        bytes.extend_from_slice(self.share.as_ref());
210        bytes
211    }
212}
213
214impl TryFrom<RawShareWithProof> for ShareWithProof {
215    type Error = Error;
216
217    fn try_from(value: RawShareWithProof) -> Result<Self, Self::Error> {
218        let leaf = NmtLeaf::new(value.data)?;
219
220        let proof = value.proof.ok_or(Error::MissingProof)?;
221        let proof = NamespaceProof::try_from(proof)?;
222
223        if proof.is_of_absence() {
224            return Err(Error::WrongProofType);
225        }
226
227        let proof_axis = value.proof_axis.try_into()?;
228
229        Ok(Self {
230            leaf,
231            proof,
232            proof_axis,
233        })
234    }
235}
236
237impl From<ShareWithProof> for RawShareWithProof {
238    fn from(value: ShareWithProof) -> Self {
239        RawShareWithProof {
240            data: value.leaf.to_vec(),
241            proof: Some(value.proof.into()),
242            proof_axis: value.proof_axis as i32,
243        }
244    }
245}
246
247impl Protobuf<RawBadEncodingFraudProof> for BadEncodingFraudProof {}
248
249impl TryFrom<RawBadEncodingFraudProof> for BadEncodingFraudProof {
250    type Error = Error;
251
252    fn try_from(value: RawBadEncodingFraudProof) -> Result<Self, Self::Error> {
253        let axis = value.axis.try_into()?;
254
255        let index = u16::try_from(value.index).map_err(|_| Error::EdsInvalidDimentions)?;
256
257        let shares = value
258            .shares
259            .into_iter()
260            .map(|share| {
261                if share.proof.is_some() {
262                    share.try_into().map(Some)
263                } else {
264                    Ok(None)
265                }
266            })
267            .collect::<Result<_, _>>()?;
268
269        Height::try_from(value.height)?;
270
271        Ok(Self {
272            header_hash: value.header_hash.try_into()?,
273            block_height: value.height,
274            shares,
275            index,
276            axis,
277        })
278    }
279}
280
281impl From<BadEncodingFraudProof> for RawBadEncodingFraudProof {
282    fn from(value: BadEncodingFraudProof) -> Self {
283        let shares = value
284            .shares
285            .into_iter()
286            .map(|share| share.map(Into::into).unwrap_or_default())
287            .collect();
288        RawBadEncodingFraudProof {
289            header_hash: value.header_hash.into(),
290            height: value.block_height,
291            shares,
292            index: value.index as u32,
293            axis: value.axis as i32,
294        }
295    }
296}
297
298#[cfg(any(test, feature = "test-utils"))]
299pub(crate) mod test_utils {
300    use nmt_rs::NamespaceProof;
301    use rand::Rng;
302    use rand::seq::index;
303
304    use crate::consts::appconsts::{FIRST_SPARSE_SHARE_CONTENT_SIZE, SHARE_SIZE};
305    use crate::eds::is_ods_square;
306    use crate::test_utils::{ExtendedHeaderGenerator, random_bytes};
307    use crate::{DataAvailabilityHeader, ExtendedDataSquare};
308
309    use super::*;
310
311    /// Corrupts the [`ExtendedDataSquare`] making some row or column unrecoverable.
312    /// Returns the [`ExtendedHeader`] with merkle proofs for incorrect data and [`BadEncodingFraudProof`].
313    pub fn corrupt_eds(
314        generator: &mut ExtendedHeaderGenerator,
315        eds: &mut ExtendedDataSquare,
316    ) -> (ExtendedHeader, BadEncodingFraudProof) {
317        let mut rng = rand::thread_rng();
318
319        let square_width = eds.square_width();
320        let axis = rng.gen_range(0..1).try_into().unwrap();
321        let axis_idx = rng.gen_range(0..square_width);
322
323        // invalidate more than a half shares in axis
324        let shares_to_break = square_width / 2 + 1;
325        for share_idx in index::sample(&mut rng, square_width.into(), shares_to_break.into()) {
326            let share_idx = u16::try_from(share_idx).unwrap();
327
328            let share = match axis {
329                AxisType::Row => eds.share_mut(axis_idx, share_idx).unwrap(),
330                AxisType::Col => eds.share_mut(share_idx, axis_idx).unwrap(),
331            };
332            // only trash data after the namespace, info byte and seq length so that we don't
333            // need to care whether the share is original or parity
334            let offset = SHARE_SIZE - FIRST_SPARSE_SHARE_CONTENT_SIZE;
335            share.as_mut()[offset..].copy_from_slice(&random_bytes(SHARE_SIZE - offset));
336        }
337
338        // create extended header with proof
339        let dah = DataAvailabilityHeader::from_eds(eds);
340        let eh = generator.next_with_dah(dah);
341
342        let befp = befp_from_header_and_eds(&eh, eds, axis_idx, axis);
343
344        (eh, befp)
345    }
346
347    pub(crate) fn befp_from_header_and_eds(
348        eh: &ExtendedHeader,
349        eds: &ExtendedDataSquare,
350        axis_idx: u16,
351        axis: AxisType,
352    ) -> BadEncodingFraudProof {
353        let square_width = eds.square_width();
354        let mut shares_with_proof: Vec<_> = Vec::with_capacity(square_width.into());
355
356        // collect the shares for fraud proof
357        for share_idx in 0..square_width {
358            let proof_axis = if rand::random() {
359                AxisType::Row
360            } else {
361                AxisType::Col
362            };
363
364            let mut nmt = match (axis, proof_axis) {
365                (AxisType::Row, AxisType::Row) => eds.row_nmt(axis_idx).unwrap(),
366                (AxisType::Row, AxisType::Col) => eds.column_nmt(share_idx).unwrap(),
367                (AxisType::Col, AxisType::Row) => eds.row_nmt(share_idx).unwrap(),
368                (AxisType::Col, AxisType::Col) => eds.column_nmt(axis_idx).unwrap(),
369            };
370
371            // The index of the share in the `nmt`.
372            let idx = match (axis, proof_axis) {
373                (AxisType::Row, AxisType::Row) => share_idx,
374                (AxisType::Row, AxisType::Col) => axis_idx,
375                (AxisType::Col, AxisType::Row) => axis_idx,
376                (AxisType::Col, AxisType::Col) => share_idx,
377            };
378
379            let (share, proof) = nmt.get_index_with_proof(idx.into());
380
381            // it doesn't matter which is row and which is column as ods is first quadrant
382            let ns = if is_ods_square(axis_idx, share_idx, square_width) {
383                Namespace::from_raw(&share[..NS_SIZE]).unwrap()
384            } else {
385                Namespace::PARITY_SHARE
386            };
387
388            let share_with_proof = ShareWithProof {
389                leaf: NmtLeaf {
390                    namespace: ns,
391                    share,
392                },
393                proof: NamespaceProof::PresenceProof {
394                    proof,
395                    ignore_max_ns: true,
396                }
397                .into(),
398                proof_axis,
399            };
400
401            shares_with_proof.push(Some(share_with_proof));
402        }
403
404        BadEncodingFraudProof {
405            header_hash: eh.hash(),
406            block_height: eh.height(),
407            shares: shares_with_proof,
408            index: axis_idx,
409            axis,
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use self::test_utils::befp_from_header_and_eds;
417    use super::*;
418    use crate::DataAvailabilityHeader;
419    use crate::consts::appconsts::AppVersion;
420    use crate::test_utils::{ExtendedHeaderGenerator, corrupt_eds, generate_dummy_eds};
421
422    #[cfg(target_arch = "wasm32")]
423    use wasm_bindgen_test::wasm_bindgen_test as test;
424
425    #[test]
426    fn validate_honest_befp_with_incorrectly_encoded_full_row() {
427        let mut generator = ExtendedHeaderGenerator::new();
428        let mut eds = generate_dummy_eds(8, AppVersion::V2);
429        let (eh, proof) = corrupt_eds(&mut generator, &mut eds);
430
431        proof.validate(&eh).unwrap();
432    }
433
434    #[test]
435    fn validate_honest_befp_with_shares_to_rebuild() {
436        let mut generator = ExtendedHeaderGenerator::new();
437        let mut eds = generate_dummy_eds(8, AppVersion::V2);
438        let (eh, mut proof) = corrupt_eds(&mut generator, &mut eds);
439
440        // remove some shares from the proof so they need to be reconstructed
441        for share in proof.shares.iter_mut().step_by(2) {
442            *share = None
443        }
444
445        proof.validate(&eh).unwrap();
446    }
447
448    #[test]
449    fn validate_fake_befp() {
450        let mut generator = ExtendedHeaderGenerator::new();
451        let mut eds = generate_dummy_eds(8, AppVersion::V2);
452        let real_dah = DataAvailabilityHeader::from_eds(&eds);
453
454        let prev_eh = generator.next();
455        let (_fake_eh, proof) = corrupt_eds(&mut generator, &mut eds);
456
457        let real_eh = generator.next_of_with_dah(&prev_eh, real_dah);
458
459        proof.validate(&real_eh).unwrap_err();
460    }
461
462    #[test]
463    fn validate_befp_over_correct_data() {
464        let mut generator = ExtendedHeaderGenerator::new();
465        let eds = generate_dummy_eds(8, AppVersion::V2);
466        let dah = DataAvailabilityHeader::from_eds(&eds);
467        let eh = generator.next_with_dah(dah);
468
469        let proof = befp_from_header_and_eds(&eh, &eds, 2, AxisType::Row);
470        proof.validate(&eh).unwrap_err();
471
472        let proof = befp_from_header_and_eds(&eh, &eds, 3, AxisType::Col);
473        proof.validate(&eh).unwrap_err();
474    }
475
476    #[test]
477    fn validate_befp_wrong_height() {
478        let mut generator = ExtendedHeaderGenerator::new();
479        let mut eds = generate_dummy_eds(8, AppVersion::V2);
480        let (mut eh, proof) = corrupt_eds(&mut generator, &mut eds);
481
482        eh.header.height = 999u32.into();
483
484        proof.validate(&eh).unwrap_err();
485    }
486
487    #[test]
488    fn validate_befp_wrong_roots_square() {
489        let mut generator = ExtendedHeaderGenerator::new();
490        let mut eds = generate_dummy_eds(8, AppVersion::V2);
491        let (mut eh, proof) = corrupt_eds(&mut generator, &mut eds);
492
493        eh.dah = DataAvailabilityHeader::new_unchecked(Vec::new(), eh.dah.column_roots().to_vec());
494
495        proof.validate(&eh).unwrap_err();
496    }
497
498    #[test]
499    fn validate_befp_wrong_index() {
500        let mut generator = ExtendedHeaderGenerator::new();
501        let mut eds = generate_dummy_eds(8, AppVersion::V2);
502        let (eh, mut proof) = corrupt_eds(&mut generator, &mut eds);
503
504        proof.index = 999;
505
506        proof.validate(&eh).unwrap_err();
507    }
508
509    #[test]
510    fn validate_befp_wrong_shares() {
511        let mut generator = ExtendedHeaderGenerator::new();
512        let mut eds = generate_dummy_eds(8, AppVersion::V2);
513        let (eh, mut proof) = corrupt_eds(&mut generator, &mut eds);
514
515        proof.shares = vec![];
516
517        proof.validate(&eh).unwrap_err();
518    }
519}