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