1use std::sync::atomic::{AtomicBool, Ordering};
5use std::{cell::Ref, sync::Arc};
6
7use crate::blocks::{CachingBlockHeader, Tipset};
8use crate::chain::{
9 ChainStore,
10 index::{ChainIndex, ResolveNullTipset},
11};
12use crate::interpreter::errors::Error;
13use crate::interpreter::resolve_to_key_addr;
14use crate::networks::{ChainConfig, Height, NetworkChain};
15use crate::shim::actors::MinerActorStateLoad as _;
16use crate::shim::actors::miner;
17use crate::shim::{
18 address::Address, gas::price_list_by_network_version, state_tree::StateTree,
19 version::NetworkVersion,
20};
21use crate::utils::encoding::from_slice_with_fallback;
22use anyhow::{Context as _, bail};
23use cid::Cid;
24use fvm_ipld_blockstore::{
25 Blockstore,
26 tracking::{BSStats, TrackingBlockstore},
27};
28use fvm_shared4::{
29 clock::ChainEpoch,
30 consensus::{ConsensusFault, ConsensusFaultType},
31};
32use fvm4::{
33 externs::{Chain, Consensus, Externs, Rand},
34 gas::{Gas, GasTracker},
35};
36
37pub struct ForestExterns<DB> {
38 rand: Box<dyn Rand>,
39 heaviest_tipset: Arc<Tipset>,
40 epoch: ChainEpoch,
41 root: Cid,
42 chain_index: Arc<ChainIndex<Arc<DB>>>,
43 chain_config: Arc<ChainConfig>,
44 bail: AtomicBool,
45}
46
47impl<DB: Blockstore + Send + Sync + 'static> ForestExterns<DB> {
48 pub fn new(
49 rand: impl Rand + 'static,
50 heaviest_tipset: Arc<Tipset>,
51 epoch: ChainEpoch,
52 root: Cid,
53 chain_index: Arc<ChainIndex<Arc<DB>>>,
54 chain_config: Arc<ChainConfig>,
55 ) -> Self {
56 ForestExterns {
57 rand: Box::new(rand),
58 heaviest_tipset,
59 epoch,
60 root,
61 chain_index,
62 chain_config,
63 bail: AtomicBool::new(false),
64 }
65 }
66
67 fn get_lookback_tipset_state_root_for_round(&self, height: ChainEpoch) -> anyhow::Result<Cid> {
68 let (_, st) = ChainStore::get_lookback_tipset_for_round(
69 self.chain_index.clone(),
70 Arc::clone(&self.chain_config),
71 Arc::clone(&self.heaviest_tipset),
72 height,
73 )?;
74 Ok(st)
75 }
76
77 fn worker_key_at_lookback(
78 &self,
79 miner_addr: &Address,
80 height: ChainEpoch,
81 ) -> anyhow::Result<(Address, i64)> {
82 if height < self.epoch - self.chain_config.policy.chain_finality {
83 bail!(
84 "cannot get worker key (current epoch: {}, height: {height}, miner address: {miner_addr})",
85 self.epoch,
86 );
87 }
88
89 let prev_root = self.get_lookback_tipset_state_root_for_round(height)?;
90 let lb_state = StateTree::new_from_root(Arc::clone(self.chain_index.db()), &prev_root)?;
91
92 let actor = lb_state
93 .get_actor(miner_addr)?
94 .ok_or_else(|| anyhow::anyhow!("actor not found {:?}", miner_addr))?;
95
96 let tbs = TrackingBlockstore::new(self.chain_index.db());
97
98 let ms = miner::State::load(&tbs, actor.code, actor.state)?;
99
100 let worker = ms.info(&tbs)?.worker.into();
101
102 let state = StateTree::new_from_root(Arc::clone(self.chain_index.db()), &self.root)?;
103
104 let addr = resolve_to_key_addr(&state, &tbs, &worker)?;
105
106 let network_version = self.chain_config.network_version(self.epoch);
107 let gas_used = cal_gas_used_from_stats(tbs.stats.borrow(), network_version)?;
108
109 Ok((addr, gas_used.round_up() as i64))
110 }
111
112 fn verify_block_signature(&self, bh: &CachingBlockHeader) -> anyhow::Result<i64, Error> {
113 let (worker_addr, gas_used) = self.worker_key_at_lookback(&bh.miner_address, bh.epoch)?;
114
115 bh.verify_signature_against(&worker_addr)?;
116
117 Ok(gas_used)
118 }
119
120 pub fn bail(&self) -> bool {
125 self.bail.load(Ordering::Relaxed)
126 }
127}
128
129impl<DB: Blockstore + Send + Sync + 'static> Externs for ForestExterns<DB> {}
130
131impl<DB: Blockstore> Chain for ForestExterns<DB> {
132 fn get_tipset_cid(&self, epoch: ChainEpoch) -> anyhow::Result<Cid> {
133 let ts = self
134 .chain_index
135 .tipset_by_height(
136 epoch,
137 self.heaviest_tipset.clone(),
138 ResolveNullTipset::TakeOlder,
139 )
140 .context("Failed to get tipset cid")?;
141 ts.key().cid()
142 }
143}
144
145impl<DB> Rand for ForestExterns<DB> {
146 fn get_chain_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
147 self.rand.get_chain_randomness(round)
148 }
149
150 fn get_beacon_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
151 self.rand.get_beacon_randomness(round)
152 }
153}
154
155impl<DB: Blockstore + Send + Sync + 'static> Consensus for ForestExterns<DB> {
156 fn verify_consensus_fault(
158 &self,
159 h1: &[u8],
160 h2: &[u8],
161 extra: &[u8],
162 ) -> anyhow::Result<(Option<ConsensusFault>, i64)> {
163 let mut total_gas: i64 = 0;
164
165 if h1 == h2 {
176 bail!(
177 "no consensus fault: submitted blocks are the same: {:?}, {:?}",
178 h1,
179 h2
180 );
181 };
182
183 let bh_1 = from_slice_with_fallback::<CachingBlockHeader>(h1)?;
184 let bh_2 = from_slice_with_fallback::<CachingBlockHeader>(h2)?;
185
186 if bh_1.cid() == bh_2.cid() {
187 bail!("no consensus fault: submitted blocks are the same");
188 }
189
190 if self.chain_config.network == NetworkChain::Calibnet {
194 let watermelonfix_height = self
195 .chain_config
196 .height_infos
197 .get(&Height::WatermelonFix)
198 .context("Missing WatermelonFix for calibnet")?
199 .epoch;
200
201 let finality = self.chain_config.policy.chain_finality;
202
203 let is_near_upgrade = |epoch| {
204 epoch > watermelonfix_height - finality && epoch < watermelonfix_height + finality
205 };
206
207 if is_near_upgrade(bh_1.epoch) || is_near_upgrade(bh_2.epoch) {
208 return Ok((None, total_gas));
209 }
210 }
211
212 if bh_1.miner_address != bh_2.miner_address {
215 bail!(
216 "no consensus fault: blocks not mined by same miner: {:?}, {:?}",
217 bh_1.miner_address,
218 bh_2.miner_address
219 );
220 };
221 if bh_2.epoch < bh_1.epoch {
224 bail!(
225 "first block must not be of higher height than second: {:?}, {:?}",
226 bh_1.epoch,
227 bh_2.epoch
228 );
229 };
230
231 let mut fault_type: Option<ConsensusFaultType> = None;
232
233 if bh_1.epoch == bh_2.epoch {
237 fault_type = Some(ConsensusFaultType::DoubleForkMining);
238 };
239
240 if bh_1.parents == bh_2.parents && bh_1.epoch != bh_2.epoch {
244 fault_type = Some(ConsensusFaultType::TimeOffsetMining);
245 };
246
247 if !extra.is_empty() {
253 let bh_3 = from_slice_with_fallback::<CachingBlockHeader>(extra)?;
254 if bh_1.parents == bh_3.parents
255 && bh_1.epoch == bh_3.epoch
256 && bh_2.parents.contains(*bh_3.cid())
257 && !bh_2.parents.contains(*bh_1.cid())
258 {
259 fault_type = Some(ConsensusFaultType::ParentGrinding);
260 }
261 };
262
263 match fault_type {
264 None => {
265 Ok((None, total_gas))
267 }
268 Some(fault_type) => {
269 for block_header in [&bh_1, &bh_2] {
275 let res = self.verify_block_signature(block_header);
276 match res {
277 Err(Error::Signature(_)) => return Ok((None, total_gas)),
279 Err(Error::Lookup(_)) => return Ok((None, total_gas)),
280 Ok(gas_used) => total_gas += gas_used,
281 }
282 }
283
284 let ret = Some(ConsensusFault {
285 target: bh_1.miner_address.into(),
286 epoch: bh_2.epoch,
287 fault_type,
288 });
289
290 Ok((ret, total_gas))
291 }
292 }
293 }
294}
295
296fn cal_gas_used_from_stats(
297 stats: Ref<BSStats>,
298 network_version: NetworkVersion,
299) -> anyhow::Result<Gas> {
300 let price_list = price_list_by_network_version(network_version);
301 let gas_tracker = GasTracker::new(Gas::new(u64::MAX), Gas::new(0), false);
302 for _ in 0..stats.r {
304 gas_tracker
305 .apply_charge(price_list.on_block_open_base().into())?
306 .stop();
307 }
308
309 if stats.w > 0 {
311 gas_tracker
313 .apply_charge(price_list.on_block_link(stats.bw).into())?
314 .stop();
315 for _ in 1..stats.w {
316 gas_tracker
317 .apply_charge(price_list.on_block_link(0).into())?
318 .stop();
319 }
320 }
321 Ok(gas_tracker.gas_used())
322}
323
324#[cfg(test)]
325mod tests {
326 use std::cell::RefCell;
327
328 use super::*;
329
330 #[test]
331 fn test_cal_gas_used_from_stats_1_read() {
332 test_cal_gas_used_from_stats_inner(1, &[])
333 }
334
335 #[test]
336 fn test_cal_gas_used_from_stats_1_write() {
337 test_cal_gas_used_from_stats_inner(0, &[100])
338 }
339
340 #[test]
341 fn test_cal_gas_used_from_stats_multi_read() {
342 test_cal_gas_used_from_stats_inner(10, &[])
343 }
344
345 #[test]
346 fn test_cal_gas_used_from_stats_multi_write() {
347 test_cal_gas_used_from_stats_inner(0, &[100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
348 }
349
350 #[test]
351 fn test_cal_gas_used_from_stats_1_read_1_write() {
352 test_cal_gas_used_from_stats_inner(1, &[100])
353 }
354
355 #[test]
356 fn test_cal_gas_used_from_stats_multi_read_multi_write() {
357 test_cal_gas_used_from_stats_inner(10, &[100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
358 }
359
360 fn test_cal_gas_used_from_stats_inner(read_count: usize, write_bytes: &[usize]) {
361 let network_version = NetworkVersion::V18;
362 let stats = BSStats {
363 r: read_count,
364 w: write_bytes.len(),
365 br: 0, bw: write_bytes.iter().sum(),
367 };
368 let result =
369 cal_gas_used_from_stats(RefCell::new(stats).borrow(), network_version).unwrap();
370
371 let price_list = price_list_by_network_version(network_version);
373 let tracker = GasTracker::new(Gas::new(u64::MAX), Gas::new(0), false);
374 std::iter::repeat_n((), read_count).for_each(|_| {
375 tracker
376 .apply_charge(price_list.on_block_open_base().into())
377 .unwrap()
378 .stop();
379 });
380 for &bytes in write_bytes {
381 tracker
382 .apply_charge(price_list.on_block_link(bytes).into())
383 .unwrap()
384 .stop()
385 }
386 let expected = tracker.gas_used();
387
388 assert_eq!(result, expected);
389 }
390}