forest/interpreter/
fvm4.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::{
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    /// Signifies whether or not we have to bail due to database lookup problems.
121    ///
122    /// NOTE: Unfortunately there isn't a better a way of dealing with this, because FVM swallows
123    /// both errors and panics alike, so we have to deal with this in the client code.
124    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    // See https://github.com/filecoin-project/lotus/blob/v1.18.0/chain/vm/fvm.go#L102-L216 for reference implementation
157    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        // Note that block syntax is not validated. Any validly signed block will be
166        // accepted pursuant to the below conditions. Whether or not it could
167        // ever have been accepted in a chain is not checked/does not matter here.
168        // for that reason when checking block parent relationships, rather than
169        // instantiating a Tipset to do so (which runs a syntactic check), we do
170        // it directly on the CIDs.
171
172        // (0) cheap preliminary checks
173
174        // are blocks the same?
175        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        // This is a workaround for the broken calibnet chain. See:
191        // https://github.com/filecoin-project/lotus/pull/11399
192        // Basically we relax the consensus fault checks around the WatermelonFix epoch.
193        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        // (1) check conditions necessary to any consensus fault
213
214        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        // block a must be earlier or equal to block b, epoch wise (ie at least as early
222        // in the chain).
223        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        // (2) check for the consensus faults themselves
234
235        // (a) double-fork mining fault
236        if bh_1.epoch == bh_2.epoch {
237            fault_type = Some(ConsensusFaultType::DoubleForkMining);
238        };
239
240        // (b) time-offset mining fault
241        // strictly speaking no need to compare heights based on double fork mining
242        // check above, but at same height this would be a different fault.
243        if bh_1.parents == bh_2.parents && bh_1.epoch != bh_2.epoch {
244            fault_type = Some(ConsensusFaultType::TimeOffsetMining);
245        };
246
247        // (c) parent-grinding fault
248        // Here extra is the "witness", a third block that shows the connection between
249        // A and B as A's sibling and B's parent.
250        // Specifically, since A is of lower height, it must be that B was mined
251        // omitting A from its tipset
252        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                // (3) return if no consensus fault
266                Ok((None, total_gas))
267            }
268            Some(fault_type) => {
269                // (4) expensive final checks
270
271                // check blocks are properly signed by their respective miner
272                // note we do not need to check extra's: it is a parent to block b
273                // which itself is signed, so it was willingly included by the miner
274                for block_header in [&bh_1, &bh_2] {
275                    let res = self.verify_block_signature(block_header);
276                    match res {
277                        // invalid consensus fault: cannot verify block header signature
278                        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    // num of reads
303    for _ in 0..stats.r {
304        gas_tracker
305            .apply_charge(price_list.on_block_open_base().into())?
306            .stop();
307    }
308
309    // num of writes
310    if stats.w > 0 {
311        // total bytes written
312        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, // Not used in current logic
366            bw: write_bytes.iter().sum(),
367        };
368        let result =
369            cal_gas_used_from_stats(RefCell::new(stats).borrow(), network_version).unwrap();
370
371        // Simulates logic in old GasBlockStore
372        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}