Skip to main content

forest/blocks/
header.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::sync::{
5    OnceLock,
6    atomic::{AtomicBool, Ordering},
7};
8
9use super::{ElectionProof, Error, Ticket, TipsetKey};
10use crate::{
11    beacon::{Beacon as _, BeaconEntry, BeaconSchedule},
12    shim::{
13        address::Address, clock::ChainEpoch, crypto::Signature, econ::TokenAmount,
14        sector::PoStProof, version::NetworkVersion,
15    },
16    utils::{encoding::blake2b_256, get_size::big_int_heap_size_helper, multihash::MultihashCode},
17};
18use cid::Cid;
19use fvm_ipld_blockstore::Blockstore;
20use fvm_ipld_encoding::CborStore as _;
21use fvm_ipld_encoding::tuple::*;
22use get_size2::GetSize;
23use multihash_derive::MultihashDigest as _;
24use num::BigInt;
25use serde::{Deserialize, Serialize};
26
27#[cfg(test)]
28mod test;
29#[cfg(test)]
30pub use test::*;
31
32#[derive(Deserialize_tuple, Serialize_tuple, Clone, Hash, Eq, PartialEq, Debug)]
33pub struct RawBlockHeader {
34    /// The address of the miner actor that mined this block
35    pub miner_address: Address,
36    pub ticket: Option<Ticket>,
37    pub election_proof: Option<ElectionProof>,
38    /// The verifiable oracle randomness used to elect this block's author leader
39    pub beacon_entries: Vec<BeaconEntry>,
40    pub winning_post_proof: Vec<PoStProof>,
41    /// The set of parents this block was based on.
42    /// Typically one, but can be several in the case where there were multiple
43    /// winning ticket-holders for an epoch
44    pub parents: TipsetKey,
45    /// The aggregate chain weight of the parent set
46    #[serde(with = "crate::shim::fvm_shared_latest::bigint::bigint_ser")]
47    pub weight: BigInt,
48    /// The period in which a new block is generated.
49    /// There may be multiple rounds in an epoch.
50    pub epoch: ChainEpoch,
51    /// The CID of the parent state root after calculating parent tipset.
52    pub state_root: Cid,
53    /// The CID of the root of an array of `MessageReceipts`
54    pub message_receipts: Cid,
55    /// The CID of the Merkle links for `bls_messages` and `secp_messages`
56    pub messages: Cid,
57    /// Aggregate signature of miner in block
58    pub bls_aggregate: Option<Signature>,
59    /// Block creation time, in seconds since the Unix epoch
60    pub timestamp: u64,
61    pub signature: Option<Signature>,
62    pub fork_signal: u64,
63    /// The base fee of the parent block
64    pub parent_base_fee: TokenAmount,
65}
66
67impl RawBlockHeader {
68    pub fn cid(&self) -> Cid {
69        self.car_block().expect("CBOR serialization failed").0
70    }
71    pub fn car_block(&self) -> anyhow::Result<(Cid, Vec<u8>)> {
72        let data = fvm_ipld_encoding::to_vec(self)?;
73        let cid = Cid::new_v1(
74            fvm_ipld_encoding::DAG_CBOR,
75            MultihashCode::Blake2b256.digest(&data),
76        );
77        Ok((cid, data))
78    }
79    pub(super) fn tipset_sort_key(&self) -> Option<([u8; 32], Vec<u8>)> {
80        let ticket_hash = blake2b_256(self.ticket.as_ref()?.vrfproof.as_bytes());
81        Some((ticket_hash, self.cid().to_bytes()))
82    }
83    /// Check to ensure block signature is valid
84    pub fn verify_signature_against(&self, addr: &Address) -> Result<(), Error> {
85        let signature = self
86            .signature
87            .as_ref()
88            .ok_or_else(|| Error::InvalidSignature("Signature is nil in header".into()))?;
89
90        signature.verify(&self.signing_bytes(), addr).map_err(|e| {
91            Error::InvalidSignature(format!("Block signature invalid: {e:#}").into())
92        })?;
93
94        Ok(())
95    }
96
97    /// Validates if the current header's Beacon entries are valid to ensure
98    /// randomness was generated correctly
99    pub fn validate_block_drand(
100        &self,
101        network_version: NetworkVersion,
102        b_schedule: &BeaconSchedule,
103        parent_epoch: ChainEpoch,
104        prev_entry: &BeaconEntry,
105    ) -> Result<(), Error> {
106        let (cb_epoch, curr_beacon) = b_schedule
107            .beacon_for_epoch(self.epoch)
108            .map_err(|e| Error::Validation(format!("{e:#}").into()))?;
109        tracing::trace!(
110            "beacon network at {}: {:?}, is_chained: {}",
111            self.epoch,
112            curr_beacon.network(),
113            curr_beacon.network().is_chained()
114        );
115        // Before quicknet upgrade, we had "chained" beacons, and so required two entries at a fork
116        if curr_beacon.network().is_chained() {
117            let (pb_epoch, _) = b_schedule
118                .beacon_for_epoch(parent_epoch)
119                .map_err(|e| Error::Validation(format!("{e:#}").into()))?;
120            if cb_epoch != pb_epoch {
121                // Fork logic
122                if self.beacon_entries.len() != 2 {
123                    return Err(Error::Validation(
124                        format!(
125                            "Expected two beacon entries at beacon fork, got {}",
126                            self.beacon_entries.len()
127                        )
128                        .into(),
129                    ));
130                }
131
132                #[allow(clippy::indexing_slicing)]
133                curr_beacon
134                    .verify_entries(&self.beacon_entries[1..], &self.beacon_entries[0])
135                    .map_err(|e| Error::Validation(format!("{e:#}").into()))?;
136
137                return Ok(());
138            }
139        }
140
141        let max_round = curr_beacon
142            .max_beacon_round_for_epoch(network_version, self.epoch)
143            .map_err(|e| Error::Validation(format!("{e:#}").into()))?;
144        // We don't expect to ever actually meet this condition
145        if max_round == prev_entry.round() {
146            if !self.beacon_entries.is_empty() {
147                return Err(Error::Validation(
148                    format!(
149                        "expected not to have any beacon entries in this block, got: {}",
150                        self.beacon_entries.len()
151                    )
152                    .into(),
153                ));
154            }
155            return Ok(());
156        }
157
158        // We skip verifying the genesis entry when randomness is "chained".
159        if curr_beacon.network().is_chained() && prev_entry.round() == 0 {
160            // This basically means that the drand entry of the first non-genesis tipset isn't verified IF we are starting on Drand mainnet (the "chained" drand)
161            // Networks that start on drand quicknet, or other unchained randomness sources, will still verify it
162            return Ok(());
163        }
164
165        let last = match self.beacon_entries.last() {
166            Some(last) => last,
167            None => {
168                return Err(Error::Validation(
169                    "Block must include at least 1 beacon entry".into(),
170                ));
171            }
172        };
173
174        if last.round() != max_round {
175            return Err(Error::Validation(
176                format!(
177                    "expected final beacon entry in block to be at round {}, got: {}",
178                    max_round,
179                    last.round()
180                )
181                .into(),
182            ));
183        }
184
185        // ref: https://github.com/filecoin-project/lotus/blob/27abf0f16a7f2a83305910f3c2a1844764d20b75/chain/beacon/beacon.go#L95
186        if curr_beacon.network().is_unchained() {
187            for (idx, beacon_entry) in self.beacon_entries.iter().enumerate() {
188                let lookup_epoch = parent_epoch + 1 + idx as i64;
189
190                let expected_round = curr_beacon
191                    .max_beacon_round_for_epoch(network_version, lookup_epoch)
192                    .map_err(|e| Error::Validation(format!("{e:#}").into()))?;
193                if beacon_entry.round() != expected_round {
194                    return Err(Error::Validation(
195                        format!(
196                            "expected max round for epoch {} to be {}, got: {}",
197                            lookup_epoch,
198                            expected_round,
199                            beacon_entry.round(),
200                        )
201                        .into(),
202                    ));
203                }
204            }
205        }
206
207        if !curr_beacon
208            .verify_entries(&self.beacon_entries, prev_entry)
209            .map_err(|e| Error::Validation(format!("{e:#}").into()))?
210        {
211            return Err(Error::Validation("beacon entry was invalid".into()));
212        }
213
214        Ok(())
215    }
216
217    /// Serializes the header to bytes for signing purposes i.e. without the
218    /// signature field
219    pub fn signing_bytes(&self) -> Vec<u8> {
220        let mut blk = self.clone();
221        blk.signature = None;
222        fvm_ipld_encoding::to_vec(&blk).expect("block serialization cannot fail")
223    }
224}
225
226// The derive macro does not compile for some reason
227impl GetSize for RawBlockHeader {
228    fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(
229        &self,
230        mut tracker: T,
231    ) -> (usize, T) {
232        let Self {
233            miner_address,
234            ticket,
235            election_proof,
236            beacon_entries,
237            winning_post_proof,
238            parents,
239            weight,
240            epoch: _,
241            state_root: _,
242            message_receipts: _,
243            messages: _,
244            bls_aggregate,
245            timestamp: _,
246            signature,
247            fork_signal: _,
248            parent_base_fee,
249        } = self;
250        (
251            miner_address.get_heap_size_with_tracker(&mut tracker).0
252                + ticket.get_heap_size_with_tracker(&mut tracker).0
253                + election_proof.get_heap_size_with_tracker(&mut tracker).0
254                + beacon_entries.get_heap_size_with_tracker(&mut tracker).0
255                + winning_post_proof
256                    .get_heap_size_with_tracker(&mut tracker)
257                    .0
258                + parents.get_heap_size_with_tracker(&mut tracker).0
259                + big_int_heap_size_helper(weight)
260                + bls_aggregate.get_heap_size_with_tracker(&mut tracker).0
261                + signature.get_heap_size_with_tracker(&mut tracker).0
262                + parent_base_fee.get_heap_size_with_tracker(&mut tracker).0,
263            tracker,
264        )
265    }
266}
267
268/// A [`RawBlockHeader`] which caches calls to [`RawBlockHeader::cid`] and [`RawBlockHeader::verify_signature_against`]
269#[cfg_attr(test, derive(Default))]
270#[derive(Debug, GetSize, derive_more::Deref)]
271pub struct CachingBlockHeader {
272    #[deref]
273    uncached: RawBlockHeader,
274    #[get_size(ignore)]
275    cid: OnceLock<Cid>,
276    has_ever_been_verified_against_any_signature: AtomicBool,
277}
278
279impl PartialEq for CachingBlockHeader {
280    fn eq(&self, other: &Self) -> bool {
281        // Epoch check is redundant but cheap.
282        self.uncached.epoch == other.uncached.epoch && self.cid() == other.cid()
283    }
284}
285
286impl Eq for CachingBlockHeader {}
287
288impl Clone for CachingBlockHeader {
289    fn clone(&self) -> Self {
290        Self {
291            uncached: self.uncached.clone(),
292            cid: self.cid.clone(),
293            has_ever_been_verified_against_any_signature: AtomicBool::new(
294                self.has_ever_been_verified_against_any_signature
295                    .load(Ordering::Acquire),
296            ),
297        }
298    }
299}
300
301impl From<RawBlockHeader> for CachingBlockHeader {
302    fn from(value: RawBlockHeader) -> Self {
303        Self::new(value)
304    }
305}
306
307impl CachingBlockHeader {
308    pub fn new(uncached: RawBlockHeader) -> Self {
309        Self {
310            uncached,
311            cid: OnceLock::new(),
312            has_ever_been_verified_against_any_signature: AtomicBool::new(false),
313        }
314    }
315    pub fn into_raw(self) -> RawBlockHeader {
316        self.uncached
317    }
318    /// Returns [`None`] if the blockstore doesn't contain the CID.
319    pub fn load(store: &impl Blockstore, cid: Cid) -> anyhow::Result<Option<Self>> {
320        if let Some(uncached) = store.get_cbor::<RawBlockHeader>(&cid)? {
321            Ok(Some(Self {
322                uncached,
323                cid: cid.into(),
324                has_ever_been_verified_against_any_signature: AtomicBool::new(false),
325            }))
326        } else {
327            Ok(None)
328        }
329    }
330    pub fn cid(&self) -> &Cid {
331        self.cid.get_or_init(|| self.uncached.cid())
332    }
333
334    pub fn verify_signature_against(&self, addr: &Address) -> Result<(), Error> {
335        match self
336            .has_ever_been_verified_against_any_signature
337            .load(Ordering::Acquire)
338        {
339            true => Ok(()),
340            false => match self.uncached.verify_signature_against(addr) {
341                Ok(()) => {
342                    self.has_ever_been_verified_against_any_signature
343                        .store(true, Ordering::Release);
344                    Ok(())
345                }
346                Err(e) => Err(e),
347            },
348        }
349    }
350}
351
352impl From<CachingBlockHeader> for RawBlockHeader {
353    fn from(value: CachingBlockHeader) -> Self {
354        value.into_raw()
355    }
356}
357
358impl Serialize for CachingBlockHeader {
359    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360    where
361        S: serde::Serializer,
362    {
363        self.uncached.serialize(serializer)
364    }
365}
366
367impl<'de> Deserialize<'de> for CachingBlockHeader {
368    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
369    where
370        D: serde::Deserializer<'de>,
371    {
372        RawBlockHeader::deserialize(deserializer).map(Self::new)
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::beacon::{
380        BeaconEntry, BeaconPoint, BeaconSchedule, mock_beacon::MockBeacon,
381        tests::drand::new_beacon_quicknet,
382    };
383    use crate::blocks::{CachingBlockHeader, Error};
384    use crate::shim::clock::ChainEpoch;
385    use crate::shim::{address::Address, version::NetworkVersion};
386    use crate::utils::encoding::from_slice_with_fallback;
387    use crate::utils::encoding::hex;
388    use crate::utils::multihash::MultihashCode;
389    use cid::Cid;
390    use fvm_ipld_encoding::{DAG_CBOR, to_vec};
391    use rstest::{fixture, rstest};
392
393    impl quickcheck::Arbitrary for CachingBlockHeader {
394        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
395            use crate::blocks::{Ticket, VRFProof};
396            // TODO(forest): https://github.com/ChainSafe/forest/issues/3571
397            CachingBlockHeader::new(RawBlockHeader {
398                miner_address: Address::new_id(0),
399                ticket: Some(Ticket::new(VRFProof::new(Vec::arbitrary(g)))),
400                epoch: ChainEpoch::arbitrary(g),
401                ..Default::default()
402            })
403        }
404    }
405
406    #[test]
407    fn symmetric_header_encoding() {
408        // This test vector is pulled from space race, and contains a valid signature
409        let bz = hex::decode("904300e8078158608798de4e49e02ee129920224ea767650aa6e693857431cc95b5a092a57d80ef4d841ebedbf09f7680a5e286cd297f40100b496648e1fa0fd55f899a45d51404a339564e7d4809741ba41d9fcc8ac0261bf521cd5f718389e81354eff2aa52b338201586084d8929eeedc654d6bec8bb750fcc8a1ebf2775d8167d3418825d9e989905a8b7656d906d23dc83e0dad6e7f7a193df70a82d37da0565ce69b776d995eefd50354c85ec896a2173a5efed53a27275e001ad72a3317b2190b98cceb0f01c46b7b81821a00013cbe5860ae1102b76dea635b2f07b7d06e1671d695c4011a73dc33cace159509eac7edc305fa74495505f0cd0046ee0d3b17fabc0fc0560d44d296c6d91bcc94df76266a8e9d5312c617ca72a2e186cadee560477f6d120f6614e21fb07c2390a166a25981820358c0b965705cec77b46200af8fb2e47c0eca175564075061132949f00473dcbe74529c623eb510081e8b8bd34418d21c646485d893f040dcfb7a7e7af9ae4ed7bd06772c24fb0cc5b8915300ab5904fbd90269d523018fbf074620fd3060d55dd6c6057b4195950ac4155a735e8fec79767f659c30ea6ccf0813a4ab2b4e60f36c04c71fb6c58efc123f60c6ea8797ab3706a80a4ccc1c249989934a391803789ab7d04f514ee0401d0f87a1f5262399c451dcf5f7ec3bb307fc6f1a41f5ff3a5ddb81d82a5827000171a0e402209a0640d0620af5d1c458effce4cbb8969779c9072b164d3fe6f5179d6378d8cd4300310001d82a5827000171a0e402208fbc07f7587e2efebab9ff1ab27c928881abf9d1b7e5ad5206781415615867aed82a5827000171a0e40220e5658b3d18cd06e1db9015b4b0ec55c123a24d5be1ea24d83938c5b8397b4f2fd82a5827000171a0e402209967f10c4c0e336b3517d3a972f701dadea5b41ce33defb126b88e650cf884545861028ec8b64e2d93272f97edcab1f56bcad4a2b145ea88c232bfae228e4adbbd807e6a41740cc8cb569197dae6b2cbf8c1a4035e81fd7805ccbe88a5ec476bcfa438db4bd677de06b45e94310533513e9d17c635940ba8fa2650cdb34d445724c5971a5f44387e5861028a45c70a39fe8e526cbb6ba2a850e9063460873d6329f26cc2fc91972256c40249dba289830cc99619109c18e695d78012f760e7fda1b68bc3f1fe20ff8a017044753da38ca6384de652f3ee13aae5b64e6f88f85fd50d5c862fed3c1f594ace004500053724e0").unwrap();
410        let header = from_slice_with_fallback::<CachingBlockHeader>(&bz).unwrap();
411        assert_eq!(to_vec(&header).unwrap(), bz);
412
413        // Verify the signature of this block header using the resolved address used to
414        // sign. This is a valid signature, but if the block header vector
415        // changes, the address should need to as well.
416        header
417            .verify_signature_against(
418                &"f3vfs6f7tagrcpnwv65wq3leznbajqyg77bmijrpvoyjv3zjyi3urq25vigfbs3ob6ug5xdihajumtgsxnz2pa"
419                .parse()
420                .unwrap())
421            .unwrap();
422    }
423
424    #[test]
425    fn beacon_entry_exists() {
426        // Setup
427        let block_header = CachingBlockHeader::new(RawBlockHeader {
428            miner_address: Address::new_id(0),
429            ..Default::default()
430        });
431        let beacon_schedule = BeaconSchedule(vec![BeaconPoint::new(0, <MockBeacon>::default())]);
432        let chain_epoch = 0;
433        let beacon_entry = BeaconEntry::new(1, vec![]);
434        // Validate_block_drand
435        if let Err(e) = block_header.validate_block_drand(
436            NetworkVersion::V16,
437            &beacon_schedule,
438            chain_epoch,
439            &beacon_entry,
440        ) {
441            // Assert error is for not including a beacon entry in the block
442            match e {
443                Error::Validation(why) => {
444                    assert_eq!(why, "Block must include at least 1 beacon entry");
445                }
446                _ => {
447                    panic!("validate block drand must detect a beacon entry in the block header");
448                }
449            }
450        }
451    }
452
453    #[test]
454    fn test_genesis_parent() {
455        assert_eq!(
456            Cid::new_v1(
457                DAG_CBOR,
458                MultihashCode::Sha2_256.digest(&FILECOIN_GENESIS_BLOCK)
459            ),
460            *FILECOIN_GENESIS_CID
461        );
462    }
463
464    #[fixture]
465    #[once]
466    fn schedule() -> BeaconSchedule {
467        BeaconSchedule(vec![BeaconPoint::new(0, new_beacon_quicknet())])
468    }
469
470    #[derive(Debug)]
471    struct BeaconEntriesCase {
472        parent_epoch: ChainEpoch,
473        prev_round: u64,
474        epoch: ChainEpoch,
475        rounds: Vec<u64>,
476        accepted: bool,
477    }
478
479    #[rstest]
480    #[case::no_null_round(BeaconEntriesCase {
481        parent_epoch: 6216199,
482        prev_round: 30662992,
483        epoch: 6216200,
484        rounds: vec![30663002],
485        accepted: true,
486    })]
487    #[case::null_round_both_entries(BeaconEntriesCase {
488        parent_epoch: 6216198,
489        prev_round: 30662982,
490        epoch: 6216200,
491        rounds: vec![30662992, 30663002],
492        accepted: true,
493    })]
494    #[case::null_round_invalid_entry(BeaconEntriesCase {
495        parent_epoch: 6216198,
496        prev_round: 30662982,
497        epoch: 6216200,
498        rounds: vec![30662990, 30663002],
499        accepted: false,
500    })]
501    #[case::null_round_missing_null_entry(BeaconEntriesCase {
502        parent_epoch: 6216198,
503        prev_round: 30662982,
504        epoch: 6216200,
505        rounds: vec![30663002],
506        accepted: false,
507    })]
508    #[case::null_round_missing_own_entry(BeaconEntriesCase {
509        parent_epoch: 6216198,
510        prev_round: 30662982,
511        epoch: 6216200,
512        rounds: vec![30662992],
513        accepted: false,
514    })]
515    #[case::extra_trailing_entry(BeaconEntriesCase {
516        parent_epoch: 6216199,
517        prev_round: 30662992,
518        epoch: 6216200,
519        rounds: vec![30663002, 30663002],
520        accepted: false,
521    })]
522    #[case::null_round_extra_trailing_entry(BeaconEntriesCase {
523        parent_epoch: 6216198,
524        prev_round: 30662982,
525        epoch: 6216200,
526        rounds: vec![30662992, 30663002, 30663002],
527        accepted: false,
528    })]
529    #[tokio::test]
530    async fn validate_beacon_entries_on_quicknet(
531        schedule: &BeaconSchedule,
532        #[case] case: BeaconEntriesCase,
533    ) {
534        let BeaconEntriesCase {
535            parent_epoch,
536            prev_round,
537            epoch,
538            rounds,
539            accepted,
540        } = case;
541
542        let (_, curr_beacon) = schedule.beacon_for_epoch(epoch).unwrap();
543
544        let mut beacon_entries = Vec::with_capacity(rounds.len());
545        for round in rounds {
546            beacon_entries.push(curr_beacon.entry(round).await.unwrap());
547        }
548
549        let prev_entry = BeaconEntry::new(prev_round, vec![]);
550        let header = RawBlockHeader {
551            miner_address: Address::new_id(0),
552            epoch,
553            beacon_entries,
554            ..Default::default()
555        };
556
557        let result =
558            header.validate_block_drand(NetworkVersion::V22, schedule, parent_epoch, &prev_entry);
559
560        assert_eq!(
561            result.is_ok(),
562            accepted,
563            "epoch {epoch}, parent {parent_epoch}: {result:?}"
564        );
565    }
566}