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, GetSize)]
171pub struct Tipset {
172 #[get_size(size_fn = nunny_vec_heap_size_helper)]
174 headers: Arc<NonEmpty<CachingBlockHeader>>,
175 key: Arc<OnceLock<TipsetKey>>,
177}
178
179impl ShallowClone for Tipset {
180 fn shallow_clone(&self) -> Self {
181 Self {
182 headers: self.headers.shallow_clone(),
183 key: self.key.shallow_clone(),
184 }
185 }
186}
187
188impl From<RawBlockHeader> for Tipset {
189 fn from(value: RawBlockHeader) -> Self {
190 Self::from(CachingBlockHeader::from(value))
191 }
192}
193
194impl From<&CachingBlockHeader> for Tipset {
195 fn from(value: &CachingBlockHeader) -> Self {
196 value.clone().into()
197 }
198}
199
200impl From<CachingBlockHeader> for Tipset {
201 fn from(value: CachingBlockHeader) -> Self {
202 Self {
203 headers: nonempty![value].into(),
204 key: OnceLock::new().into(),
205 }
206 }
207}
208
209impl From<NonEmpty<CachingBlockHeader>> for Tipset {
210 fn from(headers: NonEmpty<CachingBlockHeader>) -> Self {
211 Self {
212 headers: headers.into(),
213 key: OnceLock::new().into(),
214 }
215 }
216}
217
218impl PartialEq for Tipset {
219 fn eq(&self, other: &Self) -> bool {
220 self.headers.eq(&other.headers)
221 }
222}
223
224#[cfg(test)]
225impl quickcheck::Arbitrary for Tipset {
226 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
227 Tipset::from(CachingBlockHeader::arbitrary(g))
230 }
231}
232
233impl From<FullTipset> for Tipset {
234 fn from(FullTipset { key, blocks }: FullTipset) -> Self {
235 let headers = Arc::unwrap_or_clone(blocks)
236 .into_iter_ne()
237 .map(|block| block.header)
238 .collect_vec()
239 .into();
240 Tipset { headers, key }
241 }
242}
243
244#[derive(Error, Debug, PartialEq)]
245pub enum CreateTipsetError {
246 #[error("tipsets must not be empty")]
247 Empty,
248 #[error(
249 "parent CID is inconsistent. All block headers in a tipset must agree on their parent tipset"
250 )]
251 BadParents,
252 #[error(
253 "state root is inconsistent. All block headers in a tipset must agree on their parent state root"
254 )]
255 BadStateRoot,
256 #[error("epoch is inconsistent. All block headers in a tipset must agree on their epoch")]
257 BadEpoch,
258 #[error("duplicate miner address. All miners in a tipset must be unique.")]
259 DuplicateMiner,
260 #[error("block has no ticket. All blocks in a tipset must have a ticket.")]
261 MissingTicket,
262}
263
264pub trait TipsetLike {
266 fn epoch(&self) -> ChainEpoch;
267 fn key(&self) -> &TipsetKey;
268 fn parents(&self) -> &TipsetKey;
269 #[allow(dead_code)]
270 fn parent_state(&self) -> &Cid;
271}
272
273#[allow(clippy::len_without_is_empty)]
274impl Tipset {
275 pub fn new<H: Into<CachingBlockHeader>>(
281 headers: impl IntoIterator<Item = H>,
282 ) -> Result<Self, CreateTipsetError> {
283 let mut headers = NonEmpty::new(
284 headers
285 .into_iter()
286 .map(Into::<CachingBlockHeader>::into)
287 .sorted_by_cached_key(|it| it.tipset_sort_key())
288 .collect(),
289 )
290 .map_err(|_| CreateTipsetError::Empty)?;
291 headers.shrink_to_fit();
292 verify_block_headers(&headers)?;
293
294 Ok(Self {
295 headers: headers.into(),
296 key: OnceLock::new().into(),
297 })
298 }
299
300 pub fn load(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Option<Tipset>> {
303 Ok(tsk
304 .to_cids()
305 .into_iter()
306 .map(|key| CachingBlockHeader::load(store, key))
307 .collect::<anyhow::Result<Option<Vec<_>>>>()?
308 .map(Tipset::new)
309 .transpose()?)
310 }
311
312 pub fn load_required(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Tipset> {
315 Tipset::load(store, tsk)?.context("Required tipset missing from database")
316 }
317
318 pub fn epoch(&self) -> ChainEpoch {
320 self.min_ticket_block().epoch
321 }
322 pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
323 &self.headers
324 }
325 pub fn min_ticket(&self) -> Option<&Ticket> {
327 self.min_ticket_block().ticket.as_ref()
328 }
329 pub fn min_ticket_block(&self) -> &CachingBlockHeader {
331 self.headers.first()
332 }
333 pub fn min_timestamp(&self) -> u64 {
335 self.headers
336 .iter()
337 .map(|block| block.timestamp)
338 .min()
339 .unwrap()
340 }
341 pub fn len(&self) -> usize {
343 self.headers.len()
344 }
345 pub fn key(&self) -> &TipsetKey {
347 self.key
348 .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
349 }
350 pub fn cids(&self) -> NonEmpty<Cid> {
352 self.key().to_cids()
353 }
354 pub fn parents(&self) -> &TipsetKey {
356 &self.min_ticket_block().parents
357 }
358 pub fn parent_state(&self) -> &Cid {
360 &self.min_ticket_block().state_root
361 }
362 pub fn parent_message_receipts(&self) -> &Cid {
364 &self.min_ticket_block().message_receipts
365 }
366 pub fn weight(&self) -> &BigInt {
368 &self.min_ticket_block().weight
369 }
370 #[cfg(test)]
373 pub fn break_weight_tie(&self, other: &Tipset) -> bool {
374 let broken = self
376 .block_headers()
377 .iter()
378 .zip(other.block_headers().iter())
379 .any(|(a, b)| {
380 const MSG: &str =
381 "The function block_sanity_checks should have been called at this point.";
382 let ticket = a.ticket.as_ref().expect(MSG);
383 let other_ticket = b.ticket.as_ref().expect(MSG);
384 ticket.vrfproof < other_ticket.vrfproof
385 });
386 if broken {
387 tracing::info!("Weight tie broken in favour of {}", self.key());
388 } else {
389 tracing::info!("Weight tie left unbroken, default to {}", other.key());
390 }
391 broken
392 }
393
394 pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
396 let mut tipset = Some(self);
397 std::iter::from_fn(move || {
398 let child = tipset.take()?;
399 tipset = Tipset::load_required(&store, child.parents()).ok();
400 Some(child)
401 })
402 }
403
404 pub fn chain(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 genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
416 #[derive(Serialize, Deserialize)]
421 struct KnownHeaders {
422 calibnet: HashMap<ChainEpoch, String>,
423 mainnet: HashMap<ChainEpoch, String>,
424 }
425
426 static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
427 let headers = KNOWN_HEADERS.get_or_init(|| {
428 serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
429 });
430
431 for tipset in self.clone().chain(store) {
432 for (genesis_cid, known_blocks) in [
434 (*calibnet::GENESIS_CID, &headers.calibnet),
435 (*mainnet::GENESIS_CID, &headers.mainnet),
436 ] {
437 if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
438 && known_block_cid == &tipset.min_ticket_block().cid().to_string()
439 {
440 return store
441 .get_cbor(&genesis_cid)?
442 .context("Genesis block missing from database");
443 }
444 }
445
446 if tipset.epoch() == 0 {
448 return Ok(tipset.min_ticket_block().clone());
449 }
450 }
451 anyhow::bail!("Genesis block not found")
452 }
453}
454
455impl TipsetLike for Tipset {
456 fn epoch(&self) -> ChainEpoch {
457 self.epoch()
458 }
459
460 fn key(&self) -> &TipsetKey {
461 self.key()
462 }
463
464 fn parents(&self) -> &TipsetKey {
465 self.parents()
466 }
467
468 fn parent_state(&self) -> &Cid {
469 self.parent_state()
470 }
471}
472
473#[derive(Debug, Clone, Eq)]
476pub struct FullTipset {
477 blocks: Arc<NonEmpty<Block>>,
478 key: Arc<OnceLock<TipsetKey>>,
480}
481
482impl TipsetLike for FullTipset {
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
500impl std::hash::Hash for FullTipset {
501 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
502 self.key().hash(state)
503 }
504}
505
506impl From<Block> for FullTipset {
508 fn from(block: Block) -> Self {
509 FullTipset {
510 blocks: nonempty![block].into(),
511 key: OnceLock::new().into(),
512 }
513 }
514}
515
516impl PartialEq for FullTipset {
517 fn eq(&self, other: &Self) -> bool {
518 self.blocks.eq(&other.blocks)
519 }
520}
521
522impl FullTipset {
523 pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
524 let blocks = Arc::new(
525 NonEmpty::new(
526 blocks
529 .into_iter()
530 .sorted_by_cached_key(|it| it.header.tipset_sort_key())
531 .collect(),
532 )
533 .map_err(|_| CreateTipsetError::Empty)?,
534 );
535
536 verify_block_headers(blocks.iter().map(|it| &it.header))?;
537
538 Ok(Self {
539 blocks,
540 key: Arc::new(OnceLock::new()),
541 })
542 }
543 fn first_block(&self) -> &Block {
545 self.blocks.first()
546 }
547 pub fn blocks(&self) -> &NonEmpty<Block> {
549 &self.blocks
550 }
551 pub fn into_blocks(self) -> NonEmpty<Block> {
553 Arc::unwrap_or_clone(self.blocks)
554 }
555 pub fn into_tipset(self) -> Tipset {
558 Tipset::from(self)
559 }
560 pub fn key(&self) -> &TipsetKey {
562 self.key
563 .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
564 }
565 pub fn parent_state(&self) -> &Cid {
567 &self.first_block().header().state_root
568 }
569 pub fn parents(&self) -> &TipsetKey {
571 &self.first_block().header().parents
572 }
573 pub fn epoch(&self) -> ChainEpoch {
575 self.first_block().header().epoch
576 }
577 pub fn weight(&self) -> &BigInt {
579 &self.first_block().header().weight
580 }
581 pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
583 for block in self.blocks() {
584 TipsetValidator::validate_msg_root(db, block)?;
586 crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
587 crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
588 crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
589 }
590 Ok(())
591 }
592}
593
594fn verify_block_headers<'a>(
595 headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
596) -> Result<(), CreateTipsetError> {
597 use itertools::all;
598
599 let headers =
600 NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
601 if !all(&headers, |it| it.ticket.is_some()) {
602 return Err(CreateTipsetError::MissingTicket);
603 }
604 if !all(&headers, |it| it.parents == headers.first().parents) {
605 return Err(CreateTipsetError::BadParents);
606 }
607 if !all(&headers, |it| it.state_root == headers.first().state_root) {
608 return Err(CreateTipsetError::BadStateRoot);
609 }
610 if !all(&headers, |it| it.epoch == headers.first().epoch) {
611 return Err(CreateTipsetError::BadEpoch);
612 }
613
614 if !headers.iter().map(|it| it.miner_address).all_unique() {
615 return Err(CreateTipsetError::DuplicateMiner);
616 }
617
618 Ok(())
619}
620
621#[cfg_vis::cfg_vis(doc, pub)]
622mod lotus_json {
623 use crate::blocks::{CachingBlockHeader, Tipset};
627 use crate::lotus_json::*;
628 use nunny::Vec as NonEmpty;
629 use schemars::JsonSchema;
630 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
631
632 use super::TipsetKey;
633
634 #[derive(Debug, PartialEq, Clone, JsonSchema)]
635 #[schemars(rename = "Tipset")]
636 pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
637
638 #[derive(Serialize, Deserialize, JsonSchema)]
639 #[schemars(rename = "TipsetInner")]
640 #[serde(rename_all = "PascalCase")]
641 struct TipsetLotusJsonInner {
642 #[serde(with = "crate::lotus_json")]
643 #[schemars(with = "LotusJson<TipsetKey>")]
644 cids: TipsetKey,
645 #[serde(with = "crate::lotus_json")]
646 #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
647 blocks: NonEmpty<CachingBlockHeader>,
648 height: i64,
649 }
650
651 impl<'de> Deserialize<'de> for TipsetLotusJson {
652 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
653 where
654 D: Deserializer<'de>,
655 {
656 let TipsetLotusJsonInner {
657 cids: _ignored0,
658 blocks,
659 height: _ignored1,
660 } = Deserialize::deserialize(deserializer)?;
661
662 Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
663 }
664 }
665
666 impl Serialize for TipsetLotusJson {
667 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
668 where
669 S: Serializer,
670 {
671 let Self(tipset) = self;
672 TipsetLotusJsonInner {
673 cids: tipset.key().clone(),
674 height: tipset.epoch(),
675 blocks: tipset.block_headers().clone(),
676 }
677 .serialize(serializer)
678 }
679 }
680
681 impl HasLotusJson for Tipset {
682 type LotusJson = TipsetLotusJson;
683
684 #[cfg(test)]
685 fn snapshots() -> Vec<(serde_json::Value, Self)> {
686 use crate::blocks::header::RawBlockHeader;
687 use crate::test_utils::dummy_ticket;
688 use serde_json::json;
689 let header = CachingBlockHeader::new(RawBlockHeader {
690 ticket: dummy_ticket(0),
691 ..Default::default()
692 });
693 let header_cid = *header.cid();
694 vec![(
695 json!({
696 "Blocks": [
697 {
698 "BeaconEntries": null,
699 "ForkSignaling": 0,
700 "Height": 0,
701 "Messages": { "/": "baeaaaaa" },
702 "Miner": "f00",
703 "ParentBaseFee": "0",
704 "ParentMessageReceipts": { "/": "baeaaaaa" },
705 "ParentStateRoot": { "/":"baeaaaaa" },
706 "ParentWeight": "0",
707 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
708 "Ticket": { "VRFProof": "AA==" },
709 "Timestamp": 0,
710 "WinPoStProof": null
711 }
712 ],
713 "Cids": [
714 { "/": header_cid.to_string() }
715 ],
716 "Height": 0
717 }),
718 Self::new(vec![header]).unwrap(),
719 )]
720 }
721
722 fn into_lotus_json(self) -> Self::LotusJson {
723 TipsetLotusJson(self)
724 }
725
726 fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
727 tipset
728 }
729 }
730
731 #[test]
732 fn snapshots() {
733 assert_all_snapshots::<Tipset>()
734 }
735
736 #[cfg(test)]
737 #[quickcheck_macros::quickcheck]
738 fn quickcheck(val: Tipset) {
739 assert_unchanged_via_json(val)
740 }
741}
742
743#[cfg(test)]
744mod test {
745 use super::*;
746 use crate::blocks::{
747 CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, VRFProof,
748 header::RawBlockHeader,
749 };
750 use crate::db::MemoryDB;
751 use crate::shim::address::Address;
752 use crate::test_utils::dummy_ticket;
753 use cid::Cid;
754 use fvm_ipld_encoding::DAG_CBOR;
755 use num_bigint::BigInt;
756 use quickcheck::Arbitrary;
757 use quickcheck_macros::quickcheck;
758 use std::iter;
759
760 pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
761 let addr = Address::new_id(id);
762 let cid =
763 Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
764
765 let fmt_str = format!("===={ticket_sequence}=====");
766 let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
767 let election_proof = ElectionProof {
768 win_count: 0,
769 vrfproof: VRFProof::new(fmt_str.into_bytes()),
770 };
771 let weight_inc = BigInt::from(weight);
772 CachingBlockHeader::new(RawBlockHeader {
773 miner_address: addr,
774 election_proof: Some(election_proof),
775 ticket: Some(ticket),
776 message_receipts: cid,
777 messages: cid,
778 state_root: cid,
779 weight: weight_inc,
780 ..Default::default()
781 })
782 }
783
784 #[test]
785 fn test_break_weight_tie() {
786 let b1 = mock_block(1234561, 1, 1);
787 let ts1 = Tipset::from(&b1);
788
789 let b2 = mock_block(1234562, 1, 2);
790 let ts2 = Tipset::from(&b2);
791
792 let b3 = mock_block(1234563, 1, 1);
793 let ts3 = Tipset::from(&b3);
794
795 assert!(ts1.break_weight_tie(&ts2));
799 assert!(!ts1.break_weight_tie(&ts3));
801
802 let b4 = mock_block(1234564, 1, 41);
804 let b5 = mock_block(1234565, 1, 45);
805 let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
806 let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
807 assert!(ts4.break_weight_tie(&ts5));
809
810 let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
811 let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
812 assert!(!ts6.break_weight_tie(&ts7));
814 }
815
816 #[test]
817 fn ensure_miner_addresses_are_distinct() {
818 let h0 = RawBlockHeader {
819 miner_address: Address::new_id(0),
820 ticket: dummy_ticket(0),
821 ..Default::default()
822 };
823 let h1 = RawBlockHeader {
824 miner_address: Address::new_id(0),
825 ticket: dummy_ticket(0),
826 ..Default::default()
827 };
828 assert_eq!(
829 Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
830 CreateTipsetError::DuplicateMiner
831 );
832
833 let h_unique = RawBlockHeader {
834 miner_address: Address::new_id(1),
835 ticket: dummy_ticket(0),
836 ..Default::default()
837 };
838
839 assert_eq!(
840 Tipset::new([h_unique, h0, h1]).unwrap_err(),
841 CreateTipsetError::DuplicateMiner
842 );
843 }
844
845 #[test]
846 fn ensure_epochs_are_equal() {
847 let h0 = RawBlockHeader {
848 miner_address: Address::new_id(0),
849 ticket: dummy_ticket(0),
850 epoch: 1,
851 ..Default::default()
852 };
853 let h1 = RawBlockHeader {
854 miner_address: Address::new_id(1),
855 ticket: dummy_ticket(0),
856 epoch: 2,
857 ..Default::default()
858 };
859 assert_eq!(
860 Tipset::new([h0, h1]).unwrap_err(),
861 CreateTipsetError::BadEpoch
862 );
863 }
864
865 #[test]
866 fn ensure_state_roots_are_equal() {
867 let h0 = RawBlockHeader {
868 miner_address: Address::new_id(0),
869 ticket: dummy_ticket(0),
870 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
871 ..Default::default()
872 };
873 let h1 = RawBlockHeader {
874 miner_address: Address::new_id(1),
875 ticket: dummy_ticket(0),
876 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
877 ..Default::default()
878 };
879 assert_eq!(
880 Tipset::new([h0, h1]).unwrap_err(),
881 CreateTipsetError::BadStateRoot
882 );
883 }
884
885 #[test]
886 fn ensure_parent_cids_are_equal() {
887 let h0 = RawBlockHeader {
888 miner_address: Address::new_id(0),
889 ticket: dummy_ticket(0),
890 ..Default::default()
891 };
892 let h1 = RawBlockHeader {
893 miner_address: Address::new_id(1),
894 ticket: dummy_ticket(0),
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 #[test]
916 fn ensure_tickets_are_present() {
917 let with_ticket = RawBlockHeader {
918 miner_address: Address::new_id(0),
919 ticket: dummy_ticket(0),
920 ..Default::default()
921 };
922 let without_ticket = RawBlockHeader {
923 miner_address: Address::new_id(1),
924 ticket: None,
925 ..Default::default()
926 };
927 assert_eq!(
928 Tipset::new([with_ticket, without_ticket]).unwrap_err(),
929 CreateTipsetError::MissingTicket
930 );
931 }
932
933 impl Arbitrary for TipsetKey {
934 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
935 let blocks: nunny::Vec<Vec<u8>> = nunny::Vec::arbitrary(g);
936 let cids = nunny::Vec::new(
937 blocks
938 .into_iter()
939 .map(|b| {
940 Cid::new_v1(
941 fvm_ipld_encoding::DAG_CBOR,
942 MultihashCode::Blake2b256.digest(&b),
943 )
944 })
945 .collect_vec(),
946 )
947 .expect("infallible");
948 cids.into()
949 }
950 }
951
952 #[quickcheck]
953 fn tipset_key_bytes(tsk: TipsetKey) {
954 let bytes = tsk.bytes();
955 let tsk2 = TipsetKey::from_bytes(bytes).unwrap();
956 assert_eq!(tsk, tsk2);
957
958 let bs = MemoryDB::default();
959 let cid = tsk.save(&bs).unwrap();
960 let tsk3 = TipsetKey::load(&bs, &cid).unwrap();
961 assert_eq!(tsk, tsk3);
962 }
963}