1use alloc::vec::Vec;
2
3use primitive_types::U256;
4use zcash_encoding::CompactSize;
5
6use crate::Version;
7
8pub const MAX_NODE_DATA_SIZE: usize = 32 + 4 + 4 + 4 + 4 + 32 + 32 + 32 + 9 + 9 + 9 + 32 + 32 + 9 + 32 + 32 + 9; #[repr(C)]
30#[derive(Debug, Clone, Default)]
31#[cfg_attr(test, derive(PartialEq, Eq))]
32pub struct NodeData {
33 pub consensus_branch_id: u32,
35 pub subtree_commitment: [u8; 32],
37 pub start_time: u32,
39 pub end_time: u32,
41 pub start_target: u32,
43 pub end_target: u32,
45 pub start_sapling_root: [u8; 32],
47 pub end_sapling_root: [u8; 32],
49 pub subtree_total_work: U256,
51 pub start_height: u64,
53 pub end_height: u64,
55 pub sapling_tx: u64,
57}
58
59impl NodeData {
60 pub fn combine(left: &NodeData, right: &NodeData) -> NodeData {
62 crate::V1::combine(left, right)
63 }
64
65 pub(crate) fn combine_inner(
66 subtree_commitment: [u8; 32],
67 left: &NodeData,
68 right: &NodeData,
69 ) -> NodeData {
70 NodeData {
71 consensus_branch_id: left.consensus_branch_id,
72 subtree_commitment,
73 start_time: left.start_time,
74 end_time: right.end_time,
75 start_target: left.start_target,
76 end_target: right.end_target,
77 start_sapling_root: left.start_sapling_root,
78 end_sapling_root: right.end_sapling_root,
79 subtree_total_work: left.subtree_total_work + right.subtree_total_work,
80 start_height: left.start_height,
81 end_height: right.end_height,
82 sapling_tx: left.sapling_tx + right.sapling_tx,
83 }
84 }
85
86 pub fn write<W: corez::io::Write>(&self, w: &mut W) -> corez::io::Result<()> {
88 w.write_all(&self.subtree_commitment)?;
89 w.write_all(&self.start_time.to_le_bytes())?;
90 w.write_all(&self.end_time.to_le_bytes())?;
91 w.write_all(&self.start_target.to_le_bytes())?;
92 w.write_all(&self.end_target.to_le_bytes())?;
93 w.write_all(&self.start_sapling_root)?;
94 w.write_all(&self.end_sapling_root)?;
95
96 let mut work_buf = [0u8; 32];
97 self.subtree_total_work.to_little_endian(&mut work_buf[..]);
98 w.write_all(&work_buf)?;
99
100 CompactSize::write_unbounded(&mut *w, self.start_height)?;
101 CompactSize::write_unbounded(&mut *w, self.end_height)?;
102 CompactSize::write_unbounded(&mut *w, self.sapling_tx)?;
103 Ok(())
104 }
105
106 pub fn read<R: corez::io::Read>(
114 consensus_branch_id: u32,
115 r: &mut R,
116 ) -> corez::io::Result<Self> {
117 let mut data = NodeData {
118 consensus_branch_id,
119 ..Default::default()
120 };
121 r.read_exact(&mut data.subtree_commitment)?;
122 let mut buf = [0u8; 4];
123 r.read_exact(&mut buf)?;
124 data.start_time = u32::from_le_bytes(buf);
125 r.read_exact(&mut buf)?;
126 data.end_time = u32::from_le_bytes(buf);
127 r.read_exact(&mut buf)?;
128 data.start_target = u32::from_le_bytes(buf);
129 r.read_exact(&mut buf)?;
130 data.end_target = u32::from_le_bytes(buf);
131 r.read_exact(&mut data.start_sapling_root)?;
132 r.read_exact(&mut data.end_sapling_root)?;
133
134 let mut work_buf = [0u8; 32];
135 r.read_exact(&mut work_buf)?;
136 data.subtree_total_work = U256::from_little_endian(&work_buf);
137
138 data.start_height = CompactSize::read_unbounded(&mut *r)?;
139 data.end_height = CompactSize::read_unbounded(&mut *r)?;
140 if data
141 .end_height
142 .checked_sub(data.start_height)
143 .and_then(|height_diff| height_diff.checked_add(1))
144 .is_none()
145 {
146 return Err(corez::io::Error::new(
147 corez::io::ErrorKind::InvalidData,
148 "history node height range does not contain a representable number of blocks",
149 ));
150 }
151 data.sapling_tx = CompactSize::read_unbounded(&mut *r)?;
152
153 Ok(data)
154 }
155
156 pub fn to_bytes(&self) -> Vec<u8> {
158 crate::V1::to_bytes(self)
159 }
160
161 pub fn from_bytes<T: AsRef<[u8]>>(consensus_branch_id: u32, buf: T) -> corez::io::Result<Self> {
169 crate::V1::from_bytes(consensus_branch_id, buf)
170 }
171
172 pub fn hash(&self) -> [u8; 32] {
174 crate::V1::hash(self)
175 }
176}
177
178#[derive(Debug, Clone, Default)]
180#[cfg_attr(test, derive(PartialEq, Eq))]
181pub struct V2 {
182 pub v1: NodeData,
184 pub start_orchard_root: [u8; 32],
186 pub end_orchard_root: [u8; 32],
188 pub orchard_tx: u64,
190}
191
192impl V2 {
193 pub(crate) fn combine_inner(subtree_commitment: [u8; 32], left: &V2, right: &V2) -> V2 {
194 V2 {
195 v1: NodeData::combine_inner(subtree_commitment, &left.v1, &right.v1),
196 start_orchard_root: left.start_orchard_root,
197 end_orchard_root: right.end_orchard_root,
198 orchard_tx: left.orchard_tx + right.orchard_tx,
199 }
200 }
201
202 pub fn write<W: corez::io::Write>(&self, w: &mut W) -> corez::io::Result<()> {
204 self.v1.write(w)?;
205 w.write_all(&self.start_orchard_root)?;
206 w.write_all(&self.end_orchard_root)?;
207 CompactSize::write_unbounded(&mut *w, self.orchard_tx)?;
208 Ok(())
209 }
210
211 pub fn read<R: corez::io::Read>(
213 consensus_branch_id: u32,
214 r: &mut R,
215 ) -> corez::io::Result<Self> {
216 let mut data = V2 {
217 v1: NodeData::read(consensus_branch_id, r)?,
218 ..Default::default()
219 };
220 r.read_exact(&mut data.start_orchard_root)?;
221 r.read_exact(&mut data.end_orchard_root)?;
222 data.orchard_tx = CompactSize::read_unbounded(&mut *r)?;
223
224 Ok(data)
225 }
226}
227
228#[derive(Debug, Clone, Default)]
234#[cfg_attr(test, derive(PartialEq, Eq))]
235pub struct V3 {
236 pub v2: V2,
238 pub start_ironwood_root: [u8; 32],
244 pub end_ironwood_root: [u8; 32],
250 pub ironwood_tx: u64,
252}
253
254impl V3 {
255 pub(crate) fn combine_inner(subtree_commitment: [u8; 32], left: &V3, right: &V3) -> V3 {
256 V3 {
257 v2: V2::combine_inner(subtree_commitment, &left.v2, &right.v2),
258 start_ironwood_root: left.start_ironwood_root,
259 end_ironwood_root: right.end_ironwood_root,
260 ironwood_tx: left.ironwood_tx + right.ironwood_tx,
261 }
262 }
263
264 pub fn write<W: corez::io::Write>(&self, w: &mut W) -> corez::io::Result<()> {
266 self.v2.write(w)?;
267 w.write_all(&self.start_ironwood_root)?;
268 w.write_all(&self.end_ironwood_root)?;
269 CompactSize::write_unbounded(&mut *w, self.ironwood_tx)?;
270 Ok(())
271 }
272
273 pub fn read<R: corez::io::Read>(
275 consensus_branch_id: u32,
276 r: &mut R,
277 ) -> corez::io::Result<Self> {
278 let mut data = V3 {
279 v2: V2::read(consensus_branch_id, r)?,
280 ..Default::default()
281 };
282 r.read_exact(&mut data.start_ironwood_root)?;
283 r.read_exact(&mut data.end_ironwood_root)?;
284 data.ironwood_tx = CompactSize::read_unbounded(&mut *r)?;
285
286 Ok(data)
287 }
288}
289
290#[cfg(any(test, feature = "test-dependencies"))]
291pub mod testing {
292 use primitive_types::U256;
293 use proptest::array::uniform32;
294 use proptest::prelude::{any, prop_compose};
295
296 use super::NodeData;
297
298 prop_compose! {
299 pub fn arb_node_data()(
300 subtree_commitment in uniform32(any::<u8>()),
301 start_time in any::<u32>(),
302 end_time in any::<u32>(),
303 start_target in any::<u32>(),
304 end_target in any::<u32>(),
305 start_sapling_root in uniform32(any::<u8>()),
306 end_sapling_root in uniform32(any::<u8>()),
307 subtree_total_work in uniform32(any::<u8>()),
308 start_height in any::<u64>(),
309 end_height in any::<u64>(),
310 sapling_tx in any::<u64>(),
311 ) -> NodeData {
312 NodeData {
313 consensus_branch_id: 0,
314 subtree_commitment,
315 start_time,
316 end_time,
317 start_target,
318 end_target,
319 start_sapling_root,
320 end_sapling_root,
321 subtree_total_work: U256::from_little_endian(&subtree_total_work[..]),
322 start_height,
323 end_height,
324 sapling_tx
325 }
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use alloc::vec::Vec;
333
334 use super::testing::arb_node_data;
335 use proptest::prelude::*;
336
337 use primitive_types::U256;
338
339 use crate::{
340 Entry, EntryLink, MAX_ENTRY_SIZE, V1 as HistoryV1, V2 as HistoryV2, V3 as HistoryV3,
341 Version,
342 };
343
344 use super::{MAX_NODE_DATA_SIZE, NodeData, V2, V3};
345
346 fn node_data(start_height: u64, end_height: u64) -> NodeData {
347 NodeData {
348 consensus_branch_id: 1,
349 subtree_commitment: [1; 32],
350 start_time: 2,
351 end_time: 3,
352 start_target: 4,
353 end_target: 5,
354 start_sapling_root: [6; 32],
355 end_sapling_root: [7; 32],
356 subtree_total_work: U256::from(8u64),
357 start_height,
358 end_height,
359 sapling_tx: 9,
360 }
361 }
362
363 fn node_data_v2(start_height: u64, end_height: u64) -> V2 {
364 V2 {
365 v1: node_data(start_height, end_height),
366 start_orchard_root: [10; 32],
367 end_orchard_root: [11; 32],
368 orchard_tx: 12,
369 }
370 }
371
372 fn node_data_v3(start_height: u64, end_height: u64) -> V3 {
373 V3 {
374 v2: node_data_v2(start_height, end_height),
375 start_ironwood_root: [13; 32],
376 end_ironwood_root: [14; 32],
377 ironwood_tx: 15,
378 }
379 }
380
381 fn max_node_data() -> NodeData {
382 NodeData {
383 consensus_branch_id: u32::MAX,
384 subtree_commitment: [1; 32],
385 start_time: u32::MAX,
386 end_time: u32::MAX,
387 start_target: u32::MAX,
388 end_target: u32::MAX,
389 start_sapling_root: [2; 32],
390 end_sapling_root: [3; 32],
391 subtree_total_work: U256::MAX,
392 start_height: u64::MAX,
393 end_height: u64::MAX,
394 sapling_tx: u64::MAX,
395 }
396 }
397
398 fn max_node_data_v2() -> V2 {
399 V2 {
400 v1: max_node_data(),
401 start_orchard_root: [4; 32],
402 end_orchard_root: [5; 32],
403 orchard_tx: u64::MAX,
404 }
405 }
406
407 fn max_node_data_v3() -> V3 {
408 V3 {
409 v2: max_node_data_v2(),
410 start_ironwood_root: [6; 32],
411 end_ironwood_root: [7; 32],
412 ironwood_tx: u64::MAX,
413 }
414 }
415
416 fn v1_fixture_bytes() -> Vec<u8> {
417 let mut expected = vec![];
418 expected.extend_from_slice(&[1; 32]);
419 expected.extend_from_slice(&2u32.to_le_bytes());
420 expected.extend_from_slice(&3u32.to_le_bytes());
421 expected.extend_from_slice(&4u32.to_le_bytes());
422 expected.extend_from_slice(&5u32.to_le_bytes());
423 expected.extend_from_slice(&[6; 32]);
424 expected.extend_from_slice(&[7; 32]);
425 expected.extend_from_slice(&8u64.to_le_bytes());
426 expected.extend_from_slice(&[0; 24]);
427 expected.push(1);
428 expected.push(2);
429 expected.push(9);
430 expected
431 }
432
433 fn v2_fixture_bytes() -> Vec<u8> {
434 let mut expected = v1_fixture_bytes();
435 expected.extend_from_slice(&[10; 32]);
436 expected.extend_from_slice(&[11; 32]);
437 expected.push(12);
438 expected
439 }
440
441 fn v3_fixture_bytes() -> Vec<u8> {
442 let mut expected = v2_fixture_bytes();
443 expected.extend_from_slice(&[13; 32]);
444 expected.extend_from_slice(&[14; 32]);
445 expected.push(15);
446 expected
447 }
448
449 proptest! {
450 #[test]
451 fn serialization_round_trip(node_data in arb_node_data()) {
452 let decoded = NodeData::from_bytes(0, node_data.to_bytes());
453 let leaf_count = node_data
454 .end_height
455 .checked_sub(node_data.start_height)
456 .and_then(|height_diff| height_diff.checked_add(1));
457
458 if leaf_count.is_some() {
459 prop_assert_eq!(decoded.unwrap(), node_data);
460 } else {
461 prop_assert_eq!(
462 decoded.unwrap_err().kind(),
463 corez::io::ErrorKind::InvalidData
464 );
465 }
466 }
467 }
468
469 #[test]
470 fn genesis_height_round_trip() {
471 let node_data = NodeData {
472 start_height: 0,
473 end_height: 0,
474 ..Default::default()
475 };
476 let entry = Entry::<HistoryV1>::new_leaf(node_data);
477 let mut encoded = vec![];
478 entry.write(&mut encoded).unwrap();
479
480 assert_eq!(
481 Entry::<HistoryV1>::from_bytes(0, encoded)
482 .unwrap()
483 .leaf_count(),
484 1
485 );
486 }
487
488 #[test]
489 fn zero_start_height_combined_node_round_trip() {
490 let left = Entry::<HistoryV1>::new_leaf(node_data(0, 0));
494 let right = Entry::<HistoryV1>::new_leaf(node_data(1, 1));
495 let combined = HistoryV1::combine(left.data(), right.data());
496 let entry = Entry::<HistoryV1>::new(combined, EntryLink::Stored(0), EntryLink::Stored(1));
497 let mut encoded = vec![];
498 entry.write(&mut encoded).unwrap();
499
500 let decoded = Entry::<HistoryV1>::from_bytes(0, encoded).unwrap();
501 assert_eq!(decoded.leaf_count(), 2);
502 assert!(decoded.complete());
503 }
504
505 #[test]
506 fn invalid_height_ranges_are_rejected() {
507 for (start_height, end_height) in [
508 (200, 5),
510 (u64::MAX, 5),
511 (0, u64::MAX),
513 ] {
514 let node_data = NodeData {
515 start_height,
516 end_height,
517 ..Default::default()
518 };
519 let entry = Entry::<HistoryV1>::new_leaf(node_data);
520 let mut encoded = vec![];
521 entry.write(&mut encoded).unwrap();
522 let error = match Entry::<HistoryV1>::from_bytes(0, encoded) {
523 Ok(_) => panic!("invalid height range was accepted"),
524 Err(error) => error,
525 };
526
527 assert_eq!(error.kind(), corez::io::ErrorKind::InvalidData);
528 }
529 }
530
531 #[test]
532 fn v1_and_v2_serialization_fixtures_are_stable() {
533 assert_eq!(HistoryV1::to_bytes(&node_data(1, 2)), v1_fixture_bytes());
534 assert_eq!(HistoryV2::to_bytes(&node_data_v2(1, 2)), v2_fixture_bytes());
535 }
536
537 #[test]
538 fn v3_serialization_round_trip() {
539 let node_data = node_data_v3(1, 2);
540
541 assert_eq!(
542 HistoryV3::from_bytes(1, v3_fixture_bytes()).unwrap(),
543 node_data
544 );
545 assert_eq!(HistoryV3::to_bytes(&node_data), v3_fixture_bytes());
546 }
547
548 #[test]
549 fn max_serialized_sizes_cover_all_versions() {
550 assert_eq!(HistoryV1::to_bytes(&max_node_data()).len(), 171);
554 assert_eq!(HistoryV2::to_bytes(&max_node_data_v2()).len(), 244);
555 let max_v3_bytes = HistoryV3::to_bytes(&max_node_data_v3());
556 assert_eq!(max_v3_bytes.len(), MAX_NODE_DATA_SIZE);
557 assert_eq!(
558 HistoryV3::from_bytes(u32::MAX, &max_v3_bytes).unwrap(),
559 max_node_data_v3()
560 );
561 assert_eq!(MAX_NODE_DATA_SIZE, 317);
562
563 let entry = Entry::<HistoryV3>::new(
564 max_node_data_v3(),
565 EntryLink::Stored(u32::MAX),
566 EntryLink::Stored(u32::MAX),
567 );
568 let mut encoded = vec![];
569 entry.write(&mut encoded).unwrap();
570 assert_eq!(encoded.len(), MAX_ENTRY_SIZE);
571 }
572
573 #[test]
574 fn v3_combine_tracks_ironwood_fields() {
575 let mut left = node_data_v3(1, 1);
576 left.start_ironwood_root = [16; 32];
577 left.end_ironwood_root = [17; 32];
578 left.ironwood_tx = 18;
579
580 let mut right = node_data_v3(2, 2);
581 right.start_ironwood_root = [19; 32];
582 right.end_ironwood_root = [20; 32];
583 right.ironwood_tx = 21;
584
585 let combined = HistoryV3::combine(&left, &right);
586
587 assert_eq!(combined.v2.v1.start_height, 1);
588 assert_eq!(combined.v2.v1.end_height, 2);
589 assert_eq!(combined.start_ironwood_root, [16; 32]);
590 assert_eq!(combined.end_ironwood_root, [20; 32]);
591 assert_eq!(combined.ironwood_tx, 39);
592 }
593
594 #[test]
595 fn v3_combine_hash_commits_to_ironwood_fields() {
596 let left = node_data_v3(1, 1);
597 let right = node_data_v3(2, 2);
598 let base_hash = HistoryV3::combine(&left, &right).v2.v1.subtree_commitment;
599
600 let mut changed_start_root = left.clone();
601 changed_start_root.start_ironwood_root[0] ^= 1;
602 assert_ne!(
603 HistoryV3::combine(&changed_start_root, &right)
604 .v2
605 .v1
606 .subtree_commitment,
607 base_hash
608 );
609
610 let mut changed_left_end_root = left.clone();
611 changed_left_end_root.end_ironwood_root[0] ^= 1;
612 assert_ne!(
613 HistoryV3::combine(&changed_left_end_root, &right)
614 .v2
615 .v1
616 .subtree_commitment,
617 base_hash
618 );
619
620 let mut changed_right_start_root = right.clone();
621 changed_right_start_root.start_ironwood_root[0] ^= 1;
622 assert_ne!(
623 HistoryV3::combine(&left, &changed_right_start_root)
624 .v2
625 .v1
626 .subtree_commitment,
627 base_hash
628 );
629
630 let mut changed_end_root = right.clone();
631 changed_end_root.end_ironwood_root[0] ^= 1;
632 assert_ne!(
633 HistoryV3::combine(&left, &changed_end_root)
634 .v2
635 .v1
636 .subtree_commitment,
637 base_hash
638 );
639
640 let mut changed_tx_count = left.clone();
641 changed_tx_count.ironwood_tx += 1;
642 assert_ne!(
643 HistoryV3::combine(&changed_tx_count, &right)
644 .v2
645 .v1
646 .subtree_commitment,
647 base_hash
648 );
649
650 let mut redistributed_left_tx = left.clone();
651 let mut redistributed_right_tx = right;
652 redistributed_left_tx.ironwood_tx += 1;
653 redistributed_right_tx.ironwood_tx -= 1;
654 assert_ne!(
655 HistoryV3::combine(&redistributed_left_tx, &redistributed_right_tx)
656 .v2
657 .v1
658 .subtree_commitment,
659 base_hash
660 );
661 }
662}