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