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#[derive(Debug, Clone, PartialEq)]
29pub struct BadEncodingFraudProof {
30 header_hash: Hash,
31 block_height: u64,
32 shares: Vec<Option<ShareWithProof>>,
36 index: u16,
38 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 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 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 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 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 return Ok(());
137 }
138
139 if leopard_codec::encode(&mut rebuilt_shares, ods_width).is_err() {
141 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 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 return Ok(());
162 }
163 }
164
165 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 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 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 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 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 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 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 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 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}