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 fill_from_blockstore(&self, store: &impl Blockstore) -> Option<FullTipset> {
286 let blocks = self
288 .block_headers()
289 .iter()
290 .cloned()
291 .map(|header| {
292 let (bls_messages, secp_messages) =
293 crate::chain::store::block_messages(store, &header).ok()?;
294 Some(Block {
295 header,
296 bls_messages,
297 secp_messages,
298 })
299 })
300 .collect::<Option<Vec<_>>>()?;
301
302 Some(
304 FullTipset::new(blocks)
305 .expect("block headers have already been verified so this check cannot fail"),
306 )
307 }
308
309 pub fn epoch(&self) -> ChainEpoch {
311 self.min_ticket_block().epoch
312 }
313 pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
314 &self.headers
315 }
316 pub fn min_ticket(&self) -> Option<&Ticket> {
318 self.min_ticket_block().ticket.as_ref()
319 }
320 pub fn min_ticket_block(&self) -> &CachingBlockHeader {
322 self.headers.first()
323 }
324 pub fn min_timestamp(&self) -> u64 {
326 self.headers
327 .iter()
328 .map(|block| block.timestamp)
329 .min()
330 .unwrap()
331 }
332 pub fn len(&self) -> usize {
334 self.headers.len()
335 }
336 pub fn key(&self) -> &TipsetKey {
338 self.key
339 .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
340 }
341 pub fn cids(&self) -> NonEmpty<Cid> {
343 self.key().to_cids()
344 }
345 pub fn parents(&self) -> &TipsetKey {
347 &self.min_ticket_block().parents
348 }
349 pub fn parent_state(&self) -> &Cid {
351 &self.min_ticket_block().state_root
352 }
353 pub fn weight(&self) -> &BigInt {
355 &self.min_ticket_block().weight
356 }
357 #[cfg(test)]
360 pub fn break_weight_tie(&self, other: &Tipset) -> bool {
361 let broken = self
363 .block_headers()
364 .iter()
365 .zip(other.block_headers().iter())
366 .any(|(a, b)| {
367 const MSG: &str =
368 "The function block_sanity_checks should have been called at this point.";
369 let ticket = a.ticket.as_ref().expect(MSG);
370 let other_ticket = b.ticket.as_ref().expect(MSG);
371 ticket.vrfproof < other_ticket.vrfproof
372 });
373 if broken {
374 tracing::info!("Weight tie broken in favour of {}", self.key());
375 } else {
376 tracing::info!("Weight tie left unbroken, default to {}", other.key());
377 }
378 broken
379 }
380
381 pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
383 let mut tipset = Some(self);
384 std::iter::from_fn(move || {
385 let child = tipset.take()?;
386 tipset = Tipset::load_required(&store, child.parents()).ok();
387 Some(child)
388 })
389 }
390
391 pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
393 let mut tipset = Some(self);
394 std::iter::from_fn(move || {
395 let child = tipset.take()?;
396 tipset = Tipset::load_required(store, child.parents()).ok();
397 Some(child)
398 })
399 }
400
401 pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
403 #[derive(Serialize, Deserialize)]
408 struct KnownHeaders {
409 calibnet: HashMap<ChainEpoch, String>,
410 mainnet: HashMap<ChainEpoch, String>,
411 }
412
413 static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
414 let headers = KNOWN_HEADERS.get_or_init(|| {
415 serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
416 });
417
418 for tipset in self.clone().chain(store) {
419 for (genesis_cid, known_blocks) in [
421 (*calibnet::GENESIS_CID, &headers.calibnet),
422 (*mainnet::GENESIS_CID, &headers.mainnet),
423 ] {
424 if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
425 && known_block_cid == &tipset.min_ticket_block().cid().to_string()
426 {
427 return store
428 .get_cbor(&genesis_cid)?
429 .context("Genesis block missing from database");
430 }
431 }
432
433 if tipset.epoch() == 0 {
435 return Ok(tipset.min_ticket_block().clone());
436 }
437 }
438 anyhow::bail!("Genesis block not found")
439 }
440}
441
442#[derive(Debug, Clone, Eq)]
445pub struct FullTipset {
446 blocks: Arc<NonEmpty<Block>>,
447 key: Arc<OnceLock<TipsetKey>>,
449}
450
451impl std::hash::Hash for FullTipset {
452 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
453 self.key().hash(state)
454 }
455}
456
457impl From<Block> for FullTipset {
459 fn from(block: Block) -> Self {
460 FullTipset {
461 blocks: nonempty![block].into(),
462 key: OnceLock::new().into(),
463 }
464 }
465}
466
467impl PartialEq for FullTipset {
468 fn eq(&self, other: &Self) -> bool {
469 self.blocks.eq(&other.blocks)
470 }
471}
472
473impl FullTipset {
474 pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
475 let blocks = Arc::new(
476 NonEmpty::new(
477 blocks
480 .into_iter()
481 .sorted_by_cached_key(|it| it.header.tipset_sort_key())
482 .collect(),
483 )
484 .map_err(|_| CreateTipsetError::Empty)?,
485 );
486
487 verify_block_headers(blocks.iter().map(|it| &it.header))?;
488
489 Ok(Self {
490 blocks,
491 key: Arc::new(OnceLock::new()),
492 })
493 }
494 fn first_block(&self) -> &Block {
496 self.blocks.first()
497 }
498 pub fn blocks(&self) -> &NonEmpty<Block> {
500 &self.blocks
501 }
502 pub fn into_blocks(self) -> NonEmpty<Block> {
504 Arc::unwrap_or_clone(self.blocks)
505 }
506 pub fn into_tipset(self) -> Tipset {
509 Tipset::from(self)
510 }
511 pub fn key(&self) -> &TipsetKey {
513 self.key
514 .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
515 }
516 pub fn parent_state(&self) -> &Cid {
518 &self.first_block().header().state_root
519 }
520 pub fn parents(&self) -> &TipsetKey {
522 &self.first_block().header().parents
523 }
524 pub fn epoch(&self) -> ChainEpoch {
526 self.first_block().header().epoch
527 }
528 pub fn weight(&self) -> &BigInt {
530 &self.first_block().header().weight
531 }
532 pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
534 for block in self.blocks() {
535 TipsetValidator::validate_msg_root(db, block)?;
537 crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
538 crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
539 crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
540 }
541 Ok(())
542 }
543}
544
545fn verify_block_headers<'a>(
546 headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
547) -> Result<(), CreateTipsetError> {
548 use itertools::all;
549
550 let headers =
551 NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
552 if !all(&headers, |it| it.parents == headers.first().parents) {
553 return Err(CreateTipsetError::BadParents);
554 }
555 if !all(&headers, |it| it.state_root == headers.first().state_root) {
556 return Err(CreateTipsetError::BadStateRoot);
557 }
558 if !all(&headers, |it| it.epoch == headers.first().epoch) {
559 return Err(CreateTipsetError::BadEpoch);
560 }
561
562 if !headers.iter().map(|it| it.miner_address).all_unique() {
563 return Err(CreateTipsetError::DuplicateMiner);
564 }
565
566 Ok(())
567}
568
569#[cfg_vis::cfg_vis(doc, pub)]
570mod lotus_json {
571 use crate::blocks::{CachingBlockHeader, Tipset};
575 use crate::lotus_json::*;
576 use nunny::Vec as NonEmpty;
577 use schemars::JsonSchema;
578 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
579
580 use super::TipsetKey;
581
582 #[derive(Debug, PartialEq, Clone, JsonSchema)]
583 #[schemars(rename = "Tipset")]
584 pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
585
586 #[derive(Serialize, Deserialize, JsonSchema)]
587 #[schemars(rename = "TipsetInner")]
588 #[serde(rename_all = "PascalCase")]
589 struct TipsetLotusJsonInner {
590 #[serde(with = "crate::lotus_json")]
591 #[schemars(with = "LotusJson<TipsetKey>")]
592 cids: TipsetKey,
593 #[serde(with = "crate::lotus_json")]
594 #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
595 blocks: NonEmpty<CachingBlockHeader>,
596 height: i64,
597 }
598
599 impl<'de> Deserialize<'de> for TipsetLotusJson {
600 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
601 where
602 D: Deserializer<'de>,
603 {
604 let TipsetLotusJsonInner {
605 cids: _ignored0,
606 blocks,
607 height: _ignored1,
608 } = Deserialize::deserialize(deserializer)?;
609
610 Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
611 }
612 }
613
614 impl Serialize for TipsetLotusJson {
615 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
616 where
617 S: Serializer,
618 {
619 let Self(tipset) = self;
620 TipsetLotusJsonInner {
621 cids: tipset.key().clone(),
622 height: tipset.epoch(),
623 blocks: tipset.block_headers().clone(),
624 }
625 .serialize(serializer)
626 }
627 }
628
629 impl HasLotusJson for Tipset {
630 type LotusJson = TipsetLotusJson;
631
632 #[cfg(test)]
633 fn snapshots() -> Vec<(serde_json::Value, Self)> {
634 use serde_json::json;
635 vec![(
636 json!({
637 "Blocks": [
638 {
639 "BeaconEntries": null,
640 "ForkSignaling": 0,
641 "Height": 0,
642 "Messages": { "/": "baeaaaaa" },
643 "Miner": "f00",
644 "ParentBaseFee": "0",
645 "ParentMessageReceipts": { "/": "baeaaaaa" },
646 "ParentStateRoot": { "/":"baeaaaaa" },
647 "ParentWeight": "0",
648 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
649 "Timestamp": 0,
650 "WinPoStProof": null
651 }
652 ],
653 "Cids": [
654 { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
655 ],
656 "Height": 0
657 }),
658 Self::new(vec![CachingBlockHeader::default()]).unwrap(),
659 )]
660 }
661
662 fn into_lotus_json(self) -> Self::LotusJson {
663 TipsetLotusJson(self)
664 }
665
666 fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
667 tipset
668 }
669 }
670
671 #[test]
672 fn snapshots() {
673 assert_all_snapshots::<Tipset>()
674 }
675
676 #[cfg(test)]
677 quickcheck::quickcheck! {
678 fn quickcheck(val: Tipset) -> () {
679 assert_unchanged_via_json(val)
680 }
681 }
682}
683
684#[cfg(test)]
685mod test {
686 use super::*;
687 use crate::blocks::VRFProof;
688 use crate::blocks::{
689 CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, header::RawBlockHeader,
690 };
691 use crate::shim::address::Address;
692 use crate::utils::multihash::prelude::*;
693 use cid::Cid;
694 use fvm_ipld_encoding::DAG_CBOR;
695 use num_bigint::BigInt;
696 use std::iter;
697
698 pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
699 let addr = Address::new_id(id);
700 let cid =
701 Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
702
703 let fmt_str = format!("===={ticket_sequence}=====");
704 let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
705 let election_proof = ElectionProof {
706 win_count: 0,
707 vrfproof: VRFProof::new(fmt_str.into_bytes()),
708 };
709 let weight_inc = BigInt::from(weight);
710 CachingBlockHeader::new(RawBlockHeader {
711 miner_address: addr,
712 election_proof: Some(election_proof),
713 ticket: Some(ticket),
714 message_receipts: cid,
715 messages: cid,
716 state_root: cid,
717 weight: weight_inc,
718 ..Default::default()
719 })
720 }
721
722 #[test]
723 fn test_break_weight_tie() {
724 let b1 = mock_block(1234561, 1, 1);
725 let ts1 = Tipset::from(&b1);
726
727 let b2 = mock_block(1234562, 1, 2);
728 let ts2 = Tipset::from(&b2);
729
730 let b3 = mock_block(1234563, 1, 1);
731 let ts3 = Tipset::from(&b3);
732
733 assert!(ts1.break_weight_tie(&ts2));
737 assert!(!ts1.break_weight_tie(&ts3));
739
740 let b4 = mock_block(1234564, 1, 41);
742 let b5 = mock_block(1234565, 1, 45);
743 let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
744 let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
745 assert!(ts4.break_weight_tie(&ts5));
747
748 let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
749 let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
750 assert!(!ts6.break_weight_tie(&ts7));
752 }
753
754 #[test]
755 fn ensure_miner_addresses_are_distinct() {
756 let h0 = RawBlockHeader {
757 miner_address: Address::new_id(0),
758 ..Default::default()
759 };
760 let h1 = RawBlockHeader {
761 miner_address: Address::new_id(0),
762 ..Default::default()
763 };
764 assert_eq!(
765 Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
766 CreateTipsetError::DuplicateMiner
767 );
768
769 let h_unique = RawBlockHeader {
770 miner_address: Address::new_id(1),
771 ..Default::default()
772 };
773
774 assert_eq!(
775 Tipset::new([h_unique, h0, h1]).unwrap_err(),
776 CreateTipsetError::DuplicateMiner
777 );
778 }
779
780 #[test]
781 fn ensure_epochs_are_equal() {
782 let h0 = RawBlockHeader {
783 miner_address: Address::new_id(0),
784 epoch: 1,
785 ..Default::default()
786 };
787 let h1 = RawBlockHeader {
788 miner_address: Address::new_id(1),
789 epoch: 2,
790 ..Default::default()
791 };
792 assert_eq!(
793 Tipset::new([h0, h1]).unwrap_err(),
794 CreateTipsetError::BadEpoch
795 );
796 }
797
798 #[test]
799 fn ensure_state_roots_are_equal() {
800 let h0 = RawBlockHeader {
801 miner_address: Address::new_id(0),
802 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
803 ..Default::default()
804 };
805 let h1 = RawBlockHeader {
806 miner_address: Address::new_id(1),
807 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
808 ..Default::default()
809 };
810 assert_eq!(
811 Tipset::new([h0, h1]).unwrap_err(),
812 CreateTipsetError::BadStateRoot
813 );
814 }
815
816 #[test]
817 fn ensure_parent_cids_are_equal() {
818 let h0 = RawBlockHeader {
819 miner_address: Address::new_id(0),
820 ..Default::default()
821 };
822 let h1 = RawBlockHeader {
823 miner_address: Address::new_id(1),
824 parents: TipsetKey::from(nonempty![Cid::new_v1(
825 DAG_CBOR,
826 MultihashCode::Identity.digest(&[])
827 )]),
828 ..Default::default()
829 };
830 assert_eq!(
831 Tipset::new([h0, h1]).unwrap_err(),
832 CreateTipsetError::BadParents
833 );
834 }
835
836 #[test]
837 fn ensure_there_are_blocks() {
838 assert_eq!(
839 Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
840 CreateTipsetError::Empty
841 );
842 }
843}