forest/interpreter/
fvm2.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use 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    // See https://github.com/filecoin-project/lotus/blob/v1.18.0/chain/vm/fvm.go#L102-L216 for reference implementation
137    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        // Note that block syntax is not validated. Any validly signed block will be
146        // accepted pursuant to the below conditions. Whether or not it could
147        // ever have been accepted in a chain is not checked/does not matter here.
148        // for that reason when checking block parent relationships, rather than
149        // instantiating a Tipset to do so (which runs a syntactic check), we do
150        // it directly on the CIDs.
151
152        // (0) cheap preliminary checks
153
154        // are blocks the same?
155        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        // (1) check conditions necessary to any consensus fault
170
171        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        // block a must be earlier or equal to block b, epoch wise (ie at least as early
179        // in the chain).
180        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        // (2) check for the consensus faults themselves
191
192        // (a) double-fork mining fault
193        if bh_1.epoch == bh_2.epoch {
194            fault_type = Some(ConsensusFaultType::DoubleForkMining);
195        };
196
197        // (b) time-offset mining fault
198        // strictly speaking no need to compare heights based on double fork mining
199        // check above, but at same height this would be a different fault.
200        if bh_1.parents == bh_2.parents && bh_1.epoch != bh_2.epoch {
201            fault_type = Some(ConsensusFaultType::TimeOffsetMining);
202        };
203
204        // (c) parent-grinding fault
205        // Here extra is the "witness", a third block that shows the connection between
206        // A and B as A's sibling and B's parent.
207        // Specifically, since A is of lower height, it must be that B was mined
208        // omitting A from its tipset
209        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                // (3) return if no consensus fault
223                Ok((None, total_gas))
224            }
225            Some(fault_type) => {
226                // (4) expensive final checks
227
228                // check blocks are properly signed by their respective miner
229                // note we do not need to check extra's: it is a parent to block b
230                // which itself is signed, so it was willingly included by the miner
231                for block_header in [&bh_1, &bh_2] {
232                    let res = self.verify_block_signature(block_header);
233                    match res {
234                        // invalid consensus fault: cannot verify block header signature
235                        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    // num of reads
260    for _ in 0..stats.r {
261        gas_tracker
262            .apply_charge(price_list.on_block_open_base().into())?
263            .stop();
264    }
265    // num of writes
266    if stats.w > 0 {
267        // total bytes written
268        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, // Not used in current logic
322            bw: write_bytes.iter().sum(),
323        };
324        let result =
325            cal_gas_used_from_stats(RefCell::new(stats).borrow(), network_version).unwrap();
326
327        // Simulates logic in old GasBlockStore
328        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}