1mod 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 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 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 return Ok(());
219 }
220 let miner_info = miner_state.info(&db).map_err(map_err)?;
221 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 return Ok(());
268 }
269 let miner_info = miner_state.info(&db).map_err(map_err)?;
270 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 return Ok(());
311 }
312 let miner_info = miner_state.info(&db).map_err(map_err)?;
313 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 return Ok(());
351 }
352 let miner_info = miner_state.info(&db).map_err(map_err)?;
353 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 return Ok(());
391 }
392 let miner_info = miner_state.info(&db).map_err(map_err)?;
393 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 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 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 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
662pub 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
687pub enum F3GetLatestCertificate {}
689
690impl F3GetLatestCertificate {
691 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
823pub 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
855pub 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
887pub 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
922pub 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
958pub 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}