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