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