1use std::{
5 fmt,
6 sync::{Arc, 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::{cid::CidCborExt, get_size::nunny_vec_heap_size_helper},
16};
17use ahash::HashMap;
18use anyhow::Context as _;
19use cid::Cid;
20use fvm_ipld_blockstore::Blockstore;
21use fvm_ipld_encoding::CborStore;
22use get_size2::GetSize;
23use itertools::Itertools as _;
24use num::BigInt;
25use nunny::{Vec as NonEmpty, vec as nonempty};
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28
29#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, GetSize)]
34#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
35pub struct TipsetKey(SmallCidNonEmptyVec);
36
37impl TipsetKey {
38 pub fn cid(&self) -> anyhow::Result<Cid> {
40 use fvm_ipld_encoding::RawBytes;
41
42 let mut bytes = Vec::new();
43 for cid in self.to_cids() {
44 bytes.append(&mut cid.to_bytes())
45 }
46 Ok(Cid::from_cbor_blake2b256(&RawBytes::new(bytes))?)
47 }
48
49 pub fn contains(&self, cid: Cid) -> bool {
51 self.0.contains(cid)
52 }
53
54 pub fn into_cids(self) -> NonEmpty<Cid> {
56 self.0.into_cids()
57 }
58
59 pub fn to_cids(&self) -> NonEmpty<Cid> {
61 self.0.clone().into_cids()
62 }
63
64 pub fn iter(&self) -> impl Iterator<Item = Cid> + '_ {
66 self.0.iter()
67 }
68
69 pub fn len(&self) -> usize {
71 self.0.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
76 false
77 }
78
79 pub fn terse(&self) -> String {
83 fn terse_cid(cid: Cid) -> String {
84 let s = cid::multibase::encode(
85 cid::multibase::Base::Base32Lower,
86 cid.to_bytes().as_slice(),
87 );
88 format!("{}...{}", &s[9..12], &s[s.len() - 3..])
89 }
90 self.to_cids()
91 .into_iter()
92 .map(terse_cid)
93 .collect::<Vec<_>>()
94 .join(", ")
95 }
96
97 pub fn format_lotus(&self) -> String {
99 format!("{{{}}}", self.to_cids().into_iter().join(","))
100 }
101}
102
103impl From<NonEmpty<Cid>> for TipsetKey {
104 fn from(mut value: NonEmpty<Cid>) -> Self {
105 value.shrink_to_fit();
108 Self(value.into())
109 }
110}
111
112impl fmt::Display for TipsetKey {
113 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114 let s = self
115 .to_cids()
116 .into_iter()
117 .map(|cid| cid.to_string())
118 .collect::<Vec<_>>()
119 .join(", ");
120 write!(f, "[{s}]")
121 }
122}
123
124impl<'a> IntoIterator for &'a TipsetKey {
125 type Item = <&'a SmallCidNonEmptyVec as IntoIterator>::Item;
126
127 type IntoIter = <&'a SmallCidNonEmptyVec as IntoIterator>::IntoIter;
128
129 fn into_iter(self) -> Self::IntoIter {
130 (&self.0).into_iter()
131 }
132}
133
134impl IntoIterator for TipsetKey {
135 type Item = <SmallCidNonEmptyVec as IntoIterator>::Item;
136
137 type IntoIter = <SmallCidNonEmptyVec as IntoIterator>::IntoIter;
138
139 fn into_iter(self) -> Self::IntoIter {
140 self.0.into_iter()
141 }
142}
143
144#[cfg(test)]
145impl Default for TipsetKey {
146 fn default() -> Self {
147 nunny::vec![Cid::default()].into()
148 }
149}
150
151#[derive(Clone, Debug, GetSize)]
157pub struct Tipset {
158 #[get_size(size_fn = nunny_vec_heap_size_helper)]
160 headers: Arc<NonEmpty<CachingBlockHeader>>,
161 key: Arc<OnceLock<TipsetKey>>,
163}
164
165impl From<RawBlockHeader> for Tipset {
166 fn from(value: RawBlockHeader) -> Self {
167 Self::from(CachingBlockHeader::from(value))
168 }
169}
170
171impl From<&CachingBlockHeader> for Tipset {
172 fn from(value: &CachingBlockHeader) -> Self {
173 value.clone().into()
174 }
175}
176
177impl From<CachingBlockHeader> for Tipset {
178 fn from(value: CachingBlockHeader) -> Self {
179 Self {
180 headers: nonempty![value].into(),
181 key: OnceLock::new().into(),
182 }
183 }
184}
185
186impl From<NonEmpty<CachingBlockHeader>> for Tipset {
187 fn from(headers: NonEmpty<CachingBlockHeader>) -> Self {
188 Self {
189 headers: headers.into(),
190 key: OnceLock::new().into(),
191 }
192 }
193}
194
195impl PartialEq for Tipset {
196 fn eq(&self, other: &Self) -> bool {
197 self.headers.eq(&other.headers)
198 }
199}
200
201#[cfg(test)]
202impl quickcheck::Arbitrary for Tipset {
203 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
204 Tipset::from(CachingBlockHeader::arbitrary(g))
207 }
208}
209
210impl From<FullTipset> for Tipset {
211 fn from(FullTipset { key, blocks }: FullTipset) -> Self {
212 let headers = Arc::unwrap_or_clone(blocks)
213 .into_iter_ne()
214 .map(|block| block.header)
215 .collect_vec()
216 .into();
217 Tipset { headers, key }
218 }
219}
220
221#[derive(Error, Debug, PartialEq)]
222pub enum CreateTipsetError {
223 #[error("tipsets must not be empty")]
224 Empty,
225 #[error(
226 "parent CID is inconsistent. All block headers in a tipset must agree on their parent tipset"
227 )]
228 BadParents,
229 #[error(
230 "state root is inconsistent. All block headers in a tipset must agree on their parent state root"
231 )]
232 BadStateRoot,
233 #[error("epoch is inconsistent. All block headers in a tipset must agree on their epoch")]
234 BadEpoch,
235 #[error("duplicate miner address. All miners in a tipset must be unique.")]
236 DuplicateMiner,
237}
238
239#[allow(clippy::len_without_is_empty)]
240impl Tipset {
241 pub fn new<H: Into<CachingBlockHeader>>(
247 headers: impl IntoIterator<Item = H>,
248 ) -> Result<Self, CreateTipsetError> {
249 let mut headers = NonEmpty::new(
250 headers
251 .into_iter()
252 .map(Into::<CachingBlockHeader>::into)
253 .sorted_by_cached_key(|it| it.tipset_sort_key())
254 .collect(),
255 )
256 .map_err(|_| CreateTipsetError::Empty)?;
257 headers.shrink_to_fit();
258 verify_block_headers(&headers)?;
259
260 Ok(Self {
261 headers: headers.into(),
262 key: OnceLock::new().into(),
263 })
264 }
265
266 pub fn load(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Option<Tipset>> {
269 Ok(tsk
270 .to_cids()
271 .into_iter()
272 .map(|key| CachingBlockHeader::load(store, key))
273 .collect::<anyhow::Result<Option<Vec<_>>>>()?
274 .map(Tipset::new)
275 .transpose()?)
276 }
277
278 pub fn load_required(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Tipset> {
281 Tipset::load(store, tsk)?.context("Required tipset missing from database")
282 }
283
284 pub fn epoch(&self) -> ChainEpoch {
286 self.min_ticket_block().epoch
287 }
288 pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
289 &self.headers
290 }
291 pub fn min_ticket(&self) -> Option<&Ticket> {
293 self.min_ticket_block().ticket.as_ref()
294 }
295 pub fn min_ticket_block(&self) -> &CachingBlockHeader {
297 self.headers.first()
298 }
299 pub fn min_timestamp(&self) -> u64 {
301 self.headers
302 .iter()
303 .map(|block| block.timestamp)
304 .min()
305 .unwrap()
306 }
307 pub fn len(&self) -> usize {
309 self.headers.len()
310 }
311 pub fn key(&self) -> &TipsetKey {
313 self.key
314 .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
315 }
316 pub fn cids(&self) -> NonEmpty<Cid> {
318 self.key().to_cids()
319 }
320 pub fn parents(&self) -> &TipsetKey {
322 &self.min_ticket_block().parents
323 }
324 pub fn parent_state(&self) -> &Cid {
326 &self.min_ticket_block().state_root
327 }
328 pub fn weight(&self) -> &BigInt {
330 &self.min_ticket_block().weight
331 }
332 #[cfg(test)]
335 pub fn break_weight_tie(&self, other: &Tipset) -> bool {
336 let broken = self
338 .block_headers()
339 .iter()
340 .zip(other.block_headers().iter())
341 .any(|(a, b)| {
342 const MSG: &str =
343 "The function block_sanity_checks should have been called at this point.";
344 let ticket = a.ticket.as_ref().expect(MSG);
345 let other_ticket = b.ticket.as_ref().expect(MSG);
346 ticket.vrfproof < other_ticket.vrfproof
347 });
348 if broken {
349 tracing::info!("Weight tie broken in favour of {}", self.key());
350 } else {
351 tracing::info!("Weight tie left unbroken, default to {}", other.key());
352 }
353 broken
354 }
355
356 pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
358 let mut tipset = Some(self);
359 std::iter::from_fn(move || {
360 let child = tipset.take()?;
361 tipset = Tipset::load_required(&store, child.parents()).ok();
362 Some(child)
363 })
364 }
365
366 pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
368 let mut tipset = Some(self);
369 std::iter::from_fn(move || {
370 let child = tipset.take()?;
371 tipset = Tipset::load_required(store, child.parents()).ok();
372 Some(child)
373 })
374 }
375
376 pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
378 #[derive(Serialize, Deserialize)]
383 struct KnownHeaders {
384 calibnet: HashMap<ChainEpoch, String>,
385 mainnet: HashMap<ChainEpoch, String>,
386 }
387
388 static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
389 let headers = KNOWN_HEADERS.get_or_init(|| {
390 serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
391 });
392
393 for tipset in self.clone().chain(store) {
394 for (genesis_cid, known_blocks) in [
396 (*calibnet::GENESIS_CID, &headers.calibnet),
397 (*mainnet::GENESIS_CID, &headers.mainnet),
398 ] {
399 if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
400 && known_block_cid == &tipset.min_ticket_block().cid().to_string()
401 {
402 return store
403 .get_cbor(&genesis_cid)?
404 .context("Genesis block missing from database");
405 }
406 }
407
408 if tipset.epoch() == 0 {
410 return Ok(tipset.min_ticket_block().clone());
411 }
412 }
413 anyhow::bail!("Genesis block not found")
414 }
415}
416
417#[derive(Debug, Clone, Eq)]
420pub struct FullTipset {
421 blocks: Arc<NonEmpty<Block>>,
422 key: Arc<OnceLock<TipsetKey>>,
424}
425
426impl std::hash::Hash for FullTipset {
427 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
428 self.key().hash(state)
429 }
430}
431
432impl From<Block> for FullTipset {
434 fn from(block: Block) -> Self {
435 FullTipset {
436 blocks: nonempty![block].into(),
437 key: OnceLock::new().into(),
438 }
439 }
440}
441
442impl PartialEq for FullTipset {
443 fn eq(&self, other: &Self) -> bool {
444 self.blocks.eq(&other.blocks)
445 }
446}
447
448impl FullTipset {
449 pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
450 let blocks = Arc::new(
451 NonEmpty::new(
452 blocks
455 .into_iter()
456 .sorted_by_cached_key(|it| it.header.tipset_sort_key())
457 .collect(),
458 )
459 .map_err(|_| CreateTipsetError::Empty)?,
460 );
461
462 verify_block_headers(blocks.iter().map(|it| &it.header))?;
463
464 Ok(Self {
465 blocks,
466 key: Arc::new(OnceLock::new()),
467 })
468 }
469 fn first_block(&self) -> &Block {
471 self.blocks.first()
472 }
473 pub fn blocks(&self) -> &NonEmpty<Block> {
475 &self.blocks
476 }
477 pub fn into_blocks(self) -> NonEmpty<Block> {
479 Arc::unwrap_or_clone(self.blocks)
480 }
481 pub fn into_tipset(self) -> Tipset {
484 Tipset::from(self)
485 }
486 pub fn key(&self) -> &TipsetKey {
488 self.key
489 .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
490 }
491 pub fn parent_state(&self) -> &Cid {
493 &self.first_block().header().state_root
494 }
495 pub fn parents(&self) -> &TipsetKey {
497 &self.first_block().header().parents
498 }
499 pub fn epoch(&self) -> ChainEpoch {
501 self.first_block().header().epoch
502 }
503 pub fn weight(&self) -> &BigInt {
505 &self.first_block().header().weight
506 }
507 pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
509 for block in self.blocks() {
510 TipsetValidator::validate_msg_root(db, block)?;
512 crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
513 crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
514 crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
515 }
516 Ok(())
517 }
518}
519
520fn verify_block_headers<'a>(
521 headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
522) -> Result<(), CreateTipsetError> {
523 use itertools::all;
524
525 let headers =
526 NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
527 if !all(&headers, |it| it.parents == headers.first().parents) {
528 return Err(CreateTipsetError::BadParents);
529 }
530 if !all(&headers, |it| it.state_root == headers.first().state_root) {
531 return Err(CreateTipsetError::BadStateRoot);
532 }
533 if !all(&headers, |it| it.epoch == headers.first().epoch) {
534 return Err(CreateTipsetError::BadEpoch);
535 }
536
537 if !headers.iter().map(|it| it.miner_address).all_unique() {
538 return Err(CreateTipsetError::DuplicateMiner);
539 }
540
541 Ok(())
542}
543
544#[cfg_vis::cfg_vis(doc, pub)]
545mod lotus_json {
546 use crate::blocks::{CachingBlockHeader, Tipset};
550 use crate::lotus_json::*;
551 use nunny::Vec as NonEmpty;
552 use schemars::JsonSchema;
553 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
554
555 use super::TipsetKey;
556
557 #[derive(Debug, PartialEq, Clone, JsonSchema)]
558 #[schemars(rename = "Tipset")]
559 pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
560
561 #[derive(Serialize, Deserialize, JsonSchema)]
562 #[schemars(rename = "TipsetInner")]
563 #[serde(rename_all = "PascalCase")]
564 struct TipsetLotusJsonInner {
565 #[serde(with = "crate::lotus_json")]
566 #[schemars(with = "LotusJson<TipsetKey>")]
567 cids: TipsetKey,
568 #[serde(with = "crate::lotus_json")]
569 #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
570 blocks: NonEmpty<CachingBlockHeader>,
571 height: i64,
572 }
573
574 impl<'de> Deserialize<'de> for TipsetLotusJson {
575 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
576 where
577 D: Deserializer<'de>,
578 {
579 let TipsetLotusJsonInner {
580 cids: _ignored0,
581 blocks,
582 height: _ignored1,
583 } = Deserialize::deserialize(deserializer)?;
584
585 Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
586 }
587 }
588
589 impl Serialize for TipsetLotusJson {
590 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
591 where
592 S: Serializer,
593 {
594 let Self(tipset) = self;
595 TipsetLotusJsonInner {
596 cids: tipset.key().clone(),
597 height: tipset.epoch(),
598 blocks: tipset.block_headers().clone(),
599 }
600 .serialize(serializer)
601 }
602 }
603
604 impl HasLotusJson for Tipset {
605 type LotusJson = TipsetLotusJson;
606
607 #[cfg(test)]
608 fn snapshots() -> Vec<(serde_json::Value, Self)> {
609 use serde_json::json;
610 vec![(
611 json!({
612 "Blocks": [
613 {
614 "BeaconEntries": null,
615 "ForkSignaling": 0,
616 "Height": 0,
617 "Messages": { "/": "baeaaaaa" },
618 "Miner": "f00",
619 "ParentBaseFee": "0",
620 "ParentMessageReceipts": { "/": "baeaaaaa" },
621 "ParentStateRoot": { "/":"baeaaaaa" },
622 "ParentWeight": "0",
623 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
624 "Timestamp": 0,
625 "WinPoStProof": null
626 }
627 ],
628 "Cids": [
629 { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
630 ],
631 "Height": 0
632 }),
633 Self::new(vec![CachingBlockHeader::default()]).unwrap(),
634 )]
635 }
636
637 fn into_lotus_json(self) -> Self::LotusJson {
638 TipsetLotusJson(self)
639 }
640
641 fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
642 tipset
643 }
644 }
645
646 #[test]
647 fn snapshots() {
648 assert_all_snapshots::<Tipset>()
649 }
650
651 #[cfg(test)]
652 quickcheck::quickcheck! {
653 fn quickcheck(val: Tipset) -> () {
654 assert_unchanged_via_json(val)
655 }
656 }
657}
658
659#[cfg(test)]
660mod test {
661 use super::*;
662 use crate::blocks::VRFProof;
663 use crate::blocks::{
664 CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, header::RawBlockHeader,
665 };
666 use crate::shim::address::Address;
667 use crate::utils::multihash::prelude::*;
668 use cid::Cid;
669 use fvm_ipld_encoding::DAG_CBOR;
670 use num_bigint::BigInt;
671 use std::iter;
672
673 pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
674 let addr = Address::new_id(id);
675 let cid =
676 Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
677
678 let fmt_str = format!("===={ticket_sequence}=====");
679 let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
680 let election_proof = ElectionProof {
681 win_count: 0,
682 vrfproof: VRFProof::new(fmt_str.into_bytes()),
683 };
684 let weight_inc = BigInt::from(weight);
685 CachingBlockHeader::new(RawBlockHeader {
686 miner_address: addr,
687 election_proof: Some(election_proof),
688 ticket: Some(ticket),
689 message_receipts: cid,
690 messages: cid,
691 state_root: cid,
692 weight: weight_inc,
693 ..Default::default()
694 })
695 }
696
697 #[test]
698 fn test_break_weight_tie() {
699 let b1 = mock_block(1234561, 1, 1);
700 let ts1 = Tipset::from(&b1);
701
702 let b2 = mock_block(1234562, 1, 2);
703 let ts2 = Tipset::from(&b2);
704
705 let b3 = mock_block(1234563, 1, 1);
706 let ts3 = Tipset::from(&b3);
707
708 assert!(ts1.break_weight_tie(&ts2));
712 assert!(!ts1.break_weight_tie(&ts3));
714
715 let b4 = mock_block(1234564, 1, 41);
717 let b5 = mock_block(1234565, 1, 45);
718 let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
719 let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
720 assert!(ts4.break_weight_tie(&ts5));
722
723 let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
724 let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
725 assert!(!ts6.break_weight_tie(&ts7));
727 }
728
729 #[test]
730 fn ensure_miner_addresses_are_distinct() {
731 let h0 = RawBlockHeader {
732 miner_address: Address::new_id(0),
733 ..Default::default()
734 };
735 let h1 = RawBlockHeader {
736 miner_address: Address::new_id(0),
737 ..Default::default()
738 };
739 assert_eq!(
740 Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
741 CreateTipsetError::DuplicateMiner
742 );
743
744 let h_unique = RawBlockHeader {
745 miner_address: Address::new_id(1),
746 ..Default::default()
747 };
748
749 assert_eq!(
750 Tipset::new([h_unique, h0, h1]).unwrap_err(),
751 CreateTipsetError::DuplicateMiner
752 );
753 }
754
755 #[test]
756 fn ensure_epochs_are_equal() {
757 let h0 = RawBlockHeader {
758 miner_address: Address::new_id(0),
759 epoch: 1,
760 ..Default::default()
761 };
762 let h1 = RawBlockHeader {
763 miner_address: Address::new_id(1),
764 epoch: 2,
765 ..Default::default()
766 };
767 assert_eq!(
768 Tipset::new([h0, h1]).unwrap_err(),
769 CreateTipsetError::BadEpoch
770 );
771 }
772
773 #[test]
774 fn ensure_state_roots_are_equal() {
775 let h0 = RawBlockHeader {
776 miner_address: Address::new_id(0),
777 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
778 ..Default::default()
779 };
780 let h1 = RawBlockHeader {
781 miner_address: Address::new_id(1),
782 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
783 ..Default::default()
784 };
785 assert_eq!(
786 Tipset::new([h0, h1]).unwrap_err(),
787 CreateTipsetError::BadStateRoot
788 );
789 }
790
791 #[test]
792 fn ensure_parent_cids_are_equal() {
793 let h0 = RawBlockHeader {
794 miner_address: Address::new_id(0),
795 ..Default::default()
796 };
797 let h1 = RawBlockHeader {
798 miner_address: Address::new_id(1),
799 parents: TipsetKey::from(nonempty![Cid::new_v1(
800 DAG_CBOR,
801 MultihashCode::Identity.digest(&[])
802 )]),
803 ..Default::default()
804 };
805 assert_eq!(
806 Tipset::new([h0, h1]).unwrap_err(),
807 CreateTipsetError::BadParents
808 );
809 }
810
811 #[test]
812 fn ensure_there_are_blocks() {
813 assert_eq!(
814 Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
815 CreateTipsetError::Empty
816 );
817 }
818}