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 let state_tree = ctx.state_manager.get_state_tree(ts.parent_state())?;
193
194 macro_rules! handle_miner_state_v12_on {
195 ($version:tt, $id_power_worker_mappings:ident, $ts:expr, $state:expr, $policy:expr) => {
196 fn map_err<E: Display>(e: E) -> fil_actors_shared::$version::ActorError {
197 fil_actors_shared::$version::ActorError::unspecified(e.to_string())
198 }
199
200 let claims = $state.load_claims(&db)?;
201 claims.for_each(|miner, claim| {
202 if !claim.quality_adj_power.is_positive() {
203 return Ok(());
204 }
205
206 let id = miner.id().map_err(map_err)?;
207 let (_, ok) =
208 $state.miner_nominal_power_meets_consensus_minimum($policy, &db, id)?;
209 if !ok {
210 return Ok(());
211 }
212 let power = claim.quality_adj_power.clone();
213 let miner_state: miner::State = state_tree
214 .get_actor_state_from_address(&miner.into())
215 .map_err(map_err)?;
216 let debt = miner_state.fee_debt();
217 if !debt.is_zero() {
218 return Ok(());
220 }
221 let miner_info = miner_state.info(&db).map_err(map_err)?;
222 if $ts.epoch() <= miner_info.consensus_fault_elapsed {
224 return Ok(());
225 }
226 $id_power_worker_mappings.push((id, power, miner_info.worker.into()));
227 Ok(())
228 })?;
229 };
230 }
231
232 let state: power::State = state_tree.get_actor_state()?;
233 let mut id_power_worker_mappings = vec![];
234 let policy = &ctx.chain_config().policy;
235 match &state {
236 power::State::V8(s) => {
237 fn map_err<E: Display>(e: E) -> fil_actors_shared::v8::ActorError {
238 fil_actors_shared::v8::ActorError::unspecified(e.to_string())
239 }
240
241 let claims = fil_actors_shared::v8::make_map_with_root::<
242 _,
243 fil_actor_power_state::v8::Claim,
244 >(&s.claims, &db)?;
245 claims.for_each(|key, claim| {
246 let miner = Address::from_bytes(key)?;
247 if !claim.quality_adj_power.is_positive() {
248 return Ok(());
249 }
250
251 let id = miner.id().map_err(map_err)?;
252 let ok = s.miner_nominal_power_meets_consensus_minimum(
253 &policy.into(),
254 &db,
255 &miner.into(),
256 )?;
257 if !ok {
258 return Ok(());
259 }
260 let power = claim.quality_adj_power.clone();
261 let miner_state: miner::State = state_tree
262 .get_actor_state_from_address(&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 = state_tree
304 .get_actor_state_from_address(&miner)
305 .map_err(map_err)?;
306 let debt = miner_state.fee_debt();
307 if !debt.is_zero() {
308 return Ok(());
310 }
311 let miner_info = miner_state.info(&db).map_err(map_err)?;
312 if ts.epoch() <= miner_info.consensus_fault_elapsed {
314 return Ok(());
315 }
316 id_power_worker_mappings.push((id, power, miner_info.worker));
317 Ok(())
318 })?;
319 }
320 power::State::V10(s) => {
321 fn map_err<E: Display>(e: E) -> fil_actors_shared::v10::ActorError {
322 fil_actors_shared::v10::ActorError::unspecified(e.to_string())
323 }
324
325 let claims = fil_actors_shared::v10::make_map_with_root::<
326 _,
327 fil_actor_power_state::v10::Claim,
328 >(&s.claims, &db)?;
329 claims.for_each(|key, claim| {
330 let miner = Address::from_bytes(key)?;
331 if !claim.quality_adj_power.is_positive() {
332 return Ok(());
333 }
334
335 let id = miner.id().map_err(map_err)?;
336 let (_, ok) =
337 s.miner_nominal_power_meets_consensus_minimum(&policy.into(), &db, id)?;
338 if !ok {
339 return Ok(());
340 }
341 let power = claim.quality_adj_power.clone();
342 let miner_state: miner::State = state_tree
343 .get_actor_state_from_address(&miner)
344 .map_err(map_err)?;
345 let debt = miner_state.fee_debt();
346 if !debt.is_zero() {
347 return Ok(());
349 }
350 let miner_info = miner_state.info(&db).map_err(map_err)?;
351 if ts.epoch() <= miner_info.consensus_fault_elapsed {
353 return Ok(());
354 }
355 id_power_worker_mappings.push((id, power, miner_info.worker));
356 Ok(())
357 })?;
358 }
359 power::State::V11(s) => {
360 fn map_err<E: Display>(e: E) -> fil_actors_shared::v11::ActorError {
361 fil_actors_shared::v11::ActorError::unspecified(e.to_string())
362 }
363
364 let claims = fil_actors_shared::v11::make_map_with_root::<
365 _,
366 fil_actor_power_state::v11::Claim,
367 >(&s.claims, &db)?;
368 claims.for_each(|key, claim| {
369 let miner = Address::from_bytes(key)?;
370 if !claim.quality_adj_power.is_positive() {
371 return Ok(());
372 }
373
374 let id = miner.id().map_err(map_err)?;
375 let (_, ok) =
376 s.miner_nominal_power_meets_consensus_minimum(&policy.into(), &db, id)?;
377 if !ok {
378 return Ok(());
379 }
380 let power = claim.quality_adj_power.clone();
381 let miner_state: miner::State = state_tree
382 .get_actor_state_from_address(&miner)
383 .map_err(map_err)?;
384 let debt = miner_state.fee_debt();
385 if !debt.is_zero() {
386 return Ok(());
388 }
389 let miner_info = miner_state.info(&db).map_err(map_err)?;
390 if ts.epoch() <= miner_info.consensus_fault_elapsed {
392 return Ok(());
393 }
394 id_power_worker_mappings.push((id, power, miner_info.worker));
395 Ok(())
396 })?;
397 }
398 power::State::V12(s) => {
399 handle_miner_state_v12_on!(v12, id_power_worker_mappings, &ts, s, &policy.into());
400 }
401 power::State::V13(s) => {
402 handle_miner_state_v12_on!(v13, id_power_worker_mappings, &ts, s, &policy.into());
403 }
404 power::State::V14(s) => {
405 handle_miner_state_v12_on!(v14, id_power_worker_mappings, &ts, s, &policy.into());
406 }
407 power::State::V15(s) => {
408 handle_miner_state_v12_on!(v15, id_power_worker_mappings, &ts, s, &policy.into());
409 }
410 power::State::V16(s) => {
411 handle_miner_state_v12_on!(v16, id_power_worker_mappings, &ts, s, &policy.into());
412 }
413 power::State::V17(s) => {
414 handle_miner_state_v12_on!(v17, id_power_worker_mappings, &ts, s, &policy.into());
415 }
416 power::State::V18(s) => {
417 handle_miner_state_v12_on!(v18, id_power_worker_mappings, &ts, s, &policy.into());
418 }
419 }
420 let mut power_entries = vec![];
421 for (id, power, worker) in id_power_worker_mappings {
422 let waddr = ctx
423 .state_manager
424 .resolve_to_deterministic_address(worker, ts)
425 .await?;
426 if waddr.protocol() != Protocol::BLS {
427 anyhow::bail!("wrong type of worker address");
428 }
429 let pub_key = waddr.payload_bytes();
430 power_entries.push(F3PowerEntry { id, power, pub_key });
431 }
432 power_entries.sort();
433
434 if let Some(stats) = db.stats() {
435 tracing::debug!(epoch=%ts.epoch(), hit=%stats.hit(), miss=%stats.miss(),cache_len=%BLOCKSTORE_CACHE.len(), "F3.GetPowerTable blockstore read cache");
436 }
437
438 Ok(power_entries)
439 }
440}
441
442impl RpcMethod<1> for GetPowerTable {
443 const NAME: &'static str = "F3.GetPowerTable";
444 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
445 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
446 const PERMISSION: Permission = Permission::Read;
447 const DESCRIPTION: &'static str =
448 "Returns the power table (the participating miners and their power) at the given tipset.";
449
450 type Params = (F3TipSetKey,);
451 type Ok = Vec<F3PowerEntry>;
452
453 async fn handle(
454 ctx: Ctx,
455 (f3_tsk,): Self::Params,
456 _: &http::Extensions,
457 ) -> Result<Self::Ok, ServerError> {
458 let tsk = f3_tsk.try_into()?;
459 let start = std::time::Instant::now();
460 let ts = ctx.chain_index().load_required_tipset(&tsk)?;
461 let power_entries = Self::compute(&ctx, &ts).await?;
462 tracing::debug!(epoch=%ts.epoch(), %tsk, "F3.GetPowerTable, took {}", humantime::format_duration(start.elapsed()));
463 Ok(power_entries)
464 }
465}
466
467pub enum ProtectPeer {}
468impl RpcMethod<1> for ProtectPeer {
469 const NAME: &'static str = "F3.ProtectPeer";
470 const PARAM_NAMES: [&'static str; 1] = ["peerId"];
471 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
472 const PERMISSION: Permission = Permission::Read;
473 const DESCRIPTION: &'static str = "Protects the given peer from connection pruning.";
474
475 type Params = (String,);
476 type Ok = bool;
477
478 async fn handle(
479 ctx: Ctx,
480 (peer_id,): Self::Params,
481 _: &http::Extensions,
482 ) -> Result<Self::Ok, ServerError> {
483 let peer_id = PeerId::from_str(&peer_id)?;
484 let (tx, rx) = flume::bounded(1);
485 ctx.network_send()
486 .send_async(NetworkMessage::JSONRPCRequest {
487 method: NetRPCMethods::ProtectPeer(tx, std::iter::once(peer_id).collect()),
488 })
489 .await?;
490 rx.recv_async().await?;
491 Ok(true)
492 }
493}
494
495pub enum GetParticipatingMinerIDs {}
496
497impl RpcMethod<0> for GetParticipatingMinerIDs {
498 const NAME: &'static str = "F3.GetParticipatingMinerIDs";
499 const PARAM_NAMES: [&'static str; 0] = [];
500 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
501 const PERMISSION: Permission = Permission::Read;
502 const DESCRIPTION: &'static str =
503 "Returns the IDs of the miners currently participating in F3 through this node.";
504
505 type Params = ();
506 type Ok = Vec<u64>;
507
508 async fn handle(
509 _: Ctx,
510 _: Self::Params,
511 _: &http::Extensions,
512 ) -> Result<Self::Ok, ServerError> {
513 let participants = F3ListParticipants::run().await?;
514 let mut ids: HashSet<u64> = participants.into_iter().map(|p| p.miner_id).collect();
515 if let Some(permanent_miner_ids) = F3_PERMANENT_PARTICIPATING_MINER_IDS.as_ref() {
516 ids.extend(permanent_miner_ids.iter().copied());
517 }
518 Ok(ids.into_iter().collect())
519 }
520}
521
522pub enum Finalize {}
523impl RpcMethod<1> for Finalize {
524 const NAME: &'static str = "F3.Finalize";
525 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
526 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
527 const PERMISSION: Permission = Permission::Write;
528 const DESCRIPTION: &'static str =
529 "Marks the given tipset as F3-finalized, resetting the chain head to it when appropriate.";
530
531 type Params = (F3TipSetKey,);
532 type Ok = ();
533
534 async fn handle(
535 ctx: Ctx,
536 (f3_tsk,): Self::Params,
537 _: &http::Extensions,
538 ) -> Result<Self::Ok, ServerError> {
539 static ENV_ENABLED: LazyLock<Option<bool>> =
541 LazyLock::new(|| is_env_set_and_truthy("FOREST_F3_CONSENSUS_ENABLED"));
542 let enabled = ENV_ENABLED.unwrap_or(ctx.chain_config().f3_consensus);
543 if !enabled {
544 return Ok(());
545 }
546
547 let tsk = f3_tsk.try_into()?;
548 let finalized_ts = match ctx.chain_index().load_tipset(&tsk)? {
549 Some(ts) => ts,
550 None => ctx
551 .sync_network_context
552 .chain_exchange_headers(None, &tsk, nonzero!(1_u64))
553 .await?
554 .first()
555 .map(ShallowClone::shallow_clone)
556 .with_context(|| format!("failed to get tipset via chain exchange. tsk: {tsk}"))?,
557 };
558 let head = ctx.chain_store().heaviest_tipset();
559 if head.epoch() >= finalized_ts.epoch()
564 && head.epoch() <= finalized_ts.epoch() + ctx.chain_config().policy.chain_finality
565 {
566 tracing::debug!(
567 "F3 finalized tsk {} at epoch {}",
568 finalized_ts.key(),
569 finalized_ts.epoch()
570 );
571 if !head
572 .chain(ctx.db())
573 .take_while(|ts| ts.epoch() >= finalized_ts.epoch())
574 .any(|ts| ts == finalized_ts)
575 {
576 tracing::info!(
577 "F3 reset chain head to tsk {} at epoch {}",
578 finalized_ts.key(),
579 finalized_ts.epoch()
580 );
581 let fts = ctx
582 .sync_network_context
583 .chain_exchange_full_tipset(None, &tsk)
584 .await?;
585 fts.persist(ctx.db())?;
586 let validator = TipsetValidator(&fts);
587 validator.validate(
588 ctx.chain_store(),
589 None,
590 &ctx.chain_store().genesis_tipset(),
591 ctx.chain_config().block_delay_secs,
592 )?;
593 ctx.chain_store()
594 .set_heaviest_tipset(finalized_ts.shallow_clone())?;
595 }
596 ctx.chain_store().set_f3_finalized_tipset(finalized_ts);
597 }
598 Ok(())
599 }
600}
601
602pub enum SignMessage {}
603impl RpcMethod<2> for SignMessage {
604 const NAME: &'static str = "F3.SignMessage";
605 const PARAM_NAMES: [&'static str; 2] = ["pubkey", "message"];
606 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
607 const PERMISSION: Permission = Permission::Sign;
608 const DESCRIPTION: &'static str =
609 "Signs a message with the private key corresponding to the given BLS public key.";
610
611 type Params = (Vec<u8>, Vec<u8>);
612 type Ok = Signature;
613
614 async fn handle(
615 ctx: Ctx,
616 (pubkey, message): Self::Params,
617 ext: &http::Extensions,
618 ) -> Result<Self::Ok, ServerError> {
619 let addr = Address::new_bls(&pubkey)?;
620 WalletSign::handle(ctx, (addr, message), ext).await
622 }
623}
624
625pub enum F3ExportLatestSnapshot {}
626
627impl F3ExportLatestSnapshot {
628 pub async fn run(path: String) -> anyhow::Result<Cid> {
629 let client = get_rpc_http_client()?;
630 let mut params = ArrayParams::new();
631 params.insert(path)?;
632 let LotusJson(cid): LotusJson<Cid> = client
633 .request("Filecoin.F3ExportLatestSnapshot", params)
634 .await?;
635 Ok(cid)
636 }
637}
638
639impl RpcMethod<1> for F3ExportLatestSnapshot {
640 const NAME: &'static str = "F3.ExportLatestSnapshot";
641 const PARAM_NAMES: [&'static str; 1] = ["path"];
642 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
643 const PERMISSION: Permission = Permission::Read;
644 const DESCRIPTION: &'static str =
645 "Exports the latest F3 snapshot to the specified path and returns its CID";
646
647 type Params = (String,);
648 type Ok = Cid;
649
650 async fn handle(
651 _ctx: Ctx,
652 (path,): Self::Params,
653 _: &http::Extensions,
654 ) -> Result<Self::Ok, ServerError> {
655 Ok(Self::run(path).await?)
656 }
657}
658
659pub enum F3GetCertificate {}
661impl RpcMethod<1> for F3GetCertificate {
662 const NAME: &'static str = "Filecoin.F3GetCertificate";
663 const PARAM_NAMES: [&'static str; 1] = ["instance"];
664 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
665 const PERMISSION: Permission = Permission::Read;
666 const DESCRIPTION: &'static str = "Returns the finality certificate for the given F3 instance.";
667
668 type Params = (u64,);
669 type Ok = FinalityCertificate;
670
671 async fn handle(
672 _: Ctx,
673 (instance,): Self::Params,
674 _: &http::Extensions,
675 ) -> Result<Self::Ok, ServerError> {
676 let client = get_rpc_http_client()?;
677 let mut params = ArrayParams::new();
678 params.insert(instance)?;
679 let response: LotusJson<Self::Ok> = client.request(Self::NAME, params).await?;
680 Ok(response.into_inner())
681 }
682}
683
684pub enum F3GetLatestCertificate {}
686
687impl F3GetLatestCertificate {
688 pub async fn get() -> anyhow::Result<FinalityCertificate> {
690 let client = get_rpc_http_client()?;
691 let response: LotusJson<FinalityCertificate> = client
692 .request(<Self as RpcMethod<0>>::NAME, ArrayParams::new())
693 .await?;
694 Ok(response.into_inner())
695 }
696}
697
698impl RpcMethod<0> for F3GetLatestCertificate {
699 const NAME: &'static str = "Filecoin.F3GetLatestCertificate";
700 const PARAM_NAMES: [&'static str; 0] = [];
701 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
702 const PERMISSION: Permission = Permission::Read;
703 const DESCRIPTION: &'static str = "Returns the latest F3 finality certificate.";
704
705 type Params = ();
706 type Ok = FinalityCertificate;
707
708 async fn handle(
709 _: Ctx,
710 _: Self::Params,
711 _: &http::Extensions,
712 ) -> Result<Self::Ok, ServerError> {
713 Ok(Self::get().await?)
714 }
715}
716
717pub enum F3GetECPowerTable {}
718impl RpcMethod<1> for F3GetECPowerTable {
719 const NAME: &'static str = "Filecoin.F3GetECPowerTable";
720 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
721 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
722 const PERMISSION: Permission = Permission::Read;
723 const DESCRIPTION: &'static str = "Returns the Expected Consensus power table at the given tipset (defaults to the chain head).";
724
725 type Params = (ApiTipsetKey,);
726 type Ok = Vec<F3PowerEntry>;
727
728 async fn handle(
729 ctx: Ctx,
730 (ApiTipsetKey(tsk_opt),): Self::Params,
731 ext: &http::Extensions,
732 ) -> Result<Self::Ok, ServerError> {
733 let tsk = tsk_opt.unwrap_or_else(|| ctx.chain_store().heaviest_tipset().key().clone());
734 GetPowerTable::handle(ctx, (tsk.into(),), ext).await
735 }
736}
737
738pub enum F3GetF3PowerTable {}
739impl RpcMethod<1> for F3GetF3PowerTable {
740 const NAME: &'static str = "Filecoin.F3GetF3PowerTable";
741 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
742 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
743 const PERMISSION: Permission = Permission::Read;
744 const DESCRIPTION: &'static str =
745 "Returns F3's power table at the given tipset (defaults to the chain head).";
746
747 type Params = (ApiTipsetKey,);
748 type Ok = Vec<F3PowerEntry>;
749
750 async fn handle(
751 ctx: Ctx,
752 (ApiTipsetKey(tsk_opt),): Self::Params,
753 _: &http::Extensions,
754 ) -> Result<Self::Ok, ServerError> {
755 let tsk: F3TipSetKey = tsk_opt
756 .unwrap_or_else(|| ctx.chain_store().heaviest_tipset().key().clone())
757 .into();
758 let client = get_rpc_http_client()?;
759 let mut params = ArrayParams::new();
760 params.insert(tsk.into_lotus_json())?;
761 let response: LotusJson<Self::Ok> = client.request(Self::NAME, params).await?;
762 Ok(response.into_inner())
763 }
764}
765
766pub enum F3GetF3PowerTableByInstance {}
767impl RpcMethod<1> for F3GetF3PowerTableByInstance {
768 const NAME: &'static str = "Filecoin.F3GetF3PowerTableByInstance";
769 const PARAM_NAMES: [&'static str; 1] = ["instance"];
770 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
771 const PERMISSION: Permission = Permission::Read;
772 const DESCRIPTION: &'static str =
773 "Gets the power table (committee) used to validate the specified instance";
774
775 type Params = (u64,);
776 type Ok = Vec<F3PowerEntry>;
777
778 async fn handle(
779 _ctx: Ctx,
780 (instance,): Self::Params,
781 _: &http::Extensions,
782 ) -> Result<Self::Ok, ServerError> {
783 let client = get_rpc_http_client()?;
784 let mut params = ArrayParams::new();
785 params.insert(instance)?;
786 let response: LotusJson<Self::Ok> = client.request(Self::NAME, params).await?;
787 Ok(response.into_inner())
788 }
789}
790
791pub enum F3IsRunning {}
792
793impl F3IsRunning {
794 pub async fn is_f3_running() -> anyhow::Result<bool> {
795 let client = get_rpc_http_client()?;
796 let response = client.request(Self::NAME, ArrayParams::new()).await?;
797 Ok(response)
798 }
799}
800
801impl RpcMethod<0> for F3IsRunning {
802 const NAME: &'static str = "Filecoin.F3IsRunning";
803 const PARAM_NAMES: [&'static str; 0] = [];
804 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
805 const PERMISSION: Permission = Permission::Read;
806 const DESCRIPTION: &'static str = "Returns whether the F3 subsystem is currently running.";
807
808 type Params = ();
809 type Ok = bool;
810
811 async fn handle(
812 _: Ctx,
813 (): Self::Params,
814 _: &http::Extensions,
815 ) -> Result<Self::Ok, ServerError> {
816 Ok(Self::is_f3_running().await?)
817 }
818}
819
820pub enum F3GetProgress {}
822
823impl F3GetProgress {
824 async fn run() -> anyhow::Result<F3InstanceProgress> {
825 let client = get_rpc_http_client()?;
826 let response: LotusJson<F3InstanceProgress> =
827 client.request(Self::NAME, ArrayParams::new()).await?;
828 Ok(response.into_inner())
829 }
830}
831
832impl RpcMethod<0> for F3GetProgress {
833 const NAME: &'static str = "Filecoin.F3GetProgress";
834 const PARAM_NAMES: [&'static str; 0] = [];
835 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
836 const PERMISSION: Permission = Permission::Read;
837 const DESCRIPTION: &'static str =
838 "Returns the progress (instance, round, and phase) of the running F3 instance.";
839
840 type Params = ();
841 type Ok = F3InstanceProgress;
842
843 async fn handle(
844 _: Ctx,
845 (): Self::Params,
846 _: &http::Extensions,
847 ) -> Result<Self::Ok, ServerError> {
848 Ok(Self::run().await?)
849 }
850}
851
852pub enum F3GetManifest {}
854
855impl F3GetManifest {
856 async fn run() -> anyhow::Result<F3Manifest> {
857 let client = get_rpc_http_client()?;
858 let response: LotusJson<F3Manifest> =
859 client.request(Self::NAME, ArrayParams::new()).await?;
860 Ok(response.into_inner())
861 }
862}
863
864impl RpcMethod<0> for F3GetManifest {
865 const NAME: &'static str = "Filecoin.F3GetManifest";
866 const PARAM_NAMES: [&'static str; 0] = [];
867 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
868 const PERMISSION: Permission = Permission::Read;
869 const DESCRIPTION: &'static str =
870 "Returns the current F3 manifest (the network's F3 configuration).";
871
872 type Params = ();
873 type Ok = F3Manifest;
874
875 async fn handle(
876 _: Ctx,
877 (): Self::Params,
878 _: &http::Extensions,
879 ) -> Result<Self::Ok, ServerError> {
880 Ok(Self::run().await?)
881 }
882}
883
884pub enum F3ListParticipants {}
886impl RpcMethod<0> for F3ListParticipants {
887 const NAME: &'static str = "Filecoin.F3ListParticipants";
888 const PARAM_NAMES: [&'static str; 0] = [];
889 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
890 const PERMISSION: Permission = Permission::Read;
891 const DESCRIPTION: &'static str =
892 "Returns the miners currently participating in F3 through this node.";
893
894 type Params = ();
895 type Ok = Vec<F3Participant>;
896
897 async fn handle(
898 _: Ctx,
899 _: Self::Params,
900 _: &http::Extensions,
901 ) -> Result<Self::Ok, ServerError> {
902 Ok(Self::run().await?)
903 }
904}
905
906impl F3ListParticipants {
907 async fn run() -> anyhow::Result<Vec<F3Participant>> {
908 let current_instance = F3GetProgress::run().await?.id;
909 Ok(F3_LEASE_MANAGER
910 .get()
911 .context("F3 lease manager is not initialized")?
912 .get_active_participants(current_instance)
913 .values()
914 .map(F3Participant::from)
915 .collect())
916 }
917}
918
919pub enum F3GetOrRenewParticipationTicket {}
922impl RpcMethod<3> for F3GetOrRenewParticipationTicket {
923 const NAME: &'static str = "Filecoin.F3GetOrRenewParticipationTicket";
924 const PARAM_NAMES: [&'static str; 3] = ["minerAddress", "previousLeaseTicket", "instances"];
925 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
926 const PERMISSION: Permission = Permission::Sign;
927 const DESCRIPTION: &'static str = "Returns a new or renewed F3 participation ticket for the given miner, valid for the requested number of instances.";
928
929 type Params = (Address, Vec<u8>, u64);
930 type Ok = Vec<u8>;
931
932 async fn handle(
933 _: Ctx,
934 (miner, previous_lease_ticket, instances): Self::Params,
935 _: &http::Extensions,
936 ) -> Result<Self::Ok, ServerError> {
937 let id = miner.id()?;
938 let previous_lease = if previous_lease_ticket.is_empty() {
939 None
940 } else {
941 Some(
942 fvm_ipld_encoding::from_slice::<F3ParticipationLease>(&previous_lease_ticket)
943 .context("the previous lease ticket is invalid")?,
944 )
945 };
946 let lease = F3_LEASE_MANAGER
947 .get()
948 .context("F3 lease manager is not initialized")?
949 .get_or_renew_participation_lease(id, previous_lease, instances)
950 .await?;
951 Ok(fvm_ipld_encoding::to_vec(&lease)?)
952 }
953}
954
955pub enum F3Participate {}
959impl RpcMethod<1> for F3Participate {
960 const NAME: &'static str = "Filecoin.F3Participate";
961 const PARAM_NAMES: [&'static str; 1] = ["leaseTicket"];
962 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
963 const PERMISSION: Permission = Permission::Sign;
964 const DESCRIPTION: &'static str = "Enrolls a miner in F3 consensus using the given participation ticket, granting a temporary signing lease.";
965
966 type Params = (Vec<u8>,);
967 type Ok = F3ParticipationLease;
968
969 async fn handle(
970 _: Ctx,
971 (lease_ticket,): Self::Params,
972 _: &http::Extensions,
973 ) -> Result<Self::Ok, ServerError> {
974 let lease: F3ParticipationLease =
975 fvm_ipld_encoding::from_slice(&lease_ticket).context("invalid lease ticket")?;
976 let current_instance = F3GetProgress::run().await?.id;
977 F3_LEASE_MANAGER
978 .get()
979 .context("F3 lease manager is not initialized")?
980 .participate(&lease, current_instance)?;
981 Ok(lease)
982 }
983}
984
985pub fn get_f3_rpc_endpoint() -> Cow<'static, str> {
986 if let Ok(host) = std::env::var("FOREST_F3_SIDECAR_RPC_ENDPOINT") {
987 Cow::Owned(host)
988 } else {
989 Cow::Borrowed("127.0.0.1:23456")
990 }
991}
992
993pub fn get_rpc_http_client() -> anyhow::Result<jsonrpsee::http_client::HttpClient> {
994 let client = jsonrpsee::http_client::HttpClientBuilder::new()
995 .build(format!("http://{}", get_f3_rpc_endpoint()))?;
996 Ok(client)
997}