Skip to main content

forest/statediff/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod resolve;
5
6use std::{
7    fmt::Write as FmtWrite,
8    io::{Write, stdout},
9    sync::Arc,
10};
11
12use crate::shim::actors::{
13    account::State as AccountState, cron::State as CronState, datacap::State as DatacapState,
14    evm::State as EvmState, init::State as InitState, market::State as MarketState,
15    miner::State as MinerState, multisig::State as MultiSigState, power::State as PowerState,
16    reward::State as RewardState, system::State as SystemState,
17};
18use crate::{
19    lotus_json::HasLotusJson as _,
20    shim::{
21        actors::state_load::*,
22        address::Address,
23        state_tree::{ActorState, StateTree},
24    },
25};
26use ahash::HashMap;
27use cid::Cid;
28use colored::*;
29use fvm_ipld_blockstore::Blockstore;
30use ipld_core::ipld::Ipld;
31use itertools::Itertools as _;
32use resolve::resolve_cids_recursive;
33use serde::{Deserialize, Serialize};
34use similar::{ChangeTag, TextDiff};
35
36#[derive(Serialize, Deserialize)]
37struct ActorStateResolved {
38    #[serde(with = "crate::lotus_json")]
39    code: Cid,
40    sequence: u64,
41    balance: String,
42    #[serde(with = "crate::lotus_json")]
43    state: Ipld,
44}
45
46fn actor_to_resolved(
47    bs: &impl Blockstore,
48    actor: &ActorState,
49    depth: Option<u64>,
50) -> ActorStateResolved {
51    let resolved =
52        resolve_cids_recursive(bs, &actor.state, depth).unwrap_or(Ipld::Link(actor.state));
53    ActorStateResolved {
54        state: resolved,
55        code: actor.code,
56        balance: actor.balance.to_string(),
57        sequence: actor.sequence,
58    }
59}
60
61fn root_to_state_map<BS: Blockstore>(
62    bs: &Arc<BS>,
63    root: &Cid,
64) -> Result<HashMap<Address, ActorState>, anyhow::Error> {
65    let mut actors = HashMap::default();
66    let state_tree = StateTree::new_from_root(bs.clone(), root)?;
67    state_tree.for_each(|addr: Address, actor: &ActorState| {
68        actors.insert(addr, actor.clone());
69        Ok(())
70    })?;
71
72    Ok(actors)
73}
74
75/// Tries to resolve state tree actors, if all data exists in store.
76/// The actors HAMT is hard to parse in a diff, so this attempts to remedy this.
77/// This function will only print the actors that are added, removed, or changed
78/// so it can be used on large state trees.
79fn try_print_actor_states<BS: Blockstore>(
80    bs: &Arc<BS>,
81    root: &Cid,
82    expected_root: &Cid,
83    depth: Option<u64>,
84) -> Result<(), anyhow::Error> {
85    // For now, resolving to a map, because we need to use go implementation's
86    // inefficient caching this would probably be faster in most cases.
87    let mut e_state = root_to_state_map(bs, expected_root)?;
88
89    // Compare state with expected
90    let state_tree = StateTree::new_from_root(bs.clone(), root)?;
91
92    state_tree.for_each(|addr: Address, actor| {
93        if let Some(other) = e_state.remove(&addr) {
94            if &other != actor {
95                const COMMA: &str = ",";
96                let calc_pp = pp_actor_state(bs, actor, depth)?;
97                let expected_pp = pp_actor_state(bs, &other, depth)?;
98                let expected = expected_pp
99                    .split(COMMA)
100                    .map(|s| s.trim_start_matches('\n'))
101                    .collect_vec();
102                let calculated = calc_pp
103                    .split(COMMA)
104                    .map(|s| s.trim_start_matches('\n'))
105                    .collect_vec();
106                let diffs = TextDiff::from_slices(&expected, &calculated);
107                let stdout = stdout();
108                let mut handle = stdout.lock();
109                writeln!(handle, "Address {addr} changed: ")?;
110                print_diffs(&mut handle, diffs)?;
111            }
112        } else {
113            let calc_pp = pp_actor_state(bs, actor, depth)?;
114            // Added actor, print out the json format actor state.
115            println!("{}", format!("+ Address {addr}:\n{calc_pp}").green());
116        }
117
118        Ok(())
119    })?;
120
121    // Print all addresses that no longer have actor state
122    for (addr, state) in e_state.into_iter() {
123        let expected_json = serde_json::to_string_pretty(&actor_to_resolved(bs, &state, depth))?;
124        println!("{}", format!("- Address {addr}:\n{expected_json}").red())
125    }
126
127    Ok(())
128}
129
130fn pp_actor_state(
131    bs: &impl Blockstore,
132    actor_state: &ActorState,
133    depth: Option<u64>,
134) -> Result<String, anyhow::Error> {
135    let mut buffer = String::new();
136    writeln!(&mut buffer, "{actor_state:?}")?;
137    if let Ok(miner_state) = MinerState::load(bs, actor_state.code, actor_state.state) {
138        write!(&mut buffer, "{miner_state:?}")?;
139        return Ok(buffer);
140    }
141    if let Ok(cron_state) = CronState::load(bs, actor_state.code, actor_state.state) {
142        write!(&mut buffer, "{cron_state:?}")?;
143        return Ok(buffer);
144    }
145    if let Ok(account_state) = AccountState::load(bs, actor_state.code, actor_state.state) {
146        write!(&mut buffer, "{account_state:?}")?;
147        return Ok(buffer);
148    }
149    if let Ok(power_state) = PowerState::load(bs, actor_state.code, actor_state.state) {
150        write!(&mut buffer, "{power_state:?}")?;
151        return Ok(buffer);
152    }
153    if let Ok(init_state) = InitState::load(bs, actor_state.code, actor_state.state) {
154        write!(&mut buffer, "{init_state:?}")?;
155        return Ok(buffer);
156    }
157    if let Ok(reward_state) = RewardState::load(bs, actor_state.code, actor_state.state) {
158        write!(&mut buffer, "{reward_state:?}")?;
159        return Ok(buffer);
160    }
161    if let Ok(system_state) = SystemState::load(bs, actor_state.code, actor_state.state) {
162        write!(&mut buffer, "{system_state:?}")?;
163        return Ok(buffer);
164    }
165    if let Ok(multi_sig_state) = MultiSigState::load(bs, actor_state.code, actor_state.state) {
166        write!(&mut buffer, "{multi_sig_state:?}")?;
167        return Ok(buffer);
168    }
169    if let Ok(market_state) = MarketState::load(bs, actor_state.code, actor_state.state) {
170        write!(&mut buffer, "{market_state:?}")?;
171        return Ok(buffer);
172    }
173    if let Ok(datacap_state) = DatacapState::load(bs, actor_state.code, actor_state.state) {
174        write!(&mut buffer, "{datacap_state:?}")?;
175        return Ok(buffer);
176    }
177    if let Ok(evm_state) = EvmState::load(bs, actor_state.code, actor_state.state) {
178        write!(&mut buffer, "{evm_state:?}")?;
179        return Ok(buffer);
180    }
181
182    let resolved = actor_to_resolved(bs, actor_state, depth);
183    buffer = serde_json::to_string_pretty(&resolved)?;
184    Ok(buffer)
185}
186
187fn print_diffs(handle: &mut impl Write, diffs: TextDiff<str>) -> std::io::Result<()> {
188    for op in diffs.ops() {
189        for change in diffs.iter_changes(op) {
190            match change.tag() {
191                ChangeTag::Delete => writeln!(handle, "{}", format!("-{}", change.value()).red())?,
192                ChangeTag::Insert => {
193                    writeln!(handle, "{}", format!("+{}", change.value()).green())?
194                }
195                ChangeTag::Equal => writeln!(handle, " {}", change.value())?,
196            };
197        }
198    }
199    Ok(())
200}
201
202/// Prints a diff of the resolved state tree.
203/// If the actor's HAMT cannot be loaded, base IPLD resolution is given.
204pub fn print_state_diff<BS>(
205    bs: &Arc<BS>,
206    root: &Cid,
207    expected_root: &Cid,
208    depth: Option<u64>,
209) -> Result<(), anyhow::Error>
210where
211    BS: Blockstore,
212{
213    if let Err(e) = try_print_actor_states(bs, root, expected_root, depth) {
214        println!("Could not resolve actor states: {e}\nUsing default resolution:");
215        let expected = resolve_cids_recursive(bs, expected_root, depth)?;
216        let actual = resolve_cids_recursive(bs, root, depth)?;
217
218        let expected_json = expected.into_lotus_json_string_pretty()?;
219        let actual_json = actual.into_lotus_json_string_pretty()?;
220
221        let diffs = TextDiff::from_lines(&expected_json, &actual_json);
222
223        let stdout = stdout();
224        let mut handle = stdout.lock();
225        print_diffs(&mut handle, diffs)?
226    }
227
228    Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233    use crate::db::MemoryDB;
234    use crate::shim::{address::Address, econ::TokenAmount, state_tree::ActorState};
235    use crate::utils::db::CborStoreExt;
236    use cid::Cid;
237    use fil_actor_account_state::v10::State as AccountState;
238    use fvm_ipld_blockstore::Blockstore;
239
240    use super::pp_actor_state;
241
242    fn mk_account_v10(db: &impl Blockstore, account: &AccountState) -> ActorState {
243        // mainnet v10 account actor cid
244        let account_cid =
245            Cid::try_from("bafk2bzaceampw4romta75hyz5p4cqriypmpbgnkxncgxgqn6zptv5lsp2w2bo")
246                .unwrap();
247        let actor_state_cid = db.put_cbor_default(&account).unwrap();
248        ActorState::new(
249            account_cid,
250            actor_state_cid,
251            TokenAmount::from_atto(0),
252            0,
253            None,
254        )
255    }
256
257    // Account states should be parsed and pretty-printed.
258    #[test]
259    fn correctly_pretty_print_account_actor_state() {
260        let db = MemoryDB::default();
261
262        let account_state = AccountState {
263            address: Address::new_id(0xdeadbeef).into(),
264        };
265        let state = mk_account_v10(&db, &account_state);
266
267        let pretty = pp_actor_state(&db, &state, None).unwrap();
268
269        assert_eq!(
270            pretty,
271            "ActorState(\
272                ActorState { \
273                    code: Cid(bafk2bzaceampw4romta75hyz5p4cqriypmpbgnkxncgxgqn6zptv5lsp2w2bo), \
274                    state: Cid(bafy2bzaceaiws3hdhmfyxyfjzmbaxv5aw6eywwbipeae4n5jjg5smmfxsaeic), \
275                    sequence: 0, balance: TokenAmount(0.0), delegated_address: None })\n\
276            V10(State { address: Address { payload: ID(3735928559) } })"
277        );
278    }
279
280    // When we cannot identify (or parse) an actor state, we should print the IPLD
281    // as JSON
282    #[test]
283    fn check_json_fallback_if_unknown_actor() {
284        let db = MemoryDB::default();
285
286        let account_state = AccountState {
287            address: Address::new_id(0xdeadbeef).into(),
288        };
289        let mut state = mk_account_v10(&db, &account_state);
290        state.code = Cid::default(); // Use an unknown actor CID to force parsing to fail.
291
292        let pretty = pp_actor_state(&db, &state, None).unwrap();
293
294        assert_eq!(
295            pretty,
296            "{
297  \"code\": {
298    \"/\": \"baeaaaaa\"
299  },
300  \"sequence\": 0,
301  \"balance\": \"0.0\",
302  \"state\": [
303    {
304      \"/\": {
305        \"bytes\": \"mAO/9tvUN\"
306      }
307    }
308  ]
309}"
310        );
311    }
312}