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