Skip to main content

ab_farmer/
single_disk_farm.rs

1//! Primary [`Farm`] implementation that deals with hardware directly
2//!
3//! Single disk farm is an abstraction that contains an identity, associated plot with metadata and
4//! a small piece cache. It fully manages farming and plotting process, including listening to node
5//! notifications, producing solutions and sealing blocks.
6
7mod block_sealing;
8pub mod direct_io_file_wrapper;
9pub mod farming;
10pub mod identity;
11mod metrics;
12pub mod piece_cache;
13pub mod piece_reader;
14pub mod plot_cache;
15mod plotted_sectors;
16mod plotting;
17
18use crate::disk_piece_cache::{DiskPieceCache, DiskPieceCacheError};
19use crate::farm::{
20    Farm, FarmId, FarmingError, FarmingNotification, HandlerFn, PieceCacheId, PieceReader,
21    PlottedSectors, SectorUpdate,
22};
23use crate::node_client::NodeClient;
24use crate::plotter::Plotter;
25use crate::single_disk_farm::block_sealing::block_sealing;
26use crate::single_disk_farm::direct_io_file_wrapper::{DISK_PAGE_SIZE, DirectIoFileWrapper};
27use crate::single_disk_farm::farming::rayon_files::RayonFiles;
28use crate::single_disk_farm::farming::{
29    FarmingOptions, PlotAudit, farming, slot_notification_forwarder,
30};
31use crate::single_disk_farm::identity::{Identity, IdentityError};
32use crate::single_disk_farm::metrics::SingleDiskFarmMetrics;
33use crate::single_disk_farm::piece_cache::SingleDiskPieceCache;
34use crate::single_disk_farm::piece_reader::DiskPieceReader;
35use crate::single_disk_farm::plot_cache::DiskPlotCache;
36use crate::single_disk_farm::plotted_sectors::SingleDiskPlottedSectors;
37pub use crate::single_disk_farm::plotting::PlottingError;
38use crate::single_disk_farm::plotting::{
39    PlottingOptions, PlottingSchedulerOptions, SectorPlottingOptions, plotting, plotting_scheduler,
40};
41use crate::utils::{AsyncJoinOnDrop, tokio_rayon_spawn_handler};
42use crate::{KNOWN_PEERS_CACHE_SIZE, farm};
43use ab_core_primitives::address::Address;
44use ab_core_primitives::block::BlockRoot;
45use ab_core_primitives::ed25519::Ed25519PublicKey;
46use ab_core_primitives::hashes::Blake3Hash;
47use ab_core_primitives::pieces::Record;
48use ab_core_primitives::sectors::SectorIndex;
49use ab_core_primitives::segments::{HistorySize, SegmentIndex};
50use ab_erasure_coding::ErasureCoding;
51use ab_farmer_components::FarmerProtocolInfo;
52use ab_farmer_components::file_ext::FileExt;
53use ab_farmer_components::sector::{SectorMetadata, SectorMetadataChecksummed, sector_size};
54use ab_farmer_components::shard_commitment::ShardCommitmentsRootsCache;
55use ab_farmer_rpc_primitives::{FarmerAppInfo, SolutionResponse};
56use ab_networking::KnownPeersManager;
57use ab_proof_of_space::Table;
58use async_lock::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
59use async_trait::async_trait;
60use bytesize::ByteSize;
61use event_listener_primitives::{Bag, HandlerId};
62use futures::channel::{mpsc, oneshot};
63use futures::stream::FuturesUnordered;
64use futures::{FutureExt, StreamExt, select};
65use parity_scale_codec::{Decode, Encode};
66use parking_lot::Mutex;
67use prometheus_client::registry::Registry;
68use rayon::prelude::*;
69use rayon::{ThreadPoolBuildError, ThreadPoolBuilder};
70use serde::{Deserialize, Serialize};
71use std::collections::HashSet;
72use std::fs::{File, OpenOptions};
73use std::future::Future;
74use std::io::Write;
75use std::num::{NonZeroU32, NonZeroUsize};
76use std::path::{Path, PathBuf};
77use std::pin::Pin;
78use std::str::FromStr;
79use std::sync::Arc as StdArc;
80use std::sync::atomic::{AtomicUsize, Ordering};
81use std::time::Duration;
82use std::{fmt, fs, io};
83use thiserror::Error;
84use tokio::runtime::Handle;
85use tokio::sync::broadcast;
86use tokio::task;
87use tracing::{Instrument, Span, error, info, trace, warn};
88use triomphe::Arc;
89
90// Refuse to compile on non-64-bit platforms, offsets may fail on those when converting from u64 to
91// usize depending on chain parameters
92const {
93    assert!(size_of::<usize>() >= size_of::<u64>());
94}
95
96/// Reserve 1M of space for plot metadata (for potential future expansion)
97const RESERVED_PLOT_METADATA: u64 = 1024 * 1024;
98/// Reserve 1M of space for farm info (for potential future expansion)
99const RESERVED_FARM_INFO: u64 = 1024 * 1024;
100const NEW_SEGMENT_PROCESSING_DELAY: Duration = Duration::from_mins(10);
101
102/// Exclusive lock for single disk farm info file, ensuring no concurrent edits by cooperating
103/// processes is done
104#[derive(Debug)]
105#[must_use = "Lock file must be kept around or as long as farm is used"]
106pub struct SingleDiskFarmInfoLock {
107    _file: File,
108}
109
110/// Important information about the contents of the `SingleDiskFarm`
111#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub enum SingleDiskFarmInfo {
114    /// V0 of the info
115    #[serde(rename_all = "camelCase")]
116    V0 {
117        /// ID of the farm
118        id: FarmId,
119        /// Genesis root of the beacon chain used for farm creation
120        genesis_root: BlockRoot,
121        /// Public key of identity used for farm creation
122        public_key: Ed25519PublicKey,
123        /// Seed used for deriving shard commitments
124        shard_commitments_seed: Blake3Hash,
125        /// How many pieces does one sector contain.
126        pieces_in_sector: u16,
127        /// How much space in bytes is allocated for this farm
128        allocated_space: u64,
129    },
130}
131
132impl SingleDiskFarmInfo {
133    const FILE_NAME: &'static str = "single_disk_farm.json";
134
135    /// Create a new instance
136    pub fn new(
137        id: FarmId,
138        genesis_root: BlockRoot,
139        public_key: Ed25519PublicKey,
140        shard_commitments_seed: Blake3Hash,
141        pieces_in_sector: u16,
142        allocated_space: u64,
143    ) -> Self {
144        Self::V0 {
145            id,
146            genesis_root,
147            public_key,
148            shard_commitments_seed,
149            pieces_in_sector,
150            allocated_space,
151        }
152    }
153
154    /// Load `SingleDiskFarm` from path is supposed to be stored, `None` means no info file was
155    /// found, happens during first start.
156    pub fn load_from(directory: &Path) -> io::Result<Option<Self>> {
157        let bytes = match fs::read(directory.join(Self::FILE_NAME)) {
158            Ok(bytes) => bytes,
159            Err(error) => {
160                return if error.kind() == io::ErrorKind::NotFound {
161                    Ok(None)
162                } else {
163                    Err(error)
164                };
165            }
166        };
167
168        serde_json::from_slice(&bytes)
169            .map(Some)
170            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
171    }
172
173    /// Store `SingleDiskFarm` info to path, so it can be loaded again upon restart.
174    ///
175    /// Can optionally return a lock.
176    pub fn store_to(
177        &self,
178        directory: &Path,
179        lock: bool,
180    ) -> io::Result<Option<SingleDiskFarmInfoLock>> {
181        let mut file = OpenOptions::new()
182            .write(true)
183            .create(true)
184            .truncate(false)
185            .open(directory.join(Self::FILE_NAME))?;
186        if lock {
187            fs4::FileExt::try_lock(&file)?;
188        }
189        file.set_len(0)?;
190        file.write_all(&serde_json::to_vec(self).expect("Info serialization never fails; qed"))?;
191
192        Ok(lock.then_some(SingleDiskFarmInfoLock { _file: file }))
193    }
194
195    /// Try to acquire exclusive lock on the single disk farm info file, ensuring no concurrent
196    /// edits by cooperating processes is done
197    pub fn try_lock(directory: &Path) -> io::Result<SingleDiskFarmInfoLock> {
198        let file = File::open(directory.join(Self::FILE_NAME))?;
199        fs4::FileExt::try_lock(&file)?;
200
201        Ok(SingleDiskFarmInfoLock { _file: file })
202    }
203
204    /// ID of the farm
205    #[expect(
206        clippy::rest_pattern_accessible_field,
207        reason = "Do not need other fields"
208    )]
209    pub fn id(&self) -> &FarmId {
210        let Self::V0 { id, .. } = self;
211        id
212    }
213
214    /// Genesis hash of the chain used for farm creation
215    #[expect(
216        clippy::rest_pattern_accessible_field,
217        reason = "Do not need other fields"
218    )]
219    pub fn genesis_root(&self) -> &BlockRoot {
220        let Self::V0 { genesis_root, .. } = self;
221        genesis_root
222    }
223
224    /// Public key of identity used for farm creation
225    #[expect(
226        clippy::rest_pattern_accessible_field,
227        reason = "Do not need other fields"
228    )]
229    pub fn public_key(&self) -> &Ed25519PublicKey {
230        let Self::V0 { public_key, .. } = self;
231        public_key
232    }
233
234    /// Seed used for deriving shard commitments
235    #[expect(
236        clippy::rest_pattern_accessible_field,
237        reason = "Do not need other fields"
238    )]
239    pub fn shard_commitments_seed(&self) -> &Blake3Hash {
240        let Self::V0 {
241            shard_commitments_seed,
242            ..
243        } = self;
244        shard_commitments_seed
245    }
246
247    /// How many pieces does one sector contain
248    #[expect(
249        clippy::rest_pattern_accessible_field,
250        reason = "Do not need other fields"
251    )]
252    pub fn pieces_in_sector(&self) -> u16 {
253        match self {
254            SingleDiskFarmInfo::V0 {
255                pieces_in_sector, ..
256            } => *pieces_in_sector,
257        }
258    }
259
260    /// How much space in bytes is allocated for this farm
261    #[expect(
262        clippy::rest_pattern_accessible_field,
263        reason = "Do not need other fields"
264    )]
265    pub fn allocated_space(&self) -> u64 {
266        match self {
267            SingleDiskFarmInfo::V0 {
268                allocated_space, ..
269            } => *allocated_space,
270        }
271    }
272}
273
274/// Summary of single disk farm for presentational purposes
275#[derive(Debug)]
276pub enum SingleDiskFarmSummary {
277    /// Farm was found and read successfully
278    Found {
279        /// Farm info
280        info: SingleDiskFarmInfo,
281        /// Path to directory where farm is stored.
282        directory: PathBuf,
283    },
284    /// Farm was not found
285    NotFound {
286        /// Path to directory where farm is stored.
287        directory: PathBuf,
288    },
289    /// Failed to open farm
290    Error {
291        /// Path to directory where farm is stored.
292        directory: PathBuf,
293        /// Error itself
294        error: io::Error,
295    },
296}
297
298#[derive(Debug, Encode, Decode)]
299struct PlotMetadataHeader {
300    version: u8,
301    plotted_sector_count: u16,
302}
303
304impl PlotMetadataHeader {
305    #[inline]
306    fn encoded_size() -> usize {
307        let default = PlotMetadataHeader {
308            version: 0,
309            plotted_sector_count: 0,
310        };
311
312        default.encoded_size()
313    }
314}
315
316/// Options used to open single disk farm
317#[derive(Debug)]
318pub struct SingleDiskFarmOptions<'a, NC>
319where
320    NC: Clone,
321{
322    /// Path to directory where farm is stored.
323    pub directory: PathBuf,
324    /// Information necessary for farmer application
325    pub farmer_app_info: FarmerAppInfo,
326    /// The amount of space in bytes that was allocated
327    pub allocated_space: u64,
328    /// How many pieces one sector is supposed to contain (max)
329    pub max_pieces_in_sector: u16,
330    /// RPC client connected to the node
331    pub node_client: NC,
332    /// Address where farming rewards should go
333    pub reward_address: Address,
334    /// Plotter
335    pub plotter: StdArc<dyn Plotter + Send + Sync>,
336    /// Erasure coding instance to use.
337    pub erasure_coding: ErasureCoding,
338    /// Percentage of allocated space dedicated for caching purposes
339    pub cache_percentage: u8,
340    /// Thread pool size used for farming (mostly for blocking I/O, but also for some
341    /// compute-intensive operations during proving)
342    pub farming_thread_pool_size: usize,
343    /// Notification for plotter to start, can be used to delay plotting until some initialization
344    /// has happened externally
345    pub plotting_delay: Option<oneshot::Receiver<()>>,
346    /// Global mutex that can restrict concurrency of resource-intensive operations and make sure
347    /// that those operations that are very sensitive (like proving) have all the resources
348    /// available to them for the highest probability of success
349    pub global_mutex: Arc<AsyncMutex<()>>,
350    /// How many sectors a will be plotted concurrently per farm
351    pub max_plotting_sectors_per_farm: NonZeroUsize,
352    /// Disable farm locking, for example if file system doesn't support it
353    pub disable_farm_locking: bool,
354    /// Prometheus registry
355    pub registry: Option<&'a Mutex<&'a mut Registry>>,
356    /// Whether to create a farm if it doesn't yet exist
357    pub create: bool,
358}
359
360/// Errors happening when trying to create/open single disk farm
361#[derive(Debug, Error)]
362pub enum SingleDiskFarmError {
363    /// Failed to open or create identity
364    #[error("Failed to open or create identity: {0}")]
365    FailedToOpenIdentity(#[from] IdentityError),
366    /// Farm is likely already in use, make sure no other farmer is using it
367    #[error("Farm is likely already in use, make sure no other farmer is using it: {0}")]
368    LikelyAlreadyInUse(io::Error),
369    /// I/O error occurred
370    #[error("Single disk farm I/O error: {0}")]
371    Io(#[from] io::Error),
372    /// Failed to spawn task for blocking thread
373    #[error("Failed to spawn task for blocking thread: {0}")]
374    TokioJoinError(#[from] task::JoinError),
375    /// Piece cache error
376    #[error("Piece cache error: {0}")]
377    PieceCacheError(#[from] DiskPieceCacheError),
378    /// Can't preallocate metadata file, probably not enough space on disk
379    #[error("Can't preallocate metadata file, probably not enough space on disk: {0}")]
380    CantPreallocateMetadataFile(io::Error),
381    /// Can't preallocate plot file, probably not enough space on disk
382    #[error("Can't preallocate plot file, probably not enough space on disk: {0}")]
383    CantPreallocatePlotFile(io::Error),
384    /// Wrong chain (genesis hash)
385    #[error(
386        "Genesis hash of farm {id} {wrong_chain} is different from {correct_chain} when farm was \
387        created, it is not possible to use farm on a different chain"
388    )]
389    WrongChain {
390        /// Farm ID
391        id: FarmId,
392        /// Hex-encoded genesis hash during farm creation
393        // TODO: Wrapper type with `Display` impl for genesis hash
394        correct_chain: String,
395        /// Hex-encoded current genesis hash
396        wrong_chain: String,
397    },
398    /// Public key in identity doesn't match metadata
399    #[error(
400        "Public key of farm {id} {wrong_public_key} is different from {correct_public_key} when \
401        farm was created, something went wrong, likely due to manual edits"
402    )]
403    IdentityMismatch {
404        /// Farm ID
405        id: FarmId,
406        /// Public key used during farm creation
407        correct_public_key: Ed25519PublicKey,
408        /// Current public key
409        wrong_public_key: Ed25519PublicKey,
410    },
411    /// Invalid number pieces in sector
412    #[error(
413        "Invalid number pieces in sector: max supported {max_supported}, farm initialized with \
414        {initialized_with}"
415    )]
416    InvalidPiecesInSector {
417        /// Farm ID
418        id: FarmId,
419        /// Max supported pieces in sector
420        max_supported: u16,
421        /// Number of pieces in sector farm is initialized with
422        initialized_with: u16,
423    },
424    /// Failed to decode metadata header
425    #[error("Failed to decode metadata header: {0}")]
426    FailedToDecodeMetadataHeader(parity_scale_codec::Error),
427    /// Unexpected metadata version
428    #[error("Unexpected metadata version {0}")]
429    UnexpectedMetadataVersion(u8),
430    /// Allocated space is not enough for one sector
431    #[error(
432        "Allocated space is not enough for one sector. \
433        The lowest acceptable value for allocated space is {min_space} bytes, \
434        provided {allocated_space} bytes."
435    )]
436    InsufficientAllocatedSpace {
437        /// Minimal allocated space
438        min_space: u64,
439        /// Current allocated space
440        allocated_space: u64,
441    },
442    /// Farm is too large
443    #[error(
444        "Farm is too large: allocated {allocated_sectors} sectors ({allocated_space} bytes), max \
445        supported is {max_sectors} ({max_space} bytes). Consider creating multiple smaller farms \
446        instead."
447    )]
448    FarmTooLarge {
449        /// Allocated space
450        allocated_space: u64,
451        /// Allocated space in sectors
452        allocated_sectors: u64,
453        /// Max supported allocated space
454        max_space: u64,
455        /// Max supported allocated space in sectors
456        max_sectors: u16,
457    },
458    /// Failed to create thread pool
459    #[error("Failed to create thread pool: {0}")]
460    FailedToCreateThreadPool(ThreadPoolBuildError),
461}
462
463/// Errors happening during scrubbing
464#[derive(Debug, Error)]
465pub enum SingleDiskFarmScrubError {
466    /// Farm is likely already in use, make sure no other farmer is using it
467    #[error("Farm is likely already in use, make sure no other farmer is using it: {0}")]
468    LikelyAlreadyInUse(io::Error),
469    /// Failed to determine file size
470    #[error("Failed to file size of {file}: {error}")]
471    FailedToDetermineFileSize {
472        /// Affected file
473        file: PathBuf,
474        /// Low-level error
475        error: io::Error,
476    },
477    /// Failed to read bytes from file
478    #[error("Failed to read {size} bytes from {file} at offset {offset}: {error}")]
479    FailedToReadBytes {
480        /// Affected file
481        file: PathBuf,
482        /// Number of bytes to read
483        size: u64,
484        /// Offset in the file
485        offset: u64,
486        /// Low-level error
487        error: io::Error,
488    },
489    /// Failed to write bytes from file
490    #[error("Failed to write {size} bytes from {file} at offset {offset}: {error}")]
491    FailedToWriteBytes {
492        /// Affected file
493        file: PathBuf,
494        /// Number of bytes to read
495        size: u64,
496        /// Offset in the file
497        offset: u64,
498        /// Low-level error
499        error: io::Error,
500    },
501    /// Farm info file does not exist
502    #[error("Farm info file does not exist at {file}")]
503    FarmInfoFileDoesNotExist {
504        /// Info file
505        file: PathBuf,
506    },
507    /// Farm info can't be opened
508    #[error("Farm info at {file} can't be opened: {error}")]
509    FarmInfoCantBeOpened {
510        /// Info file
511        file: PathBuf,
512        /// Low-level error
513        error: io::Error,
514    },
515    /// Identity file does not exist
516    #[error("Identity file does not exist at {file}")]
517    IdentityFileDoesNotExist {
518        /// Identity file
519        file: PathBuf,
520    },
521    /// Identity can't be opened
522    #[error("Identity at {file} can't be opened: {error}")]
523    IdentityCantBeOpened {
524        /// Identity file
525        file: PathBuf,
526        /// Low-level error
527        error: IdentityError,
528    },
529    /// Identity public key doesn't match public key in the disk farm info
530    #[error("Identity public key {identity} doesn't match public key in the disk farm info {info}")]
531    PublicKeyMismatch {
532        /// Identity public key
533        identity: Ed25519PublicKey,
534        /// Disk farm info public key
535        info: Ed25519PublicKey,
536    },
537    /// Metadata file does not exist
538    #[error("Metadata file does not exist at {file}")]
539    MetadataFileDoesNotExist {
540        /// Metadata file
541        file: PathBuf,
542    },
543    /// Metadata can't be opened
544    #[error("Metadata at {file} can't be opened: {error}")]
545    MetadataCantBeOpened {
546        /// Metadata file
547        file: PathBuf,
548        /// Low-level error
549        error: io::Error,
550    },
551    /// Metadata file too small
552    #[error(
553        "Metadata file at {file} is too small: reserved size is {reserved_size} bytes, file size \
554        is {size}"
555    )]
556    MetadataFileTooSmall {
557        /// Metadata file
558        file: PathBuf,
559        /// Reserved size
560        reserved_size: u64,
561        /// File size
562        size: u64,
563    },
564    /// Failed to decode metadata header
565    #[error("Failed to decode metadata header: {0}")]
566    FailedToDecodeMetadataHeader(parity_scale_codec::Error),
567    /// Unexpected metadata version
568    #[error("Unexpected metadata version {0}")]
569    UnexpectedMetadataVersion(u8),
570    /// Cache can't be opened
571    #[error("Cache at {file} can't be opened: {error}")]
572    CacheCantBeOpened {
573        /// Cache file
574        file: PathBuf,
575        /// Low-level error
576        error: io::Error,
577    },
578}
579
580/// Errors that happen in background tasks
581#[derive(Debug, Error)]
582pub enum BackgroundTaskError {
583    /// Plotting error
584    #[error(transparent)]
585    Plotting(#[from] PlottingError),
586    /// Farming error
587    #[error(transparent)]
588    Farming(#[from] FarmingError),
589    /// Block sealing
590    #[error(transparent)]
591    BlockSealing(#[from] anyhow::Error),
592    /// Background task panicked
593    #[error("Background task {task} panicked")]
594    BackgroundTaskPanicked {
595        /// Name of the task
596        task: String,
597    },
598}
599
600type BackgroundTask = Pin<Box<dyn Future<Output = Result<(), BackgroundTaskError>> + Send>>;
601
602/// Scrub target
603#[derive(Debug, Copy, Clone)]
604pub enum ScrubTarget {
605    /// Scrub everything
606    All,
607    /// Scrub just metadata
608    Metadata,
609    /// Scrub metadata and corresponding plot
610    Plot,
611    /// Only scrub cache
612    Cache,
613}
614
615impl fmt::Display for ScrubTarget {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        match self {
618            Self::All => f.write_str("all"),
619            Self::Metadata => f.write_str("metadata"),
620            Self::Plot => f.write_str("plot"),
621            Self::Cache => f.write_str("cache"),
622        }
623    }
624}
625
626impl FromStr for ScrubTarget {
627    type Err = String;
628
629    fn from_str(s: &str) -> Result<Self, Self::Err> {
630        match s {
631            "all" => Ok(Self::All),
632            "metadata" => Ok(Self::Metadata),
633            "plot" => Ok(Self::Plot),
634            "cache" => Ok(Self::Cache),
635            s => Err(format!("Can't parse {s} as `ScrubTarget`")),
636        }
637    }
638}
639
640impl ScrubTarget {
641    fn metadata(self) -> bool {
642        match self {
643            Self::All | Self::Metadata | Self::Plot => true,
644            Self::Cache => false,
645        }
646    }
647
648    fn plot(self) -> bool {
649        match self {
650            Self::All | Self::Plot => true,
651            Self::Metadata | Self::Cache => false,
652        }
653    }
654
655    fn cache(self) -> bool {
656        match self {
657            Self::All | Self::Cache => true,
658            Self::Metadata | Self::Plot => false,
659        }
660    }
661}
662
663struct AllocatedSpaceDistribution {
664    piece_cache_file_size: u64,
665    piece_cache_capacity: u32,
666    plot_file_size: u64,
667    target_sector_count: u16,
668    metadata_file_size: u64,
669}
670
671impl AllocatedSpaceDistribution {
672    fn new(
673        allocated_space: u64,
674        sector_size: u64,
675        cache_percentage: u8,
676        sector_metadata_size: u64,
677    ) -> Result<Self, SingleDiskFarmError> {
678        let single_sector_overhead = sector_size + sector_metadata_size;
679        // Fixed space usage regardless of plot size
680        let fixed_space_usage = RESERVED_PLOT_METADATA
681            + RESERVED_FARM_INFO
682            + Identity::file_size() as u64
683            + KnownPeersManager::file_size(KNOWN_PEERS_CACHE_SIZE) as u64;
684        // Calculate how many sectors can fit
685        let target_sector_count = {
686            let potentially_plottable_space = allocated_space.saturating_sub(fixed_space_usage)
687                / 100
688                * (100 - u64::from(cache_percentage));
689            // Do the rounding to make sure we have exactly as much space as fits whole number of
690            // sectors, account for disk sector size just in case
691            (potentially_plottable_space - DISK_PAGE_SIZE as u64) / single_sector_overhead
692        };
693
694        if target_sector_count == 0 {
695            let mut single_plot_with_cache_space =
696                single_sector_overhead.div_ceil(100 - u64::from(cache_percentage)) * 100;
697            // Cache must not be empty, ensure it contains at least one element even if
698            // percentage-wise it will use more space
699            if single_plot_with_cache_space - single_sector_overhead
700                < u64::from(DiskPieceCache::element_size())
701            {
702                single_plot_with_cache_space =
703                    single_sector_overhead + u64::from(DiskPieceCache::element_size());
704            }
705
706            return Err(SingleDiskFarmError::InsufficientAllocatedSpace {
707                min_space: fixed_space_usage + single_plot_with_cache_space,
708                allocated_space,
709            });
710        }
711        let plot_file_size = target_sector_count * sector_size;
712        // Align plot file size for disk sector size
713        let plot_file_size = plot_file_size.div_ceil(DISK_PAGE_SIZE as u64) * DISK_PAGE_SIZE as u64;
714
715        // Remaining space will be used for caching purposes
716        let piece_cache_capacity = if cache_percentage > 0 {
717            let cache_space = allocated_space
718                - fixed_space_usage
719                - plot_file_size
720                - (sector_metadata_size * target_sector_count);
721            (cache_space / u64::from(DiskPieceCache::element_size())) as u32
722        } else {
723            0
724        };
725        let target_sector_count = match u16::try_from(target_sector_count).map(SectorIndex::from) {
726            Ok(target_sector_count) if target_sector_count < SectorIndex::MAX => {
727                u16::from(target_sector_count)
728            }
729            _ => {
730                // We use `u16` for both count and index, hence index must not reach actual `MAX`
731                // (consensus doesn't care about this, just farmer implementation detail)
732                let max_sectors = u16::from(SectorIndex::MAX) - 1;
733                return Err(SingleDiskFarmError::FarmTooLarge {
734                    allocated_space: target_sector_count * sector_size,
735                    allocated_sectors: target_sector_count,
736                    max_space: u64::from(max_sectors) * sector_size,
737                    max_sectors,
738                });
739            }
740        };
741
742        Ok(Self {
743            piece_cache_file_size: u64::from(piece_cache_capacity)
744                * u64::from(DiskPieceCache::element_size()),
745            piece_cache_capacity,
746            plot_file_size,
747            target_sector_count,
748            metadata_file_size: RESERVED_PLOT_METADATA
749                + sector_metadata_size * u64::from(target_sector_count),
750        })
751    }
752}
753
754type Handler<A> = Bag<HandlerFn<A>, A>;
755
756#[derive(Default, Debug)]
757struct Handlers {
758    sector_update: Handler<(SectorIndex, SectorUpdate)>,
759    farming_notification: Handler<FarmingNotification>,
760    solution: Handler<SolutionResponse>,
761}
762
763struct SingleDiskFarmInit {
764    identity: Identity,
765    single_disk_farm_info: SingleDiskFarmInfo,
766    single_disk_farm_info_lock: Option<SingleDiskFarmInfoLock>,
767    plot_file: StdArc<DirectIoFileWrapper>,
768    metadata_file: DirectIoFileWrapper,
769    metadata_header: PlotMetadataHeader,
770    target_sector_count: u16,
771    sectors_metadata: StdArc<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
772    piece_cache_capacity: u32,
773    plot_cache: DiskPlotCache,
774}
775
776/// Single disk farm abstraction is a container for everything necessary to plot/farm with a single
777/// disk.
778///
779/// Farm starts operating during creation and doesn't stop until dropped (or error happens).
780#[derive(Debug)]
781#[must_use = "Plot does not function properly unless run() method is called"]
782pub struct SingleDiskFarm {
783    farmer_protocol_info: FarmerProtocolInfo,
784    single_disk_farm_info: SingleDiskFarmInfo,
785    shard_commitments_roots_cache: ShardCommitmentsRootsCache,
786    /// Metadata of all sectors plotted so far
787    sectors_metadata: StdArc<AsyncRwLock<Vec<SectorMetadataChecksummed>>>,
788    pieces_in_sector: u16,
789    total_sectors_count: u16,
790    span: Span,
791    tasks: FuturesUnordered<BackgroundTask>,
792    handlers: Arc<Handlers>,
793    piece_cache: SingleDiskPieceCache,
794    plot_cache: DiskPlotCache,
795    piece_reader: DiskPieceReader,
796    /// Sender that will be used to signal to background threads that they should start
797    start_sender: Option<broadcast::Sender<()>>,
798    /// Sender that will be used to signal to background threads that they must stop
799    stop_sender: Option<broadcast::Sender<()>>,
800    _single_disk_farm_info_lock: Option<SingleDiskFarmInfoLock>,
801}
802
803impl Drop for SingleDiskFarm {
804    #[inline]
805    fn drop(&mut self) {
806        self.piece_reader.close_all_readers();
807        // Make background threads that are waiting to do something exit immediately
808        self.start_sender.take();
809        // Notify background tasks that they must stop
810        self.stop_sender.take();
811    }
812}
813
814#[async_trait(?Send)]
815impl Farm for SingleDiskFarm {
816    fn id(&self) -> &FarmId {
817        self.id()
818    }
819
820    fn total_sectors_count(&self) -> u16 {
821        self.total_sectors_count
822    }
823
824    fn plotted_sectors(&self) -> StdArc<dyn PlottedSectors + 'static> {
825        StdArc::new(self.plotted_sectors())
826    }
827
828    fn piece_reader(&self) -> StdArc<dyn PieceReader + 'static> {
829        StdArc::new(self.piece_reader())
830    }
831
832    fn on_sector_update(
833        &self,
834        callback: HandlerFn<(SectorIndex, SectorUpdate)>,
835    ) -> Box<dyn farm::HandlerId> {
836        Box::new(self.on_sector_update(callback))
837    }
838
839    fn on_farming_notification(
840        &self,
841        callback: HandlerFn<FarmingNotification>,
842    ) -> Box<dyn farm::HandlerId> {
843        Box::new(self.on_farming_notification(callback))
844    }
845
846    fn on_solution(&self, callback: HandlerFn<SolutionResponse>) -> Box<dyn farm::HandlerId> {
847        Box::new(self.on_solution(callback))
848    }
849
850    fn run(self: Box<Self>) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>> {
851        Box::pin((*self).run())
852    }
853}
854
855impl SingleDiskFarm {
856    /// Name of the plot file
857    pub const PLOT_FILE: &'static str = "plot.bin";
858    /// Name of the metadata file
859    pub const METADATA_FILE: &'static str = "metadata.bin";
860    const SUPPORTED_PLOT_VERSION: u8 = 0;
861
862    /// Create new single disk farm instance
863    pub async fn new<NC, PosTable>(
864        options: SingleDiskFarmOptions<'_, NC>,
865        farm_index: usize,
866    ) -> Result<Self, SingleDiskFarmError>
867    where
868        NC: NodeClient + Clone,
869        PosTable: Table,
870    {
871        let span = Span::current();
872
873        let SingleDiskFarmOptions {
874            directory,
875            farmer_app_info,
876            allocated_space,
877            max_pieces_in_sector,
878            node_client,
879            reward_address,
880            plotter,
881            erasure_coding,
882            cache_percentage,
883            farming_thread_pool_size,
884            plotting_delay,
885            global_mutex,
886            max_plotting_sectors_per_farm,
887            disable_farm_locking,
888            registry,
889            create,
890        } = options;
891
892        let single_disk_farm_init_fut = task::spawn_blocking({
893            let directory = directory.clone();
894            let farmer_app_info = farmer_app_info.clone();
895            let span = span.clone();
896
897            move || {
898                let _span_guard = span.enter();
899                Self::init(
900                    &directory,
901                    &farmer_app_info,
902                    allocated_space,
903                    max_pieces_in_sector,
904                    cache_percentage,
905                    disable_farm_locking,
906                    create,
907                )
908            }
909        });
910
911        let single_disk_farm_init =
912            AsyncJoinOnDrop::new(single_disk_farm_init_fut, false).await??;
913
914        let SingleDiskFarmInit {
915            identity,
916            single_disk_farm_info,
917            single_disk_farm_info_lock,
918            plot_file,
919            metadata_file,
920            metadata_header,
921            target_sector_count,
922            sectors_metadata,
923            piece_cache_capacity,
924            plot_cache,
925        } = single_disk_farm_init;
926
927        let piece_cache = {
928            // Convert farm ID into cache ID for single disk farm
929            let FarmId::Ulid(id) = *single_disk_farm_info.id();
930            let id = PieceCacheId::Ulid(id);
931
932            SingleDiskPieceCache::new(
933                id,
934                if let Some(piece_cache_capacity) = NonZeroU32::new(piece_cache_capacity) {
935                    Some(task::block_in_place(|| {
936                        if let Some(registry) = registry {
937                            DiskPieceCache::open(
938                                &directory,
939                                piece_cache_capacity,
940                                Some(id),
941                                Some(*registry.lock()),
942                            )
943                        } else {
944                            DiskPieceCache::open(&directory, piece_cache_capacity, Some(id), None)
945                        }
946                    })?)
947                } else {
948                    None
949                },
950            )
951        };
952
953        let public_key = *single_disk_farm_info.public_key();
954        let shard_commitments_roots_cache =
955            ShardCommitmentsRootsCache::new(*single_disk_farm_info.shard_commitments_seed());
956        let pieces_in_sector = single_disk_farm_info.pieces_in_sector();
957        let sector_size = sector_size(pieces_in_sector);
958
959        let metrics = registry.map(|registry| {
960            Arc::new(SingleDiskFarmMetrics::new(
961                *registry.lock(),
962                single_disk_farm_info.id(),
963                target_sector_count,
964                sectors_metadata.read_blocking().len() as u16,
965            ))
966        });
967
968        let (error_sender, error_receiver) = oneshot::channel();
969        let error_sender = Arc::new(Mutex::new(Some(error_sender)));
970
971        let tasks = FuturesUnordered::<BackgroundTask>::new();
972
973        tasks.push(Box::pin(async move {
974            if let Ok(error) = error_receiver.await {
975                return Err(error);
976            }
977
978            Ok(())
979        }));
980
981        let handlers = Arc::<Handlers>::default();
982        let (start_sender, mut start_receiver) = broadcast::channel::<()>(1);
983        let (stop_sender, mut stop_receiver) = broadcast::channel::<()>(1);
984        let sectors_being_modified = Arc::<AsyncRwLock<HashSet<SectorIndex>>>::default();
985        let (sectors_to_plot_sender, sectors_to_plot_receiver) = mpsc::channel(1);
986        // Some sectors may already be plotted, skip them
987        let sectors_indices_left_to_plot = SectorIndex::from(metadata_header.plotted_sector_count)
988            ..SectorIndex::from(target_sector_count);
989
990        let farming_thread_pool = ThreadPoolBuilder::new()
991            .thread_name(move |thread_index| format!("farming-{farm_index}.{thread_index}"))
992            .num_threads(farming_thread_pool_size)
993            .spawn_handler(tokio_rayon_spawn_handler())
994            .build()
995            .map_err(SingleDiskFarmError::FailedToCreateThreadPool)?;
996        let farming_plot_fut = task::spawn_blocking(|| {
997            farming_thread_pool
998                .install(move || {
999                    RayonFiles::open_with(directory.join(Self::PLOT_FILE), |path| {
1000                        DirectIoFileWrapper::open(path)
1001                    })
1002                })
1003                .map(|farming_plot| (farming_plot, farming_thread_pool))
1004        });
1005
1006        let (farming_plot, farming_thread_pool) =
1007            AsyncJoinOnDrop::new(farming_plot_fut, false).await??;
1008
1009        let plotting_join_handle = task::spawn_blocking({
1010            let shard_commitments_roots_cache = shard_commitments_roots_cache.clone();
1011            let sectors_metadata = StdArc::clone(&sectors_metadata);
1012            let handlers = Arc::clone(&handlers);
1013            let sectors_being_modified = Arc::clone(&sectors_being_modified);
1014            let node_client = node_client.clone();
1015            let plot_file = StdArc::clone(&plot_file);
1016            let error_sender = Arc::clone(&error_sender);
1017            let span = span.clone();
1018            let global_mutex = Arc::clone(&global_mutex);
1019            let metrics = metrics.clone();
1020
1021            move || {
1022                let _span_guard = span.enter();
1023
1024                let plotting_options = PlottingOptions {
1025                    metadata_header,
1026                    sectors_metadata: &sectors_metadata,
1027                    sectors_being_modified: &sectors_being_modified,
1028                    sectors_to_plot_receiver,
1029                    sector_plotting_options: SectorPlottingOptions {
1030                        public_key,
1031                        shard_commitments_roots_cache,
1032                        node_client: &node_client,
1033                        pieces_in_sector,
1034                        sector_size,
1035                        plot_file,
1036                        metadata_file: Arc::new(metadata_file),
1037                        handlers: &handlers,
1038                        global_mutex: &global_mutex,
1039                        plotter,
1040                        metrics,
1041                    },
1042                    max_plotting_sectors_per_farm,
1043                };
1044
1045                let plotting_fut = async {
1046                    if start_receiver.recv().await.is_err() {
1047                        // Dropped before starting
1048                        return Ok(());
1049                    }
1050
1051                    if let Some(plotting_delay) = plotting_delay
1052                        && plotting_delay.await.is_err()
1053                    {
1054                        // Dropped before resolving
1055                        return Ok(());
1056                    }
1057
1058                    plotting(plotting_options).await
1059                };
1060
1061                Handle::current().block_on(async {
1062                    select! {
1063                        plotting_result = plotting_fut.fuse() => {
1064                            if let Err(error) = plotting_result
1065                                && let Some(error_sender) = error_sender.lock().take()
1066                                && let Err(error) = error_sender.send(error.into())
1067                            {
1068                                error!(
1069                                    %error,
1070                                    "Plotting failed to send error to background task"
1071                                );
1072                            }
1073                        }
1074                        _ = stop_receiver.recv().fuse() => {
1075                            // Nothing, just exit
1076                        }
1077                    }
1078                });
1079            }
1080        });
1081        let plotting_join_handle = AsyncJoinOnDrop::new(plotting_join_handle, false);
1082
1083        tasks.push(Box::pin(async move {
1084            // Panic will already be printed by now
1085            plotting_join_handle.await.map_err(|_error| {
1086                BackgroundTaskError::BackgroundTaskPanicked {
1087                    task: format!("plotting-{farm_index}"),
1088                }
1089            })
1090        }));
1091
1092        let public_key_hash = public_key.hash();
1093
1094        let plotting_scheduler_options = PlottingSchedulerOptions {
1095            public_key_hash,
1096            shard_commitments_roots_cache: shard_commitments_roots_cache.clone(),
1097            sectors_indices_left_to_plot,
1098            target_sector_count,
1099            last_archived_segment_index: farmer_app_info.protocol_info.history_size.segment_index(),
1100            min_sector_lifetime: farmer_app_info.protocol_info.min_sector_lifetime,
1101            node_client: node_client.clone(),
1102            handlers: Arc::clone(&handlers),
1103            sectors_metadata: StdArc::clone(&sectors_metadata),
1104            sectors_to_plot_sender,
1105            new_segment_processing_delay: NEW_SEGMENT_PROCESSING_DELAY,
1106            metrics: metrics.clone(),
1107        };
1108        tasks.push(Box::pin(plotting_scheduler(plotting_scheduler_options)));
1109
1110        let (slot_info_forwarder_sender, slot_info_forwarder_receiver) = mpsc::channel(0);
1111
1112        tasks.push(Box::pin({
1113            let node_client = node_client.clone();
1114            let metrics = metrics.clone();
1115
1116            async move {
1117                slot_notification_forwarder(&node_client, slot_info_forwarder_sender, metrics)
1118                    .await
1119                    .map_err(BackgroundTaskError::Farming)
1120            }
1121        }));
1122
1123        let farming_join_handle = task::spawn_blocking({
1124            let shard_commitments_roots_cache = shard_commitments_roots_cache.clone();
1125            let erasure_coding = erasure_coding.clone();
1126            let handlers = Arc::clone(&handlers);
1127            let sectors_being_modified = Arc::clone(&sectors_being_modified);
1128            let sectors_metadata = StdArc::clone(&sectors_metadata);
1129            let mut start_receiver = start_sender.subscribe();
1130            let mut stop_receiver = stop_sender.subscribe();
1131            let node_client = node_client.clone();
1132            let span = span.clone();
1133            let global_mutex = Arc::clone(&global_mutex);
1134
1135            move || {
1136                let _span_guard = span.enter();
1137
1138                let farming_fut = async move {
1139                    if start_receiver.recv().await.is_err() {
1140                        // Dropped before starting
1141                        return Ok(());
1142                    }
1143
1144                    let plot_audit = PlotAudit::new(&farming_plot);
1145
1146                    let farming_options = FarmingOptions {
1147                        public_key_hash,
1148                        shard_commitments_roots_cache,
1149                        reward_address,
1150                        node_client,
1151                        plot_audit,
1152                        sectors_metadata,
1153                        erasure_coding,
1154                        handlers,
1155                        sectors_being_modified,
1156                        slot_info_notifications: slot_info_forwarder_receiver,
1157                        thread_pool: farming_thread_pool,
1158                        global_mutex,
1159                        metrics,
1160                    };
1161                    farming::<PosTable, _, _>(farming_options).await
1162                };
1163
1164                Handle::current().block_on(async {
1165                    select! {
1166                        farming_result = farming_fut.fuse() => {
1167                            if let Err(error) = farming_result
1168                                && let Some(error_sender) = error_sender.lock().take()
1169                                && let Err(error) = error_sender.send(error.into())
1170                            {
1171                                error!(
1172                                    %error,
1173                                    "Farming failed to send error to background task",
1174                                );
1175                            }
1176                        }
1177                        _ = stop_receiver.recv().fuse() => {
1178                            // Nothing, just exit
1179                        }
1180                    }
1181                });
1182            }
1183        });
1184        let farming_join_handle = AsyncJoinOnDrop::new(farming_join_handle, false);
1185
1186        tasks.push(Box::pin(async move {
1187            // Panic will already be printed by now
1188            farming_join_handle.await.map_err(|_error| {
1189                BackgroundTaskError::BackgroundTaskPanicked {
1190                    task: format!("farming-{farm_index}"),
1191                }
1192            })
1193        }));
1194
1195        let (piece_reader, reading_fut) = DiskPieceReader::new::<PosTable>(
1196            public_key_hash,
1197            shard_commitments_roots_cache.clone(),
1198            pieces_in_sector,
1199            plot_file,
1200            StdArc::clone(&sectors_metadata),
1201            erasure_coding,
1202            sectors_being_modified,
1203            global_mutex,
1204        );
1205
1206        let reading_join_handle = task::spawn_blocking({
1207            let mut stop_receiver = stop_sender.subscribe();
1208            let reading_fut = reading_fut.instrument(span.clone());
1209
1210            move || {
1211                Handle::current().block_on(async {
1212                    select! {
1213                        () = reading_fut.fuse() => {
1214                            // Nothing, just exit
1215                        }
1216                        _ = stop_receiver.recv().fuse() => {
1217                            // Nothing, just exit
1218                        }
1219                    }
1220                });
1221            }
1222        });
1223
1224        let reading_join_handle = AsyncJoinOnDrop::new(reading_join_handle, false);
1225
1226        tasks.push(Box::pin(async move {
1227            // Panic will already be printed by now
1228            reading_join_handle.await.map_err(|_error| {
1229                BackgroundTaskError::BackgroundTaskPanicked {
1230                    task: format!("reading-{farm_index}"),
1231                }
1232            })
1233        }));
1234
1235        tasks.push(Box::pin(async move {
1236            match block_sealing(node_client, identity).await {
1237                Ok(block_sealing_fut) => {
1238                    block_sealing_fut.await;
1239                }
1240                Err(error) => {
1241                    return Err(BackgroundTaskError::BlockSealing(anyhow::anyhow!(
1242                        "Failed to subscribe to block sealing notifications: {error}"
1243                    )));
1244                }
1245            }
1246
1247            Ok(())
1248        }));
1249
1250        let farm = Self {
1251            farmer_protocol_info: farmer_app_info.protocol_info,
1252            single_disk_farm_info,
1253            shard_commitments_roots_cache,
1254            sectors_metadata,
1255            pieces_in_sector,
1256            total_sectors_count: target_sector_count,
1257            span,
1258            tasks,
1259            handlers,
1260            piece_cache,
1261            plot_cache,
1262            piece_reader,
1263            start_sender: Some(start_sender),
1264            stop_sender: Some(stop_sender),
1265            _single_disk_farm_info_lock: single_disk_farm_info_lock,
1266        };
1267        Ok(farm)
1268    }
1269
1270    fn init(
1271        directory: &PathBuf,
1272        farmer_app_info: &FarmerAppInfo,
1273        allocated_space: u64,
1274        max_pieces_in_sector: u16,
1275        cache_percentage: u8,
1276        disable_farm_locking: bool,
1277        create: bool,
1278    ) -> Result<SingleDiskFarmInit, SingleDiskFarmError> {
1279        fs::create_dir_all(directory)?;
1280
1281        let identity = if create {
1282            Identity::open_or_create(directory)?
1283        } else {
1284            Identity::open(directory)?.ok_or_else(|| {
1285                IdentityError::Io(io::Error::new(
1286                    io::ErrorKind::NotFound,
1287                    "Farm does not exist and creation was explicitly disabled",
1288                ))
1289            })?
1290        };
1291        let public_key = identity.public_key();
1292
1293        let (single_disk_farm_info, single_disk_farm_info_lock) = if let Some(
1294            mut single_disk_farm_info,
1295        ) =
1296            SingleDiskFarmInfo::load_from(directory)?
1297        {
1298            if &farmer_app_info.genesis_root != single_disk_farm_info.genesis_root() {
1299                return Err(SingleDiskFarmError::WrongChain {
1300                    id: *single_disk_farm_info.id(),
1301                    correct_chain: hex::encode(single_disk_farm_info.genesis_root()),
1302                    wrong_chain: hex::encode(farmer_app_info.genesis_root),
1303                });
1304            }
1305
1306            if &public_key != single_disk_farm_info.public_key() {
1307                return Err(SingleDiskFarmError::IdentityMismatch {
1308                    id: *single_disk_farm_info.id(),
1309                    correct_public_key: *single_disk_farm_info.public_key(),
1310                    wrong_public_key: public_key,
1311                });
1312            }
1313
1314            let pieces_in_sector = single_disk_farm_info.pieces_in_sector();
1315
1316            if max_pieces_in_sector < pieces_in_sector {
1317                return Err(SingleDiskFarmError::InvalidPiecesInSector {
1318                    id: *single_disk_farm_info.id(),
1319                    max_supported: max_pieces_in_sector,
1320                    initialized_with: pieces_in_sector,
1321                });
1322            }
1323
1324            if max_pieces_in_sector > pieces_in_sector {
1325                info!(
1326                    pieces_in_sector,
1327                    max_pieces_in_sector,
1328                    "Farm initialized with smaller number of pieces in sector, farm needs \
1329                        to be re-created for increase"
1330                );
1331            }
1332
1333            let mut single_disk_farm_info_lock = None;
1334
1335            if allocated_space != single_disk_farm_info.allocated_space() {
1336                info!(
1337                    old_space = %ByteSize::b(single_disk_farm_info.allocated_space()).display().iec(),
1338                    new_space = %ByteSize::b(allocated_space).display().iec(),
1339                    "Farm size has changed"
1340                );
1341
1342                let new_allocated_space = allocated_space;
1343                #[expect(
1344                    clippy::rest_pattern_accessible_field,
1345                    reason = "Do not need other fields"
1346                )]
1347                match &mut single_disk_farm_info {
1348                    SingleDiskFarmInfo::V0 {
1349                        allocated_space, ..
1350                    } => {
1351                        *allocated_space = new_allocated_space;
1352                    }
1353                }
1354
1355                single_disk_farm_info_lock =
1356                    single_disk_farm_info.store_to(directory, !disable_farm_locking)?;
1357            } else if !disable_farm_locking {
1358                single_disk_farm_info_lock = Some(
1359                    SingleDiskFarmInfo::try_lock(directory)
1360                        .map_err(SingleDiskFarmError::LikelyAlreadyInUse)?,
1361                );
1362            }
1363
1364            (single_disk_farm_info, single_disk_farm_info_lock)
1365        } else {
1366            let single_disk_farm_info = SingleDiskFarmInfo::new(
1367                FarmId::new(),
1368                farmer_app_info.genesis_root,
1369                public_key,
1370                identity.shard_commitments_seed(),
1371                max_pieces_in_sector,
1372                allocated_space,
1373            );
1374
1375            let single_disk_farm_info_lock =
1376                single_disk_farm_info.store_to(directory, !disable_farm_locking)?;
1377
1378            (single_disk_farm_info, single_disk_farm_info_lock)
1379        };
1380
1381        let pieces_in_sector = single_disk_farm_info.pieces_in_sector();
1382        let sector_size = sector_size(pieces_in_sector) as u64;
1383        let sector_metadata_size = SectorMetadataChecksummed::encoded_size();
1384        let allocated_space_distribution = AllocatedSpaceDistribution::new(
1385            allocated_space,
1386            sector_size,
1387            cache_percentage,
1388            sector_metadata_size as u64,
1389        )?;
1390        let target_sector_count = allocated_space_distribution.target_sector_count;
1391
1392        let metadata_file_path = directory.join(Self::METADATA_FILE);
1393        let metadata_file = DirectIoFileWrapper::open(&metadata_file_path)?;
1394
1395        let metadata_size = metadata_file.size()?;
1396        let expected_metadata_size = allocated_space_distribution.metadata_file_size;
1397        // Align plot file size for disk sector size
1398        let expected_metadata_size =
1399            expected_metadata_size.div_ceil(DISK_PAGE_SIZE as u64) * DISK_PAGE_SIZE as u64;
1400        let metadata_header = if metadata_size == 0 {
1401            let metadata_header = PlotMetadataHeader {
1402                version: SingleDiskFarm::SUPPORTED_PLOT_VERSION,
1403                plotted_sector_count: 0,
1404            };
1405
1406            metadata_file
1407                .preallocate(expected_metadata_size)
1408                .map_err(SingleDiskFarmError::CantPreallocateMetadataFile)?;
1409            metadata_file.write_all_at(metadata_header.encode().as_slice(), 0)?;
1410
1411            metadata_header
1412        } else {
1413            if metadata_size != expected_metadata_size {
1414                // Allocating the whole file (`set_len` below can create a sparse file, which will
1415                // cause writes to fail later)
1416                metadata_file
1417                    .preallocate(expected_metadata_size)
1418                    .map_err(SingleDiskFarmError::CantPreallocateMetadataFile)?;
1419                // Truncating file (if necessary)
1420                metadata_file.set_len(expected_metadata_size)?;
1421            }
1422
1423            let mut metadata_header_bytes = vec![0; PlotMetadataHeader::encoded_size()];
1424            metadata_file.read_exact_at(&mut metadata_header_bytes, 0)?;
1425
1426            let mut metadata_header =
1427                PlotMetadataHeader::decode(&mut metadata_header_bytes.as_ref())
1428                    .map_err(SingleDiskFarmError::FailedToDecodeMetadataHeader)?;
1429
1430            if metadata_header.version != SingleDiskFarm::SUPPORTED_PLOT_VERSION {
1431                return Err(SingleDiskFarmError::UnexpectedMetadataVersion(
1432                    metadata_header.version,
1433                ));
1434            }
1435
1436            if metadata_header.plotted_sector_count > target_sector_count {
1437                metadata_header.plotted_sector_count = target_sector_count;
1438                metadata_file.write_all_at(&metadata_header.encode(), 0)?;
1439            }
1440
1441            metadata_header
1442        };
1443
1444        let sectors_metadata = {
1445            let mut sectors_metadata =
1446                Vec::<SectorMetadataChecksummed>::with_capacity(usize::from(target_sector_count));
1447
1448            let mut sector_metadata_bytes = vec![0; sector_metadata_size];
1449            for sector_index in
1450                SectorIndex::ZERO..SectorIndex::from(metadata_header.plotted_sector_count)
1451            {
1452                let sector_offset =
1453                    RESERVED_PLOT_METADATA + sector_metadata_size as u64 * u64::from(sector_index);
1454                metadata_file.read_exact_at(&mut sector_metadata_bytes, sector_offset)?;
1455
1456                let sector_metadata =
1457                    match SectorMetadataChecksummed::decode(&mut sector_metadata_bytes.as_ref()) {
1458                        Ok(sector_metadata) => sector_metadata,
1459                        Err(error) => {
1460                            warn!(
1461                                path = %metadata_file_path.display(),
1462                                %error,
1463                                %sector_index,
1464                                "Failed to decode sector metadata, replacing with dummy expired \
1465                                sector metadata"
1466                            );
1467
1468                            let dummy_sector = SectorMetadataChecksummed::from(SectorMetadata {
1469                                sector_index,
1470                                pieces_in_sector,
1471                                s_bucket_sizes: Box::new([0; Record::NUM_S_BUCKETS]),
1472                                history_size: HistorySize::from(SegmentIndex::ZERO),
1473                            });
1474                            metadata_file.write_all_at(&dummy_sector.encode(), sector_offset)?;
1475
1476                            dummy_sector
1477                        }
1478                    };
1479                sectors_metadata.push(sector_metadata);
1480            }
1481
1482            StdArc::new(AsyncRwLock::new(sectors_metadata))
1483        };
1484
1485        let plot_file = DirectIoFileWrapper::open(directory.join(Self::PLOT_FILE))?;
1486
1487        if plot_file.size()? != allocated_space_distribution.plot_file_size {
1488            // Allocating the whole file (`set_len` below can create a sparse file, which will cause
1489            // writes to fail later)
1490            plot_file
1491                .preallocate(allocated_space_distribution.plot_file_size)
1492                .map_err(SingleDiskFarmError::CantPreallocatePlotFile)?;
1493            // Truncating file (if necessary)
1494            plot_file.set_len(allocated_space_distribution.plot_file_size)?;
1495        }
1496
1497        let plot_file = StdArc::new(plot_file);
1498
1499        let plot_cache = DiskPlotCache::new(
1500            &plot_file,
1501            &sectors_metadata,
1502            target_sector_count,
1503            sector_size,
1504        );
1505
1506        Ok(SingleDiskFarmInit {
1507            identity,
1508            single_disk_farm_info,
1509            single_disk_farm_info_lock,
1510            plot_file,
1511            metadata_file,
1512            metadata_header,
1513            target_sector_count,
1514            sectors_metadata,
1515            piece_cache_capacity: allocated_space_distribution.piece_cache_capacity,
1516            plot_cache,
1517        })
1518    }
1519
1520    /// Collect summary of single disk farm for presentational purposes
1521    pub fn collect_summary(directory: PathBuf) -> SingleDiskFarmSummary {
1522        let single_disk_farm_info = match SingleDiskFarmInfo::load_from(&directory) {
1523            Ok(Some(single_disk_farm_info)) => single_disk_farm_info,
1524            Ok(None) => {
1525                return SingleDiskFarmSummary::NotFound { directory };
1526            }
1527            Err(error) => {
1528                return SingleDiskFarmSummary::Error { directory, error };
1529            }
1530        };
1531
1532        SingleDiskFarmSummary::Found {
1533            info: single_disk_farm_info,
1534            directory,
1535        }
1536    }
1537
1538    /// Effective on-disk allocation of the files related to the farm (takes some buffer space
1539    /// into consideration).
1540    ///
1541    /// This is a helpful number in case some files were not allocated properly or were removed and
1542    /// do not correspond to allocated space in the farm info accurately.
1543    pub fn effective_disk_usage(
1544        directory: &Path,
1545        cache_percentage: u8,
1546    ) -> Result<u64, SingleDiskFarmError> {
1547        let mut effective_disk_usage;
1548        match SingleDiskFarmInfo::load_from(directory)? {
1549            Some(single_disk_farm_info) => {
1550                let allocated_space_distribution = AllocatedSpaceDistribution::new(
1551                    single_disk_farm_info.allocated_space(),
1552                    sector_size(single_disk_farm_info.pieces_in_sector()) as u64,
1553                    cache_percentage,
1554                    SectorMetadataChecksummed::encoded_size() as u64,
1555                )?;
1556
1557                effective_disk_usage = single_disk_farm_info.allocated_space();
1558                effective_disk_usage -= Identity::file_size() as u64;
1559                effective_disk_usage -= allocated_space_distribution.metadata_file_size;
1560                effective_disk_usage -= allocated_space_distribution.plot_file_size;
1561                effective_disk_usage -= allocated_space_distribution.piece_cache_file_size;
1562            }
1563            None => {
1564                // No farm info, try to collect actual file sizes is any
1565                effective_disk_usage = 0;
1566            }
1567        }
1568
1569        if Identity::open(directory)?.is_some() {
1570            effective_disk_usage += Identity::file_size() as u64;
1571        }
1572
1573        match OpenOptions::new()
1574            .read(true)
1575            .open(directory.join(Self::METADATA_FILE))
1576        {
1577            Ok(metadata_file) => {
1578                effective_disk_usage += metadata_file.size()?;
1579            }
1580            Err(error) => {
1581                if error.kind() == io::ErrorKind::NotFound {
1582                    // File is not stored on disk
1583                } else {
1584                    return Err(error.into());
1585                }
1586            }
1587        }
1588
1589        match OpenOptions::new()
1590            .read(true)
1591            .open(directory.join(Self::PLOT_FILE))
1592        {
1593            Ok(plot_file) => {
1594                effective_disk_usage += plot_file.size()?;
1595            }
1596            Err(error) => {
1597                if error.kind() == io::ErrorKind::NotFound {
1598                    // File is not stored on disk
1599                } else {
1600                    return Err(error.into());
1601                }
1602            }
1603        }
1604
1605        match OpenOptions::new()
1606            .read(true)
1607            .open(directory.join(DiskPieceCache::FILE_NAME))
1608        {
1609            Ok(piece_cache) => {
1610                effective_disk_usage += piece_cache.size()?;
1611            }
1612            Err(error) => {
1613                if error.kind() == io::ErrorKind::NotFound {
1614                    // File is not stored on disk
1615                } else {
1616                    return Err(error.into());
1617                }
1618            }
1619        }
1620
1621        Ok(effective_disk_usage)
1622    }
1623
1624    /// Read all sectors metadata
1625    pub fn read_all_sectors_metadata(
1626        directory: &Path,
1627    ) -> io::Result<Vec<SectorMetadataChecksummed>> {
1628        let metadata_file = DirectIoFileWrapper::open(directory.join(Self::METADATA_FILE))?;
1629
1630        let metadata_size = metadata_file.size()?;
1631        let sector_metadata_size = SectorMetadataChecksummed::encoded_size();
1632
1633        let mut metadata_header_bytes = vec![0; PlotMetadataHeader::encoded_size()];
1634        metadata_file.read_exact_at(&mut metadata_header_bytes, 0)?;
1635
1636        let metadata_header = PlotMetadataHeader::decode(&mut metadata_header_bytes.as_ref())
1637            .map_err(|error| {
1638                io::Error::other(format!("Failed to decode metadata header: {error}"))
1639            })?;
1640
1641        if metadata_header.version != SingleDiskFarm::SUPPORTED_PLOT_VERSION {
1642            return Err(io::Error::other(format!(
1643                "Unsupported metadata version {}",
1644                metadata_header.version
1645            )));
1646        }
1647
1648        let mut sectors_metadata = Vec::<SectorMetadataChecksummed>::with_capacity(
1649            ((metadata_size - RESERVED_PLOT_METADATA) / sector_metadata_size as u64) as usize,
1650        );
1651
1652        let mut sector_metadata_bytes = vec![0; sector_metadata_size];
1653        for sector_index in 0..metadata_header.plotted_sector_count {
1654            metadata_file.read_exact_at(
1655                &mut sector_metadata_bytes,
1656                RESERVED_PLOT_METADATA + sector_metadata_size as u64 * u64::from(sector_index),
1657            )?;
1658            sectors_metadata.push(
1659                SectorMetadataChecksummed::decode(&mut sector_metadata_bytes.as_ref()).map_err(
1660                    |error| io::Error::other(format!("Failed to decode sector metadata: {error}")),
1661                )?,
1662            );
1663        }
1664
1665        Ok(sectors_metadata)
1666    }
1667
1668    /// ID of this farm
1669    pub fn id(&self) -> &FarmId {
1670        self.single_disk_farm_info.id()
1671    }
1672
1673    /// Info of this farm
1674    pub fn info(&self) -> &SingleDiskFarmInfo {
1675        &self.single_disk_farm_info
1676    }
1677
1678    /// Number of sectors in this farm
1679    pub fn total_sectors_count(&self) -> u16 {
1680        self.total_sectors_count
1681    }
1682
1683    /// Read information about sectors plotted so far
1684    pub fn plotted_sectors(&self) -> SingleDiskPlottedSectors {
1685        SingleDiskPlottedSectors {
1686            public_key_hash: self.single_disk_farm_info.public_key().hash(),
1687            shard_commitments_roots_cache: self.shard_commitments_roots_cache.clone(),
1688            pieces_in_sector: self.pieces_in_sector,
1689            farmer_protocol_info: self.farmer_protocol_info,
1690            sectors_metadata: StdArc::clone(&self.sectors_metadata),
1691        }
1692    }
1693
1694    /// Get piece cache instance
1695    pub fn piece_cache(&self) -> SingleDiskPieceCache {
1696        self.piece_cache.clone()
1697    }
1698
1699    /// Get plot cache instance
1700    pub fn plot_cache(&self) -> DiskPlotCache {
1701        self.plot_cache.clone()
1702    }
1703
1704    /// Get piece reader to read plotted pieces later
1705    pub fn piece_reader(&self) -> DiskPieceReader {
1706        self.piece_reader.clone()
1707    }
1708
1709    /// Subscribe to sector updates
1710    pub fn on_sector_update(&self, callback: HandlerFn<(SectorIndex, SectorUpdate)>) -> HandlerId {
1711        self.handlers.sector_update.add(callback)
1712    }
1713
1714    /// Subscribe to farming notifications
1715    pub fn on_farming_notification(&self, callback: HandlerFn<FarmingNotification>) -> HandlerId {
1716        self.handlers.farming_notification.add(callback)
1717    }
1718
1719    /// Subscribe to new solution notification
1720    pub fn on_solution(&self, callback: HandlerFn<SolutionResponse>) -> HandlerId {
1721        self.handlers.solution.add(callback)
1722    }
1723
1724    /// Run and wait for background threads to exit or return an error
1725    pub async fn run(mut self) -> anyhow::Result<()> {
1726        if let Some(start_sender) = self.start_sender.take() {
1727            // Do not care if anyone is listening on the other side
1728            let _: Result<_, _> = start_sender.send(());
1729        }
1730
1731        while let Some(result) = self.tasks.next().instrument(self.span.clone()).await {
1732            result?;
1733        }
1734
1735        Ok(())
1736    }
1737
1738    /// Wipe everything that belongs to this single disk farm
1739    pub fn wipe(directory: &Path) -> io::Result<()> {
1740        let single_disk_info_info_path = directory.join(SingleDiskFarmInfo::FILE_NAME);
1741        match SingleDiskFarmInfo::load_from(directory) {
1742            Ok(Some(single_disk_farm_info)) => {
1743                info!("Found single disk farm {}", single_disk_farm_info.id());
1744            }
1745            Ok(None) => {
1746                return Err(io::Error::new(
1747                    io::ErrorKind::NotFound,
1748                    format!(
1749                        "Single disk farm info not found at {}",
1750                        single_disk_info_info_path.display()
1751                    ),
1752                ));
1753            }
1754            Err(error) => {
1755                warn!("Found unknown single disk farm: {}", error);
1756            }
1757        }
1758
1759        {
1760            let plot = directory.join(Self::PLOT_FILE);
1761            if plot.exists() {
1762                info!("Deleting plot file at {}", plot.display());
1763                fs::remove_file(plot)?;
1764            }
1765        }
1766        {
1767            let metadata = directory.join(Self::METADATA_FILE);
1768            if metadata.exists() {
1769                info!("Deleting metadata file at {}", metadata.display());
1770                fs::remove_file(metadata)?;
1771            }
1772        }
1773        // TODO: Identity should be able to wipe itself instead of assuming a specific file name
1774        //  here
1775        {
1776            let identity = directory.join("identity.bin");
1777            if identity.exists() {
1778                info!("Deleting identity file at {}", identity.display());
1779                fs::remove_file(identity)?;
1780            }
1781        }
1782
1783        DiskPieceCache::wipe(directory)?;
1784
1785        info!(
1786            "Deleting info file at {}",
1787            single_disk_info_info_path.display()
1788        );
1789        fs::remove_file(single_disk_info_info_path)
1790    }
1791
1792    /// Check the farm for corruption and repair errors (caused by disk errors or something else),
1793    /// returns an error when irrecoverable errors occur.
1794    pub fn scrub(
1795        directory: &Path,
1796        disable_farm_locking: bool,
1797        target: ScrubTarget,
1798        dry_run: bool,
1799    ) -> Result<(), SingleDiskFarmScrubError> {
1800        let span = Span::current();
1801
1802        if dry_run {
1803            info!("Dry run is used, no changes will be written to disk");
1804        }
1805
1806        if target.metadata() || target.plot() {
1807            let info = {
1808                let file = directory.join(SingleDiskFarmInfo::FILE_NAME);
1809                info!(path = %file.display(), "Checking info file");
1810
1811                match SingleDiskFarmInfo::load_from(directory) {
1812                    Ok(Some(info)) => info,
1813                    Ok(None) => {
1814                        return Err(SingleDiskFarmScrubError::FarmInfoFileDoesNotExist { file });
1815                    }
1816                    Err(error) => {
1817                        return Err(SingleDiskFarmScrubError::FarmInfoCantBeOpened { file, error });
1818                    }
1819                }
1820            };
1821
1822            let _single_disk_farm_info_lock = if disable_farm_locking {
1823                None
1824            } else {
1825                Some(
1826                    SingleDiskFarmInfo::try_lock(directory)
1827                        .map_err(SingleDiskFarmScrubError::LikelyAlreadyInUse)?,
1828                )
1829            };
1830
1831            let identity = {
1832                let file = directory.join(Identity::FILE_NAME);
1833                info!(path = %file.display(), "Checking identity file");
1834
1835                match Identity::open(directory) {
1836                    Ok(Some(identity)) => identity,
1837                    Ok(None) => {
1838                        return Err(SingleDiskFarmScrubError::IdentityFileDoesNotExist { file });
1839                    }
1840                    Err(error) => {
1841                        return Err(SingleDiskFarmScrubError::IdentityCantBeOpened { file, error });
1842                    }
1843                }
1844            };
1845
1846            if &identity.public_key() != info.public_key() {
1847                return Err(SingleDiskFarmScrubError::PublicKeyMismatch {
1848                    identity: identity.public_key(),
1849                    info: *info.public_key(),
1850                });
1851            }
1852
1853            let sector_metadata_size = SectorMetadataChecksummed::encoded_size();
1854
1855            let metadata_file_path = directory.join(Self::METADATA_FILE);
1856            let (metadata_file, mut metadata_header) = {
1857                info!(path = %metadata_file_path.display(), "Checking metadata file");
1858
1859                let metadata_file = match OpenOptions::new()
1860                    .read(true)
1861                    .write(!dry_run)
1862                    .open(&metadata_file_path)
1863                {
1864                    Ok(metadata_file) => metadata_file,
1865                    Err(error) => {
1866                        return Err(if error.kind() == io::ErrorKind::NotFound {
1867                            SingleDiskFarmScrubError::MetadataFileDoesNotExist {
1868                                file: metadata_file_path,
1869                            }
1870                        } else {
1871                            SingleDiskFarmScrubError::MetadataCantBeOpened {
1872                                file: metadata_file_path,
1873                                error,
1874                            }
1875                        });
1876                    }
1877                };
1878
1879                // Error doesn't matter here
1880                let _: Result<(), _> = metadata_file.advise_sequential_access();
1881
1882                let metadata_size = match metadata_file.size() {
1883                    Ok(metadata_size) => metadata_size,
1884                    Err(error) => {
1885                        return Err(SingleDiskFarmScrubError::FailedToDetermineFileSize {
1886                            file: metadata_file_path,
1887                            error,
1888                        });
1889                    }
1890                };
1891
1892                if metadata_size < RESERVED_PLOT_METADATA {
1893                    return Err(SingleDiskFarmScrubError::MetadataFileTooSmall {
1894                        file: metadata_file_path,
1895                        reserved_size: RESERVED_PLOT_METADATA,
1896                        size: metadata_size,
1897                    });
1898                }
1899
1900                let mut metadata_header = {
1901                    let mut reserved_metadata = vec![0; RESERVED_PLOT_METADATA as usize];
1902
1903                    if let Err(error) = metadata_file.read_exact_at(&mut reserved_metadata, 0) {
1904                        return Err(SingleDiskFarmScrubError::FailedToReadBytes {
1905                            file: metadata_file_path,
1906                            size: RESERVED_PLOT_METADATA,
1907                            offset: 0,
1908                            error,
1909                        });
1910                    }
1911
1912                    PlotMetadataHeader::decode(&mut reserved_metadata.as_slice())
1913                        .map_err(SingleDiskFarmScrubError::FailedToDecodeMetadataHeader)?
1914                };
1915
1916                if metadata_header.version != SingleDiskFarm::SUPPORTED_PLOT_VERSION {
1917                    return Err(SingleDiskFarmScrubError::UnexpectedMetadataVersion(
1918                        metadata_header.version,
1919                    ));
1920                }
1921
1922                let plotted_sector_count = metadata_header.plotted_sector_count;
1923
1924                let expected_metadata_size = RESERVED_PLOT_METADATA
1925                    + sector_metadata_size as u64 * u64::from(plotted_sector_count);
1926
1927                if metadata_size < expected_metadata_size {
1928                    warn!(
1929                        %metadata_size,
1930                        %expected_metadata_size,
1931                        "Metadata file size is smaller than expected, shrinking number of plotted \
1932                        sectors to correct value"
1933                    );
1934
1935                    metadata_header.plotted_sector_count =
1936                        ((metadata_size - RESERVED_PLOT_METADATA) / sector_metadata_size as u64)
1937                            as u16;
1938                    let metadata_header_bytes = metadata_header.encode();
1939
1940                    if !dry_run
1941                        && let Err(error) = metadata_file.write_all_at(&metadata_header_bytes, 0)
1942                    {
1943                        return Err(SingleDiskFarmScrubError::FailedToWriteBytes {
1944                            file: metadata_file_path,
1945                            size: metadata_header_bytes.len() as u64,
1946                            offset: 0,
1947                            error,
1948                        });
1949                    }
1950                }
1951
1952                (metadata_file, metadata_header)
1953            };
1954
1955            let pieces_in_sector = info.pieces_in_sector();
1956            let sector_size = sector_size(pieces_in_sector) as u64;
1957
1958            let plot_file_path = directory.join(Self::PLOT_FILE);
1959            let plot_file = {
1960                let plot_file_path = directory.join(Self::PLOT_FILE);
1961                info!(path = %plot_file_path.display(), "Checking plot file");
1962
1963                let plot_file = match OpenOptions::new()
1964                    .read(true)
1965                    .write(!dry_run)
1966                    .open(&plot_file_path)
1967                {
1968                    Ok(plot_file) => plot_file,
1969                    Err(error) => {
1970                        return Err(if error.kind() == io::ErrorKind::NotFound {
1971                            SingleDiskFarmScrubError::MetadataFileDoesNotExist {
1972                                file: plot_file_path,
1973                            }
1974                        } else {
1975                            SingleDiskFarmScrubError::MetadataCantBeOpened {
1976                                file: plot_file_path,
1977                                error,
1978                            }
1979                        });
1980                    }
1981                };
1982
1983                // Error doesn't matter here
1984                let _: Result<(), _> = plot_file.advise_sequential_access();
1985
1986                let plot_size = match plot_file.size() {
1987                    Ok(metadata_size) => metadata_size,
1988                    Err(error) => {
1989                        return Err(SingleDiskFarmScrubError::FailedToDetermineFileSize {
1990                            file: plot_file_path,
1991                            error,
1992                        });
1993                    }
1994                };
1995
1996                let min_expected_plot_size =
1997                    u64::from(metadata_header.plotted_sector_count) * sector_size;
1998                if plot_size < min_expected_plot_size {
1999                    warn!(
2000                        %plot_size,
2001                        %min_expected_plot_size,
2002                        "Plot file size is smaller than expected, shrinking number of plotted \
2003                        sectors to correct value"
2004                    );
2005
2006                    metadata_header.plotted_sector_count = (plot_size / sector_size) as u16;
2007                    let metadata_header_bytes = metadata_header.encode();
2008
2009                    if !dry_run
2010                        && let Err(error) = metadata_file.write_all_at(&metadata_header_bytes, 0)
2011                    {
2012                        return Err(SingleDiskFarmScrubError::FailedToWriteBytes {
2013                            file: plot_file_path,
2014                            size: metadata_header_bytes.len() as u64,
2015                            offset: 0,
2016                            error,
2017                        });
2018                    }
2019                }
2020
2021                plot_file
2022            };
2023
2024            let sector_bytes_range = 0..(sector_size as usize - Blake3Hash::SIZE);
2025
2026            info!("Checking sectors and corresponding metadata");
2027            (0..metadata_header.plotted_sector_count)
2028                .into_par_iter()
2029                .map(SectorIndex::from)
2030                .map_init(
2031                    || vec![0u8; Record::SIZE],
2032                    |scratch_buffer, sector_index| {
2033                        let _span_guard = span.enter();
2034
2035                        let offset = RESERVED_PLOT_METADATA
2036                            + u64::from(sector_index) * sector_metadata_size as u64;
2037                        if let Err(error) = metadata_file
2038                            .read_exact_at(&mut scratch_buffer[..sector_metadata_size], offset)
2039                        {
2040                            warn!(
2041                                path = %metadata_file_path.display(),
2042                                %error,
2043                                %offset,
2044                                size = %sector_metadata_size,
2045                                %sector_index,
2046                                "Failed to read sector metadata, replacing with dummy expired \
2047                                sector metadata"
2048                            );
2049
2050                            if !dry_run {
2051                                write_dummy_sector_metadata(
2052                                    &metadata_file,
2053                                    &metadata_file_path,
2054                                    sector_index,
2055                                    pieces_in_sector,
2056                                )?;
2057                            }
2058                            return Ok(());
2059                        }
2060
2061                        let sector_metadata = match SectorMetadataChecksummed::decode(
2062                            &mut &scratch_buffer[..sector_metadata_size],
2063                        ) {
2064                            Ok(sector_metadata) => sector_metadata,
2065                            Err(error) => {
2066                                warn!(
2067                                    path = %metadata_file_path.display(),
2068                                    %error,
2069                                    %sector_index,
2070                                    "Failed to decode sector metadata, replacing with dummy \
2071                                    expired sector metadata"
2072                                );
2073
2074                                if !dry_run {
2075                                    write_dummy_sector_metadata(
2076                                        &metadata_file,
2077                                        &metadata_file_path,
2078                                        sector_index,
2079                                        pieces_in_sector,
2080                                    )?;
2081                                }
2082                                return Ok(());
2083                            }
2084                        };
2085
2086                        if sector_metadata.sector_index != sector_index {
2087                            warn!(
2088                                path = %metadata_file_path.display(),
2089                                %sector_index,
2090                                found_sector_index = %sector_metadata.sector_index,
2091                                "Sector index mismatch, replacing with dummy expired sector \
2092                                metadata"
2093                            );
2094
2095                            if !dry_run {
2096                                write_dummy_sector_metadata(
2097                                    &metadata_file,
2098                                    &metadata_file_path,
2099                                    sector_index,
2100                                    pieces_in_sector,
2101                                )?;
2102                            }
2103                            return Ok(());
2104                        }
2105
2106                        if sector_metadata.pieces_in_sector != pieces_in_sector {
2107                            warn!(
2108                                path = %metadata_file_path.display(),
2109                                %sector_index,
2110                                %pieces_in_sector,
2111                                found_pieces_in_sector = sector_metadata.pieces_in_sector,
2112                                "Pieces in sector mismatch, replacing with dummy expired sector \
2113                                metadata"
2114                            );
2115
2116                            if !dry_run {
2117                                write_dummy_sector_metadata(
2118                                    &metadata_file,
2119                                    &metadata_file_path,
2120                                    sector_index,
2121                                    pieces_in_sector,
2122                                )?;
2123                            }
2124                            return Ok(());
2125                        }
2126
2127                        if target.plot() {
2128                            let mut hasher = blake3::Hasher::new();
2129                            // Read sector bytes and compute checksum
2130                            for offset_in_sector in
2131                                sector_bytes_range.clone().step_by(scratch_buffer.len())
2132                            {
2133                                let offset =
2134                                    u64::from(sector_index) * sector_size + offset_in_sector as u64;
2135                                let bytes_to_read = (offset_in_sector + scratch_buffer.len())
2136                                    .min(sector_bytes_range.end)
2137                                    - offset_in_sector;
2138
2139                                let bytes = &mut scratch_buffer[..bytes_to_read];
2140
2141                                if let Err(error) = plot_file.read_exact_at(bytes, offset) {
2142                                    warn!(
2143                                        path = %plot_file_path.display(),
2144                                        %error,
2145                                        %sector_index,
2146                                        %offset,
2147                                        size = %bytes.len() as u64,
2148                                        "Failed to read sector bytes"
2149                                    );
2150
2151                                    continue;
2152                                }
2153
2154                                hasher.update(bytes);
2155                            }
2156
2157                            let actual_checksum = *hasher.finalize().as_bytes();
2158                            let mut expected_checksum = [0; Blake3Hash::SIZE];
2159                            {
2160                                let offset = u64::from(sector_index) * sector_size
2161                                    + sector_bytes_range.end as u64;
2162                                if let Err(error) =
2163                                    plot_file.read_exact_at(&mut expected_checksum, offset)
2164                                {
2165                                    warn!(
2166                                        path = %plot_file_path.display(),
2167                                        %error,
2168                                        %sector_index,
2169                                        %offset,
2170                                        size = %expected_checksum.len() as u64,
2171                                        "Failed to read sector checksum bytes"
2172                                    );
2173                                }
2174                            }
2175
2176                            // Verify checksum
2177                            if actual_checksum != expected_checksum {
2178                                warn!(
2179                                    path = %plot_file_path.display(),
2180                                    %sector_index,
2181                                    actual_checksum = %hex::encode(actual_checksum),
2182                                    expected_checksum = %hex::encode(expected_checksum),
2183                                    "Plotted sector checksum mismatch, replacing with dummy \
2184                                    expired sector"
2185                                );
2186
2187                                if !dry_run {
2188                                    write_dummy_sector_metadata(
2189                                        &metadata_file,
2190                                        &metadata_file_path,
2191                                        sector_index,
2192                                        pieces_in_sector,
2193                                    )?;
2194                                }
2195
2196                                scratch_buffer.fill(0);
2197
2198                                hasher.reset();
2199                                // Fill sector with zeroes and compute checksum
2200                                for offset_in_sector in
2201                                    sector_bytes_range.clone().step_by(scratch_buffer.len())
2202                                {
2203                                    let offset = u64::from(sector_index) * sector_size
2204                                        + offset_in_sector as u64;
2205                                    let bytes_to_write = (offset_in_sector + scratch_buffer.len())
2206                                        .min(sector_bytes_range.end)
2207                                        - offset_in_sector;
2208                                    let bytes = &mut scratch_buffer[..bytes_to_write];
2209
2210                                    if !dry_run
2211                                        && let Err(error) = plot_file.write_all_at(bytes, offset)
2212                                    {
2213                                        return Err(SingleDiskFarmScrubError::FailedToWriteBytes {
2214                                            file: plot_file_path.clone(),
2215                                            size: scratch_buffer.len() as u64,
2216                                            offset,
2217                                            error,
2218                                        });
2219                                    }
2220
2221                                    hasher.update(bytes);
2222                                }
2223                                // Write checksum
2224                                {
2225                                    let checksum = *hasher.finalize().as_bytes();
2226                                    let offset = u64::from(sector_index) * sector_size
2227                                        + sector_bytes_range.end as u64;
2228                                    if !dry_run
2229                                        && let Err(error) =
2230                                            plot_file.write_all_at(&checksum, offset)
2231                                    {
2232                                        return Err(SingleDiskFarmScrubError::FailedToWriteBytes {
2233                                            file: plot_file_path.clone(),
2234                                            size: checksum.len() as u64,
2235                                            offset,
2236                                            error,
2237                                        });
2238                                    }
2239                                }
2240
2241                                return Ok(());
2242                            }
2243                        }
2244
2245                        trace!(%sector_index, "Sector is in good shape");
2246
2247                        Ok(())
2248                    },
2249                )
2250                .try_for_each({
2251                    let span = &span;
2252                    let checked_sectors = AtomicUsize::new(0);
2253
2254                    move |result| {
2255                        let _span_guard = span.enter();
2256
2257                        let checked_sectors = checked_sectors.fetch_add(1, Ordering::Relaxed);
2258                        if checked_sectors > 1 && checked_sectors.is_multiple_of(10) {
2259                            info!(
2260                                "Checked {}/{} sectors",
2261                                checked_sectors, metadata_header.plotted_sector_count
2262                            );
2263                        }
2264
2265                        result
2266                    }
2267                })?;
2268        }
2269
2270        if target.cache() {
2271            Self::scrub_cache(directory, dry_run)?;
2272        }
2273
2274        info!("Farm check completed");
2275
2276        Ok(())
2277    }
2278
2279    fn scrub_cache(directory: &Path, dry_run: bool) -> Result<(), SingleDiskFarmScrubError> {
2280        let span = Span::current();
2281
2282        let file = directory.join(DiskPieceCache::FILE_NAME);
2283        info!(path = %file.display(), "Checking cache file");
2284
2285        let cache_file = match OpenOptions::new().read(true).write(!dry_run).open(&file) {
2286            Ok(plot_file) => plot_file,
2287            Err(error) => {
2288                return if error.kind() == io::ErrorKind::NotFound {
2289                    warn!(
2290                        file = %file.display(),
2291                        "Cache file does not exist, this is expected in farming cluster"
2292                    );
2293                    Ok(())
2294                } else {
2295                    Err(SingleDiskFarmScrubError::CacheCantBeOpened { file, error })
2296                };
2297            }
2298        };
2299
2300        // Error doesn't matter here
2301        let _: Result<(), _> = cache_file.advise_sequential_access();
2302
2303        let cache_size = match cache_file.size() {
2304            Ok(cache_size) => cache_size,
2305            Err(error) => {
2306                return Err(SingleDiskFarmScrubError::FailedToDetermineFileSize { file, error });
2307            }
2308        };
2309
2310        let element_size = DiskPieceCache::element_size();
2311        let number_of_cached_elements = cache_size / u64::from(element_size);
2312        let dummy_element = vec![0; element_size as usize];
2313        (0..number_of_cached_elements)
2314            .into_par_iter()
2315            .map_with(vec![0; element_size as usize], |element, cache_offset| {
2316                let _span_guard = span.enter();
2317
2318                let offset = cache_offset * u64::from(element_size);
2319                if let Err(error) = cache_file.read_exact_at(element, offset) {
2320                    warn!(
2321                        path = %file.display(),
2322                        %cache_offset,
2323                        size = %element.len() as u64,
2324                        %offset,
2325                        %error,
2326                        "Failed to read cached piece, replacing with dummy element"
2327                    );
2328
2329                    if !dry_run && let Err(error) = cache_file.write_all_at(&dummy_element, offset)
2330                    {
2331                        return Err(SingleDiskFarmScrubError::FailedToWriteBytes {
2332                            file: file.clone(),
2333                            size: u64::from(element_size),
2334                            offset,
2335                            error,
2336                        });
2337                    }
2338
2339                    return Ok(());
2340                }
2341
2342                let (index_and_piece_bytes, expected_checksum) =
2343                    element.split_at(element_size as usize - Blake3Hash::SIZE);
2344                let actual_checksum = *blake3::hash(index_and_piece_bytes).as_bytes();
2345                if actual_checksum != expected_checksum && element != &dummy_element {
2346                    warn!(
2347                        %cache_offset,
2348                        actual_checksum = %hex::encode(actual_checksum),
2349                        expected_checksum = %hex::encode(expected_checksum),
2350                        "Cached piece checksum mismatch, replacing with dummy element"
2351                    );
2352
2353                    if !dry_run && let Err(error) = cache_file.write_all_at(&dummy_element, offset)
2354                    {
2355                        return Err(SingleDiskFarmScrubError::FailedToWriteBytes {
2356                            file: file.clone(),
2357                            size: u64::from(element_size),
2358                            offset,
2359                            error,
2360                        });
2361                    }
2362
2363                    return Ok(());
2364                }
2365
2366                Ok(())
2367            })
2368            .try_for_each({
2369                let span = &span;
2370                let checked_elements = AtomicUsize::new(0);
2371
2372                move |result| {
2373                    let _span_guard = span.enter();
2374
2375                    let checked_elements = checked_elements.fetch_add(1, Ordering::Relaxed);
2376                    if checked_elements > 1 && checked_elements.is_multiple_of(1000) {
2377                        info!(
2378                            "Checked {}/{} cache elements",
2379                            checked_elements, number_of_cached_elements
2380                        );
2381                    }
2382
2383                    result
2384                }
2385            })?;
2386
2387        Ok(())
2388    }
2389}
2390
2391fn write_dummy_sector_metadata(
2392    metadata_file: &File,
2393    metadata_file_path: &Path,
2394    sector_index: SectorIndex,
2395    pieces_in_sector: u16,
2396) -> Result<(), SingleDiskFarmScrubError> {
2397    let dummy_sector_bytes = SectorMetadataChecksummed::from(SectorMetadata {
2398        sector_index,
2399        pieces_in_sector,
2400        s_bucket_sizes: Box::new([0; Record::NUM_S_BUCKETS]),
2401        history_size: HistorySize::from(SegmentIndex::ZERO),
2402    })
2403    .encode();
2404    let sector_offset = RESERVED_PLOT_METADATA
2405        + u64::from(sector_index) * SectorMetadataChecksummed::encoded_size() as u64;
2406    metadata_file
2407        .write_all_at(&dummy_sector_bytes, sector_offset)
2408        .map_err(|error| SingleDiskFarmScrubError::FailedToWriteBytes {
2409            file: metadata_file_path.to_path_buf(),
2410            size: dummy_sector_bytes.len() as u64,
2411            offset: sector_offset,
2412            error,
2413        })
2414}