Skip to main content

forest/rpc/methods/
f3.rs

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