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)?
331 .with_context(|| format!("Required tipset missing from database, tipset key: {tsk}"))
332 }
333
334 pub fn epoch(&self) -> ChainEpoch {
336 self.min_ticket_block().epoch
337 }
338 pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
339 &self.headers
340 }
341 pub fn min_ticket(&self) -> Option<&Ticket> {
343 self.min_ticket_block().ticket.as_ref()
344 }
345 pub fn min_ticket_block(&self) -> &CachingBlockHeader {
347 self.headers.first()
348 }
349 pub fn min_timestamp(&self) -> u64 {
351 self.headers
352 .iter()
353 .map(|block| block.timestamp)
354 .min()
355 .expect("tipset headers are non-empty")
356 }
357 pub fn len(&self) -> usize {
359 self.headers.len()
360 }
361 pub fn key(&self) -> &TipsetKey {
363 self.key
364 .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
365 }
366 pub fn cids(&self) -> NonEmpty<Cid> {
368 self.key().to_cids()
369 }
370 pub fn parents(&self) -> &TipsetKey {
372 &self.min_ticket_block().parents
373 }
374 pub fn parent_state(&self) -> &Cid {
376 &self.min_ticket_block().state_root
377 }
378 pub fn parent_message_receipts(&self) -> &Cid {
380 &self.min_ticket_block().message_receipts
381 }
382 pub fn weight(&self) -> &BigInt {
384 &self.min_ticket_block().weight
385 }
386 #[cfg(test)]
389 pub fn break_weight_tie(&self, other: &Tipset) -> bool {
390 let broken = self
392 .block_headers()
393 .iter()
394 .zip(other.block_headers().iter())
395 .any(|(a, b)| {
396 const MSG: &str =
397 "The function block_sanity_checks should have been called at this point.";
398 let ticket = a.ticket.as_ref().expect(MSG);
399 let other_ticket = b.ticket.as_ref().expect(MSG);
400 ticket.vrfproof < other_ticket.vrfproof
401 });
402 if broken {
403 tracing::info!("Weight tie broken in favour of {}", self.key());
404 } else {
405 tracing::info!("Weight tie left unbroken, default to {}", other.key());
406 }
407 broken
408 }
409
410 pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
412 let mut tipset = Some(self);
413 std::iter::from_fn(move || {
414 let child = tipset.take()?;
415 tipset = Tipset::load_required(&store, child.parents()).ok();
416 Some(child)
417 })
418 }
419
420 pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
422 let mut tipset = Some(self);
423 std::iter::from_fn(move || {
424 let child = tipset.take()?;
425 tipset = Tipset::load_required(store, child.parents()).ok();
426 Some(child)
427 })
428 }
429
430 pub async fn genesis(
432 &self,
433 store: impl Blockstore + Send + Sync + 'static,
434 ) -> anyhow::Result<Tipset> {
435 let this = self.shallow_clone();
436 tokio::task::spawn_blocking(move || this.genesis_blocking(&store)).await?
437 }
438
439 pub fn genesis_blocking(&self, store: &impl Blockstore) -> anyhow::Result<Tipset> {
443 #[derive(Serialize, Deserialize)]
448 struct KnownHeaders {
449 calibnet: HashMap<ChainEpoch, String>,
450 mainnet: HashMap<ChainEpoch, String>,
451 }
452
453 static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
454 let headers = KNOWN_HEADERS.get_or_init(|| {
455 serde_yaml::from_str(include_str!("../../build/known_blocks.yaml"))
456 .expect("bundled known_blocks.yaml is valid")
457 });
458
459 for tipset in self.shallow_clone().chain(store) {
460 for (genesis_cid, known_blocks) in [
462 (*calibnet::GENESIS_CID, &headers.calibnet),
463 (*mainnet::GENESIS_CID, &headers.mainnet),
464 ] {
465 if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
466 && known_block_cid == &tipset.min_ticket_block().cid().to_string()
467 {
468 let genesis_block: CachingBlockHeader = store
469 .get_cbor(&genesis_cid)?
470 .context("Genesis block missing from database")?;
471 return Ok(genesis_block.into());
472 }
473 }
474
475 if tipset.epoch() == 0 {
477 return Ok(tipset);
478 }
479 }
480 anyhow::bail!("Genesis block not found")
481 }
482}
483
484impl TipsetLike for Tipset {
485 fn epoch(&self) -> ChainEpoch {
486 self.epoch()
487 }
488
489 fn key(&self) -> &TipsetKey {
490 self.key()
491 }
492
493 fn parents(&self) -> &TipsetKey {
494 self.parents()
495 }
496
497 fn parent_state(&self) -> &Cid {
498 self.parent_state()
499 }
500}
501
502#[derive(Debug, Clone, Eq)]
505pub struct FullTipset {
506 blocks: Arc<NonEmpty<Block>>,
507 key: Arc<OnceLock<TipsetKey>>,
509}
510
511impl TipsetLike for FullTipset {
512 fn epoch(&self) -> ChainEpoch {
513 self.epoch()
514 }
515
516 fn key(&self) -> &TipsetKey {
517 self.key()
518 }
519
520 fn parents(&self) -> &TipsetKey {
521 self.parents()
522 }
523
524 fn parent_state(&self) -> &Cid {
525 self.parent_state()
526 }
527}
528
529impl std::hash::Hash for FullTipset {
530 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
531 self.key().hash(state)
532 }
533}
534
535impl From<Block> for FullTipset {
537 fn from(block: Block) -> Self {
538 FullTipset {
539 blocks: nonempty![block].into(),
540 key: OnceLock::new().into(),
541 }
542 }
543}
544
545impl PartialEq for FullTipset {
546 fn eq(&self, other: &Self) -> bool {
547 self.blocks.eq(&other.blocks)
548 }
549}
550
551impl FullTipset {
552 pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
553 let blocks = Arc::new(
554 NonEmpty::new(
555 blocks
558 .into_iter()
559 .sorted_by_cached_key(|it| it.header.tipset_sort_key())
560 .collect(),
561 )
562 .map_err(|_| CreateTipsetError::Empty)?,
563 );
564
565 verify_block_headers(blocks.iter().map(|it| &it.header))?;
566
567 Ok(Self {
568 blocks,
569 key: Arc::new(OnceLock::new()),
570 })
571 }
572 fn first_block(&self) -> &Block {
574 self.blocks.first()
575 }
576 pub fn blocks(&self) -> &NonEmpty<Block> {
578 &self.blocks
579 }
580 pub fn into_blocks(self) -> NonEmpty<Block> {
582 Arc::unwrap_or_clone(self.blocks)
583 }
584 pub fn into_tipset(self) -> Tipset {
587 Tipset::from(self)
588 }
589 pub fn key(&self) -> &TipsetKey {
591 self.key
592 .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
593 }
594 pub fn parent_state(&self) -> &Cid {
596 &self.first_block().header().state_root
597 }
598 pub fn parents(&self) -> &TipsetKey {
600 &self.first_block().header().parents
601 }
602 pub fn epoch(&self) -> ChainEpoch {
604 self.first_block().header().epoch
605 }
606 pub fn weight(&self) -> &BigInt {
608 &self.first_block().header().weight
609 }
610 pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
612 for block in self.blocks() {
613 TipsetValidator::validate_msg_root(db, block)?;
615 crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
616 crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
617 crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
618 }
619 Ok(())
620 }
621}
622
623fn verify_block_headers<'a>(
624 headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
625) -> Result<(), CreateTipsetError> {
626 use itertools::all;
627
628 let headers =
629 NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
630 if !all(&headers, |it| it.ticket.is_some()) {
631 return Err(CreateTipsetError::MissingTicket);
632 }
633 if !all(&headers, |it| it.parents == headers.first().parents) {
634 return Err(CreateTipsetError::BadParents);
635 }
636 if !all(&headers, |it| it.state_root == headers.first().state_root) {
637 return Err(CreateTipsetError::BadStateRoot);
638 }
639 if !all(&headers, |it| it.epoch == headers.first().epoch) {
640 return Err(CreateTipsetError::BadEpoch);
641 }
642
643 if !headers.iter().map(|it| it.miner_address).all_unique() {
644 return Err(CreateTipsetError::DuplicateMiner);
645 }
646
647 Ok(())
648}
649
650#[cfg_vis::cfg_vis(doc, pub)]
651mod lotus_json {
652 use super::*;
656 use crate::blocks::{CachingBlockHeader, Tipset};
657 use crate::lotus_json::*;
658 use nunny::Vec as NonEmpty;
659 use schemars::JsonSchema;
660 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
661
662 #[derive(Debug, PartialEq, Clone, JsonSchema)]
663 #[schemars(rename = "Tipset")]
664 pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
665
666 #[derive(Serialize, Deserialize, JsonSchema)]
667 #[schemars(rename = "TipsetInner")]
668 #[serde(rename_all = "PascalCase")]
669 struct TipsetLotusJsonInner {
670 #[serde(with = "crate::lotus_json")]
671 #[schemars(with = "LotusJson<TipsetKey>")]
672 cids: TipsetKey,
673 #[serde(with = "crate::lotus_json")]
674 #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
675 blocks: NonEmpty<CachingBlockHeader>,
676 height: ChainEpoch,
677 }
678
679 impl<'de> Deserialize<'de> for TipsetLotusJson {
680 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
681 where
682 D: Deserializer<'de>,
683 {
684 let TipsetLotusJsonInner {
685 cids: _ignored0,
686 blocks,
687 height: _ignored1,
688 } = Deserialize::deserialize(deserializer)?;
689
690 Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
691 }
692 }
693
694 impl Serialize for TipsetLotusJson {
695 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
696 where
697 S: Serializer,
698 {
699 let Self(tipset) = self;
700 TipsetLotusJsonInner {
701 cids: tipset.key().clone(),
702 height: tipset.epoch(),
703 blocks: tipset.block_headers().clone(),
704 }
705 .serialize(serializer)
706 }
707 }
708
709 impl HasLotusJson for Tipset {
710 type LotusJson = TipsetLotusJson;
711
712 #[cfg(test)]
713 fn snapshots() -> Vec<(serde_json::Value, Self)> {
714 use crate::blocks::header::RawBlockHeader;
715 use crate::test_utils::dummy_ticket;
716 use serde_json::json;
717 let header = CachingBlockHeader::new(RawBlockHeader {
718 ticket: dummy_ticket(0),
719 ..Default::default()
720 });
721 let header_cid = *header.cid();
722 vec![(
723 json!({
724 "Blocks": [
725 {
726 "BeaconEntries": null,
727 "ForkSignaling": 0,
728 "Height": 0,
729 "Messages": { "/": "baeaaaaa" },
730 "Miner": "f00",
731 "ParentBaseFee": "0",
732 "ParentMessageReceipts": { "/": "baeaaaaa" },
733 "ParentStateRoot": { "/":"baeaaaaa" },
734 "ParentWeight": "0",
735 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
736 "Ticket": { "VRFProof": "AA==" },
737 "Timestamp": 0,
738 "WinPoStProof": null
739 }
740 ],
741 "Cids": [
742 { "/": header_cid.to_string() }
743 ],
744 "Height": 0
745 }),
746 Self::new(vec![header]).unwrap(),
747 )]
748 }
749
750 fn into_lotus_json(self) -> Self::LotusJson {
751 TipsetLotusJson(self)
752 }
753
754 fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
755 tipset
756 }
757 }
758
759 #[test]
760 fn snapshots() {
761 assert_all_snapshots::<Tipset>()
762 }
763
764 #[cfg(test)]
765 #[quickcheck_macros::quickcheck]
766 fn quickcheck(val: Tipset) {
767 assert_unchanged_via_json(val)
768 }
769}
770
771#[cfg(test)]
772mod test {
773 use super::*;
774 use crate::blocks::{
775 CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, VRFProof,
776 header::RawBlockHeader,
777 };
778 use crate::db::MemoryDB;
779 use crate::shim::address::Address;
780 use crate::test_utils::dummy_ticket;
781 use cid::Cid;
782 use fvm_ipld_encoding::DAG_CBOR;
783 use num_bigint::BigInt;
784 use quickcheck::Arbitrary;
785 use quickcheck_macros::quickcheck;
786 use std::iter;
787
788 pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
789 let addr = Address::new_id(id);
790 let cid =
791 Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
792
793 let fmt_str = format!("===={ticket_sequence}=====");
794 let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
795 let election_proof = ElectionProof {
796 win_count: 0,
797 vrfproof: VRFProof::new(fmt_str.into_bytes()),
798 };
799 let weight_inc = BigInt::from(weight);
800 CachingBlockHeader::new(RawBlockHeader {
801 miner_address: addr,
802 election_proof: Some(election_proof),
803 ticket: Some(ticket),
804 message_receipts: cid,
805 messages: cid,
806 state_root: cid,
807 weight: weight_inc,
808 ..Default::default()
809 })
810 }
811
812 #[test]
813 fn test_break_weight_tie() {
814 let b1 = mock_block(1234561, 1, 1);
815 let ts1 = Tipset::from(&b1);
816
817 let b2 = mock_block(1234562, 1, 2);
818 let ts2 = Tipset::from(&b2);
819
820 let b3 = mock_block(1234563, 1, 1);
821 let ts3 = Tipset::from(&b3);
822
823 assert!(ts1.break_weight_tie(&ts2));
827 assert!(!ts1.break_weight_tie(&ts3));
829
830 let b4 = mock_block(1234564, 1, 41);
832 let b5 = mock_block(1234565, 1, 45);
833 let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
834 let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
835 assert!(ts4.break_weight_tie(&ts5));
837
838 let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
839 let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
840 assert!(!ts6.break_weight_tie(&ts7));
842 }
843
844 #[test]
845 fn ensure_miner_addresses_are_distinct() {
846 let h0 = RawBlockHeader {
847 miner_address: Address::new_id(0),
848 ticket: dummy_ticket(0),
849 ..Default::default()
850 };
851 let h1 = RawBlockHeader {
852 miner_address: Address::new_id(0),
853 ticket: dummy_ticket(0),
854 ..Default::default()
855 };
856 assert_eq!(
857 Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
858 CreateTipsetError::DuplicateMiner
859 );
860
861 let h_unique = RawBlockHeader {
862 miner_address: Address::new_id(1),
863 ticket: dummy_ticket(0),
864 ..Default::default()
865 };
866
867 assert_eq!(
868 Tipset::new([h_unique, h0, h1]).unwrap_err(),
869 CreateTipsetError::DuplicateMiner
870 );
871 }
872
873 #[test]
874 fn ensure_epochs_are_equal() {
875 let h0 = RawBlockHeader {
876 miner_address: Address::new_id(0),
877 ticket: dummy_ticket(0),
878 epoch: 1,
879 ..Default::default()
880 };
881 let h1 = RawBlockHeader {
882 miner_address: Address::new_id(1),
883 ticket: dummy_ticket(0),
884 epoch: 2,
885 ..Default::default()
886 };
887 assert_eq!(
888 Tipset::new([h0, h1]).unwrap_err(),
889 CreateTipsetError::BadEpoch
890 );
891 }
892
893 #[test]
894 fn ensure_state_roots_are_equal() {
895 let h0 = RawBlockHeader {
896 miner_address: Address::new_id(0),
897 ticket: dummy_ticket(0),
898 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
899 ..Default::default()
900 };
901 let h1 = RawBlockHeader {
902 miner_address: Address::new_id(1),
903 ticket: dummy_ticket(0),
904 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
905 ..Default::default()
906 };
907 assert_eq!(
908 Tipset::new([h0, h1]).unwrap_err(),
909 CreateTipsetError::BadStateRoot
910 );
911 }
912
913 #[test]
914 fn ensure_parent_cids_are_equal() {
915 let h0 = RawBlockHeader {
916 miner_address: Address::new_id(0),
917 ticket: dummy_ticket(0),
918 ..Default::default()
919 };
920 let h1 = RawBlockHeader {
921 miner_address: Address::new_id(1),
922 ticket: dummy_ticket(0),
923 parents: TipsetKey::from(nonempty![Cid::new_v1(
924 DAG_CBOR,
925 MultihashCode::Identity.digest(&[])
926 )]),
927 ..Default::default()
928 };
929 assert_eq!(
930 Tipset::new([h0, h1]).unwrap_err(),
931 CreateTipsetError::BadParents
932 );
933 }
934
935 #[test]
936 fn ensure_there_are_blocks() {
937 assert_eq!(
938 Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
939 CreateTipsetError::Empty
940 );
941 }
942
943 #[test]
944 fn ensure_tickets_are_present() {
945 let with_ticket = RawBlockHeader {
946 miner_address: Address::new_id(0),
947 ticket: dummy_ticket(0),
948 ..Default::default()
949 };
950 let without_ticket = RawBlockHeader {
951 miner_address: Address::new_id(1),
952 ticket: None,
953 ..Default::default()
954 };
955 assert_eq!(
956 Tipset::new([with_ticket, without_ticket]).unwrap_err(),
957 CreateTipsetError::MissingTicket
958 );
959 }
960
961 impl Arbitrary for TipsetKey {
962 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
963 let blocks: nunny::Vec<Vec<u8>> = nunny::Vec::arbitrary(g);
964 let cids = nunny::Vec::new(
965 blocks
966 .into_iter()
967 .map(|b| {
968 Cid::new_v1(
969 fvm_ipld_encoding::DAG_CBOR,
970 MultihashCode::Blake2b256.digest(&b),
971 )
972 })
973 .collect_vec(),
974 )
975 .expect("infallible");
976 cids.into()
977 }
978 }
979
980 #[quickcheck]
981 fn tipset_key_bytes(tsk: TipsetKey) {
982 let bytes = tsk.bytes();
983 let tsk2 = TipsetKey::from_bytes(bytes).unwrap();
984 assert_eq!(tsk, tsk2);
985
986 let bs = MemoryDB::default();
987 let cid = tsk.save(&bs).unwrap();
988 let tsk3 = TipsetKey::load(&bs, &cid).unwrap();
989 assert_eq!(tsk, tsk3);
990 }
991}