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