forest/blocks/
header.rs

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