Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions miner-app/lib/features/miner/miner_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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';

Expand Down Expand Up @@ -211,6 +212,23 @@ class _MinerControlsState extends State<MinerControls> {
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);
Expand Down
16 changes: 16 additions & 0 deletions miner-app/lib/features/setup/node_setup_screen.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -66,6 +68,20 @@ class _NodeSetupScreenState extends State<NodeSetupScreen> {
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(() {
Expand Down
13 changes: 13 additions & 0 deletions miner-app/lib/src/config/miner_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 48 additions & 0 deletions miner-app/lib/src/services/disk_space_service.dart
Original file line number Diff line number Diff line change
@@ -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<int?> 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;
}
}
}
45 changes: 45 additions & 0 deletions miner-app/lib/src/services/pair_compatibility_service.dart
Original file line number Diff line number Diff line change
@@ -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<PairCompatibility?> 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;
}
}
}