Skip to main content

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