From b3bde4cc8ff39e463921ea4020909dd39511d6fa Mon Sep 17 00:00:00 2001 From: bubbapang Date: Fri, 4 Sep 2026 11:13:09 +0800 Subject: [PATCH 1/4] miner-app: check free disk space before a fresh node install A fresh install downloads the node and then syncs roughly 100 GB of chain data into ~/.quantus/node_data (chain/MINING.md: 100 GB minimum, 500 GB recommended). The setup screen had no free-space check, so a machine with too little room would download successfully and then fail partway through sync with no explanation. Observed on a Windows 11 machine with 27 GB free. - DiskSpaceService.freeBytesForPath: probes the volume holding a path via PowerShell on Windows and POSIX `df -kP` elsewhere. Returns null instead of throwing so a broken probe can never block setup on its own. - MinerConfig: bytesPerGb, minNodeDiskBytes (100 GB), recommendedNodeDiskBytes (500 GB), sourced from chain/MINING.md. - node_setup_screen: inside the fresh-install branch only, throw a plain Exception before fetching the node version when free space is known and below the minimum. It surfaces through the existing catch (progress text plus SnackBar), so no new UI is introduced. The update path is deliberately not gated: a returning user's binary update is a small download onto a volume that already holds their chain data, and blocking a protocol or security update over disk space would be wrong. Co-Authored-By: Claude Sonnet 5 --- .../lib/features/setup/node_setup_screen.dart | 16 +++++++ miner-app/lib/src/config/miner_config.dart | 13 +++++ .../lib/src/services/disk_space_service.dart | 48 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 miner-app/lib/src/services/disk_space_service.dart diff --git a/miner-app/lib/features/setup/node_setup_screen.dart b/miner-app/lib/features/setup/node_setup_screen.dart index 6d89ca02a..f3ad71b00 100644 --- a/miner-app/lib/features/setup/node_setup_screen.dart +++ b/miner-app/lib/features/setup/node_setup_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:quantus_miner/src/services/binary_manager.dart'; +import 'package:quantus_miner/src/config/miner_config.dart'; +import 'package:quantus_miner/src/services/disk_space_service.dart'; import 'dart:io'; import 'package:flutter_svg/flutter_svg.dart'; @@ -66,6 +68,20 @@ class _NodeSetupScreenState extends State { try { // Install node binary first if (!_isNodeInstalled) { + // A fresh node install commits this volume to roughly 100 GB of chain data during sync. + // Refuse here rather than fail an hour into syncing. An unknown probe result passes. + final quantusHome = await BinaryManager.getQuantusHomeDirectoryPath(); + final freeBytes = await DiskSpaceService.freeBytesForPath(quantusHome); + if (freeBytes != null && freeBytes < MinerConfig.minNodeDiskBytes) { + final freeGb = (freeBytes / MinerConfig.bytesPerGb).toStringAsFixed(0); + final minGb = MinerConfig.minNodeDiskBytes ~/ MinerConfig.bytesPerGb; + final recommendedGb = MinerConfig.recommendedNodeDiskBytes ~/ MinerConfig.bytesPerGb; + throw Exception( + 'Not enough free disk space for a Quantus node: $freeGb GB free at $quantusHome, ' + '$minGb GB required ($recommendedGb GB recommended). The node stores chain data there while syncing.', + ); + } + final nodeVersion = await BinaryManager.getLatestNodeVersion(); setState(() { diff --git a/miner-app/lib/src/config/miner_config.dart b/miner-app/lib/src/config/miner_config.dart index 60684c267..63fef3c35 100644 --- a/miner-app/lib/src/config/miner_config.dart +++ b/miner-app/lib/src/config/miner_config.dart @@ -224,4 +224,17 @@ class ChainConfig { @override String toString() => 'ChainConfig(id: $id, displayName: $displayName, rpcUrl: $rpcUrl)'; + + // ============================================================ + // Disk Space + // ============================================================ + + /// Bytes in one gibibyte, for disk arithmetic + static const int bytesPerGb = 1024 * 1024 * 1024; + + /// Minimum free space required before a fresh node install, per chain/MINING.md + static const int minNodeDiskBytes = 100 * bytesPerGb; + + /// Recommended free space for a node, per chain/MINING.md + static const int recommendedNodeDiskBytes = 500 * bytesPerGb; } diff --git a/miner-app/lib/src/services/disk_space_service.dart b/miner-app/lib/src/services/disk_space_service.dart new file mode 100644 index 000000000..36813150f --- /dev/null +++ b/miner-app/lib/src/services/disk_space_service.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('DiskSpace'); + +/// Reports free disk space for the volume that holds a given path. +class DiskSpaceService { + /// Free bytes on the volume containing [path], or null if it could not be determined. + /// + /// Returns null instead of throwing so a failed probe never blocks setup by itself; + /// callers decide whether an unknown value should be treated as a pass. + static Future freeBytesForPath(String path) async { + try { + if (Platform.isWindows) { + final escaped = path.replaceAll("'", "''"); + final result = await Process.run('powershell', [ + '-NoProfile', + '-NonInteractive', + '-Command', + "(Get-Item -LiteralPath '$escaped').PSDrive.Free", + ]); + if (result.exitCode != 0) { + _log.w('Free space probe failed for $path: ${result.stderr}'); + return null; + } + return int.tryParse(result.stdout.toString().trim()); + } + + // POSIX output format keeps one filesystem per line with a fixed column order: + // Filesystem, 1024-blocks, Used, Available, Capacity, Mounted on. + final result = await Process.run('df', ['-kP', path]); + if (result.exitCode != 0) { + _log.w('Free space probe failed for $path: ${result.stderr}'); + return null; + } + final lines = result.stdout.toString().trim().split('\n'); + if (lines.length < 2) return null; + final cols = lines.last.trim().split(RegExp(r'\s+')); + if (cols.length < 4) return null; + final availableKb = int.tryParse(cols[3]); + return availableKb == null ? null : availableKb * 1024; + } catch (e) { + _log.w('Free space probe failed for $path: $e'); + return null; + } + } +} From 65e47c3e27ae3c77edd0343090ce42929d9619f1 Mon Sep 17 00:00:00 2001 From: bubbapang Date: Fri, 4 Sep 2026 16:38:35 +0800 Subject: [PATCH 2/4] miner-app: refuse to start the miner when node and miner are a mixed auth pair Node v0.10.0 (2026-08-13) requires miner QUIC auth; miner releases before v4.0.0 do not support it. The app checks each binary for updates independently and does not gate Start Mining on the pair matching, so a returning user whose miner is older than their node, or whose update was interrupted, can start mining and see only "Miner died during startup". Reproduced on a real install: miner-cli 2.1.2 (no --auth-token-file) alongside quantus-node 0.10.0 (advertises --miner-auth-token-file), which the official script's own probe classifies as "Mixed: stop". - PairCompatibilityService.check: runs the same --help probe the script runs, on the installed binaries. Returns null if a probe fails so a broken binary is handled by the existing "binary not found" path. - _startMiner: before starting, refuse a mixed pair with a snackbar that names both versions and which one to update. Compatible or unknown pairs proceed unchanged. Co-Authored-By: Claude Sonnet 5 --- .../lib/features/miner/miner_controls.dart | 18 ++++++++ .../services/pair_compatibility_service.dart | 45 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 miner-app/lib/src/services/pair_compatibility_service.dart diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index 70b9c5f56..13ed410be 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -11,6 +11,7 @@ import 'package:quantus_miner/src/utils/app_logger.dart'; import '../../main.dart'; import '../../src/services/binary_manager.dart'; +import 'package:quantus_miner/src/services/pair_compatibility_service.dart'; import '../../src/services/gpu_detection_service.dart'; import '../../src/services/miner_settings_service.dart'; @@ -211,6 +212,23 @@ class _MinerControlsState extends State { return; } + // Check the pair speaks the same miner-auth protocol before starting, the same probe the + // official script runs. A mixed pair fails at connection time with only a generic crash. + final nodeBinPath = await BinaryManager.getNodeBinaryFilePath(); + final pair = await PairCompatibilityService.check(nodeBinPath: nodeBinPath, minerBinPath: minerBinPath); + if (pair != null && pair != PairCompatibility.compatible) { + final nodeVersion = (await BinaryManager.getNodeBinaryVersion())?.version ?? 'installed'; + final minerVersion = (await BinaryManager.getMinerBinaryVersion())?.version ?? 'installed'; + final message = pair == PairCompatibility.minerTooOld + ? 'Miner $minerVersion predates the authentication node $nodeVersion requires. Update the miner.' + : 'Node $nodeVersion predates the authentication miner $minerVersion expects. Update the node.'; + _log.w('Refusing to start miner: $pair (node $nodeVersion, miner $minerVersion)'); + if (mounted) { + context.showWarningSnackbar(title: 'Node and miner versions do not match', message: message); + } + return; + } + try { // Update settings in case they changed while miner was stopped widget.orchestrator!.updateMinerSettings(cpuWorkers: _cpuWorkers, gpuDevices: _gpuDevices); diff --git a/miner-app/lib/src/services/pair_compatibility_service.dart b/miner-app/lib/src/services/pair_compatibility_service.dart new file mode 100644 index 000000000..892c44535 --- /dev/null +++ b/miner-app/lib/src/services/pair_compatibility_service.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:quantus_miner/src/utils/app_logger.dart'; + +final _log = log.withTag('PairCompat'); + +/// Whether the installed node and miner speak the same miner-auth protocol. +enum PairCompatibility { + /// Both advertise miner QUIC auth, or neither does. + compatible, + + /// The node requires auth but the miner predates it. The miner exits on the unknown flags. + minerTooOld, + + /// The miner expects auth but the node predates it. The handshake fails with no application protocol. + nodeTooOld, +} + +/// Probes the installed binaries the same way the official mining script does. A node whose help +/// lists `--miner-auth-token-file` and a miner whose help lists `--auth-token-file` and +/// `--tls-cert-sha256-file` are an auth pair. Mixing one of each fails at connection time with a +/// generic crash, so it is checked before the miner is started. +class PairCompatibilityService { + /// Returns null if either probe fails to run, so a broken or wrong-architecture binary is left to + /// the caller's existing checks instead of being misreported as a version mismatch. + static Future check({required String nodeBinPath, required String minerBinPath}) async { + try { + final node = await Process.run(nodeBinPath, ['--help']); + final miner = await Process.run(minerBinPath, ['serve', '--help']); + if (node.exitCode != 0 || miner.exitCode != 0) { + _log.w('Pair probe failed: node exit ${node.exitCode}, miner exit ${miner.exitCode}'); + return null; + } + final nodeHelp = '${node.stdout}${node.stderr}'; + final minerHelp = '${miner.stdout}${miner.stderr}'; + final nodeAuth = nodeHelp.contains('miner-auth-token-file'); + final minerAuth = minerHelp.contains('auth-token-file') && minerHelp.contains('tls-cert-sha256-file'); + if (nodeAuth == minerAuth) return PairCompatibility.compatible; + return nodeAuth ? PairCompatibility.minerTooOld : PairCompatibility.nodeTooOld; + } catch (e) { + _log.w('Pair probe failed: $e'); + return null; + } + } +} From f819a7ded2a4f8d6c557b76a7a9a0811031c938d Mon Sep 17 00:00:00 2001 From: bubbapang Date: Fri, 4 Sep 2026 16:51:08 +0800 Subject: [PATCH 3/4] miner-app: move the pair-compatibility import into the package block It landed among the relative imports, which reads as an accident. Co-Authored-By: Claude Sonnet 5 --- miner-app/lib/features/miner/miner_controls.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miner-app/lib/features/miner/miner_controls.dart b/miner-app/lib/features/miner/miner_controls.dart index 13ed410be..ccd49df7d 100644 --- a/miner-app/lib/features/miner/miner_controls.dart +++ b/miner-app/lib/features/miner/miner_controls.dart @@ -6,12 +6,12 @@ import 'package:quantus_miner/src/config/miner_config.dart'; import 'package:quantus_miner/src/services/miner_wallet_service.dart'; import 'package:quantus_miner/src/services/mining_orchestrator.dart'; import 'package:quantus_miner/src/services/mining_stats_service.dart'; +import 'package:quantus_miner/src/services/pair_compatibility_service.dart'; import 'package:quantus_miner/src/shared/extensions/snackbar_extensions.dart'; import 'package:quantus_miner/src/utils/app_logger.dart'; import '../../main.dart'; import '../../src/services/binary_manager.dart'; -import 'package:quantus_miner/src/services/pair_compatibility_service.dart'; import '../../src/services/gpu_detection_service.dart'; import '../../src/services/miner_settings_service.dart'; From b77928c94a4b79584eb2d035db374f6f8b60e2a7 Mon Sep 17 00:00:00 2001 From: bubbapang Date: Fri, 4 Sep 2026 17:19:51 +0800 Subject: [PATCH 4/4] miner-app: put the disk-space constants inside MinerConfig They were added to ChainConfig by mistake, so MinerConfig.bytesPerGb, minNodeDiskBytes, and recommendedNodeDiskBytes did not exist and the setup screen referenced three getters that were never declared. Caught by flutter analyze, which reported six undefined_getter errors in node_setup_screen.dart. Both classes live in miner_config.dart and the constants had been appended before the file's last closing brace rather than the one that ends MinerConfig. flutter analyze now reports no issues, and dart format at line length 120 leaves all 47 files unchanged. Co-Authored-By: Claude Sonnet 5 --- miner-app/lib/src/config/miner_config.dart | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/miner-app/lib/src/config/miner_config.dart b/miner-app/lib/src/config/miner_config.dart index 63fef3c35..e6021b976 100644 --- a/miner-app/lib/src/config/miner_config.dart +++ b/miner-app/lib/src/config/miner_config.dart @@ -197,6 +197,19 @@ class MinerConfig { /// Number of ports to try when finding an alternative static const int portSearchRange = 10; + + // ============================================================ + // Disk Space + // ============================================================ + + /// Bytes in one gibibyte, for disk arithmetic + static const int bytesPerGb = 1024 * 1024 * 1024; + + /// Minimum free space required before a fresh node install, per chain/MINING.md + static const int minNodeDiskBytes = 100 * bytesPerGb; + + /// Recommended free space for a node, per chain/MINING.md + static const int recommendedNodeDiskBytes = 500 * bytesPerGb; } /// Configuration for a blockchain network. @@ -224,17 +237,4 @@ class ChainConfig { @override String toString() => 'ChainConfig(id: $id, displayName: $displayName, rpcUrl: $rpcUrl)'; - - // ============================================================ - // Disk Space - // ============================================================ - - /// Bytes in one gibibyte, for disk arithmetic - static const int bytesPerGb = 1024 * 1024 * 1024; - - /// Minimum free space required before a fresh node install, per chain/MINING.md - static const int minNodeDiskBytes = 100 * bytesPerGb; - - /// Recommended free space for a node, per chain/MINING.md - static const int recommendedNodeDiskBytes = 500 * bytesPerGb; }