Skip to main content

subspace_farmer/single_disk_farm/
farming.rs

1//! Farming-related utilities
2//!
3//! These utilities do not expose the whole farming workflow, but rather small bits of it that can
4//! be useful externally (for example for benchmarking purposes in CLI).
5
6pub mod rayon_files;
7
8use crate::farm::{
9    AuditingDetails, FarmingError, FarmingNotification, ProvingDetails, ProvingResult,
10};
11use crate::node_client::NodeClient;
12use crate::single_disk_farm::Handlers;
13use crate::single_disk_farm::metrics::SingleDiskFarmMetrics;
14use async_lock::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
15use futures::StreamExt;
16use futures::channel::mpsc;
17use rayon::ThreadPool;
18use std::collections::HashSet;
19use std::sync::Arc;
20use std::time::Instant;
21use subspace_core_primitives::PublicKey;
22use subspace_core_primitives::pieces::Record;
23use subspace_core_primitives::pos::PosSeed;
24use subspace_core_primitives::sectors::SectorIndex;
25use subspace_core_primitives::segments::{HistorySize, SegmentIndex};
26use subspace_core_primitives::solutions::{Solution, SolutionRange};
27use subspace_erasure_coding::ErasureCoding;
28use subspace_farmer_components::ReadAtSync;
29use subspace_farmer_components::auditing::{AuditingError, audit_plot_sync};
30use subspace_farmer_components::proving::{ProvableSolutions, ProvingError};
31use subspace_farmer_components::reading::ReadSectorRecordChunksMode;
32use subspace_farmer_components::sector::{SectorMetadata, SectorMetadataChecksummed};
33use subspace_kzg::Kzg;
34use subspace_proof_of_space::{Table, TableGenerator};
35use subspace_rpc_primitives::{SlotInfo, SolutionResponse};
36use tracing::{Span, debug, error, info, trace, warn};
37
38/// How many non-fatal errors should happen in a row before farm is considered non-operational
39const NON_FATAL_ERROR_LIMIT: usize = 10;
40
41pub(super) async fn slot_notification_forwarder<NC>(
42    node_client: &NC,
43    mut slot_info_forwarder_sender: mpsc::Sender<SlotInfo>,
44    metrics: Option<Arc<SingleDiskFarmMetrics>>,
45) -> Result<(), FarmingError>
46where
47    NC: NodeClient,
48{
49    info!("Subscribing to slot info notifications");
50
51    let mut slot_info_notifications = node_client
52        .subscribe_slot_info()
53        .await
54        .map_err(|error| FarmingError::FailedToSubscribeSlotInfo { error })?;
55
56    while let Some(slot_info) = slot_info_notifications.next().await {
57        debug!(?slot_info, "New slot");
58
59        let slot = slot_info.slot_number;
60
61        // Error means farmer is still solving for previous slot, which is too late, and we need to
62        // skip this slot
63        if slot_info_forwarder_sender.try_send(slot_info).is_err() {
64            if let Some(metrics) = &metrics {
65                metrics.skipped_slots.inc();
66            }
67            debug!(%slot, "Slow farming, skipping slot");
68        }
69    }
70
71    Err(FarmingError::SlotNotificationStreamEnded)
72}
73
74/// Plot audit options
75#[derive(Debug, Clone, Copy)]
76pub struct PlotAuditOptions<'a, 'b> {
77    /// Public key of the farm
78    pub public_key: &'a PublicKey,
79    /// Reward address to use for solutions
80    pub reward_address: &'a PublicKey,
81    /// Slot info for the audit
82    pub slot_info: SlotInfo,
83    /// Metadata of all sectors plotted so far
84    pub sectors_metadata: &'a [SectorMetadataChecksummed],
85    /// Kzg instance
86    pub kzg: &'a Kzg,
87    /// Erasure coding instance
88    pub erasure_coding: &'a ErasureCoding,
89    /// Optional sector that is currently being modified (for example replotted) and should not be
90    /// audited
91    pub sectors_being_modified: &'b HashSet<SectorIndex>,
92    /// Mode of reading chunks during proving
93    pub read_sector_record_chunks_mode: ReadSectorRecordChunksMode,
94    /// Proof-of-space cutover: sectors with a history size above this use the new implementation,
95    /// `None` on a farm with no pre-cutover sectors.
96    pub cutover: Option<HistorySize>,
97}
98
99/// Plot auditing implementation
100#[derive(Debug)]
101pub struct PlotAudit<Plot>(Plot)
102where
103    Plot: ReadAtSync;
104
105impl<'a, Plot> PlotAudit<Plot>
106where
107    Plot: ReadAtSync + 'a,
108{
109    /// Create new instance
110    pub fn new(plot: Plot) -> Self {
111        Self(plot)
112    }
113
114    /// Audit this plot
115    #[allow(clippy::type_complexity)]
116    pub fn audit<'b, PosTable>(
117        &'a self,
118        options: PlotAuditOptions<'a, 'b>,
119    ) -> Result<
120        Vec<(
121            SectorIndex,
122            impl ProvableSolutions<Item = Result<Solution<PublicKey>, ProvingError>>
123            + use<'a, PosTable, Plot>,
124        )>,
125        AuditingError,
126    >
127    where
128        PosTable: Table,
129    {
130        let PlotAuditOptions {
131            public_key,
132            reward_address,
133            slot_info,
134            sectors_metadata,
135            kzg,
136            erasure_coding,
137            sectors_being_modified,
138            read_sector_record_chunks_mode: mode,
139            cutover,
140        } = options;
141
142        let audit_results = audit_plot_sync(
143            public_key,
144            &slot_info.global_challenge,
145            slot_info.voting_solution_range,
146            &self.0,
147            sectors_metadata,
148            sectors_being_modified,
149        )?;
150
151        Ok(audit_results
152            .into_iter()
153            .filter_map(|audit_results| {
154                let sector_index = audit_results.sector_index;
155
156                let solution_candidates = audit_results.solution_candidates;
157                let is_post_cutover =
158                    super::is_post_cutover(cutover, solution_candidates.history_size());
159                let table_generator = PosTable::generator_for(is_post_cutover);
160
161                let sector_solutions = solution_candidates.into_solutions(
162                    reward_address,
163                    kzg,
164                    erasure_coding,
165                    mode,
166                    move |seed: &PosSeed| table_generator.generate_parallel(seed),
167                );
168
169                let sector_solutions = match sector_solutions {
170                    Ok(solutions) => solutions,
171                    Err(error) => {
172                        warn!(
173                            %error,
174                            %sector_index,
175                            "Failed to turn solution candidates into solutions",
176                        );
177
178                        return None;
179                    }
180                };
181
182                if sector_solutions.len() == 0 {
183                    return None;
184                }
185
186                Some((sector_index, sector_solutions))
187            })
188            .collect())
189    }
190}
191
192pub(super) struct FarmingOptions<NC, PlotAudit> {
193    pub(super) public_key: PublicKey,
194    pub(super) reward_address: PublicKey,
195    pub(super) node_client: NC,
196    pub(super) plot_audit: PlotAudit,
197    pub(super) sectors_metadata: Arc<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
198    pub(super) kzg: Kzg,
199    pub(super) erasure_coding: ErasureCoding,
200    pub(super) handlers: Arc<Handlers>,
201    pub(super) sectors_being_modified: Arc<AsyncRwLock<HashSet<SectorIndex>>>,
202    pub(super) slot_info_notifications: mpsc::Receiver<SlotInfo>,
203    pub(super) thread_pool: ThreadPool,
204    pub(super) read_sector_record_chunks_mode: ReadSectorRecordChunksMode,
205    pub(super) global_mutex: Arc<AsyncMutex<()>>,
206    pub(super) metrics: Option<Arc<SingleDiskFarmMetrics>>,
207    pub(super) cutover: Option<HistorySize>,
208}
209
210/// Starts farming process.
211///
212/// NOTE: Returned future is async, but does blocking operations and should be running in dedicated
213/// thread.
214pub(super) async fn farming<'a, PosTable, NC, Plot>(
215    farming_options: FarmingOptions<NC, PlotAudit<Plot>>,
216) -> Result<(), FarmingError>
217where
218    PosTable: Table,
219    NC: NodeClient,
220    Plot: ReadAtSync + 'a,
221{
222    let FarmingOptions {
223        public_key,
224        reward_address,
225        node_client,
226        plot_audit,
227        sectors_metadata,
228        kzg,
229        erasure_coding,
230        handlers,
231        sectors_being_modified,
232        mut slot_info_notifications,
233        thread_pool,
234        read_sector_record_chunks_mode,
235        global_mutex,
236        metrics,
237        cutover,
238    } = farming_options;
239
240    let farmer_app_info = node_client
241        .farmer_app_info()
242        .await
243        .map_err(|error| FarmingError::FailedToGetFarmerInfo { error })?;
244
245    // We assume that each slot is one second
246    let farming_timeout = farmer_app_info.farming_timeout;
247
248    let span = Span::current();
249
250    let mut non_fatal_errors = 0;
251
252    while let Some(slot_info) = slot_info_notifications.next().await {
253        let slot = slot_info.slot_number;
254
255        // Take mutex briefly to make sure farming is allowed right now
256        global_mutex.lock().await;
257
258        let mut problematic_sectors = Vec::new();
259        let result = try {
260            let start = Instant::now();
261            let sectors_metadata = sectors_metadata.read().await;
262
263            debug!(%slot, sector_count = %sectors_metadata.len(), "Reading sectors");
264
265            let mut sectors_solutions = {
266                let sectors_being_modified = &*sectors_being_modified.read().await;
267
268                thread_pool
269                    .install(|| {
270                        let _span_guard = span.enter();
271
272                        plot_audit.audit::<PosTable>(PlotAuditOptions {
273                            public_key: &public_key,
274                            reward_address: &reward_address,
275                            slot_info,
276                            sectors_metadata: &sectors_metadata,
277                            kzg: &kzg,
278                            erasure_coding: &erasure_coding,
279                            sectors_being_modified,
280                            read_sector_record_chunks_mode,
281                            cutover,
282                        })
283                    })
284                    .map_err(FarmingError::LowLevelAuditing)?
285            };
286
287            sectors_solutions.sort_by(|a, b| {
288                let a_solution_distance =
289                    a.1.best_solution_distance().unwrap_or(SolutionRange::MAX);
290                let b_solution_distance =
291                    b.1.best_solution_distance().unwrap_or(SolutionRange::MAX);
292
293                a_solution_distance.cmp(&b_solution_distance)
294            });
295
296            {
297                let time = start.elapsed();
298                if let Some(metrics) = &metrics {
299                    metrics.auditing_time.observe(time.as_secs_f64());
300                }
301                handlers
302                    .farming_notification
303                    .call_simple(&FarmingNotification::Auditing(AuditingDetails {
304                        sectors_count: sectors_metadata.len() as SectorIndex,
305                        time,
306                    }));
307            }
308
309            // Take mutex and hold until proving end to make sure nothing else major happens at the
310            // same time
311            let _proving_guard = global_mutex.lock().await;
312
313            'solutions_processing: for (sector_index, mut sector_solutions) in sectors_solutions {
314                if sector_solutions.is_empty() {
315                    continue;
316                }
317                let mut start = Instant::now();
318                while let Some(maybe_solution) = thread_pool.install(|| {
319                    let _span_guard = span.enter();
320
321                    sector_solutions.next()
322                }) {
323                    let solution = match maybe_solution {
324                        Ok(solution) => solution,
325                        Err(error) => {
326                            if let Some(metrics) = &metrics {
327                                metrics
328                                    .observe_proving_time(&start.elapsed(), ProvingResult::Failed);
329                            }
330                            error!(
331                                %slot,
332                                %sector_index,
333                                %error,
334                                "Failed to prove, scheduling sector for replotting"
335                            );
336                            problematic_sectors.push(sector_index);
337                            // Do not error completely as disk corruption or other reasons why
338                            // proving might fail
339                            start = Instant::now();
340                            continue;
341                        }
342                    };
343
344                    debug!(%slot, %sector_index, "Solution found");
345                    trace!(?solution, "Solution found");
346
347                    {
348                        let time = start.elapsed();
349                        if time >= farming_timeout {
350                            if let Some(metrics) = &metrics {
351                                metrics.observe_proving_time(&time, ProvingResult::Timeout);
352                            }
353                            handlers.farming_notification.call_simple(
354                                &FarmingNotification::Proving(ProvingDetails {
355                                    result: ProvingResult::Timeout,
356                                    time,
357                                }),
358                            );
359                            warn!(
360                                %slot,
361                                %sector_index,
362                                "Proving for solution skipped due to farming time limit",
363                            );
364
365                            break 'solutions_processing;
366                        }
367                    }
368
369                    let response = SolutionResponse {
370                        slot_number: slot,
371                        solution,
372                    };
373
374                    handlers.solution.call_simple(&response);
375
376                    if let Err(error) = node_client.submit_solution_response(response).await {
377                        let time = start.elapsed();
378                        if let Some(metrics) = &metrics {
379                            metrics.observe_proving_time(&time, ProvingResult::Rejected);
380                        }
381                        handlers
382                            .farming_notification
383                            .call_simple(&FarmingNotification::Proving(ProvingDetails {
384                                result: ProvingResult::Rejected,
385                                time,
386                            }));
387                        warn!(
388                            %slot,
389                            %sector_index,
390                            %error,
391                            "Failed to send solution to node, skipping further proving for this slot",
392                        );
393                        break 'solutions_processing;
394                    }
395
396                    let time = start.elapsed();
397                    if let Some(metrics) = &metrics {
398                        metrics.observe_proving_time(&time, ProvingResult::Success);
399                    }
400                    handlers
401                        .farming_notification
402                        .call_simple(&FarmingNotification::Proving(ProvingDetails {
403                            result: ProvingResult::Success,
404                            time,
405                        }));
406                    start = Instant::now();
407                }
408            }
409        };
410
411        if let Err(error) = result {
412            if error.is_fatal() {
413                return Err(error);
414            }
415
416            non_fatal_errors += 1;
417
418            if non_fatal_errors >= NON_FATAL_ERROR_LIMIT {
419                return Err(error);
420            }
421
422            warn!(
423                %error,
424                "Non-fatal farming error"
425            );
426
427            if let Some(metrics) = &metrics {
428                metrics.note_farming_error(&error);
429            }
430            handlers
431                .farming_notification
432                .call_simple(&FarmingNotification::NonFatalError(Arc::new(error)));
433
434            for sector_index in problematic_sectors.drain(..) {
435                // Inform others that this sector is being modified
436                sectors_being_modified.write().await.insert(sector_index);
437                // Replace metadata with a dummy one, so it will be picked up for replotting next
438                if let Some(existing_sector_metadata) = sectors_metadata
439                    .write()
440                    .await
441                    .get_mut(sector_index as usize)
442                {
443                    *existing_sector_metadata = SectorMetadataChecksummed::from(SectorMetadata {
444                        sector_index,
445                        pieces_in_sector: existing_sector_metadata.pieces_in_sector,
446                        s_bucket_sizes: Box::new([0; Record::NUM_S_BUCKETS]),
447                        history_size: HistorySize::from(SegmentIndex::ZERO),
448                    });
449                }
450                // Inform others that this sector is no longer being modified
451                sectors_being_modified.write().await.remove(&sector_index);
452            }
453        } else {
454            non_fatal_errors = 0;
455        }
456    }
457
458    Ok(())
459}