ab_farmer/single_disk_farm/
farming.rs1pub 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
39const 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 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#[derive(Debug)]
77pub struct PlotAuditOptions<'a, 'b, PosTable>
78where
79 PosTable: Table,
80{
81 pub public_key_hash: &'a Blake3Hash,
83 pub shard_commitments_roots_cache: &'a ShardCommitmentsRootsCache,
85 pub slot_info: SlotInfo,
87 pub sectors_metadata: &'a [SectorMetadataChecksummed],
89 pub erasure_coding: &'a ErasureCoding,
91 pub sectors_being_modified: &'b HashSet<SectorIndex>,
94 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#[derive(Debug)]
112pub struct PlotAudit<Plot>(Plot)
113where
114 Plot: ReadAtSync;
115
116impl<'a, Plot> PlotAudit<Plot>
117where
118 Plot: ReadAtSync + 'a,
119{
120 pub fn new(plot: Plot) -> Self {
122 Self(plot)
123 }
124
125 #[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 #[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
220pub(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 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 let farming_timeout = farmer_app_info.farming_timeout;
256
257 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 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: §ors_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 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 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 sectors_being_modified.write().await.insert(sector_index);
448 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 sectors_being_modified.write().await.remove(§or_index);
463 }
464 } else {
465 non_fatal_errors = 0;
466 }
467 }
468
469 Ok(())
470}