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