1use 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 pub miner_address: Address,
36 pub ticket: Option<Ticket>,
37 pub election_proof: Option<ElectionProof>,
38 pub beacon_entries: Vec<BeaconEntry>,
40 pub winning_post_proof: Vec<PoStProof>,
41 pub parents: TipsetKey,
45 #[serde(with = "crate::shim::fvm_shared_latest::bigint::bigint_ser")]
47 pub weight: BigInt,
48 pub epoch: ChainEpoch,
51 pub state_root: Cid,
53 pub message_receipts: Cid,
55 pub messages: Cid,
57 pub bls_aggregate: Option<Signature>,
59 pub timestamp: u64,
61 pub signature: Option<Signature>,
62 pub fork_signal: u64,
63 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 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 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 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 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 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 if curr_beacon.network().is_chained() && prev_entry.round() == 0 {
160 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 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 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
226impl 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#[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 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 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 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 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 header
417 .verify_signature_against(
418 &"f3vfs6f7tagrcpnwv65wq3leznbajqyg77bmijrpvoyjv3zjyi3urq25vigfbs3ob6ug5xdihajumtgsxnz2pa"
419 .parse()
420 .unwrap())
421 .unwrap();
422 }
423
424 #[test]
425 fn beacon_entry_exists() {
426 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 if let Err(e) = block_header.validate_block_drand(
436 NetworkVersion::V16,
437 &beacon_schedule,
438 chain_epoch,
439 &beacon_entry,
440 ) {
441 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}