forest/rpc/methods/
f3.rs

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