1use std::sync::LazyLock;
5
6use crate::blocks::{Tipset, TipsetKey};
7use crate::chain::*;
8use crate::networks::{ChainConfig, Height};
9use crate::prelude::*;
10use crate::rpc::types::CirculatingSupply;
11use crate::shim::actors::{
12 MarketActorStateLoad as _, MinerActorStateLoad as _, MultisigActorStateLoad as _,
13 PowerActorStateLoad as _, is_account_actor, is_ethaccount_actor, is_evm_actor, is_miner_actor,
14 is_multisig_actor, is_paymentchannel_actor, is_placeholder_actor,
15};
16use crate::shim::actors::{market, miner, multisig, power, reward};
17use crate::shim::version::NetworkVersion;
18use crate::shim::{
19 address::Address,
20 clock::{ChainEpoch, EPOCHS_IN_DAY},
21 econ::{TOTAL_FILECOIN, TokenAmount},
22 state_tree::{ActorState, StateTree},
23};
24use anyhow::bail;
25use num_traits::Zero;
26
27const EPOCHS_IN_YEAR: ChainEpoch = 365 * EPOCHS_IN_DAY;
28const PRE_CALICO_VESTING: [(ChainEpoch, usize); 5] = [
29 (183 * EPOCHS_IN_DAY, 82_717_041),
30 (EPOCHS_IN_YEAR, 22_421_712),
31 (2 * EPOCHS_IN_YEAR, 7_223_364),
32 (3 * EPOCHS_IN_YEAR, 87_637_883),
33 (6 * EPOCHS_IN_YEAR, 400_000_000),
34];
35const CALICO_VESTING: [(ChainEpoch, usize); 6] = [
36 (0, 10_632_000),
37 (183 * EPOCHS_IN_DAY, 19_015_887 + 32_787_700),
38 (EPOCHS_IN_YEAR, 22_421_712 + 9_400_000),
39 (2 * EPOCHS_IN_YEAR, 7_223_364),
40 (3 * EPOCHS_IN_YEAR, 87_637_883 + 898_958),
41 (6 * EPOCHS_IN_YEAR, 100_000_000 + 300_000_000 + 9_805_053),
42];
43
44#[derive(Default, Clone)]
46pub struct GenesisInfo {
47 vesting: GenesisInfoVesting,
48
49 genesis_pledge: TokenAmount,
51 genesis_market_funds: TokenAmount,
52
53 chain_config: Arc<ChainConfig>,
54}
55
56impl GenesisInfo {
57 pub fn from_chain_config(chain_config: Arc<ChainConfig>) -> Self {
58 let liftoff_height = chain_config.epoch(Height::Liftoff);
59 Self {
60 vesting: GenesisInfoVesting::new(liftoff_height),
61 chain_config,
62 ..GenesisInfo::default()
63 }
64 }
65
66 pub fn get_vm_circulating_supply<DB: Blockstore + ShallowClone>(
72 &self,
73 height: ChainEpoch,
74 db: &DB,
75 root: &Cid,
76 ) -> anyhow::Result<TokenAmount> {
77 let detailed = self.get_vm_circulating_supply_detailed(height, db, root)?;
78 Ok(detailed.fil_circulating)
79 }
80
81 pub fn get_vm_circulating_supply_detailed<DB: Blockstore + ShallowClone>(
84 &self,
85 height: ChainEpoch,
86 db: &DB,
87 root: &Cid,
88 ) -> anyhow::Result<CirculatingSupply> {
89 let state_tree = StateTree::new_from_root(db, root)?;
90
91 let fil_vested = get_fil_vested(self, height);
92 let fil_mined = get_fil_mined(&state_tree)?;
93 let fil_burnt = get_fil_burnt(&state_tree)?;
94
95 let network_version = self.chain_config.network_version(height);
96 let fil_locked = get_fil_locked(&state_tree, network_version)?;
97 let fil_reserve_disbursed = if height > self.chain_config.epoch(Height::Assembly) {
98 get_fil_reserve_disbursed(&self.chain_config, height, &state_tree)?
99 } else {
100 TokenAmount::default()
101 };
102 let fil_circulating = TokenAmount::max(
103 &fil_vested + &fil_mined + &fil_reserve_disbursed - &fil_burnt - &fil_locked,
104 TokenAmount::default(),
105 );
106 Ok(CirculatingSupply {
107 fil_vested,
108 fil_mined,
109 fil_burnt,
110 fil_locked,
111 fil_circulating,
112 fil_reserve_disbursed,
113 })
114 }
115
116 pub async fn get_state_circulating_supply_with_cache(
122 self,
123 db: impl Blockstore + ShallowClone + Send + Sync + 'static,
124 ts: Tipset,
125 ) -> anyhow::Result<TokenAmount> {
126 static CACHE: LazyLock<quick_cache::sync::Cache<TipsetKey, Result<TokenAmount, String>>> =
127 LazyLock::new(|| quick_cache::sync::Cache::new(120)); let height = ts.epoch() - 1;
130 let root = *ts.parent_state();
131 let this = self;
132
133 CACHE
134 .get_or_insert_async(ts.key(), async move {
135 tokio::task::spawn_blocking(move || {
136 this.get_state_circulating_supply_raw_blocking(height, &db, &root)
137 .map_err(|e| {
138 let mut e = e.to_string();
139 e.truncate(e.floor_char_boundary(100)); e
141 })
142 })
143 .await
144 .context("error joining tokio handle")
145 })
146 .await?
147 .map_err(|e| anyhow::anyhow!(e))
148 }
149
150 pub fn get_state_circulating_supply_raw_blocking(
156 &self,
157 height: ChainEpoch,
158 db: &(impl Blockstore + ShallowClone),
159 root: &Cid,
160 ) -> anyhow::Result<TokenAmount> {
161 let mut circ = TokenAmount::default();
162 let mut un_circ = TokenAmount::default();
163
164 let state_tree = StateTree::new_from_root(db, root)?;
165
166 state_tree.for_each_cacheless(|addr: Address, actor: &ActorState| {
167 let actor_balance = TokenAmount::from(actor.balance.clone());
168 if !actor_balance.is_zero() {
169 match addr {
170 Address::INIT_ACTOR
171 | Address::REWARD_ACTOR
172 | Address::VERIFIED_REGISTRY_ACTOR
173 | Address::POWER_ACTOR
175 | Address::SYSTEM_ACTOR
176 | Address::CRON_ACTOR
177 | Address::BURNT_FUNDS_ACTOR
178 | Address::SAFT_ACTOR
179 | Address::RESERVE_ACTOR
180 | Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR
181 | Address::DATACAP_TOKEN_ACTOR => {
182 un_circ += actor_balance;
183 }
184 Address::MARKET_ACTOR => {
185 let network_version = self.chain_config.network_version(height);
186 if network_version >= NetworkVersion::V23 {
187 circ += actor_balance;
188 } else {
189 let ms = market::State::load(db, actor.code, actor.state)?;
190 let locked_balance = ms.total_locked();
191 circ += actor_balance - &locked_balance;
192 un_circ += locked_balance;
193 }
194 }
195 _ if is_account_actor(&actor.code)
196 || is_paymentchannel_actor(&actor.code)
197 || is_ethaccount_actor(&actor.code)
198 || is_evm_actor(&actor.code)
199 || is_placeholder_actor(&actor.code) => {
200 circ += actor_balance;
201 },
202 _ if is_miner_actor(&actor.code) => {
203 let ms = miner::State::load(&db, actor.code, actor.state)?;
204
205 if let Ok(avail_balance) = ms.available_balance(actor.balance.atto()) {
206 circ += avail_balance.clone();
207 un_circ += actor_balance.clone() - &avail_balance;
208 } else {
209 un_circ += actor_balance;
212 }
213 }
214 _ if is_multisig_actor(&actor.code) => {
215 let ms = multisig::State::load(&db, actor.code, actor.state)?;
216
217 let locked_balance = ms.locked_balance(height)?;
218 let avail_balance = actor_balance.clone() - &locked_balance;
219 circ += avail_balance.max(TokenAmount::zero());
220 un_circ += actor_balance.min(locked_balance);
221 }
222 _ => bail!("unexpected actor at epoch {height}: {actor:?}"),
223 }
224 } else {
225 }
227 Ok(())
228 })?;
229
230 let total = circ.clone() + un_circ;
231 if total != *TOTAL_FILECOIN {
232 bail!(
233 "total filecoin didn't add to expected amount: {} != {}",
234 total,
235 *TOTAL_FILECOIN
236 );
237 }
238
239 Ok(circ)
240 }
241}
242
243#[derive(Default, Clone)]
246struct GenesisInfoVesting {
247 genesis: Vec<(ChainEpoch, TokenAmount)>,
248 ignition: Vec<(ChainEpoch, ChainEpoch, TokenAmount)>,
249 calico: Vec<(ChainEpoch, ChainEpoch, TokenAmount)>,
250}
251
252impl GenesisInfoVesting {
253 fn new(liftoff_height: ChainEpoch) -> Self {
254 Self {
255 genesis: setup_genesis_vesting_schedule(),
256 ignition: setup_ignition_vesting_schedule(liftoff_height),
257 calico: setup_calico_vesting_schedule(liftoff_height),
258 }
259 }
260}
261
262fn get_actor_state<DB: Blockstore>(
263 state_tree: &StateTree<DB>,
264 addr: &Address,
265) -> anyhow::Result<ActorState> {
266 state_tree
267 .get_actor(addr)?
268 .with_context(|| format!("Failed to get Actor for address {addr}"))
269}
270
271fn get_fil_vested(genesis_info: &GenesisInfo, height: ChainEpoch) -> TokenAmount {
272 let mut return_value = TokenAmount::default();
273
274 let pre_ignition = &genesis_info.vesting.genesis;
275 let post_ignition = &genesis_info.vesting.ignition;
276 let calico_vesting = &genesis_info.vesting.calico;
277
278 if height <= genesis_info.chain_config.epoch(Height::Ignition) {
279 for (unlock_duration, initial_balance) in pre_ignition {
280 return_value +=
281 initial_balance - v0_amount_locked(*unlock_duration, initial_balance, height);
282 }
283 } else if height <= genesis_info.chain_config.epoch(Height::Calico) {
284 for (start_epoch, unlock_duration, initial_balance) in post_ignition {
285 return_value += initial_balance
286 - v0_amount_locked(*unlock_duration, initial_balance, height - start_epoch);
287 }
288 } else {
289 for (start_epoch, unlock_duration, initial_balance) in calico_vesting {
290 return_value += initial_balance
291 - v0_amount_locked(*unlock_duration, initial_balance, height - start_epoch);
292 }
293 }
294
295 if height <= genesis_info.chain_config.epoch(Height::Assembly) {
296 return_value += &genesis_info.genesis_pledge + &genesis_info.genesis_market_funds;
297 }
298
299 return_value
300}
301
302fn get_fil_mined<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
303 let state: reward::State = state_tree.get_actor_state()?;
304 Ok(state.into_total_storage_power_reward())
305}
306
307fn get_fil_market_locked<DB: Blockstore>(
308 state_tree: &StateTree<DB>,
309) -> anyhow::Result<TokenAmount> {
310 let actor = state_tree
311 .get_actor(&Address::MARKET_ACTOR)?
312 .ok_or_else(|| Error::state("Market actor address could not be resolved"))?;
313 let state = market::State::load(state_tree.store(), actor.code, actor.state)?;
314
315 Ok(state.total_locked())
316}
317
318fn get_fil_power_locked<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
319 let actor = state_tree
320 .get_actor(&Address::POWER_ACTOR)?
321 .ok_or_else(|| Error::state("Power actor address could not be resolved"))?;
322 let state = power::State::load(state_tree.store(), actor.code, actor.state)?;
323 Ok(state.into_total_locked())
324}
325
326fn get_fil_reserve_disbursed<DB: Blockstore>(
327 chain_config: &ChainConfig,
328 height: ChainEpoch,
329 state_tree: &StateTree<DB>,
330) -> anyhow::Result<TokenAmount> {
331 let fil_reserved = chain_config.initial_fil_reserved_at_height(height);
335 let reserve_actor = get_actor_state(state_tree, &Address::RESERVE_ACTOR)?;
336
337 Ok(fil_reserved - TokenAmount::from(&reserve_actor.balance))
339}
340
341fn get_fil_locked<DB: Blockstore>(
342 state_tree: &StateTree<DB>,
343 network_version: NetworkVersion,
344) -> anyhow::Result<TokenAmount> {
345 let total = if network_version >= NetworkVersion::V23 {
346 get_fil_power_locked(state_tree)?
347 } else {
348 get_fil_market_locked(state_tree)? + get_fil_power_locked(state_tree)?
349 };
350
351 Ok(total)
352}
353
354fn get_fil_burnt<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
355 let burnt_actor = get_actor_state(state_tree, &Address::BURNT_FUNDS_ACTOR)?;
356
357 Ok(TokenAmount::from(&burnt_actor.balance))
358}
359
360fn setup_genesis_vesting_schedule() -> Vec<(ChainEpoch, TokenAmount)> {
361 PRE_CALICO_VESTING
362 .into_iter()
363 .map(|(unlock_duration, initial_balance)| {
364 (unlock_duration, TokenAmount::from_atto(initial_balance))
365 })
366 .collect()
367}
368
369fn setup_ignition_vesting_schedule(
370 liftoff_height: ChainEpoch,
371) -> Vec<(ChainEpoch, ChainEpoch, TokenAmount)> {
372 PRE_CALICO_VESTING
373 .into_iter()
374 .map(|(unlock_duration, initial_balance)| {
375 (
376 liftoff_height,
377 unlock_duration,
378 TokenAmount::from_whole(initial_balance),
379 )
380 })
381 .collect()
382}
383fn setup_calico_vesting_schedule(
384 liftoff_height: ChainEpoch,
385) -> Vec<(ChainEpoch, ChainEpoch, TokenAmount)> {
386 CALICO_VESTING
387 .into_iter()
388 .map(|(unlock_duration, initial_balance)| {
389 (
390 liftoff_height,
391 unlock_duration,
392 TokenAmount::from_whole(initial_balance),
393 )
394 })
395 .collect()
396}
397
398fn v0_amount_locked(
402 unlock_duration: ChainEpoch,
403 initial_balance: &TokenAmount,
404 elapsed_epoch: ChainEpoch,
405) -> TokenAmount {
406 if elapsed_epoch >= unlock_duration {
407 return TokenAmount::zero();
408 }
409 if elapsed_epoch < 0 {
410 return initial_balance.clone();
411 }
412 let unit_locked: TokenAmount = initial_balance.div_floor(unlock_duration);
414 unit_locked * (unlock_duration - elapsed_epoch)
415}