1use std::sync::atomic::{AtomicBool, Ordering};
5use std::{cell::Ref, sync::Arc};
6
7use crate::blocks::{CachingBlockHeader, Tipset};
8use crate::chain::{index::ChainIndex, store::ChainStore};
9use crate::interpreter::errors::Error;
10use crate::interpreter::resolve_to_key_addr;
11use crate::networks::ChainConfig;
12use crate::shim::actors::miner;
13use crate::shim::{
14 actors::MinerActorStateLoad as _,
15 gas::{Gas, GasTracker, price_list_by_network_version},
16 state_tree::StateTree,
17 version::NetworkVersion,
18};
19use crate::utils::encoding::from_slice_with_fallback;
20use anyhow::bail;
21use cid::Cid;
22use fvm_ipld_blockstore::{
23 Blockstore,
24 tracking::{BSStats, TrackingBlockstore},
25};
26use fvm_shared2::{
27 address::Address,
28 clock::ChainEpoch,
29 consensus::{ConsensusFault, ConsensusFaultType},
30};
31use fvm2::externs::{Consensus, Externs, Rand};
32
33pub struct ForestExternsV2<DB> {
34 rand: Box<dyn Rand>,
35 heaviest_tipset: Arc<Tipset>,
36 epoch: ChainEpoch,
37 root: Cid,
38 chain_index: Arc<ChainIndex<Arc<DB>>>,
39 chain_config: Arc<ChainConfig>,
40 bail: AtomicBool,
41}
42
43impl<DB: Blockstore + Send + Sync + 'static> ForestExternsV2<DB> {
44 pub fn new(
45 rand: impl Rand + 'static,
46 heaviest_tipset: Arc<Tipset>,
47 epoch: ChainEpoch,
48 root: Cid,
49 chain_index: Arc<ChainIndex<Arc<DB>>>,
50 chain_config: Arc<ChainConfig>,
51 ) -> Self {
52 ForestExternsV2 {
53 rand: Box::new(rand),
54 heaviest_tipset,
55 epoch,
56 root,
57 chain_index,
58 chain_config,
59 bail: AtomicBool::new(false),
60 }
61 }
62
63 fn get_lookback_tipset_state_root_for_round(&self, height: ChainEpoch) -> anyhow::Result<Cid> {
64 let (_, st) = ChainStore::get_lookback_tipset_for_round(
65 self.chain_index.clone(),
66 Arc::clone(&self.chain_config),
67 Arc::clone(&self.heaviest_tipset),
68 height,
69 )?;
70 Ok(st)
71 }
72
73 fn worker_key_at_lookback(
74 &self,
75 miner_addr: &Address,
76 height: ChainEpoch,
77 ) -> anyhow::Result<(Address, i64)> {
78 if height < self.epoch - self.chain_config.policy.chain_finality {
79 bail!(
80 "cannot get worker key (current epoch: {}, height: {})",
81 self.epoch,
82 height
83 );
84 }
85
86 let prev_root = self.get_lookback_tipset_state_root_for_round(height)?;
87 let lb_state = StateTree::new_from_root(Arc::clone(self.chain_index.db()), &prev_root)?;
88
89 let actor = lb_state
90 .get_actor(&miner_addr.into())?
91 .ok_or_else(|| anyhow::anyhow!("actor not found {:?}", miner_addr))?;
92
93 let tbs = TrackingBlockstore::new(self.chain_index.db());
94
95 let ms = miner::State::load(&tbs, actor.code, actor.state)?;
96
97 let worker = ms.info(&tbs)?.worker;
98
99 let state = StateTree::new_from_root(Arc::clone(self.chain_index.db()), &self.root)?;
100
101 let addr = resolve_to_key_addr(&state, &tbs, &worker.into())?;
102
103 let network_version = self.chain_config.network_version(self.epoch);
104 let gas_used = cal_gas_used_from_stats(tbs.stats.borrow(), network_version)?;
105
106 Ok((addr.into(), gas_used.round_up() as i64))
107 }
108
109 fn verify_block_signature(&self, bh: &CachingBlockHeader) -> anyhow::Result<i64, Error> {
110 let (worker_addr, gas_used) =
111 self.worker_key_at_lookback(&bh.miner_address.into(), bh.epoch)?;
112
113 bh.verify_signature_against(&worker_addr.into())?;
114
115 Ok(gas_used)
116 }
117
118 pub fn bail(&self) -> bool {
119 self.bail.load(Ordering::Relaxed)
120 }
121}
122
123impl<DB: Blockstore + Send + Sync + 'static> Externs for ForestExternsV2<DB> {}
124
125impl<DB> Rand for ForestExternsV2<DB> {
126 fn get_chain_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
127 self.rand.get_chain_randomness(round)
128 }
129
130 fn get_beacon_randomness(&self, round: ChainEpoch) -> anyhow::Result<[u8; 32]> {
131 self.rand.get_beacon_randomness(round)
132 }
133}
134
135impl<DB: Blockstore + Send + Sync + 'static> Consensus for ForestExternsV2<DB> {
136 fn verify_consensus_fault(
138 &self,
139 h1: &[u8],
140 h2: &[u8],
141 extra: &[u8],
142 ) -> anyhow::Result<(Option<ConsensusFault>, i64)> {
143 let mut total_gas: i64 = 0;
144
145 if h1 == h2 {
156 bail!(
157 "no consensus fault: submitted blocks are the same: {:?}, {:?}",
158 h1,
159 h2
160 );
161 };
162 let bh_1 = from_slice_with_fallback::<CachingBlockHeader>(h1)?;
163 let bh_2 = from_slice_with_fallback::<CachingBlockHeader>(h2)?;
164
165 if bh_1.cid() == bh_2.cid() {
166 bail!("no consensus fault: submitted blocks are the same");
167 }
168
169 if bh_1.miner_address != bh_2.miner_address {
172 bail!(
173 "no consensus fault: blocks not mined by same miner: {:?}, {:?}",
174 bh_1.miner_address,
175 bh_2.miner_address
176 );
177 };
178 if bh_2.epoch < bh_1.epoch {
181 bail!(
182 "first block must not be of higher height than second: {:?}, {:?}",
183 bh_1.epoch,
184 bh_2.epoch
185 );
186 };
187
188 let mut fault_type: Option<ConsensusFaultType> = None;
189
190 if bh_1.epoch == bh_2.epoch {
194 fault_type = Some(ConsensusFaultType::DoubleForkMining);
195 };
196
197 if bh_1.parents == bh_2.parents && bh_1.epoch != bh_2.epoch {
201 fault_type = Some(ConsensusFaultType::TimeOffsetMining);
202 };
203
204 if !extra.is_empty() {
210 let bh_3 = from_slice_with_fallback::<CachingBlockHeader>(extra)?;
211 if bh_1.parents == bh_3.parents
212 && bh_1.epoch == bh_3.epoch
213 && bh_2.parents.contains(*bh_3.cid())
214 && !bh_2.parents.contains(*bh_1.cid())
215 {
216 fault_type = Some(ConsensusFaultType::ParentGrinding);
217 }
218 };
219
220 match fault_type {
221 None => {
222 Ok((None, total_gas))
224 }
225 Some(fault_type) => {
226 for block_header in [&bh_1, &bh_2] {
232 let res = self.verify_block_signature(block_header);
233 match res {
234 Err(Error::Signature(_)) => return Ok((None, total_gas)),
236 Err(Error::Lookup(_)) => return Ok((None, total_gas)),
237 Ok(gas_used) => total_gas += gas_used,
238 }
239 }
240
241 let ret = Some(ConsensusFault {
242 target: bh_1.miner_address.into(),
243 epoch: bh_2.epoch,
244 fault_type,
245 });
246
247 Ok((ret, total_gas))
248 }
249 }
250 }
251}
252
253fn cal_gas_used_from_stats(
254 stats: Ref<BSStats>,
255 network_version: NetworkVersion,
256) -> anyhow::Result<Gas> {
257 let price_list = price_list_by_network_version(network_version);
258 let gas_tracker = GasTracker::new(Gas::new(i64::MAX as u64).into(), Gas::new(0).into(), false);
259 for _ in 0..stats.r {
261 gas_tracker
262 .apply_charge(price_list.on_block_open_base().into())?
263 .stop();
264 }
265 if stats.w > 0 {
267 gas_tracker
269 .apply_charge(price_list.on_block_link(stats.bw).into())?
270 .stop();
271 for _ in 1..stats.w {
272 gas_tracker
273 .apply_charge(price_list.on_block_link(0).into())?
274 .stop();
275 }
276 }
277 Ok(gas_tracker.gas_used().into())
278}
279
280#[cfg(test)]
281mod tests {
282 use std::cell::RefCell;
283
284 use super::*;
285
286 #[test]
287 fn test_cal_gas_used_from_stats_1_read() {
288 test_cal_gas_used_from_stats_inner(1, &[])
289 }
290
291 #[test]
292 fn test_cal_gas_used_from_stats_1_write() {
293 test_cal_gas_used_from_stats_inner(0, &[100])
294 }
295
296 #[test]
297 fn test_cal_gas_used_from_stats_multi_read() {
298 test_cal_gas_used_from_stats_inner(10, &[])
299 }
300
301 #[test]
302 fn test_cal_gas_used_from_stats_multi_write() {
303 test_cal_gas_used_from_stats_inner(0, &[100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
304 }
305
306 #[test]
307 fn test_cal_gas_used_from_stats_1_read_1_write() {
308 test_cal_gas_used_from_stats_inner(1, &[100])
309 }
310
311 #[test]
312 fn test_cal_gas_used_from_stats_multi_read_multi_write() {
313 test_cal_gas_used_from_stats_inner(10, &[100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
314 }
315
316 fn test_cal_gas_used_from_stats_inner(read_count: usize, write_bytes: &[usize]) {
317 let network_version = NetworkVersion::V8;
318 let stats = BSStats {
319 r: read_count,
320 w: write_bytes.len(),
321 br: 0, bw: write_bytes.iter().sum(),
323 };
324 let result =
325 cal_gas_used_from_stats(RefCell::new(stats).borrow(), network_version).unwrap();
326
327 let price_list = price_list_by_network_version(network_version);
329 let tracker = GasTracker::new(Gas::new(u64::MAX).into(), Gas::new(0).into(), false);
330 std::iter::repeat_n((), read_count).for_each(|_| {
331 tracker
332 .apply_charge(price_list.on_block_open_base().into())
333 .unwrap()
334 .stop();
335 });
336 for &bytes in write_bytes {
337 tracker
338 .apply_charge(price_list.on_block_link(bytes).into())
339 .unwrap()
340 .stop();
341 }
342 let expected = tracker.gas_used();
343
344 assert_eq!(result, expected.into());
345 }
346}