Skip to main content

forest/rpc/methods/
f3.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//!
5//! This module contains F3(fast finality) related V1 RPC methods
6//! as well as some internal RPC methods(F3.*) that power
7//! the go-f3 node in sidecar mode.
8//!
9
10mod types;
11mod util;
12
13pub use self::types::{
14    F3InstanceProgress, F3LeaseManager, F3Manifest, F3PowerEntry, FinalityCertificate,
15};
16use self::{types::*, util::*};
17use super::wallet::WalletSign;
18use crate::{
19    blocks::Tipset,
20    chain::index::ResolveNullTipset,
21    chain_sync::TipsetValidator,
22    db::{
23        BlockstoreReadCacheStats as _, BlockstoreWithReadCache, DefaultBlockstoreReadCache,
24        DefaultBlockstoreReadCacheStats,
25    },
26    libp2p::{NetRPCMethods, NetworkMessage},
27    lotus_json::{HasLotusJson as _, LotusJson},
28    prelude::*,
29    rpc::{ApiPaths, Ctx, Permission, RpcMethod, ServerError, types::ApiTipsetKey},
30    shim::{
31        actors::{miner, power},
32        address::{Address, Protocol},
33        clock::ChainEpoch,
34        crypto::Signature,
35    },
36    utils::misc::env::is_env_set_and_truthy,
37};
38use ahash::{HashMap, HashSet};
39use anyhow::Context as _;
40use enumflags2::BitFlags;
41use jsonrpsee::core::{client::ClientT as _, params::ArrayParams};
42use libp2p::PeerId;
43use nonzero_ext::nonzero;
44use num::Signed as _;
45use parking_lot::RwLock;
46use std::num::NonZeroUsize;
47use std::{
48    borrow::Cow,
49    fmt::Display,
50    str::FromStr as _,
51    sync::{LazyLock, OnceLock},
52};
53
54pub static F3_LEASE_MANAGER: OnceLock<F3LeaseManager> = OnceLock::new();
55
56pub enum GetRawNetworkName {}
57
58impl RpcMethod<0> for GetRawNetworkName {
59    const NAME: &'static str = "F3.GetRawNetworkName";
60    const PARAM_NAMES: [&'static str; 0] = [];
61    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
62    const PERMISSION: Permission = Permission::Read;
63    const DESCRIPTION: &'static str = "Returns the raw (genesis) network name.";
64
65    type Params = ();
66    type Ok = Arc<str>;
67
68    async fn handle(
69        ctx: Ctx,
70        (): Self::Params,
71        _: &http::Extensions,
72    ) -> Result<Self::Ok, ServerError> {
73        // Network is fixed for the process lifetime; cache the genesis name.
74        static CACHED: OnceLock<Arc<str>> = OnceLock::new();
75        Ok(CACHED
76            .get_or_init(|| {
77                Arc::<str>::from(String::from(ctx.chain_config().network.genesis_name()))
78            })
79            .clone())
80    }
81}
82
83pub enum GetTipsetByEpoch {}
84impl RpcMethod<1> for GetTipsetByEpoch {
85    const NAME: &'static str = "F3.GetTipsetByEpoch";
86    const PARAM_NAMES: [&'static str; 1] = ["epoch"];
87    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
88    const PERMISSION: Permission = Permission::Read;
89    const DESCRIPTION: &'static str = "Returns the tipset at the given epoch.";
90
91    type Params = (ChainEpoch,);
92    type Ok = F3TipSet;
93
94    async fn handle(
95        ctx: Ctx,
96        (epoch,): Self::Params,
97        _: &http::Extensions,
98    ) -> Result<Self::Ok, ServerError> {
99        let ts = ctx
100            .chain_index()
101            .load_required_tipset_by_height(
102                epoch,
103                ctx.chain_store().heaviest_tipset(),
104                ResolveNullTipset::TakeOlder,
105            )
106            .await?;
107        Ok(ts.into())
108    }
109}
110
111pub enum GetTipset {}
112impl RpcMethod<1> for GetTipset {
113    const NAME: &'static str = "F3.GetTipset";
114    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
115    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
116    const PERMISSION: Permission = Permission::Read;
117    const DESCRIPTION: &'static str = "Returns the tipset with the given F3 tipset key.";
118
119    type Params = (F3TipSetKey,);
120    type Ok = F3TipSet;
121
122    async fn handle(
123        ctx: Ctx,
124        (f3_tsk,): Self::Params,
125        _: &http::Extensions,
126    ) -> Result<Self::Ok, ServerError> {
127        let tsk = f3_tsk.try_into()?;
128        let ts = ctx.chain_index().load_required_tipset(&tsk)?;
129        Ok(ts.into())
130    }
131}
132
133pub enum GetHead {}
134impl RpcMethod<0> for GetHead {
135    const NAME: &'static str = "F3.GetHead";
136    const PARAM_NAMES: [&'static str; 0] = [];
137    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
138    const PERMISSION: Permission = Permission::Read;
139    const DESCRIPTION: &'static str = "Returns the current chain head as an F3 tipset.";
140
141    type Params = ();
142    type Ok = F3TipSet;
143
144    async fn handle(
145        ctx: Ctx,
146        _: Self::Params,
147        _: &http::Extensions,
148    ) -> Result<Self::Ok, ServerError> {
149        Ok(ctx.chain_store().heaviest_tipset().into())
150    }
151}
152
153pub enum GetParent {}
154impl RpcMethod<1> for GetParent {
155    const NAME: &'static str = "F3.GetParent";
156    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
157    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
158    const PERMISSION: Permission = Permission::Read;
159    const DESCRIPTION: &'static str =
160        "Returns the parent of the tipset with the given F3 tipset key.";
161
162    type Params = (F3TipSetKey,);
163    type Ok = F3TipSet;
164
165    async fn handle(
166        ctx: Ctx,
167        (f3_tsk,): Self::Params,
168        _: &http::Extensions,
169    ) -> Result<Self::Ok, ServerError> {
170        let tsk = f3_tsk.try_into()?;
171        let ts = ctx.chain_index().load_required_tipset(&tsk)?;
172        let parent = ctx.chain_index().load_required_tipset(ts.parents())?;
173        Ok(parent.into())
174    }
175}
176
177pub enum GetPowerTable {}
178
179impl GetPowerTable {
180    async fn compute(ctx: &Ctx, ts: &Tipset) -> anyhow::Result<Vec<F3PowerEntry>> {
181        // The RAM overhead on mainnet is ~14MiB
182        const BLOCKSTORE_CACHE_CAP: NonZeroUsize = nonzero!(65536_usize);
183        static BLOCKSTORE_CACHE: LazyLock<DefaultBlockstoreReadCache> = LazyLock::new(|| {
184            DefaultBlockstoreReadCache::new_with_metrics("get_powertable", BLOCKSTORE_CACHE_CAP)
185        });
186        let db = BlockstoreWithReadCache::new(
187            ctx.db_owned(),
188            BLOCKSTORE_CACHE.shallow_clone(),
189            Some(DefaultBlockstoreReadCacheStats::default()),
190        );
191
192        let state_tree = ctx.state_manager.get_state_tree(ts.parent_state())?;
193
194        macro_rules! handle_miner_state_v12_on {
195            ($version:tt, $id_power_worker_mappings:ident, $ts:expr, $state:expr, $policy:expr) => {
196                fn map_err<E: Display>(e: E) -> fil_actors_shared::$version::ActorError {
197                    fil_actors_shared::$version::ActorError::unspecified(e.to_string())
198                }
199
200                let claims = $state.load_claims(&db)?;
201                claims.for_each(|miner, claim| {
202                    if !claim.quality_adj_power.is_positive() {
203                        return Ok(());
204                    }
205
206                    let id = miner.id().map_err(map_err)?;
207                    let (_, ok) =
208                        $state.miner_nominal_power_meets_consensus_minimum($policy, &db, id)?;
209                    if !ok {
210                        return Ok(());
211                    }
212                    let power = claim.quality_adj_power.clone();
213                    let miner_state: miner::State = state_tree
214                        .get_actor_state_from_address(&miner.into())
215                        .map_err(map_err)?;
216                    let debt = miner_state.fee_debt();
217                    if !debt.is_zero() {
218                        // fee debt don't add the miner to power table
219                        return Ok(());
220                    }
221                    let miner_info = miner_state.info(&db).map_err(map_err)?;
222                    // check consensus faults
223                    if $ts.epoch() <= miner_info.consensus_fault_elapsed {
224                        return Ok(());
225                    }
226                    $id_power_worker_mappings.push((id, power, miner_info.worker.into()));
227                    Ok(())
228                })?;
229            };
230        }
231
232        let state: power::State = state_tree.get_actor_state()?;
233        let mut id_power_worker_mappings = vec![];
234        let policy = &ctx.chain_config().policy;
235        match &state {
236            power::State::V8(s) => {
237                fn map_err<E: Display>(e: E) -> fil_actors_shared::v8::ActorError {
238                    fil_actors_shared::v8::ActorError::unspecified(e.to_string())
239                }
240
241                let claims = fil_actors_shared::v8::make_map_with_root::<
242                    _,
243                    fil_actor_power_state::v8::Claim,
244                >(&s.claims, &db)?;
245                claims.for_each(|key, claim| {
246                    let miner = Address::from_bytes(key)?;
247                    if !claim.quality_adj_power.is_positive() {
248                        return Ok(());
249                    }
250
251                    let id = miner.id().map_err(map_err)?;
252                    let ok = s.miner_nominal_power_meets_consensus_minimum(
253                        &policy.into(),
254                        &db,
255                        &miner.into(),
256                    )?;
257                    if !ok {
258                        return Ok(());
259                    }
260                    let power = claim.quality_adj_power.clone();
261                    let miner_state: miner::State = state_tree
262                        .get_actor_state_from_address(&miner)
263                        .map_err(map_err)?;
264                    let debt = miner_state.fee_debt();
265                    if !debt.is_zero() {
266                        // fee debt don't add the miner to power table
267                        return Ok(());
268                    }
269                    let miner_info = miner_state.info(&db).map_err(map_err)?;
270                    // check consensus faults
271                    if ts.epoch() <= miner_info.consensus_fault_elapsed {
272                        return Ok(());
273                    }
274                    id_power_worker_mappings.push((id, power, miner_info.worker));
275                    Ok(())
276                })?;
277            }
278            power::State::V9(s) => {
279                fn map_err<E: Display>(e: E) -> fil_actors_shared::v9::ActorError {
280                    fil_actors_shared::v9::ActorError::unspecified(e.to_string())
281                }
282
283                let claims = fil_actors_shared::v9::make_map_with_root::<
284                    _,
285                    fil_actor_power_state::v9::Claim,
286                >(&s.claims, &db)?;
287                claims.for_each(|key, claim| {
288                    let miner = Address::from_bytes(key)?;
289                    if !claim.quality_adj_power.is_positive() {
290                        return Ok(());
291                    }
292
293                    let id = miner.id().map_err(map_err)?;
294                    let ok = s.miner_nominal_power_meets_consensus_minimum(
295                        &policy.into(),
296                        &db,
297                        &miner.into(),
298                    )?;
299                    if !ok {
300                        return Ok(());
301                    }
302                    let power = claim.quality_adj_power.clone();
303                    let miner_state: miner::State = state_tree
304                        .get_actor_state_from_address(&miner)
305                        .map_err(map_err)?;
306                    let debt = miner_state.fee_debt();
307                    if !debt.is_zero() {
308                        // fee debt don't add the miner to power table
309                        return Ok(());
310                    }
311                    let miner_info = miner_state.info(&db).map_err(map_err)?;
312                    // check consensus faults
313                    if ts.epoch() <= miner_info.consensus_fault_elapsed {
314                        return Ok(());
315                    }
316                    id_power_worker_mappings.push((id, power, miner_info.worker));
317                    Ok(())
318                })?;
319            }
320            power::State::V10(s) => {
321                fn map_err<E: Display>(e: E) -> fil_actors_shared::v10::ActorError {
322                    fil_actors_shared::v10::ActorError::unspecified(e.to_string())
323                }
324
325                let claims = fil_actors_shared::v10::make_map_with_root::<
326                    _,
327                    fil_actor_power_state::v10::Claim,
328                >(&s.claims, &db)?;
329                claims.for_each(|key, claim| {
330                    let miner = Address::from_bytes(key)?;
331                    if !claim.quality_adj_power.is_positive() {
332                        return Ok(());
333                    }
334
335                    let id = miner.id().map_err(map_err)?;
336                    let (_, ok) =
337                        s.miner_nominal_power_meets_consensus_minimum(&policy.into(), &db, id)?;
338                    if !ok {
339                        return Ok(());
340                    }
341                    let power = claim.quality_adj_power.clone();
342                    let miner_state: miner::State = state_tree
343                        .get_actor_state_from_address(&miner)
344                        .map_err(map_err)?;
345                    let debt = miner_state.fee_debt();
346                    if !debt.is_zero() {
347                        // fee debt don't add the miner to power table
348                        return Ok(());
349                    }
350                    let miner_info = miner_state.info(&db).map_err(map_err)?;
351                    // check consensus faults
352                    if ts.epoch() <= miner_info.consensus_fault_elapsed {
353                        return Ok(());
354                    }
355                    id_power_worker_mappings.push((id, power, miner_info.worker));
356                    Ok(())
357                })?;
358            }
359            power::State::V11(s) => {
360                fn map_err<E: Display>(e: E) -> fil_actors_shared::v11::ActorError {
361                    fil_actors_shared::v11::ActorError::unspecified(e.to_string())
362                }
363
364                let claims = fil_actors_shared::v11::make_map_with_root::<
365                    _,
366                    fil_actor_power_state::v11::Claim,
367                >(&s.claims, &db)?;
368                claims.for_each(|key, claim| {
369                    let miner = Address::from_bytes(key)?;
370                    if !claim.quality_adj_power.is_positive() {
371                        return Ok(());
372                    }
373
374                    let id = miner.id().map_err(map_err)?;
375                    let (_, ok) =
376                        s.miner_nominal_power_meets_consensus_minimum(&policy.into(), &db, id)?;
377                    if !ok {
378                        return Ok(());
379                    }
380                    let power = claim.quality_adj_power.clone();
381                    let miner_state: miner::State = state_tree
382                        .get_actor_state_from_address(&miner)
383                        .map_err(map_err)?;
384                    let debt = miner_state.fee_debt();
385                    if !debt.is_zero() {
386                        // fee debt don't add the miner to power table
387                        return Ok(());
388                    }
389                    let miner_info = miner_state.info(&db).map_err(map_err)?;
390                    // check consensus faults
391                    if ts.epoch() <= miner_info.consensus_fault_elapsed {
392                        return Ok(());
393                    }
394                    id_power_worker_mappings.push((id, power, miner_info.worker));
395                    Ok(())
396                })?;
397            }
398            power::State::V12(s) => {
399                handle_miner_state_v12_on!(v12, id_power_worker_mappings, &ts, s, &policy.into());
400            }
401            power::State::V13(s) => {
402                handle_miner_state_v12_on!(v13, id_power_worker_mappings, &ts, s, &policy.into());
403            }
404            power::State::V14(s) => {
405                handle_miner_state_v12_on!(v14, id_power_worker_mappings, &ts, s, &policy.into());
406            }
407            power::State::V15(s) => {
408                handle_miner_state_v12_on!(v15, id_power_worker_mappings, &ts, s, &policy.into());
409            }
410            power::State::V16(s) => {
411                handle_miner_state_v12_on!(v16, id_power_worker_mappings, &ts, s, &policy.into());
412            }
413            power::State::V17(s) => {
414                handle_miner_state_v12_on!(v17, id_power_worker_mappings, &ts, s, &policy.into());
415            }
416            power::State::V18(s) => {
417                handle_miner_state_v12_on!(v18, id_power_worker_mappings, &ts, s, &policy.into());
418            }
419        }
420        let mut power_entries = vec![];
421        for (id, power, worker) in id_power_worker_mappings {
422            let waddr = ctx
423                .state_manager
424                .resolve_to_deterministic_address(worker, ts)
425                .await?;
426            if waddr.protocol() != Protocol::BLS {
427                anyhow::bail!("wrong type of worker address");
428            }
429            let pub_key = waddr.payload_bytes();
430            power_entries.push(F3PowerEntry { id, power, pub_key });
431        }
432        power_entries.sort();
433
434        if let Some(stats) = db.stats() {
435            tracing::debug!(epoch=%ts.epoch(), hit=%stats.hit(), miss=%stats.miss(),cache_len=%BLOCKSTORE_CACHE.len(), "F3.GetPowerTable blockstore read cache");
436        }
437
438        Ok(power_entries)
439    }
440}
441
442impl RpcMethod<1> for GetPowerTable {
443    const NAME: &'static str = "F3.GetPowerTable";
444    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
445    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
446    const PERMISSION: Permission = Permission::Read;
447    const DESCRIPTION: &'static str =
448        "Returns the power table (the participating miners and their power) at the given tipset.";
449
450    type Params = (F3TipSetKey,);
451    type Ok = Vec<F3PowerEntry>;
452
453    async fn handle(
454        ctx: Ctx,
455        (f3_tsk,): Self::Params,
456        _: &http::Extensions,
457    ) -> Result<Self::Ok, ServerError> {
458        let tsk = f3_tsk.try_into()?;
459        let start = std::time::Instant::now();
460        let ts = ctx.chain_index().load_required_tipset(&tsk)?;
461        let power_entries = Self::compute(&ctx, &ts).await?;
462        tracing::debug!(epoch=%ts.epoch(), %tsk, "F3.GetPowerTable, took {}", humantime::format_duration(start.elapsed()));
463        Ok(power_entries)
464    }
465}
466
467pub enum ProtectPeer {}
468impl RpcMethod<1> for ProtectPeer {
469    const NAME: &'static str = "F3.ProtectPeer";
470    const PARAM_NAMES: [&'static str; 1] = ["peerId"];
471    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
472    const PERMISSION: Permission = Permission::Read;
473    const DESCRIPTION: &'static str = "Protects the given peer from connection pruning.";
474
475    type Params = (String,);
476    type Ok = bool;
477
478    async fn handle(
479        ctx: Ctx,
480        (peer_id,): Self::Params,
481        _: &http::Extensions,
482    ) -> Result<Self::Ok, ServerError> {
483        let peer_id = PeerId::from_str(&peer_id)?;
484        let (tx, rx) = flume::bounded(1);
485        ctx.network_send()
486            .send_async(NetworkMessage::JSONRPCRequest {
487                method: NetRPCMethods::ProtectPeer(tx, std::iter::once(peer_id).collect()),
488            })
489            .await?;
490        rx.recv_async().await?;
491        Ok(true)
492    }
493}
494
495pub enum GetParticipatingMinerIDs {}
496
497impl RpcMethod<0> for GetParticipatingMinerIDs {
498    const NAME: &'static str = "F3.GetParticipatingMinerIDs";
499    const PARAM_NAMES: [&'static str; 0] = [];
500    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
501    const PERMISSION: Permission = Permission::Read;
502    const DESCRIPTION: &'static str =
503        "Returns the IDs of the miners currently participating in F3 through this node.";
504
505    type Params = ();
506    type Ok = Vec<u64>;
507
508    async fn handle(
509        _: Ctx,
510        _: Self::Params,
511        _: &http::Extensions,
512    ) -> Result<Self::Ok, ServerError> {
513        let participants = F3ListParticipants::run().await?;
514        let mut ids: HashSet<u64> = participants.into_iter().map(|p| p.miner_id).collect();
515        if let Some(permanent_miner_ids) = F3_PERMANENT_PARTICIPATING_MINER_IDS.as_ref() {
516            ids.extend(permanent_miner_ids.iter().copied());
517        }
518        Ok(ids.into_iter().collect())
519    }
520}
521
522pub enum Finalize {}
523impl RpcMethod<1> for Finalize {
524    const NAME: &'static str = "F3.Finalize";
525    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
526    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
527    const PERMISSION: Permission = Permission::Write;
528    const DESCRIPTION: &'static str =
529        "Marks the given tipset as F3-finalized, resetting the chain head to it when appropriate.";
530
531    type Params = (F3TipSetKey,);
532    type Ok = ();
533
534    async fn handle(
535        ctx: Ctx,
536        (f3_tsk,): Self::Params,
537        _: &http::Extensions,
538    ) -> Result<Self::Ok, ServerError> {
539        // Respect the environment variable when set, and fallback to chain config when not set.
540        static ENV_ENABLED: LazyLock<Option<bool>> =
541            LazyLock::new(|| is_env_set_and_truthy("FOREST_F3_CONSENSUS_ENABLED"));
542        let enabled = ENV_ENABLED.unwrap_or(ctx.chain_config().f3_consensus);
543        if !enabled {
544            return Ok(());
545        }
546
547        let tsk = f3_tsk.try_into()?;
548        let finalized_ts = match ctx.chain_index().load_tipset(&tsk)? {
549            Some(ts) => ts,
550            None => ctx
551                .sync_network_context
552                .chain_exchange_headers(None, &tsk, nonzero!(1_u64))
553                .await?
554                .first()
555                .map(ShallowClone::shallow_clone)
556                .with_context(|| format!("failed to get tipset via chain exchange. tsk: {tsk}"))?,
557        };
558        let head = ctx.chain_store().heaviest_tipset();
559        // When finalized_ts is not part of the current chain,
560        // reset the current head to finalized_ts.
561        // Note that when finalized_ts is newer than head or older than head - chain_finality,
562        // we don't reset the head to allow the chain or F3 to catch up.
563        if head.epoch() >= finalized_ts.epoch()
564            && head.epoch() <= finalized_ts.epoch() + ctx.chain_config().policy.chain_finality
565        {
566            tracing::debug!(
567                "F3 finalized tsk {} at epoch {}",
568                finalized_ts.key(),
569                finalized_ts.epoch()
570            );
571            if !head
572                .chain(ctx.db())
573                .take_while(|ts| ts.epoch() >= finalized_ts.epoch())
574                .any(|ts| ts == finalized_ts)
575            {
576                tracing::info!(
577                    "F3 reset chain head to tsk {} at epoch {}",
578                    finalized_ts.key(),
579                    finalized_ts.epoch()
580                );
581                let fts = ctx
582                    .sync_network_context
583                    .chain_exchange_full_tipset(None, &tsk)
584                    .await?;
585                fts.persist(ctx.db())?;
586                let validator = TipsetValidator(&fts);
587                validator.validate(
588                    ctx.chain_store(),
589                    None,
590                    &ctx.chain_store().genesis_tipset(),
591                    ctx.chain_config().block_delay_secs,
592                )?;
593                ctx.chain_store()
594                    .set_heaviest_tipset(finalized_ts.shallow_clone())?;
595            }
596            ctx.chain_store().set_f3_finalized_tipset(finalized_ts);
597        }
598        Ok(())
599    }
600}
601
602pub enum SignMessage {}
603impl RpcMethod<2> for SignMessage {
604    const NAME: &'static str = "F3.SignMessage";
605    const PARAM_NAMES: [&'static str; 2] = ["pubkey", "message"];
606    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
607    const PERMISSION: Permission = Permission::Sign;
608    const DESCRIPTION: &'static str =
609        "Signs a message with the private key corresponding to the given BLS public key.";
610
611    type Params = (Vec<u8>, Vec<u8>);
612    type Ok = Signature;
613
614    async fn handle(
615        ctx: Ctx,
616        (pubkey, message): Self::Params,
617        ext: &http::Extensions,
618    ) -> Result<Self::Ok, ServerError> {
619        let addr = Address::new_bls(&pubkey)?;
620        // Signing can be delegated to curio, we will follow how lotus does it once the feature lands.
621        WalletSign::handle(ctx, (addr, message), ext).await
622    }
623}
624
625pub enum F3ExportLatestSnapshot {}
626
627impl F3ExportLatestSnapshot {
628    pub async fn run(path: String) -> anyhow::Result<Cid> {
629        let client = get_rpc_http_client()?;
630        let mut params = ArrayParams::new();
631        params.insert(path)?;
632        let LotusJson(cid): LotusJson<Cid> = client
633            .request("Filecoin.F3ExportLatestSnapshot", params)
634            .await?;
635        Ok(cid)
636    }
637}
638
639impl RpcMethod<1> for F3ExportLatestSnapshot {
640    const NAME: &'static str = "F3.ExportLatestSnapshot";
641    const PARAM_NAMES: [&'static str; 1] = ["path"];
642    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
643    const PERMISSION: Permission = Permission::Read;
644    const DESCRIPTION: &'static str =
645        "Exports the latest F3 snapshot to the specified path and returns its CID";
646
647    type Params = (String,);
648    type Ok = Cid;
649
650    async fn handle(
651        _ctx: Ctx,
652        (path,): Self::Params,
653        _: &http::Extensions,
654    ) -> Result<Self::Ok, ServerError> {
655        Ok(Self::run(path).await?)
656    }
657}
658
659/// returns a finality certificate at given instance number
660pub enum F3GetCertificate {}
661impl RpcMethod<1> for F3GetCertificate {
662    const NAME: &'static str = "Filecoin.F3GetCertificate";
663    const PARAM_NAMES: [&'static str; 1] = ["instance"];
664    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
665    const PERMISSION: Permission = Permission::Read;
666    const DESCRIPTION: &'static str = "Returns the finality certificate for the given F3 instance.";
667
668    type Params = (u64,);
669    type Ok = FinalityCertificate;
670
671    async fn handle(
672        _: Ctx,
673        (instance,): Self::Params,
674        _: &http::Extensions,
675    ) -> Result<Self::Ok, ServerError> {
676        let client = get_rpc_http_client()?;
677        let mut params = ArrayParams::new();
678        params.insert(instance)?;
679        let response: LotusJson<Self::Ok> = client.request(Self::NAME, params).await?;
680        Ok(response.into_inner())
681    }
682}
683
684/// returns the latest finality certificate
685pub enum F3GetLatestCertificate {}
686
687impl F3GetLatestCertificate {
688    /// Fetches the latest finality certificate via RPC.
689    pub async fn get() -> anyhow::Result<FinalityCertificate> {
690        let client = get_rpc_http_client()?;
691        let response: LotusJson<FinalityCertificate> = client
692            .request(<Self as RpcMethod<0>>::NAME, ArrayParams::new())
693            .await?;
694        Ok(response.into_inner())
695    }
696}
697
698impl RpcMethod<0> for F3GetLatestCertificate {
699    const NAME: &'static str = "Filecoin.F3GetLatestCertificate";
700    const PARAM_NAMES: [&'static str; 0] = [];
701    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
702    const PERMISSION: Permission = Permission::Read;
703    const DESCRIPTION: &'static str = "Returns the latest F3 finality certificate.";
704
705    type Params = ();
706    type Ok = FinalityCertificate;
707
708    async fn handle(
709        _: Ctx,
710        _: Self::Params,
711        _: &http::Extensions,
712    ) -> Result<Self::Ok, ServerError> {
713        Ok(Self::get().await?)
714    }
715}
716
717pub enum F3GetECPowerTable {}
718impl RpcMethod<1> for F3GetECPowerTable {
719    const NAME: &'static str = "Filecoin.F3GetECPowerTable";
720    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
721    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
722    const PERMISSION: Permission = Permission::Read;
723    const DESCRIPTION: &'static str = "Returns the Expected Consensus power table at the given tipset (defaults to the chain head).";
724
725    type Params = (ApiTipsetKey,);
726    type Ok = Vec<F3PowerEntry>;
727
728    async fn handle(
729        ctx: Ctx,
730        (ApiTipsetKey(tsk_opt),): Self::Params,
731        ext: &http::Extensions,
732    ) -> Result<Self::Ok, ServerError> {
733        let tsk = tsk_opt.unwrap_or_else(|| ctx.chain_store().heaviest_tipset().key().clone());
734        GetPowerTable::handle(ctx, (tsk.into(),), ext).await
735    }
736}
737
738pub enum F3GetF3PowerTable {}
739impl RpcMethod<1> for F3GetF3PowerTable {
740    const NAME: &'static str = "Filecoin.F3GetF3PowerTable";
741    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
742    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
743    const PERMISSION: Permission = Permission::Read;
744    const DESCRIPTION: &'static str =
745        "Returns F3's power table at the given tipset (defaults to the chain head).";
746
747    type Params = (ApiTipsetKey,);
748    type Ok = Vec<F3PowerEntry>;
749
750    async fn handle(
751        ctx: Ctx,
752        (ApiTipsetKey(tsk_opt),): Self::Params,
753        _: &http::Extensions,
754    ) -> Result<Self::Ok, ServerError> {
755        let tsk: F3TipSetKey = tsk_opt
756            .unwrap_or_else(|| ctx.chain_store().heaviest_tipset().key().clone())
757            .into();
758        let client = get_rpc_http_client()?;
759        let mut params = ArrayParams::new();
760        params.insert(tsk.into_lotus_json())?;
761        let response: LotusJson<Self::Ok> = client.request(Self::NAME, params).await?;
762        Ok(response.into_inner())
763    }
764}
765
766pub enum F3GetF3PowerTableByInstance {}
767impl RpcMethod<1> for F3GetF3PowerTableByInstance {
768    const NAME: &'static str = "Filecoin.F3GetF3PowerTableByInstance";
769    const PARAM_NAMES: [&'static str; 1] = ["instance"];
770    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
771    const PERMISSION: Permission = Permission::Read;
772    const DESCRIPTION: &'static str =
773        "Gets the power table (committee) used to validate the specified instance";
774
775    type Params = (u64,);
776    type Ok = Vec<F3PowerEntry>;
777
778    async fn handle(
779        _ctx: Ctx,
780        (instance,): Self::Params,
781        _: &http::Extensions,
782    ) -> Result<Self::Ok, ServerError> {
783        let client = get_rpc_http_client()?;
784        let mut params = ArrayParams::new();
785        params.insert(instance)?;
786        let response: LotusJson<Self::Ok> = client.request(Self::NAME, params).await?;
787        Ok(response.into_inner())
788    }
789}
790
791pub enum F3IsRunning {}
792
793impl F3IsRunning {
794    pub async fn is_f3_running() -> anyhow::Result<bool> {
795        let client = get_rpc_http_client()?;
796        let response = client.request(Self::NAME, ArrayParams::new()).await?;
797        Ok(response)
798    }
799}
800
801impl RpcMethod<0> for F3IsRunning {
802    const NAME: &'static str = "Filecoin.F3IsRunning";
803    const PARAM_NAMES: [&'static str; 0] = [];
804    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
805    const PERMISSION: Permission = Permission::Read;
806    const DESCRIPTION: &'static str = "Returns whether the F3 subsystem is currently running.";
807
808    type Params = ();
809    type Ok = bool;
810
811    async fn handle(
812        _: Ctx,
813        (): Self::Params,
814        _: &http::Extensions,
815    ) -> Result<Self::Ok, ServerError> {
816        Ok(Self::is_f3_running().await?)
817    }
818}
819
820/// See <https://github.com/filecoin-project/lotus/blob/master/documentation/en/api-methods-v1-stable.md#F3GetProgress>
821pub enum F3GetProgress {}
822
823impl F3GetProgress {
824    async fn run() -> anyhow::Result<F3InstanceProgress> {
825        let client = get_rpc_http_client()?;
826        let response: LotusJson<F3InstanceProgress> =
827            client.request(Self::NAME, ArrayParams::new()).await?;
828        Ok(response.into_inner())
829    }
830}
831
832impl RpcMethod<0> for F3GetProgress {
833    const NAME: &'static str = "Filecoin.F3GetProgress";
834    const PARAM_NAMES: [&'static str; 0] = [];
835    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
836    const PERMISSION: Permission = Permission::Read;
837    const DESCRIPTION: &'static str =
838        "Returns the progress (instance, round, and phase) of the running F3 instance.";
839
840    type Params = ();
841    type Ok = F3InstanceProgress;
842
843    async fn handle(
844        _: Ctx,
845        (): Self::Params,
846        _: &http::Extensions,
847    ) -> Result<Self::Ok, ServerError> {
848        Ok(Self::run().await?)
849    }
850}
851
852/// See <https://github.com/filecoin-project/lotus/blob/master/documentation/en/api-methods-v1-stable.md#F3GetManifest>
853pub enum F3GetManifest {}
854
855impl F3GetManifest {
856    async fn run() -> anyhow::Result<F3Manifest> {
857        let client = get_rpc_http_client()?;
858        let response: LotusJson<F3Manifest> =
859            client.request(Self::NAME, ArrayParams::new()).await?;
860        Ok(response.into_inner())
861    }
862}
863
864impl RpcMethod<0> for F3GetManifest {
865    const NAME: &'static str = "Filecoin.F3GetManifest";
866    const PARAM_NAMES: [&'static str; 0] = [];
867    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
868    const PERMISSION: Permission = Permission::Read;
869    const DESCRIPTION: &'static str =
870        "Returns the current F3 manifest (the network's F3 configuration).";
871
872    type Params = ();
873    type Ok = F3Manifest;
874
875    async fn handle(
876        _: Ctx,
877        (): Self::Params,
878        _: &http::Extensions,
879    ) -> Result<Self::Ok, ServerError> {
880        Ok(Self::run().await?)
881    }
882}
883
884/// returns the list of miner addresses that are currently participating in F3 via this node.
885pub enum F3ListParticipants {}
886impl RpcMethod<0> for F3ListParticipants {
887    const NAME: &'static str = "Filecoin.F3ListParticipants";
888    const PARAM_NAMES: [&'static str; 0] = [];
889    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
890    const PERMISSION: Permission = Permission::Read;
891    const DESCRIPTION: &'static str =
892        "Returns the miners currently participating in F3 through this node.";
893
894    type Params = ();
895    type Ok = Vec<F3Participant>;
896
897    async fn handle(
898        _: Ctx,
899        _: Self::Params,
900        _: &http::Extensions,
901    ) -> Result<Self::Ok, ServerError> {
902        Ok(Self::run().await?)
903    }
904}
905
906impl F3ListParticipants {
907    async fn run() -> anyhow::Result<Vec<F3Participant>> {
908        let current_instance = F3GetProgress::run().await?.id;
909        Ok(F3_LEASE_MANAGER
910            .get()
911            .context("F3 lease manager is not initialized")?
912            .get_active_participants(current_instance)
913            .values()
914            .map(F3Participant::from)
915            .collect())
916    }
917}
918
919/// retrieves or renews a participation ticket necessary for a miner to engage in
920/// the F3 consensus process for the given number of instances.
921pub enum F3GetOrRenewParticipationTicket {}
922impl RpcMethod<3> for F3GetOrRenewParticipationTicket {
923    const NAME: &'static str = "Filecoin.F3GetOrRenewParticipationTicket";
924    const PARAM_NAMES: [&'static str; 3] = ["minerAddress", "previousLeaseTicket", "instances"];
925    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
926    const PERMISSION: Permission = Permission::Sign;
927    const DESCRIPTION: &'static str = "Returns a new or renewed F3 participation ticket for the given miner, valid for the requested number of instances.";
928
929    type Params = (Address, Vec<u8>, u64);
930    type Ok = Vec<u8>;
931
932    async fn handle(
933        _: Ctx,
934        (miner, previous_lease_ticket, instances): Self::Params,
935        _: &http::Extensions,
936    ) -> Result<Self::Ok, ServerError> {
937        let id = miner.id()?;
938        let previous_lease = if previous_lease_ticket.is_empty() {
939            None
940        } else {
941            Some(
942                fvm_ipld_encoding::from_slice::<F3ParticipationLease>(&previous_lease_ticket)
943                    .context("the previous lease ticket is invalid")?,
944            )
945        };
946        let lease = F3_LEASE_MANAGER
947            .get()
948            .context("F3 lease manager is not initialized")?
949            .get_or_renew_participation_lease(id, previous_lease, instances)
950            .await?;
951        Ok(fvm_ipld_encoding::to_vec(&lease)?)
952    }
953}
954
955/// enrolls a storage provider in the F3 consensus process using a
956/// provided participation ticket. This ticket grants a temporary lease that enables
957/// the provider to sign transactions as part of the F3 consensus.
958pub enum F3Participate {}
959impl RpcMethod<1> for F3Participate {
960    const NAME: &'static str = "Filecoin.F3Participate";
961    const PARAM_NAMES: [&'static str; 1] = ["leaseTicket"];
962    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
963    const PERMISSION: Permission = Permission::Sign;
964    const DESCRIPTION: &'static str = "Enrolls a miner in F3 consensus using the given participation ticket, granting a temporary signing lease.";
965
966    type Params = (Vec<u8>,);
967    type Ok = F3ParticipationLease;
968
969    async fn handle(
970        _: Ctx,
971        (lease_ticket,): Self::Params,
972        _: &http::Extensions,
973    ) -> Result<Self::Ok, ServerError> {
974        let lease: F3ParticipationLease =
975            fvm_ipld_encoding::from_slice(&lease_ticket).context("invalid lease ticket")?;
976        let current_instance = F3GetProgress::run().await?.id;
977        F3_LEASE_MANAGER
978            .get()
979            .context("F3 lease manager is not initialized")?
980            .participate(&lease, current_instance)?;
981        Ok(lease)
982    }
983}
984
985pub fn get_f3_rpc_endpoint() -> Cow<'static, str> {
986    if let Ok(host) = std::env::var("FOREST_F3_SIDECAR_RPC_ENDPOINT") {
987        Cow::Owned(host)
988    } else {
989        Cow::Borrowed("127.0.0.1:23456")
990    }
991}
992
993pub fn get_rpc_http_client() -> anyhow::Result<jsonrpsee::http_client::HttpClient> {
994    let client = jsonrpsee::http_client::HttpClientBuilder::new()
995        .build(format!("http://{}", get_f3_rpc_endpoint()))?;
996    Ok(client)
997}