1use std::{
5 fmt,
6 sync::{Arc, OnceLock},
7};
8
9use super::{Block, CachingBlockHeader, RawBlockHeader, Ticket};
10use crate::{
11 chain_sync::TipsetValidator,
12 cid_collections::SmallCidNonEmptyVec,
13 networks::{calibnet, mainnet},
14 shim::clock::ChainEpoch,
15 utils::{cid::CidCborExt, get_size::nunny_vec_heap_size_helper},
16};
17use ahash::HashMap;
18use anyhow::Context as _;
19use cid::Cid;
20use fvm_ipld_blockstore::Blockstore;
21use fvm_ipld_encoding::CborStore;
22use get_size2::GetSize;
23use itertools::Itertools as _;
24use num::BigInt;
25use nunny::{Vec as NonEmpty, vec as nonempty};
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28
29#[derive(
34 Clone,
35 Debug,
36 PartialEq,
37 Eq,
38 Hash,
39 Serialize,
40 Deserialize,
41 PartialOrd,
42 Ord,
43 GetSize,
44 derive_more::IntoIterator,
45)]
46#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
47pub struct TipsetKey(#[into_iterator(owned, ref)] SmallCidNonEmptyVec);
48
49impl TipsetKey {
50 pub fn cid(&self) -> anyhow::Result<Cid> {
52 Ok(Cid::from_cbor_blake2b256(&self.bytes())?)
53 }
54
55 pub fn contains(&self, cid: Cid) -> bool {
57 self.0.contains(cid)
58 }
59
60 pub fn into_cids(self) -> NonEmpty<Cid> {
62 self.0.into_cids()
63 }
64
65 pub fn to_cids(&self) -> NonEmpty<Cid> {
67 self.0.clone().into_cids()
68 }
69
70 pub fn iter(&self) -> impl Iterator<Item = Cid> + '_ {
72 self.0.iter()
73 }
74
75 pub fn len(&self) -> usize {
77 self.0.len()
78 }
79
80 pub fn is_empty(&self) -> bool {
82 false
83 }
84
85 pub fn terse(&self) -> String {
89 fn terse_cid(cid: Cid) -> String {
90 let s = cid::multibase::encode(
91 cid::multibase::Base::Base32Lower,
92 cid.to_bytes().as_slice(),
93 );
94 format!("{}...{}", &s[9..12], &s[s.len() - 3..])
95 }
96 self.to_cids()
97 .into_iter()
98 .map(terse_cid)
99 .collect_vec()
100 .join(", ")
101 }
102
103 pub fn format_lotus(&self) -> String {
105 format!("{{{}}}", self.to_cids().into_iter().join(","))
106 }
107
108 pub fn bytes(&self) -> fvm_ipld_encoding::RawBytes {
110 fvm_ipld_encoding::RawBytes::new(self.iter().flat_map(|cid| cid.to_bytes()).collect())
111 }
112}
113
114impl From<NonEmpty<Cid>> for TipsetKey {
115 fn from(mut value: NonEmpty<Cid>) -> Self {
116 value.shrink_to_fit();
119 Self(value.into())
120 }
121}
122
123impl fmt::Display for TipsetKey {
124 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125 let s = self
126 .to_cids()
127 .into_iter()
128 .map(|cid| cid.to_string())
129 .collect_vec()
130 .join(", ");
131 write!(f, "[{s}]")
132 }
133}
134
135#[cfg(test)]
136impl Default for TipsetKey {
137 fn default() -> Self {
138 nunny::vec![Cid::default()].into()
139 }
140}
141
142#[derive(Clone, Debug, GetSize)]
148pub struct Tipset {
149 #[get_size(size_fn = nunny_vec_heap_size_helper)]
151 headers: Arc<NonEmpty<CachingBlockHeader>>,
152 key: Arc<OnceLock<TipsetKey>>,
154}
155
156impl From<RawBlockHeader> for Tipset {
157 fn from(value: RawBlockHeader) -> Self {
158 Self::from(CachingBlockHeader::from(value))
159 }
160}
161
162impl From<&CachingBlockHeader> for Tipset {
163 fn from(value: &CachingBlockHeader) -> Self {
164 value.clone().into()
165 }
166}
167
168impl From<CachingBlockHeader> for Tipset {
169 fn from(value: CachingBlockHeader) -> Self {
170 Self {
171 headers: nonempty![value].into(),
172 key: OnceLock::new().into(),
173 }
174 }
175}
176
177impl From<NonEmpty<CachingBlockHeader>> for Tipset {
178 fn from(headers: NonEmpty<CachingBlockHeader>) -> Self {
179 Self {
180 headers: headers.into(),
181 key: OnceLock::new().into(),
182 }
183 }
184}
185
186impl PartialEq for Tipset {
187 fn eq(&self, other: &Self) -> bool {
188 self.headers.eq(&other.headers)
189 }
190}
191
192#[cfg(test)]
193impl quickcheck::Arbitrary for Tipset {
194 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
195 Tipset::from(CachingBlockHeader::arbitrary(g))
198 }
199}
200
201impl From<FullTipset> for Tipset {
202 fn from(FullTipset { key, blocks }: FullTipset) -> Self {
203 let headers = Arc::unwrap_or_clone(blocks)
204 .into_iter_ne()
205 .map(|block| block.header)
206 .collect_vec()
207 .into();
208 Tipset { headers, key }
209 }
210}
211
212#[derive(Error, Debug, PartialEq)]
213pub enum CreateTipsetError {
214 #[error("tipsets must not be empty")]
215 Empty,
216 #[error(
217 "parent CID is inconsistent. All block headers in a tipset must agree on their parent tipset"
218 )]
219 BadParents,
220 #[error(
221 "state root is inconsistent. All block headers in a tipset must agree on their parent state root"
222 )]
223 BadStateRoot,
224 #[error("epoch is inconsistent. All block headers in a tipset must agree on their epoch")]
225 BadEpoch,
226 #[error("duplicate miner address. All miners in a tipset must be unique.")]
227 DuplicateMiner,
228}
229
230#[allow(clippy::len_without_is_empty)]
231impl Tipset {
232 pub fn new<H: Into<CachingBlockHeader>>(
238 headers: impl IntoIterator<Item = H>,
239 ) -> Result<Self, CreateTipsetError> {
240 let mut headers = NonEmpty::new(
241 headers
242 .into_iter()
243 .map(Into::<CachingBlockHeader>::into)
244 .sorted_by_cached_key(|it| it.tipset_sort_key())
245 .collect(),
246 )
247 .map_err(|_| CreateTipsetError::Empty)?;
248 headers.shrink_to_fit();
249 verify_block_headers(&headers)?;
250
251 Ok(Self {
252 headers: headers.into(),
253 key: OnceLock::new().into(),
254 })
255 }
256
257 pub fn load(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Option<Tipset>> {
260 Ok(tsk
261 .to_cids()
262 .into_iter()
263 .map(|key| CachingBlockHeader::load(store, key))
264 .collect::<anyhow::Result<Option<Vec<_>>>>()?
265 .map(Tipset::new)
266 .transpose()?)
267 }
268
269 pub fn load_required(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Tipset> {
272 Tipset::load(store, tsk)?.context("Required tipset missing from database")
273 }
274
275 pub fn epoch(&self) -> ChainEpoch {
277 self.min_ticket_block().epoch
278 }
279 pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
280 &self.headers
281 }
282 pub fn min_ticket(&self) -> Option<&Ticket> {
284 self.min_ticket_block().ticket.as_ref()
285 }
286 pub fn min_ticket_block(&self) -> &CachingBlockHeader {
288 self.headers.first()
289 }
290 pub fn min_timestamp(&self) -> u64 {
292 self.headers
293 .iter()
294 .map(|block| block.timestamp)
295 .min()
296 .unwrap()
297 }
298 pub fn len(&self) -> usize {
300 self.headers.len()
301 }
302 pub fn key(&self) -> &TipsetKey {
304 self.key
305 .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
306 }
307 pub fn cids(&self) -> NonEmpty<Cid> {
309 self.key().to_cids()
310 }
311 pub fn parents(&self) -> &TipsetKey {
313 &self.min_ticket_block().parents
314 }
315 pub fn parent_state(&self) -> &Cid {
317 &self.min_ticket_block().state_root
318 }
319 pub fn parent_message_receipts(&self) -> &Cid {
321 &self.min_ticket_block().message_receipts
322 }
323 pub fn weight(&self) -> &BigInt {
325 &self.min_ticket_block().weight
326 }
327 #[cfg(test)]
330 pub fn break_weight_tie(&self, other: &Tipset) -> bool {
331 let broken = self
333 .block_headers()
334 .iter()
335 .zip(other.block_headers().iter())
336 .any(|(a, b)| {
337 const MSG: &str =
338 "The function block_sanity_checks should have been called at this point.";
339 let ticket = a.ticket.as_ref().expect(MSG);
340 let other_ticket = b.ticket.as_ref().expect(MSG);
341 ticket.vrfproof < other_ticket.vrfproof
342 });
343 if broken {
344 tracing::info!("Weight tie broken in favour of {}", self.key());
345 } else {
346 tracing::info!("Weight tie left unbroken, default to {}", other.key());
347 }
348 broken
349 }
350
351 pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
353 let mut tipset = Some(self);
354 std::iter::from_fn(move || {
355 let child = tipset.take()?;
356 tipset = Tipset::load_required(&store, child.parents()).ok();
357 Some(child)
358 })
359 }
360
361 pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
363 let mut tipset = Some(self);
364 std::iter::from_fn(move || {
365 let child = tipset.take()?;
366 tipset = Tipset::load_required(store, child.parents()).ok();
367 Some(child)
368 })
369 }
370
371 pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
373 #[derive(Serialize, Deserialize)]
378 struct KnownHeaders {
379 calibnet: HashMap<ChainEpoch, String>,
380 mainnet: HashMap<ChainEpoch, String>,
381 }
382
383 static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
384 let headers = KNOWN_HEADERS.get_or_init(|| {
385 serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
386 });
387
388 for tipset in self.clone().chain(store) {
389 for (genesis_cid, known_blocks) in [
391 (*calibnet::GENESIS_CID, &headers.calibnet),
392 (*mainnet::GENESIS_CID, &headers.mainnet),
393 ] {
394 if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
395 && known_block_cid == &tipset.min_ticket_block().cid().to_string()
396 {
397 return store
398 .get_cbor(&genesis_cid)?
399 .context("Genesis block missing from database");
400 }
401 }
402
403 if tipset.epoch() == 0 {
405 return Ok(tipset.min_ticket_block().clone());
406 }
407 }
408 anyhow::bail!("Genesis block not found")
409 }
410}
411
412#[derive(Debug, Clone, Eq)]
415pub struct FullTipset {
416 blocks: Arc<NonEmpty<Block>>,
417 key: Arc<OnceLock<TipsetKey>>,
419}
420
421impl std::hash::Hash for FullTipset {
422 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
423 self.key().hash(state)
424 }
425}
426
427impl From<Block> for FullTipset {
429 fn from(block: Block) -> Self {
430 FullTipset {
431 blocks: nonempty![block].into(),
432 key: OnceLock::new().into(),
433 }
434 }
435}
436
437impl PartialEq for FullTipset {
438 fn eq(&self, other: &Self) -> bool {
439 self.blocks.eq(&other.blocks)
440 }
441}
442
443impl FullTipset {
444 pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
445 let blocks = Arc::new(
446 NonEmpty::new(
447 blocks
450 .into_iter()
451 .sorted_by_cached_key(|it| it.header.tipset_sort_key())
452 .collect(),
453 )
454 .map_err(|_| CreateTipsetError::Empty)?,
455 );
456
457 verify_block_headers(blocks.iter().map(|it| &it.header))?;
458
459 Ok(Self {
460 blocks,
461 key: Arc::new(OnceLock::new()),
462 })
463 }
464 fn first_block(&self) -> &Block {
466 self.blocks.first()
467 }
468 pub fn blocks(&self) -> &NonEmpty<Block> {
470 &self.blocks
471 }
472 pub fn into_blocks(self) -> NonEmpty<Block> {
474 Arc::unwrap_or_clone(self.blocks)
475 }
476 pub fn into_tipset(self) -> Tipset {
479 Tipset::from(self)
480 }
481 pub fn key(&self) -> &TipsetKey {
483 self.key
484 .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
485 }
486 pub fn parent_state(&self) -> &Cid {
488 &self.first_block().header().state_root
489 }
490 pub fn parents(&self) -> &TipsetKey {
492 &self.first_block().header().parents
493 }
494 pub fn epoch(&self) -> ChainEpoch {
496 self.first_block().header().epoch
497 }
498 pub fn weight(&self) -> &BigInt {
500 &self.first_block().header().weight
501 }
502 pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
504 for block in self.blocks() {
505 TipsetValidator::validate_msg_root(db, block)?;
507 crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
508 crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
509 crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
510 }
511 Ok(())
512 }
513}
514
515fn verify_block_headers<'a>(
516 headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
517) -> Result<(), CreateTipsetError> {
518 use itertools::all;
519
520 let headers =
521 NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
522 if !all(&headers, |it| it.parents == headers.first().parents) {
523 return Err(CreateTipsetError::BadParents);
524 }
525 if !all(&headers, |it| it.state_root == headers.first().state_root) {
526 return Err(CreateTipsetError::BadStateRoot);
527 }
528 if !all(&headers, |it| it.epoch == headers.first().epoch) {
529 return Err(CreateTipsetError::BadEpoch);
530 }
531
532 if !headers.iter().map(|it| it.miner_address).all_unique() {
533 return Err(CreateTipsetError::DuplicateMiner);
534 }
535
536 Ok(())
537}
538
539#[cfg_vis::cfg_vis(doc, pub)]
540mod lotus_json {
541 use crate::blocks::{CachingBlockHeader, Tipset};
545 use crate::lotus_json::*;
546 use nunny::Vec as NonEmpty;
547 use schemars::JsonSchema;
548 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
549
550 use super::TipsetKey;
551
552 #[derive(Debug, PartialEq, Clone, JsonSchema)]
553 #[schemars(rename = "Tipset")]
554 pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
555
556 #[derive(Serialize, Deserialize, JsonSchema)]
557 #[schemars(rename = "TipsetInner")]
558 #[serde(rename_all = "PascalCase")]
559 struct TipsetLotusJsonInner {
560 #[serde(with = "crate::lotus_json")]
561 #[schemars(with = "LotusJson<TipsetKey>")]
562 cids: TipsetKey,
563 #[serde(with = "crate::lotus_json")]
564 #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
565 blocks: NonEmpty<CachingBlockHeader>,
566 height: i64,
567 }
568
569 impl<'de> Deserialize<'de> for TipsetLotusJson {
570 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
571 where
572 D: Deserializer<'de>,
573 {
574 let TipsetLotusJsonInner {
575 cids: _ignored0,
576 blocks,
577 height: _ignored1,
578 } = Deserialize::deserialize(deserializer)?;
579
580 Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
581 }
582 }
583
584 impl Serialize for TipsetLotusJson {
585 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
586 where
587 S: Serializer,
588 {
589 let Self(tipset) = self;
590 TipsetLotusJsonInner {
591 cids: tipset.key().clone(),
592 height: tipset.epoch(),
593 blocks: tipset.block_headers().clone(),
594 }
595 .serialize(serializer)
596 }
597 }
598
599 impl HasLotusJson for Tipset {
600 type LotusJson = TipsetLotusJson;
601
602 #[cfg(test)]
603 fn snapshots() -> Vec<(serde_json::Value, Self)> {
604 use serde_json::json;
605 vec![(
606 json!({
607 "Blocks": [
608 {
609 "BeaconEntries": null,
610 "ForkSignaling": 0,
611 "Height": 0,
612 "Messages": { "/": "baeaaaaa" },
613 "Miner": "f00",
614 "ParentBaseFee": "0",
615 "ParentMessageReceipts": { "/": "baeaaaaa" },
616 "ParentStateRoot": { "/":"baeaaaaa" },
617 "ParentWeight": "0",
618 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
619 "Timestamp": 0,
620 "WinPoStProof": null
621 }
622 ],
623 "Cids": [
624 { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
625 ],
626 "Height": 0
627 }),
628 Self::new(vec![CachingBlockHeader::default()]).unwrap(),
629 )]
630 }
631
632 fn into_lotus_json(self) -> Self::LotusJson {
633 TipsetLotusJson(self)
634 }
635
636 fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
637 tipset
638 }
639 }
640
641 #[test]
642 fn snapshots() {
643 assert_all_snapshots::<Tipset>()
644 }
645
646 #[cfg(test)]
647 quickcheck::quickcheck! {
648 fn quickcheck(val: Tipset) -> () {
649 assert_unchanged_via_json(val)
650 }
651 }
652}
653
654#[cfg(test)]
655mod test {
656 use super::*;
657 use crate::blocks::VRFProof;
658 use crate::blocks::{
659 CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, header::RawBlockHeader,
660 };
661 use crate::shim::address::Address;
662 use crate::utils::multihash::prelude::*;
663 use cid::Cid;
664 use fvm_ipld_encoding::DAG_CBOR;
665 use num_bigint::BigInt;
666 use std::iter;
667
668 pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
669 let addr = Address::new_id(id);
670 let cid =
671 Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
672
673 let fmt_str = format!("===={ticket_sequence}=====");
674 let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
675 let election_proof = ElectionProof {
676 win_count: 0,
677 vrfproof: VRFProof::new(fmt_str.into_bytes()),
678 };
679 let weight_inc = BigInt::from(weight);
680 CachingBlockHeader::new(RawBlockHeader {
681 miner_address: addr,
682 election_proof: Some(election_proof),
683 ticket: Some(ticket),
684 message_receipts: cid,
685 messages: cid,
686 state_root: cid,
687 weight: weight_inc,
688 ..Default::default()
689 })
690 }
691
692 #[test]
693 fn test_break_weight_tie() {
694 let b1 = mock_block(1234561, 1, 1);
695 let ts1 = Tipset::from(&b1);
696
697 let b2 = mock_block(1234562, 1, 2);
698 let ts2 = Tipset::from(&b2);
699
700 let b3 = mock_block(1234563, 1, 1);
701 let ts3 = Tipset::from(&b3);
702
703 assert!(ts1.break_weight_tie(&ts2));
707 assert!(!ts1.break_weight_tie(&ts3));
709
710 let b4 = mock_block(1234564, 1, 41);
712 let b5 = mock_block(1234565, 1, 45);
713 let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
714 let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
715 assert!(ts4.break_weight_tie(&ts5));
717
718 let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
719 let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
720 assert!(!ts6.break_weight_tie(&ts7));
722 }
723
724 #[test]
725 fn ensure_miner_addresses_are_distinct() {
726 let h0 = RawBlockHeader {
727 miner_address: Address::new_id(0),
728 ..Default::default()
729 };
730 let h1 = RawBlockHeader {
731 miner_address: Address::new_id(0),
732 ..Default::default()
733 };
734 assert_eq!(
735 Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
736 CreateTipsetError::DuplicateMiner
737 );
738
739 let h_unique = RawBlockHeader {
740 miner_address: Address::new_id(1),
741 ..Default::default()
742 };
743
744 assert_eq!(
745 Tipset::new([h_unique, h0, h1]).unwrap_err(),
746 CreateTipsetError::DuplicateMiner
747 );
748 }
749
750 #[test]
751 fn ensure_epochs_are_equal() {
752 let h0 = RawBlockHeader {
753 miner_address: Address::new_id(0),
754 epoch: 1,
755 ..Default::default()
756 };
757 let h1 = RawBlockHeader {
758 miner_address: Address::new_id(1),
759 epoch: 2,
760 ..Default::default()
761 };
762 assert_eq!(
763 Tipset::new([h0, h1]).unwrap_err(),
764 CreateTipsetError::BadEpoch
765 );
766 }
767
768 #[test]
769 fn ensure_state_roots_are_equal() {
770 let h0 = RawBlockHeader {
771 miner_address: Address::new_id(0),
772 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
773 ..Default::default()
774 };
775 let h1 = RawBlockHeader {
776 miner_address: Address::new_id(1),
777 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
778 ..Default::default()
779 };
780 assert_eq!(
781 Tipset::new([h0, h1]).unwrap_err(),
782 CreateTipsetError::BadStateRoot
783 );
784 }
785
786 #[test]
787 fn ensure_parent_cids_are_equal() {
788 let h0 = RawBlockHeader {
789 miner_address: Address::new_id(0),
790 ..Default::default()
791 };
792 let h1 = RawBlockHeader {
793 miner_address: Address::new_id(1),
794 parents: TipsetKey::from(nonempty![Cid::new_v1(
795 DAG_CBOR,
796 MultihashCode::Identity.digest(&[])
797 )]),
798 ..Default::default()
799 };
800 assert_eq!(
801 Tipset::new([h0, h1]).unwrap_err(),
802 CreateTipsetError::BadParents
803 );
804 }
805
806 #[test]
807 fn ensure_there_are_blocks() {
808 assert_eq!(
809 Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
810 CreateTipsetError::Empty
811 );
812 }
813}