1use std::{
5 fmt,
6 sync::{Arc, LazyLock, OnceLock},
7};
8
9use super::{Block, CachingBlockHeader, RawBlockHeader, Ticket};
10use crate::{
11 chain_sync::TipsetValidator,
12 cid_collections::SmallCidNonEmptyVec,
13 networks::{calibnet, mainnet},
14 shim::clock::ChainEpoch,
15 utils::{
16 ShallowClone,
17 cid::CidCborExt,
18 db::{CborStoreExt, car_stream::CarBlock},
19 get_size::nunny_vec_heap_size_helper,
20 multihash::MultihashCode,
21 },
22};
23use ahash::HashMap;
24use anyhow::Context as _;
25use cid::Cid;
26use fvm_ipld_blockstore::Blockstore;
27use fvm_ipld_encoding::CborStore;
28use get_size2::GetSize;
29use itertools::Itertools as _;
30use multihash_derive::MultihashDigest as _;
31use num::BigInt;
32use nunny::{Vec as NonEmpty, vec as nonempty};
33use serde::{Deserialize, Serialize};
34use thiserror::Error;
35
36#[derive(
41 Clone,
42 Debug,
43 PartialEq,
44 Eq,
45 Hash,
46 Serialize,
47 Deserialize,
48 PartialOrd,
49 Ord,
50 GetSize,
51 derive_more::IntoIterator,
52)]
53pub struct TipsetKey(#[into_iterator(owned, ref)] SmallCidNonEmptyVec);
54
55impl TipsetKey {
56 pub fn cid(&self) -> anyhow::Result<Cid> {
58 Ok(self.car_block()?.cid)
59 }
60
61 pub fn car_block(&self) -> anyhow::Result<CarBlock> {
62 let data = fvm_ipld_encoding::to_vec(&self.bytes())?;
63 let cid = Cid::from_cbor_encoded_raw_bytes_blake2b256(&data);
64 Ok(CarBlock { cid, data })
65 }
66
67 pub fn contains(&self, cid: Cid) -> bool {
69 self.0.contains(cid)
70 }
71
72 pub fn into_cids(self) -> NonEmpty<Cid> {
74 self.0.into_cids()
75 }
76
77 pub fn to_cids(&self) -> NonEmpty<Cid> {
79 self.0.clone().into_cids()
80 }
81
82 pub fn iter(&self) -> impl Iterator<Item = Cid> + '_ {
84 self.0.iter()
85 }
86
87 pub fn len(&self) -> usize {
89 self.0.len()
90 }
91
92 pub fn is_empty(&self) -> bool {
94 false
95 }
96
97 pub fn terse(&self) -> String {
101 fn terse_cid(cid: Cid) -> String {
102 let s = cid::multibase::encode(
103 cid::multibase::Base::Base32Lower,
104 cid.to_bytes().as_slice(),
105 );
106 format!("{}...{}", &s[9..12], &s[s.len() - 3..])
107 }
108 self.to_cids()
109 .into_iter()
110 .map(terse_cid)
111 .collect_vec()
112 .join(", ")
113 }
114
115 pub fn format_lotus(&self) -> String {
117 format!("{{{}}}", self.to_cids().into_iter().join(","))
118 }
119
120 pub fn bytes(&self) -> fvm_ipld_encoding::RawBytes {
122 fvm_ipld_encoding::RawBytes::new(self.iter().flat_map(|cid| cid.to_bytes()).collect())
123 }
124
125 pub fn from_bytes(bytes: fvm_ipld_encoding::RawBytes) -> anyhow::Result<Self> {
127 static BLOCK_HEADER_CID_LEN: LazyLock<usize> = LazyLock::new(|| {
128 let buf = [0_u8; 256];
129 let cid = Cid::new_v1(
130 fvm_ipld_encoding::DAG_CBOR,
131 MultihashCode::Blake2b256.digest(&buf),
132 );
133 cid.encoded_len()
134 });
135
136 let cids: Vec<Cid> = Vec::<u8>::from(bytes)
137 .chunks(*BLOCK_HEADER_CID_LEN)
138 .map(Cid::read_bytes)
139 .try_collect()?;
140
141 Ok(nunny::Vec::new(cids)
142 .map_err(|_| anyhow::anyhow!("tipset key cannot be empty"))?
143 .into())
144 }
145
146 pub fn save(&self, bs: &impl Blockstore) -> anyhow::Result<Cid> {
148 bs.put_cbor_default(&self.bytes())
149 }
150
151 pub fn load(bs: &impl Blockstore, cid: &Cid) -> anyhow::Result<Self> {
153 Self::from_bytes(bs.get_cbor_required(cid)?)
154 }
155}
156
157impl From<NonEmpty<Cid>> for TipsetKey {
158 fn from(mut value: NonEmpty<Cid>) -> Self {
159 value.shrink_to_fit();
162 Self(value.into())
163 }
164}
165
166impl fmt::Display for TipsetKey {
167 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168 let s = self
169 .to_cids()
170 .into_iter()
171 .map(|cid| cid.to_string())
172 .collect_vec()
173 .join(", ");
174 write!(f, "[{s}]")
175 }
176}
177
178#[cfg(test)]
179impl Default for TipsetKey {
180 fn default() -> Self {
181 nunny::vec![Cid::default()].into()
182 }
183}
184
185#[derive(Clone, Debug, GetSize)]
191pub struct Tipset {
192 #[get_size(size_fn = nunny_vec_heap_size_helper)]
194 headers: Arc<NonEmpty<CachingBlockHeader>>,
195 key: Arc<OnceLock<TipsetKey>>,
197}
198
199impl ShallowClone for Tipset {
200 fn shallow_clone(&self) -> Self {
201 Self {
202 headers: self.headers.shallow_clone(),
203 key: self.key.shallow_clone(),
204 }
205 }
206}
207
208impl From<RawBlockHeader> for Tipset {
209 fn from(value: RawBlockHeader) -> Self {
210 Self::from(CachingBlockHeader::from(value))
211 }
212}
213
214impl From<&CachingBlockHeader> for Tipset {
215 fn from(value: &CachingBlockHeader) -> Self {
216 value.clone().into()
217 }
218}
219
220impl From<CachingBlockHeader> for Tipset {
221 fn from(value: CachingBlockHeader) -> Self {
222 Self {
223 headers: nonempty![value].into(),
224 key: OnceLock::new().into(),
225 }
226 }
227}
228
229impl From<NonEmpty<CachingBlockHeader>> for Tipset {
230 fn from(headers: NonEmpty<CachingBlockHeader>) -> Self {
231 Self {
232 headers: headers.into(),
233 key: OnceLock::new().into(),
234 }
235 }
236}
237
238impl PartialEq for Tipset {
239 fn eq(&self, other: &Self) -> bool {
240 self.headers.eq(&other.headers)
241 }
242}
243
244#[cfg(test)]
245impl quickcheck::Arbitrary for Tipset {
246 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
247 Tipset::from(CachingBlockHeader::arbitrary(g))
250 }
251}
252
253impl From<FullTipset> for Tipset {
254 fn from(FullTipset { key, blocks }: FullTipset) -> Self {
255 let headers = Arc::unwrap_or_clone(blocks)
256 .into_iter_ne()
257 .map(|block| block.header)
258 .collect_vec()
259 .into();
260 Tipset { headers, key }
261 }
262}
263
264#[derive(Error, Debug, PartialEq)]
265pub enum CreateTipsetError {
266 #[error("tipsets must not be empty")]
267 Empty,
268 #[error(
269 "parent CID is inconsistent. All block headers in a tipset must agree on their parent tipset"
270 )]
271 BadParents,
272 #[error(
273 "state root is inconsistent. All block headers in a tipset must agree on their parent state root"
274 )]
275 BadStateRoot,
276 #[error("epoch is inconsistent. All block headers in a tipset must agree on their epoch")]
277 BadEpoch,
278 #[error("duplicate miner address. All miners in a tipset must be unique.")]
279 DuplicateMiner,
280}
281
282pub trait TipsetLike {
284 fn epoch(&self) -> ChainEpoch;
285 fn key(&self) -> &TipsetKey;
286 fn parents(&self) -> &TipsetKey;
287 #[allow(dead_code)]
288 fn parent_state(&self) -> &Cid;
289}
290
291#[allow(clippy::len_without_is_empty)]
292impl Tipset {
293 pub fn new<H: Into<CachingBlockHeader>>(
299 headers: impl IntoIterator<Item = H>,
300 ) -> Result<Self, CreateTipsetError> {
301 let mut headers = NonEmpty::new(
302 headers
303 .into_iter()
304 .map(Into::<CachingBlockHeader>::into)
305 .sorted_by_cached_key(|it| it.tipset_sort_key())
306 .collect(),
307 )
308 .map_err(|_| CreateTipsetError::Empty)?;
309 headers.shrink_to_fit();
310 verify_block_headers(&headers)?;
311
312 Ok(Self {
313 headers: headers.into(),
314 key: OnceLock::new().into(),
315 })
316 }
317
318 pub fn load(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Option<Tipset>> {
321 Ok(tsk
322 .to_cids()
323 .into_iter()
324 .map(|key| CachingBlockHeader::load(store, key))
325 .collect::<anyhow::Result<Option<Vec<_>>>>()?
326 .map(Tipset::new)
327 .transpose()?)
328 }
329
330 pub fn load_required(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Tipset> {
333 Tipset::load(store, tsk)?.context("Required tipset missing from database")
334 }
335
336 pub fn epoch(&self) -> ChainEpoch {
338 self.min_ticket_block().epoch
339 }
340 pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
341 &self.headers
342 }
343 pub fn min_ticket(&self) -> Option<&Ticket> {
345 self.min_ticket_block().ticket.as_ref()
346 }
347 pub fn min_ticket_block(&self) -> &CachingBlockHeader {
349 self.headers.first()
350 }
351 pub fn min_timestamp(&self) -> u64 {
353 self.headers
354 .iter()
355 .map(|block| block.timestamp)
356 .min()
357 .unwrap()
358 }
359 pub fn len(&self) -> usize {
361 self.headers.len()
362 }
363 pub fn key(&self) -> &TipsetKey {
365 self.key
366 .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
367 }
368 pub fn cids(&self) -> NonEmpty<Cid> {
370 self.key().to_cids()
371 }
372 pub fn parents(&self) -> &TipsetKey {
374 &self.min_ticket_block().parents
375 }
376 pub fn parent_state(&self) -> &Cid {
378 &self.min_ticket_block().state_root
379 }
380 pub fn parent_message_receipts(&self) -> &Cid {
382 &self.min_ticket_block().message_receipts
383 }
384 pub fn weight(&self) -> &BigInt {
386 &self.min_ticket_block().weight
387 }
388 #[cfg(test)]
391 pub fn break_weight_tie(&self, other: &Tipset) -> bool {
392 let broken = self
394 .block_headers()
395 .iter()
396 .zip(other.block_headers().iter())
397 .any(|(a, b)| {
398 const MSG: &str =
399 "The function block_sanity_checks should have been called at this point.";
400 let ticket = a.ticket.as_ref().expect(MSG);
401 let other_ticket = b.ticket.as_ref().expect(MSG);
402 ticket.vrfproof < other_ticket.vrfproof
403 });
404 if broken {
405 tracing::info!("Weight tie broken in favour of {}", self.key());
406 } else {
407 tracing::info!("Weight tie left unbroken, default to {}", other.key());
408 }
409 broken
410 }
411
412 pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
414 let mut tipset = Some(self);
415 std::iter::from_fn(move || {
416 let child = tipset.take()?;
417 tipset = Tipset::load_required(&store, child.parents()).ok();
418 Some(child)
419 })
420 }
421
422 pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
424 let mut tipset = Some(self);
425 std::iter::from_fn(move || {
426 let child = tipset.take()?;
427 tipset = Tipset::load_required(store, child.parents()).ok();
428 Some(child)
429 })
430 }
431
432 pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
434 #[derive(Serialize, Deserialize)]
439 struct KnownHeaders {
440 calibnet: HashMap<ChainEpoch, String>,
441 mainnet: HashMap<ChainEpoch, String>,
442 }
443
444 static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
445 let headers = KNOWN_HEADERS.get_or_init(|| {
446 serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
447 });
448
449 for tipset in self.clone().chain(store) {
450 for (genesis_cid, known_blocks) in [
452 (*calibnet::GENESIS_CID, &headers.calibnet),
453 (*mainnet::GENESIS_CID, &headers.mainnet),
454 ] {
455 if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
456 && known_block_cid == &tipset.min_ticket_block().cid().to_string()
457 {
458 return store
459 .get_cbor(&genesis_cid)?
460 .context("Genesis block missing from database");
461 }
462 }
463
464 if tipset.epoch() == 0 {
466 return Ok(tipset.min_ticket_block().clone());
467 }
468 }
469 anyhow::bail!("Genesis block not found")
470 }
471}
472
473impl TipsetLike for Tipset {
474 fn epoch(&self) -> ChainEpoch {
475 self.epoch()
476 }
477
478 fn key(&self) -> &TipsetKey {
479 self.key()
480 }
481
482 fn parents(&self) -> &TipsetKey {
483 self.parents()
484 }
485
486 fn parent_state(&self) -> &Cid {
487 self.parent_state()
488 }
489}
490
491#[derive(Debug, Clone, Eq)]
494pub struct FullTipset {
495 blocks: Arc<NonEmpty<Block>>,
496 key: Arc<OnceLock<TipsetKey>>,
498}
499
500impl TipsetLike for FullTipset {
501 fn epoch(&self) -> ChainEpoch {
502 self.epoch()
503 }
504
505 fn key(&self) -> &TipsetKey {
506 self.key()
507 }
508
509 fn parents(&self) -> &TipsetKey {
510 self.parents()
511 }
512
513 fn parent_state(&self) -> &Cid {
514 self.parent_state()
515 }
516}
517
518impl std::hash::Hash for FullTipset {
519 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
520 self.key().hash(state)
521 }
522}
523
524impl From<Block> for FullTipset {
526 fn from(block: Block) -> Self {
527 FullTipset {
528 blocks: nonempty![block].into(),
529 key: OnceLock::new().into(),
530 }
531 }
532}
533
534impl PartialEq for FullTipset {
535 fn eq(&self, other: &Self) -> bool {
536 self.blocks.eq(&other.blocks)
537 }
538}
539
540impl FullTipset {
541 pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
542 let blocks = Arc::new(
543 NonEmpty::new(
544 blocks
547 .into_iter()
548 .sorted_by_cached_key(|it| it.header.tipset_sort_key())
549 .collect(),
550 )
551 .map_err(|_| CreateTipsetError::Empty)?,
552 );
553
554 verify_block_headers(blocks.iter().map(|it| &it.header))?;
555
556 Ok(Self {
557 blocks,
558 key: Arc::new(OnceLock::new()),
559 })
560 }
561 fn first_block(&self) -> &Block {
563 self.blocks.first()
564 }
565 pub fn blocks(&self) -> &NonEmpty<Block> {
567 &self.blocks
568 }
569 pub fn into_blocks(self) -> NonEmpty<Block> {
571 Arc::unwrap_or_clone(self.blocks)
572 }
573 pub fn into_tipset(self) -> Tipset {
576 Tipset::from(self)
577 }
578 pub fn key(&self) -> &TipsetKey {
580 self.key
581 .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
582 }
583 pub fn parent_state(&self) -> &Cid {
585 &self.first_block().header().state_root
586 }
587 pub fn parents(&self) -> &TipsetKey {
589 &self.first_block().header().parents
590 }
591 pub fn epoch(&self) -> ChainEpoch {
593 self.first_block().header().epoch
594 }
595 pub fn weight(&self) -> &BigInt {
597 &self.first_block().header().weight
598 }
599 pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
601 for block in self.blocks() {
602 TipsetValidator::validate_msg_root(db, block)?;
604 crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
605 crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
606 crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
607 }
608 Ok(())
609 }
610}
611
612fn verify_block_headers<'a>(
613 headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
614) -> Result<(), CreateTipsetError> {
615 use itertools::all;
616
617 let headers =
618 NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
619 if !all(&headers, |it| it.parents == headers.first().parents) {
620 return Err(CreateTipsetError::BadParents);
621 }
622 if !all(&headers, |it| it.state_root == headers.first().state_root) {
623 return Err(CreateTipsetError::BadStateRoot);
624 }
625 if !all(&headers, |it| it.epoch == headers.first().epoch) {
626 return Err(CreateTipsetError::BadEpoch);
627 }
628
629 if !headers.iter().map(|it| it.miner_address).all_unique() {
630 return Err(CreateTipsetError::DuplicateMiner);
631 }
632
633 Ok(())
634}
635
636#[cfg_vis::cfg_vis(doc, pub)]
637mod lotus_json {
638 use crate::blocks::{CachingBlockHeader, Tipset};
642 use crate::lotus_json::*;
643 use nunny::Vec as NonEmpty;
644 use schemars::JsonSchema;
645 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
646
647 use super::TipsetKey;
648
649 #[derive(Debug, PartialEq, Clone, JsonSchema)]
650 #[schemars(rename = "Tipset")]
651 pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
652
653 #[derive(Serialize, Deserialize, JsonSchema)]
654 #[schemars(rename = "TipsetInner")]
655 #[serde(rename_all = "PascalCase")]
656 struct TipsetLotusJsonInner {
657 #[serde(with = "crate::lotus_json")]
658 #[schemars(with = "LotusJson<TipsetKey>")]
659 cids: TipsetKey,
660 #[serde(with = "crate::lotus_json")]
661 #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
662 blocks: NonEmpty<CachingBlockHeader>,
663 height: i64,
664 }
665
666 impl<'de> Deserialize<'de> for TipsetLotusJson {
667 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
668 where
669 D: Deserializer<'de>,
670 {
671 let TipsetLotusJsonInner {
672 cids: _ignored0,
673 blocks,
674 height: _ignored1,
675 } = Deserialize::deserialize(deserializer)?;
676
677 Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
678 }
679 }
680
681 impl Serialize for TipsetLotusJson {
682 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
683 where
684 S: Serializer,
685 {
686 let Self(tipset) = self;
687 TipsetLotusJsonInner {
688 cids: tipset.key().clone(),
689 height: tipset.epoch(),
690 blocks: tipset.block_headers().clone(),
691 }
692 .serialize(serializer)
693 }
694 }
695
696 impl HasLotusJson for Tipset {
697 type LotusJson = TipsetLotusJson;
698
699 #[cfg(test)]
700 fn snapshots() -> Vec<(serde_json::Value, Self)> {
701 use serde_json::json;
702 vec![(
703 json!({
704 "Blocks": [
705 {
706 "BeaconEntries": null,
707 "ForkSignaling": 0,
708 "Height": 0,
709 "Messages": { "/": "baeaaaaa" },
710 "Miner": "f00",
711 "ParentBaseFee": "0",
712 "ParentMessageReceipts": { "/": "baeaaaaa" },
713 "ParentStateRoot": { "/":"baeaaaaa" },
714 "ParentWeight": "0",
715 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
716 "Timestamp": 0,
717 "WinPoStProof": null
718 }
719 ],
720 "Cids": [
721 { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
722 ],
723 "Height": 0
724 }),
725 Self::new(vec![CachingBlockHeader::default()]).unwrap(),
726 )]
727 }
728
729 fn into_lotus_json(self) -> Self::LotusJson {
730 TipsetLotusJson(self)
731 }
732
733 fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
734 tipset
735 }
736 }
737
738 #[test]
739 fn snapshots() {
740 assert_all_snapshots::<Tipset>()
741 }
742
743 #[cfg(test)]
744 #[quickcheck_macros::quickcheck]
745 fn quickcheck(val: Tipset) {
746 assert_unchanged_via_json(val)
747 }
748}
749
750#[cfg(test)]
751mod test {
752 use super::*;
753 use crate::blocks::{
754 CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, VRFProof,
755 header::RawBlockHeader,
756 };
757 use crate::db::MemoryDB;
758 use crate::shim::address::Address;
759 use cid::Cid;
760 use fvm_ipld_encoding::DAG_CBOR;
761 use num_bigint::BigInt;
762 use quickcheck::Arbitrary;
763 use quickcheck_macros::quickcheck;
764 use std::iter;
765
766 pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
767 let addr = Address::new_id(id);
768 let cid =
769 Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
770
771 let fmt_str = format!("===={ticket_sequence}=====");
772 let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
773 let election_proof = ElectionProof {
774 win_count: 0,
775 vrfproof: VRFProof::new(fmt_str.into_bytes()),
776 };
777 let weight_inc = BigInt::from(weight);
778 CachingBlockHeader::new(RawBlockHeader {
779 miner_address: addr,
780 election_proof: Some(election_proof),
781 ticket: Some(ticket),
782 message_receipts: cid,
783 messages: cid,
784 state_root: cid,
785 weight: weight_inc,
786 ..Default::default()
787 })
788 }
789
790 #[test]
791 fn test_break_weight_tie() {
792 let b1 = mock_block(1234561, 1, 1);
793 let ts1 = Tipset::from(&b1);
794
795 let b2 = mock_block(1234562, 1, 2);
796 let ts2 = Tipset::from(&b2);
797
798 let b3 = mock_block(1234563, 1, 1);
799 let ts3 = Tipset::from(&b3);
800
801 assert!(ts1.break_weight_tie(&ts2));
805 assert!(!ts1.break_weight_tie(&ts3));
807
808 let b4 = mock_block(1234564, 1, 41);
810 let b5 = mock_block(1234565, 1, 45);
811 let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
812 let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
813 assert!(ts4.break_weight_tie(&ts5));
815
816 let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
817 let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
818 assert!(!ts6.break_weight_tie(&ts7));
820 }
821
822 #[test]
823 fn ensure_miner_addresses_are_distinct() {
824 let h0 = RawBlockHeader {
825 miner_address: Address::new_id(0),
826 ..Default::default()
827 };
828 let h1 = RawBlockHeader {
829 miner_address: Address::new_id(0),
830 ..Default::default()
831 };
832 assert_eq!(
833 Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
834 CreateTipsetError::DuplicateMiner
835 );
836
837 let h_unique = RawBlockHeader {
838 miner_address: Address::new_id(1),
839 ..Default::default()
840 };
841
842 assert_eq!(
843 Tipset::new([h_unique, h0, h1]).unwrap_err(),
844 CreateTipsetError::DuplicateMiner
845 );
846 }
847
848 #[test]
849 fn ensure_epochs_are_equal() {
850 let h0 = RawBlockHeader {
851 miner_address: Address::new_id(0),
852 epoch: 1,
853 ..Default::default()
854 };
855 let h1 = RawBlockHeader {
856 miner_address: Address::new_id(1),
857 epoch: 2,
858 ..Default::default()
859 };
860 assert_eq!(
861 Tipset::new([h0, h1]).unwrap_err(),
862 CreateTipsetError::BadEpoch
863 );
864 }
865
866 #[test]
867 fn ensure_state_roots_are_equal() {
868 let h0 = RawBlockHeader {
869 miner_address: Address::new_id(0),
870 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
871 ..Default::default()
872 };
873 let h1 = RawBlockHeader {
874 miner_address: Address::new_id(1),
875 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
876 ..Default::default()
877 };
878 assert_eq!(
879 Tipset::new([h0, h1]).unwrap_err(),
880 CreateTipsetError::BadStateRoot
881 );
882 }
883
884 #[test]
885 fn ensure_parent_cids_are_equal() {
886 let h0 = RawBlockHeader {
887 miner_address: Address::new_id(0),
888 ..Default::default()
889 };
890 let h1 = RawBlockHeader {
891 miner_address: Address::new_id(1),
892 parents: TipsetKey::from(nonempty![Cid::new_v1(
893 DAG_CBOR,
894 MultihashCode::Identity.digest(&[])
895 )]),
896 ..Default::default()
897 };
898 assert_eq!(
899 Tipset::new([h0, h1]).unwrap_err(),
900 CreateTipsetError::BadParents
901 );
902 }
903
904 #[test]
905 fn ensure_there_are_blocks() {
906 assert_eq!(
907 Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
908 CreateTipsetError::Empty
909 );
910 }
911
912 impl Arbitrary for TipsetKey {
913 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
914 let blocks: nunny::Vec<Vec<u8>> = nunny::Vec::arbitrary(g);
915 let cids = nunny::Vec::new(
916 blocks
917 .into_iter()
918 .map(|b| {
919 Cid::new_v1(
920 fvm_ipld_encoding::DAG_CBOR,
921 MultihashCode::Blake2b256.digest(&b),
922 )
923 })
924 .collect_vec(),
925 )
926 .expect("infallible");
927 cids.into()
928 }
929 }
930
931 #[quickcheck]
932 fn tipset_key_bytes(tsk: TipsetKey) {
933 let bytes = tsk.bytes();
934 let tsk2 = TipsetKey::from_bytes(bytes).unwrap();
935 assert_eq!(tsk, tsk2);
936
937 let bs = MemoryDB::default();
938 let cid = tsk.save(&bs).unwrap();
939 let tsk3 = TipsetKey::load(&bs, &cid).unwrap();
940 assert_eq!(tsk, tsk3);
941 }
942}