clawrtc 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/clawrtc.js CHANGED
@@ -5,10 +5,12 @@
5
5
  * Modern machines get 1x multiplier. Vintage hardware gets bonus.
6
6
  * VMs are detected and penalized — real iron only.
7
7
  *
8
+ * All miner scripts are bundled with this package — no external downloads.
9
+ * Network endpoint uses CA-signed TLS certificate.
10
+ *
8
11
  * Security:
9
- * clawrtc install --dry-run Preview without downloading
10
- * clawrtc install --show-urls Show exact download sources
11
- * clawrtc install --verify Show SHA256 hashes after download
12
+ * clawrtc install --dry-run Preview without installing
13
+ * clawrtc install --verify Show SHA256 hashes of bundled files
12
14
  * clawrtc start --service Opt-in background service
13
15
  */
14
16
 
@@ -16,16 +18,15 @@ const { execSync, spawn } = require('child_process');
16
18
  const crypto = require('crypto');
17
19
  const fs = require('fs');
18
20
  const https = require('https');
19
- const http = require('http');
20
21
  const os = require('os');
21
22
  const path = require('path');
22
23
  const readline = require('readline');
23
24
 
24
- const VERSION = '1.1.0';
25
- const REPO_BASE = 'https://raw.githubusercontent.com/Scottcjn/Rustchain/main';
25
+ const VERSION = '1.2.0';
26
26
  const INSTALL_DIR = path.join(os.homedir(), '.clawrtc');
27
27
  const VENV_DIR = path.join(INSTALL_DIR, 'venv');
28
- const NODE_URL = 'https://50.28.86.131';
28
+ const NODE_URL = 'https://bulbous-bouffant.metalseed.net';
29
+ const DATA_DIR = path.join(__dirname, '..', 'data');
29
30
 
30
31
  // ANSI colors
31
32
  const C = '\x1b[36m', G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m';
@@ -35,33 +36,17 @@ const log = (m) => console.log(`${C}[clawrtc]${NC} ${m}`);
35
36
  const ok = (m) => console.log(`${G}[OK]${NC} ${m}`);
36
37
  const warn = (m) => console.log(`${Y}[WARN]${NC} ${m}`);
37
38
 
39
+ // Bundled files shipped with the package
40
+ const BUNDLED_FILES = [
41
+ ['miner.py', 'miner.py'],
42
+ ['fingerprint_checks.py', 'fingerprint_checks.py'],
43
+ ];
44
+
38
45
  function sha256File(filepath) {
39
46
  const data = fs.readFileSync(filepath);
40
47
  return crypto.createHash('sha256').update(data).digest('hex');
41
48
  }
42
49
 
43
- function downloadFile(url, dest) {
44
- return new Promise((resolve, reject) => {
45
- const file = fs.createWriteStream(dest);
46
- const mod = url.startsWith('https') ? https : http;
47
- const opts = { rejectUnauthorized: false };
48
- mod.get(url, opts, (res) => {
49
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
50
- file.close();
51
- fs.unlinkSync(dest);
52
- return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
53
- }
54
- res.pipe(file);
55
- file.on('finish', () => {
56
- file.close();
57
- const size = fs.statSync(dest).size;
58
- if (size < 100) return reject(new Error(`File too small (${size} bytes)`));
59
- resolve(size);
60
- });
61
- }).on('error', (e) => { file.close(); reject(e); });
62
- });
63
- }
64
-
65
50
  function ask(prompt) {
66
51
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
67
52
  return new Promise((resolve) => {
@@ -88,33 +73,21 @@ function detectVM() {
88
73
  return hints;
89
74
  }
90
75
 
91
- function getDownloadUrls() {
92
- const plat = os.platform();
93
- const downloads = [
94
- [`${REPO_BASE}/miners/linux/fingerprint_checks.py`, 'fingerprint_checks.py'],
95
- ];
96
- if (plat === 'linux') {
97
- downloads.push([`${REPO_BASE}/miners/linux/rustchain_linux_miner.py`, 'miner.py']);
98
- } else if (plat === 'darwin') {
99
- downloads.push([`${REPO_BASE}/miners/macos/rustchain_mac_miner_v2.4.py`, 'miner.py']);
100
- }
101
- return downloads;
102
- }
103
-
104
76
  function showConsentDisclosure() {
105
77
  console.log(`
106
78
  ${B}What ClawRTC will do:${NC}
107
79
 
108
- ${C}1. Download${NC} Two Python scripts from the RustChain GitHub repository:
80
+ ${C}1. Extract${NC} Two Python scripts bundled with this package:
109
81
  - fingerprint_checks.py (hardware detection)
110
82
  - miner.py (attestation client)
111
- Source: ${REPO_BASE}
83
+ ${D}No external downloads — all code ships with the package.${NC}
112
84
 
113
85
  ${C}2. Install${NC} A Python virtual environment in ~/.clawrtc/
114
86
  with one dependency: 'requests' (HTTP library)
115
87
 
116
88
  ${C}3. Attest${NC} When started, the miner contacts the RustChain network
117
89
  every few minutes to prove your hardware is real.
90
+ Endpoint: ${NODE_URL} (CA-signed TLS certificate)
118
91
 
119
92
  ${C}4. Collect${NC} Hardware fingerprint data sent during attestation:
120
93
  - CPU model, architecture, vendor
@@ -128,9 +101,9 @@ function showConsentDisclosure() {
128
101
 
129
102
  ${D}Verify yourself:${NC}
130
103
  clawrtc install --dry-run Preview without installing
131
- clawrtc install --show-urls See exact download URLs
104
+ clawrtc install --verify Show SHA256 hashes of bundled files
132
105
  Source code: https://github.com/Scottcjn/Rustchain
133
- Block explorer: https://50.28.86.131/explorer
106
+ Block explorer: ${NODE_URL}/explorer
134
107
  `);
135
108
  }
136
109
 
@@ -158,21 +131,24 @@ ${D} Version ${VERSION}${NC}
158
131
  process.exit(1);
159
132
  }
160
133
 
161
- // --show-urls: print URLs and exit
162
- if (flags.showUrls) {
163
- log('Files that will be downloaded:');
164
- for (const [url, filename] of getDownloadUrls()) {
165
- console.log(` ${filename}: ${url}`);
134
+ // --verify: show bundled file hashes and exit
135
+ if (flags.verify) {
136
+ log('Bundled file hashes (SHA256):');
137
+ for (const [srcName, destName] of BUNDLED_FILES) {
138
+ const src = path.join(DATA_DIR, srcName);
139
+ if (fs.existsSync(src)) {
140
+ console.log(` ${destName}: ${sha256File(src)}`);
141
+ } else {
142
+ console.log(` ${destName}: NOT FOUND in package`);
143
+ }
166
144
  }
167
- console.log(`\n Network node: ${NODE_URL}`);
168
- console.log(` Source repo: https://github.com/Scottcjn/Rustchain`);
169
145
  return;
170
146
  }
171
147
 
172
148
  // --dry-run: show what would happen
173
149
  if (flags.dryRun) {
174
150
  showConsentDisclosure();
175
- log('DRY RUN — no files downloaded, no services created.');
151
+ log('DRY RUN — no files extracted, no services created.');
176
152
  return;
177
153
  }
178
154
 
@@ -191,14 +167,14 @@ ${D} Version ${VERSION}${NC}
191
167
  if (vmHints.length > 0) {
192
168
  console.log(`
193
169
  ${R}${B} ╔══════════════════════════════════════════════════════════╗
194
- VM DETECTED — READ THIS
170
+ ║ VM DETECTED — READ THIS
195
171
  ╠══════════════════════════════════════════════════════════╣
196
172
  ║ This machine appears to be a virtual machine. ║
197
173
  ║ RustChain will detect VMs and assign near-zero weight. ║
198
174
  ║ Your miner will attest but earn effectively nothing. ║
199
175
  ║ To earn RTC, run on bare-metal hardware. ║
200
176
  ╚══════════════════════════════════════════════════════════╝${NC}`);
201
- for (const h of vmHints.slice(0, 4)) console.log(` ${R} ${h}${NC}`);
177
+ for (const h of vmHints.slice(0, 4)) console.log(` ${R} * ${h}${NC}`);
202
178
  console.log();
203
179
  }
204
180
 
@@ -242,28 +218,21 @@ ${R}${B} ╔══════════════════════
242
218
  execSync(`"${pip}" install requests -q`, { stdio: 'pipe' });
243
219
  ok('Dependencies ready');
244
220
 
245
- // Download miner files
246
- log('Downloading miner from RustChain repo...');
247
- const downloads = getDownloadUrls();
248
-
249
- for (const [url, filename] of downloads) {
250
- log(` Fetching: ${url}`);
251
- const dest = path.join(INSTALL_DIR, filename);
252
- const size = await downloadFile(url, dest);
253
- const hash = sha256File(dest);
254
- log(` ${filename} (${(size / 1024).toFixed(1)} KB) SHA256: ${hash.slice(0, 16)}...`);
255
- }
256
- ok('Miner files downloaded and verified');
257
-
258
- // --verify: show full hashes and exit
259
- if (flags.verify) {
260
- log('File hashes (SHA256):');
261
- for (const [url, filename] of downloads) {
262
- const dest = path.join(INSTALL_DIR, filename);
263
- console.log(` ${filename}: ${sha256File(dest)}`);
221
+ // Extract bundled miner files (no download!)
222
+ log('Extracting bundled miner scripts...');
223
+ for (const [srcName, destName] of BUNDLED_FILES) {
224
+ const src = path.join(DATA_DIR, srcName);
225
+ const dest = path.join(INSTALL_DIR, destName);
226
+ if (!fs.existsSync(src)) {
227
+ console.error(`${R}[ERROR]${NC} Bundled file missing: ${srcName}. Package may be corrupted.`);
228
+ process.exit(1);
264
229
  }
265
- return;
230
+ fs.copyFileSync(src, dest);
231
+ const hash = sha256File(dest);
232
+ const size = fs.statSync(dest).size;
233
+ log(` ${destName} (${(size / 1024).toFixed(1)} KB) SHA256: ${hash.slice(0, 16)}...`);
266
234
  }
235
+ ok('Miner files extracted from package (no external downloads)');
267
236
 
268
237
  // Setup service ONLY if --service flag is passed
269
238
  if (flags.service) {
@@ -278,11 +247,11 @@ ${R}${B} ╔══════════════════════
278
247
  log('Or start manually: clawrtc start');
279
248
  }
280
249
 
281
- // Network check
250
+ // Network check (CA-signed, no rejectUnauthorized needed)
282
251
  log('Checking RustChain network...');
283
252
  try {
284
253
  const data = await new Promise((resolve, reject) => {
285
- https.get(`${NODE_URL}/api/miners`, { rejectUnauthorized: false }, (res) => {
254
+ https.get(`${NODE_URL}/api/miners`, (res) => {
286
255
  let d = '';
287
256
  res.on('data', c => d += c);
288
257
  res.on('end', () => resolve(d));
@@ -294,6 +263,20 @@ ${R}${B} ╔══════════════════════
294
263
  warn('Could not reach network (node may be temporarily unavailable)');
295
264
  }
296
265
 
266
+ // Anonymous install telemetry — fire-and-forget, no PII
267
+ try {
268
+ const payload = JSON.stringify({
269
+ package: 'clawrtc', version: VERSION,
270
+ platform: os.platform(), arch: os.arch(), source: 'npm'
271
+ });
272
+ const req = https.request('https://bottube.ai/api/telemetry/install', {
273
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
274
+ timeout: 5000
275
+ });
276
+ req.on('error', () => {});
277
+ req.end(payload);
278
+ } catch (e) {}
279
+
297
280
  console.log(`
298
281
  ${G}${B}═══════════════════════════════════════════════════════════
299
282
  ClawRTC installed! Your agent is ready to mine RTC.
@@ -301,6 +284,7 @@ ${G}${B}════════════════════════
301
284
  Wallet: ${wallet}
302
285
  Location: ${INSTALL_DIR}
303
286
  Reward: 1x multiplier (modern hardware)
287
+ Node: ${NODE_URL} (CA-signed TLS)
304
288
 
305
289
  Next steps:
306
290
  clawrtc start Start mining (foreground)
@@ -317,7 +301,7 @@ ${G}${B}════════════════════════
317
301
 
318
302
  Verify & audit:
319
303
  * Source: https://github.com/Scottcjn/Rustchain
320
- * Explorer: https://50.28.86.131/explorer
304
+ * Explorer: ${NODE_URL}/explorer
321
305
  * clawrtc uninstall Remove everything cleanly
322
306
  ═══════════════════════════════════════════════════════════${NC}
323
307
  `);
@@ -404,7 +388,6 @@ function setupLaunchd(wallet) {
404
388
  function cmdStart(flags) {
405
389
  const plat = os.platform();
406
390
 
407
- // If --service flag, set up persistence
408
391
  if (flags.service) {
409
392
  const wf = path.join(INSTALL_DIR, '.wallet');
410
393
  const wallet = fs.existsSync(wf) ? fs.readFileSync(wf, 'utf8').trim() : 'agent';
@@ -413,36 +396,23 @@ function cmdStart(flags) {
413
396
  return;
414
397
  }
415
398
 
416
- // Try existing service first
417
399
  if (plat === 'linux') {
418
400
  const sf = path.join(os.homedir(), '.config', 'systemd', 'user', 'clawrtc-miner.service');
419
401
  if (fs.existsSync(sf)) {
420
- try {
421
- execSync('systemctl --user start clawrtc-miner', { stdio: 'inherit' });
422
- ok('Miner started (systemd)');
423
- return;
424
- } catch (e) {}
402
+ try { execSync('systemctl --user start clawrtc-miner', { stdio: 'inherit' }); ok('Miner started (systemd)'); return; } catch (e) {}
425
403
  }
426
404
  } else if (plat === 'darwin') {
427
405
  const pf = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.clawrtc.miner.plist');
428
406
  if (fs.existsSync(pf)) {
429
- try {
430
- execSync(`launchctl load "${pf}"`, { stdio: 'inherit' });
431
- ok('Miner started (launchd)');
432
- return;
433
- } catch (e) {}
407
+ try { execSync(`launchctl load "${pf}"`, { stdio: 'inherit' }); ok('Miner started (launchd)'); return; } catch (e) {}
434
408
  }
435
409
  }
436
410
 
437
- // Fallback: run in foreground
438
411
  const minerPy = path.join(INSTALL_DIR, 'miner.py');
439
412
  const pythonBin = path.join(VENV_DIR, 'bin', 'python');
440
413
  const wf = path.join(INSTALL_DIR, '.wallet');
441
414
 
442
- if (!fs.existsSync(minerPy)) {
443
- console.error(`${R}[ERROR]${NC} Miner not installed. Run: clawrtc install`);
444
- process.exit(1);
445
- }
415
+ if (!fs.existsSync(minerPy)) { console.error(`${R}[ERROR]${NC} Miner not installed. Run: clawrtc install`); process.exit(1); }
446
416
 
447
417
  const wallet = fs.existsSync(wf) ? fs.readFileSync(wf, 'utf8').trim() : '';
448
418
  const walletArgs = wallet ? ['--wallet', wallet] : [];
@@ -466,34 +436,24 @@ function cmdStatus() {
466
436
  const plat = os.platform();
467
437
  if (plat === 'linux') {
468
438
  const sf = path.join(os.homedir(), '.config', 'systemd', 'user', 'clawrtc-miner.service');
469
- if (fs.existsSync(sf)) {
470
- try { execSync('systemctl --user status clawrtc-miner', { stdio: 'inherit' }); } catch (e) {}
471
- } else {
472
- log('No background service configured. Use: clawrtc start --service');
473
- }
474
- } else if (plat === 'darwin') {
475
- try { execSync('launchctl list | grep clawrtc', { stdio: 'inherit' }); } catch (e) {}
439
+ if (fs.existsSync(sf)) { try { execSync('systemctl --user status clawrtc-miner', { stdio: 'inherit' }); } catch (e) {} }
440
+ else log('No background service configured. Use: clawrtc start --service');
476
441
  }
477
442
 
478
443
  const wf = path.join(INSTALL_DIR, '.wallet');
479
444
  if (fs.existsSync(wf)) log(`Wallet: ${fs.readFileSync(wf, 'utf8').trim()}`);
480
445
 
481
- // File integrity
482
446
  for (const filename of ['miner.py', 'fingerprint_checks.py']) {
483
447
  const fp = path.join(INSTALL_DIR, filename);
484
- if (fs.existsSync(fp)) {
485
- log(`${filename} SHA256: ${sha256File(fp).slice(0, 16)}...`);
486
- }
448
+ if (fs.existsSync(fp)) log(`${filename} SHA256: ${sha256File(fp).slice(0, 16)}...`);
487
449
  }
488
450
 
489
- https.get(`${NODE_URL}/health`, { rejectUnauthorized: false }, (res) => {
451
+ https.get(`${NODE_URL}/health`, (res) => {
490
452
  let d = '';
491
453
  res.on('data', c => d += c);
492
454
  res.on('end', () => {
493
- try {
494
- const h = JSON.parse(d);
495
- log(`Network: ${h.ok ? 'online' : 'offline'} (v${h.version || '?'})`);
496
- } catch (e) { warn('Could not parse network status'); }
455
+ try { const h = JSON.parse(d); log(`Network: ${h.ok ? 'online' : 'offline'} (v${h.version || '?'})`); }
456
+ catch (e) { warn('Could not parse network status'); }
497
457
  });
498
458
  }).on('error', () => warn('Could not reach network'));
499
459
  }
@@ -501,17 +461,11 @@ function cmdStatus() {
501
461
  function cmdLogs() {
502
462
  if (os.platform() === 'linux') {
503
463
  const sf = path.join(os.homedir(), '.config', 'systemd', 'user', 'clawrtc-miner.service');
504
- if (fs.existsSync(sf)) {
505
- spawn('journalctl', ['--user', '-u', 'clawrtc-miner', '-f', '--no-pager', '-n', '50'], { stdio: 'inherit' });
506
- } else {
507
- const lf = path.join(INSTALL_DIR, 'miner.log');
508
- if (fs.existsSync(lf)) spawn('tail', ['-f', lf], { stdio: 'inherit' });
509
- else warn('No logs found. Start the miner first: clawrtc start');
510
- }
464
+ if (fs.existsSync(sf)) { spawn('journalctl', ['--user', '-u', 'clawrtc-miner', '-f', '--no-pager', '-n', '50'], { stdio: 'inherit' }); }
465
+ else { const lf = path.join(INSTALL_DIR, 'miner.log'); if (fs.existsSync(lf)) spawn('tail', ['-f', lf], { stdio: 'inherit' }); else warn('No logs found.'); }
511
466
  } else {
512
467
  const lf = path.join(INSTALL_DIR, 'miner.log');
513
- if (fs.existsSync(lf)) spawn('tail', ['-f', lf], { stdio: 'inherit' });
514
- else warn('No log file found');
468
+ if (fs.existsSync(lf)) spawn('tail', ['-f', lf], { stdio: 'inherit' }); else warn('No log file found');
515
469
  }
516
470
  }
517
471
 
@@ -545,13 +499,12 @@ Commands:
545
499
  clawrtc uninstall Remove everything cleanly
546
500
 
547
501
  Security & Verification:
548
- clawrtc install --dry-run Preview without downloading or installing
549
- clawrtc install --show-urls Show exact URLs that will be fetched
550
- clawrtc install --verify Show SHA256 hashes of downloaded files
502
+ clawrtc install --dry-run Preview without installing
503
+ clawrtc install --verify Show SHA256 hashes of bundled files
551
504
  clawrtc install -y Skip consent prompt (for CI/automation)
552
505
 
553
- Modern hardware gets 1x multiplier. VMs are detected and penalized.
554
- Vintage hardware (PowerPC G4/G5) gets up to 2.5x bonus.
506
+ All miner code is bundled in the package. No external downloads.
507
+ Network endpoint: ${NODE_URL} (CA-signed TLS certificate)
555
508
 
556
509
  Source: https://github.com/Scottcjn/Rustchain
557
510
  `);
@@ -563,7 +516,6 @@ const cmd = args[0];
563
516
  const flags = {
564
517
  wallet: null,
565
518
  dryRun: args.includes('--dry-run'),
566
- showUrls: args.includes('--show-urls'),
567
519
  verify: args.includes('--verify'),
568
520
  service: args.includes('--service'),
569
521
  yes: args.includes('-y') || args.includes('--yes'),
@@ -0,0 +1,450 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ RIP-PoA Hardware Fingerprint Validation
4
+ ========================================
5
+ 7 Required Checks for RTC Reward Approval
6
+ ALL MUST PASS for antiquity multiplier rewards
7
+
8
+ Checks:
9
+ 1. Clock-Skew & Oscillator Drift
10
+ 2. Cache Timing Fingerprint
11
+ 3. SIMD Unit Identity
12
+ 4. Thermal Drift Entropy
13
+ 5. Instruction Path Jitter
14
+ 6. Anti-Emulation Behavioral Checks
15
+ 7. ROM Fingerprint (retro platforms only)
16
+ """
17
+
18
+ import hashlib
19
+ import os
20
+ import platform
21
+ import statistics
22
+ import subprocess
23
+ import time
24
+ from typing import Dict, List, Optional, Tuple
25
+
26
+ # Import ROM fingerprint database if available
27
+ try:
28
+ from rom_fingerprint_db import (
29
+ identify_rom,
30
+ is_known_emulator_rom,
31
+ compute_file_hash,
32
+ detect_platform_roms,
33
+ get_real_hardware_rom_signature,
34
+ )
35
+ ROM_DB_AVAILABLE = True
36
+ except ImportError:
37
+ ROM_DB_AVAILABLE = False
38
+
39
+ def check_clock_drift(samples: int = 200) -> Tuple[bool, Dict]:
40
+ """Check 1: Clock-Skew & Oscillator Drift"""
41
+ intervals = []
42
+ reference_ops = 5000
43
+
44
+ for i in range(samples):
45
+ data = "drift_{}".format(i).encode()
46
+ start = time.perf_counter_ns()
47
+ for _ in range(reference_ops):
48
+ hashlib.sha256(data).digest()
49
+ elapsed = time.perf_counter_ns() - start
50
+ intervals.append(elapsed)
51
+ if i % 50 == 0:
52
+ time.sleep(0.001)
53
+
54
+ mean_ns = statistics.mean(intervals)
55
+ stdev_ns = statistics.stdev(intervals)
56
+ cv = stdev_ns / mean_ns if mean_ns > 0 else 0
57
+
58
+ drift_pairs = [intervals[i] - intervals[i-1] for i in range(1, len(intervals))]
59
+ drift_stdev = statistics.stdev(drift_pairs) if len(drift_pairs) > 1 else 0
60
+
61
+ data = {
62
+ "mean_ns": int(mean_ns),
63
+ "stdev_ns": int(stdev_ns),
64
+ "cv": round(cv, 6),
65
+ "drift_stdev": int(drift_stdev),
66
+ }
67
+
68
+ valid = True
69
+ if cv < 0.0001:
70
+ valid = False
71
+ data["fail_reason"] = "synthetic_timing"
72
+ elif drift_stdev == 0:
73
+ valid = False
74
+ data["fail_reason"] = "no_drift"
75
+
76
+ return valid, data
77
+
78
+
79
+ def check_cache_timing(iterations: int = 100) -> Tuple[bool, Dict]:
80
+ """Check 2: Cache Timing Fingerprint (L1/L2/L3 Latency)"""
81
+ l1_size = 8 * 1024
82
+ l2_size = 128 * 1024
83
+ l3_size = 4 * 1024 * 1024
84
+
85
+ def measure_access_time(buffer_size: int, accesses: int = 1000) -> float:
86
+ buf = bytearray(buffer_size)
87
+ for i in range(0, buffer_size, 64):
88
+ buf[i] = i % 256
89
+ start = time.perf_counter_ns()
90
+ for i in range(accesses):
91
+ _ = buf[(i * 64) % buffer_size]
92
+ elapsed = time.perf_counter_ns() - start
93
+ return elapsed / accesses
94
+
95
+ l1_times = [measure_access_time(l1_size) for _ in range(iterations)]
96
+ l2_times = [measure_access_time(l2_size) for _ in range(iterations)]
97
+ l3_times = [measure_access_time(l3_size) for _ in range(iterations)]
98
+
99
+ l1_avg = statistics.mean(l1_times)
100
+ l2_avg = statistics.mean(l2_times)
101
+ l3_avg = statistics.mean(l3_times)
102
+
103
+ l2_l1_ratio = l2_avg / l1_avg if l1_avg > 0 else 0
104
+ l3_l2_ratio = l3_avg / l2_avg if l2_avg > 0 else 0
105
+
106
+ data = {
107
+ "l1_ns": round(l1_avg, 2),
108
+ "l2_ns": round(l2_avg, 2),
109
+ "l3_ns": round(l3_avg, 2),
110
+ "l2_l1_ratio": round(l2_l1_ratio, 3),
111
+ "l3_l2_ratio": round(l3_l2_ratio, 3),
112
+ }
113
+
114
+ valid = True
115
+ if l2_l1_ratio < 1.01 and l3_l2_ratio < 1.01:
116
+ valid = False
117
+ data["fail_reason"] = "no_cache_hierarchy"
118
+ elif l1_avg == 0 or l2_avg == 0 or l3_avg == 0:
119
+ valid = False
120
+ data["fail_reason"] = "zero_latency"
121
+
122
+ return valid, data
123
+
124
+
125
+ def check_simd_identity() -> Tuple[bool, Dict]:
126
+ """Check 3: SIMD Unit Identity (SSE/AVX/AltiVec/NEON)"""
127
+ flags = []
128
+ arch = platform.machine().lower()
129
+
130
+ try:
131
+ with open("/proc/cpuinfo", "r") as f:
132
+ for line in f:
133
+ if "flags" in line.lower() or "features" in line.lower():
134
+ parts = line.split(":")
135
+ if len(parts) > 1:
136
+ flags = parts[1].strip().split()
137
+ break
138
+ except:
139
+ pass
140
+
141
+ if not flags:
142
+ try:
143
+ result = subprocess.run(
144
+ ["sysctl", "-a"],
145
+ capture_output=True, text=True, timeout=5
146
+ )
147
+ for line in result.stdout.split("\n"):
148
+ if "feature" in line.lower() or "altivec" in line.lower():
149
+ flags.append(line.split(":")[-1].strip())
150
+ except:
151
+ pass
152
+
153
+ has_sse = any("sse" in f.lower() for f in flags)
154
+ has_avx = any("avx" in f.lower() for f in flags)
155
+ has_altivec = any("altivec" in f.lower() for f in flags) or "ppc" in arch
156
+ has_neon = any("neon" in f.lower() for f in flags) or "arm" in arch
157
+
158
+ data = {
159
+ "arch": arch,
160
+ "simd_flags_count": len(flags),
161
+ "has_sse": has_sse,
162
+ "has_avx": has_avx,
163
+ "has_altivec": has_altivec,
164
+ "has_neon": has_neon,
165
+ "sample_flags": flags[:10] if flags else [],
166
+ }
167
+
168
+ valid = has_sse or has_avx or has_altivec or has_neon or len(flags) > 0
169
+ if not valid:
170
+ data["fail_reason"] = "no_simd_detected"
171
+
172
+ return valid, data
173
+
174
+
175
+ def check_thermal_drift(samples: int = 50) -> Tuple[bool, Dict]:
176
+ """Check 4: Thermal Drift Entropy"""
177
+ cold_times = []
178
+ for i in range(samples):
179
+ start = time.perf_counter_ns()
180
+ for _ in range(10000):
181
+ hashlib.sha256("cold_{}".format(i).encode()).digest()
182
+ cold_times.append(time.perf_counter_ns() - start)
183
+
184
+ for _ in range(100):
185
+ for __ in range(50000):
186
+ hashlib.sha256(b"warmup").digest()
187
+
188
+ hot_times = []
189
+ for i in range(samples):
190
+ start = time.perf_counter_ns()
191
+ for _ in range(10000):
192
+ hashlib.sha256("hot_{}".format(i).encode()).digest()
193
+ hot_times.append(time.perf_counter_ns() - start)
194
+
195
+ cold_avg = statistics.mean(cold_times)
196
+ hot_avg = statistics.mean(hot_times)
197
+ cold_stdev = statistics.stdev(cold_times)
198
+ hot_stdev = statistics.stdev(hot_times)
199
+ drift_ratio = hot_avg / cold_avg if cold_avg > 0 else 0
200
+
201
+ data = {
202
+ "cold_avg_ns": int(cold_avg),
203
+ "hot_avg_ns": int(hot_avg),
204
+ "cold_stdev": int(cold_stdev),
205
+ "hot_stdev": int(hot_stdev),
206
+ "drift_ratio": round(drift_ratio, 4),
207
+ }
208
+
209
+ valid = True
210
+ if cold_stdev == 0 and hot_stdev == 0:
211
+ valid = False
212
+ data["fail_reason"] = "no_thermal_variance"
213
+
214
+ return valid, data
215
+
216
+
217
+ def check_instruction_jitter(samples: int = 100) -> Tuple[bool, Dict]:
218
+ """Check 5: Instruction Path Jitter"""
219
+ def measure_int_ops(count: int = 10000) -> float:
220
+ start = time.perf_counter_ns()
221
+ x = 1
222
+ for i in range(count):
223
+ x = (x * 7 + 13) % 65537
224
+ return time.perf_counter_ns() - start
225
+
226
+ def measure_fp_ops(count: int = 10000) -> float:
227
+ start = time.perf_counter_ns()
228
+ x = 1.5
229
+ for i in range(count):
230
+ x = (x * 1.414 + 0.5) % 1000.0
231
+ return time.perf_counter_ns() - start
232
+
233
+ def measure_branch_ops(count: int = 10000) -> float:
234
+ start = time.perf_counter_ns()
235
+ x = 0
236
+ for i in range(count):
237
+ if i % 2 == 0:
238
+ x += 1
239
+ else:
240
+ x -= 1
241
+ return time.perf_counter_ns() - start
242
+
243
+ int_times = [measure_int_ops() for _ in range(samples)]
244
+ fp_times = [measure_fp_ops() for _ in range(samples)]
245
+ branch_times = [measure_branch_ops() for _ in range(samples)]
246
+
247
+ int_avg = statistics.mean(int_times)
248
+ fp_avg = statistics.mean(fp_times)
249
+ branch_avg = statistics.mean(branch_times)
250
+
251
+ int_stdev = statistics.stdev(int_times)
252
+ fp_stdev = statistics.stdev(fp_times)
253
+ branch_stdev = statistics.stdev(branch_times)
254
+
255
+ data = {
256
+ "int_avg_ns": int(int_avg),
257
+ "fp_avg_ns": int(fp_avg),
258
+ "branch_avg_ns": int(branch_avg),
259
+ "int_stdev": int(int_stdev),
260
+ "fp_stdev": int(fp_stdev),
261
+ "branch_stdev": int(branch_stdev),
262
+ }
263
+
264
+ valid = True
265
+ if int_stdev == 0 and fp_stdev == 0 and branch_stdev == 0:
266
+ valid = False
267
+ data["fail_reason"] = "no_jitter"
268
+
269
+ return valid, data
270
+
271
+
272
+ def check_anti_emulation() -> Tuple[bool, Dict]:
273
+ """Check 6: Anti-Emulation Behavioral Checks"""
274
+ vm_indicators = []
275
+
276
+ vm_paths = [
277
+ "/sys/class/dmi/id/product_name",
278
+ "/sys/class/dmi/id/sys_vendor",
279
+ "/proc/scsi/scsi",
280
+ ]
281
+
282
+ vm_strings = ["vmware", "virtualbox", "kvm", "qemu", "xen", "hyperv", "parallels"]
283
+
284
+ for path in vm_paths:
285
+ try:
286
+ with open(path, "r") as f:
287
+ content = f.read().lower()
288
+ for vm in vm_strings:
289
+ if vm in content:
290
+ vm_indicators.append("{}:{}".format(path, vm))
291
+ except:
292
+ pass
293
+
294
+ for key in ["KUBERNETES", "DOCKER", "VIRTUAL", "container"]:
295
+ if key in os.environ:
296
+ vm_indicators.append("ENV:{}".format(key))
297
+
298
+ try:
299
+ with open("/proc/cpuinfo", "r") as f:
300
+ if "hypervisor" in f.read().lower():
301
+ vm_indicators.append("cpuinfo:hypervisor")
302
+ except:
303
+ pass
304
+
305
+ data = {
306
+ "vm_indicators": vm_indicators,
307
+ "indicator_count": len(vm_indicators),
308
+ "is_likely_vm": len(vm_indicators) > 0,
309
+ }
310
+
311
+ valid = len(vm_indicators) == 0
312
+ if not valid:
313
+ data["fail_reason"] = "vm_detected"
314
+
315
+ return valid, data
316
+
317
+
318
+ def check_rom_fingerprint() -> Tuple[bool, Dict]:
319
+ """
320
+ Check 7: ROM Fingerprint (for retro platforms)
321
+
322
+ Detects if running with a known emulator ROM dump.
323
+ Real vintage hardware should have unique/variant ROMs.
324
+ Emulators all use the same pirated ROM packs.
325
+ """
326
+ if not ROM_DB_AVAILABLE:
327
+ # Skip for modern hardware or if DB not available
328
+ return True, {"skipped": True, "reason": "rom_db_not_available_or_modern_hw"}
329
+
330
+ arch = platform.machine().lower()
331
+ rom_hashes = {}
332
+ emulator_detected = False
333
+ detection_details = []
334
+
335
+ # Check for PowerPC (Mac emulation target)
336
+ if "ppc" in arch or "powerpc" in arch:
337
+ # Try to get real hardware ROM signature
338
+ real_rom = get_real_hardware_rom_signature()
339
+ if real_rom:
340
+ rom_hashes["real_hardware"] = real_rom
341
+ else:
342
+ # Check if running under emulator with known ROM
343
+ platform_roms = detect_platform_roms()
344
+ if platform_roms:
345
+ for platform_name, rom_hash in platform_roms.items():
346
+ if is_known_emulator_rom(rom_hash, "md5"):
347
+ emulator_detected = True
348
+ rom_info = identify_rom(rom_hash, "md5")
349
+ detection_details.append({
350
+ "platform": platform_name,
351
+ "hash": rom_hash,
352
+ "known_as": rom_info,
353
+ })
354
+
355
+ # Check for 68K (Amiga, Atari ST, old Mac)
356
+ elif "m68k" in arch or "68000" in arch:
357
+ platform_roms = detect_platform_roms()
358
+ for platform_name, rom_hash in platform_roms.items():
359
+ if "amiga" in platform_name.lower():
360
+ if is_known_emulator_rom(rom_hash, "sha1"):
361
+ emulator_detected = True
362
+ rom_info = identify_rom(rom_hash, "sha1")
363
+ detection_details.append({
364
+ "platform": platform_name,
365
+ "hash": rom_hash,
366
+ "known_as": rom_info,
367
+ })
368
+ elif "mac" in platform_name.lower():
369
+ if is_known_emulator_rom(rom_hash, "apple"):
370
+ emulator_detected = True
371
+ rom_info = identify_rom(rom_hash, "apple")
372
+ detection_details.append({
373
+ "platform": platform_name,
374
+ "hash": rom_hash,
375
+ "known_as": rom_info,
376
+ })
377
+
378
+ # For modern hardware, report "N/A" but pass
379
+ else:
380
+ return True, {
381
+ "skipped": False,
382
+ "arch": arch,
383
+ "is_retro_platform": False,
384
+ "rom_check": "not_applicable_modern_hw",
385
+ }
386
+
387
+ data = {
388
+ "arch": arch,
389
+ "is_retro_platform": True,
390
+ "rom_hashes": rom_hashes,
391
+ "emulator_detected": emulator_detected,
392
+ "detection_details": detection_details,
393
+ }
394
+
395
+ if emulator_detected:
396
+ data["fail_reason"] = "known_emulator_rom"
397
+ return False, data
398
+
399
+ return True, data
400
+
401
+
402
+ def validate_all_checks(include_rom_check: bool = True) -> Tuple[bool, Dict]:
403
+ """Run all 7 fingerprint checks. ALL MUST PASS for RTC approval."""
404
+ results = {}
405
+ all_passed = True
406
+
407
+ checks = [
408
+ ("clock_drift", "Clock-Skew & Oscillator Drift", check_clock_drift),
409
+ ("cache_timing", "Cache Timing Fingerprint", check_cache_timing),
410
+ ("simd_identity", "SIMD Unit Identity", check_simd_identity),
411
+ ("thermal_drift", "Thermal Drift Entropy", check_thermal_drift),
412
+ ("instruction_jitter", "Instruction Path Jitter", check_instruction_jitter),
413
+ ("anti_emulation", "Anti-Emulation Checks", check_anti_emulation),
414
+ ]
415
+
416
+ # Add ROM check for retro platforms
417
+ if include_rom_check and ROM_DB_AVAILABLE:
418
+ checks.append(("rom_fingerprint", "ROM Fingerprint (Retro)", check_rom_fingerprint))
419
+
420
+ print(f"Running {len(checks)} Hardware Fingerprint Checks...")
421
+ print("=" * 50)
422
+
423
+ total_checks = len(checks)
424
+ for i, (key, name, func) in enumerate(checks, 1):
425
+ print(f"\n[{i}/{total_checks}] {name}...")
426
+ try:
427
+ passed, data = func()
428
+ except Exception as e:
429
+ passed = False
430
+ data = {"error": str(e)}
431
+ results[key] = {"passed": passed, "data": data}
432
+ if not passed:
433
+ all_passed = False
434
+ print(" Result: {}".format("PASS" if passed else "FAIL"))
435
+
436
+ print("\n" + "=" * 50)
437
+ print("OVERALL RESULT: {}".format("ALL CHECKS PASSED" if all_passed else "FAILED"))
438
+
439
+ if not all_passed:
440
+ failed = [k for k, v in results.items() if not v["passed"]]
441
+ print("Failed checks: {}".format(failed))
442
+
443
+ return all_passed, results
444
+
445
+
446
+ if __name__ == "__main__":
447
+ import json
448
+ passed, results = validate_all_checks()
449
+ print("\n\nDetailed Results:")
450
+ print(json.dumps(results, indent=2, default=str))
package/data/miner.py ADDED
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ RustChain Local x86 Miner - Modern Ryzen
4
+ """
5
+ import os, sys, json, time, hashlib, uuid, requests, socket, subprocess, platform, statistics, re
6
+ from datetime import datetime
7
+
8
+ NODE_URL = "https://bulbous-bouffant.metalseed.net"
9
+ BLOCK_TIME = 600 # 10 minutes
10
+
11
+ class LocalMiner:
12
+ def __init__(self, wallet=None):
13
+ self.node_url = NODE_URL
14
+ self.wallet = wallet or self._gen_wallet()
15
+ self.hw_info = {}
16
+ self.enrolled = False
17
+ self.attestation_valid_until = 0
18
+ self.last_entropy = {}
19
+
20
+ print("="*70)
21
+ print("RustChain Local Miner - Ryzen 5 5500")
22
+ print("="*70)
23
+ print(f"Node: {self.node_url}")
24
+ print(f"Wallet: {self.wallet}")
25
+ print("="*70)
26
+
27
+ def _gen_wallet(self):
28
+ data = f"ryzen5-{uuid.uuid4().hex}-{time.time()}"
29
+ return hashlib.sha256(data.encode()).hexdigest()[:38] + "RTC"
30
+
31
+ def _run_cmd(self, cmd):
32
+ try:
33
+ return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
34
+ text=True, timeout=10, shell=True).stdout.strip()
35
+ except:
36
+ return ""
37
+
38
+ def _get_mac_addresses(self):
39
+ """Return list of real MAC addresses present on the system."""
40
+ macs = []
41
+ # Try `ip -o link`
42
+ try:
43
+ output = subprocess.run(
44
+ ["ip", "-o", "link"],
45
+ stdout=subprocess.PIPE,
46
+ stderr=subprocess.DEVNULL,
47
+ text=True,
48
+ timeout=5,
49
+ ).stdout.splitlines()
50
+ for line in output:
51
+ m = re.search(r"link/(?:ether|loopback)\s+([0-9a-f:]{17})", line, re.IGNORECASE)
52
+ if m:
53
+ mac = m.group(1).lower()
54
+ if mac != "00:00:00:00:00:00":
55
+ macs.append(mac)
56
+ except Exception:
57
+ pass
58
+
59
+ # Fallback to ifconfig
60
+ if not macs:
61
+ try:
62
+ output = subprocess.run(
63
+ ["ifconfig", "-a"],
64
+ stdout=subprocess.PIPE,
65
+ stderr=subprocess.DEVNULL,
66
+ text=True,
67
+ timeout=5,
68
+ ).stdout.splitlines()
69
+ for line in output:
70
+ m = re.search(r"(?:ether|HWaddr)\s+([0-9a-f:]{17})", line, re.IGNORECASE)
71
+ if m:
72
+ mac = m.group(1).lower()
73
+ if mac != "00:00:00:00:00:00":
74
+ macs.append(mac)
75
+ except Exception:
76
+ pass
77
+
78
+ return macs or ["00:00:00:00:00:01"]
79
+
80
+ def _collect_entropy(self, cycles: int = 48, inner_loop: int = 25000):
81
+ """
82
+ Collect simple timing entropy by measuring tight CPU loops.
83
+ Returns summary statistics the node can score.
84
+ """
85
+ samples = []
86
+ for _ in range(cycles):
87
+ start = time.perf_counter_ns()
88
+ acc = 0
89
+ for j in range(inner_loop):
90
+ acc ^= (j * 31) & 0xFFFFFFFF
91
+ duration = time.perf_counter_ns() - start
92
+ samples.append(duration)
93
+
94
+ mean_ns = sum(samples) / len(samples)
95
+ variance_ns = statistics.pvariance(samples) if len(samples) > 1 else 0.0
96
+
97
+ return {
98
+ "mean_ns": mean_ns,
99
+ "variance_ns": variance_ns,
100
+ "min_ns": min(samples),
101
+ "max_ns": max(samples),
102
+ "sample_count": len(samples),
103
+ "samples_preview": samples[:12],
104
+ }
105
+
106
+ def _get_hw_info(self):
107
+ """Collect hardware info"""
108
+ hw = {
109
+ "platform": platform.system(),
110
+ "machine": platform.machine(),
111
+ "hostname": socket.gethostname(),
112
+ "family": "x86",
113
+ "arch": "modern" # Less than 10 years old
114
+ }
115
+
116
+ # Get CPU
117
+ cpu = self._run_cmd("lscpu | grep 'Model name' | cut -d: -f2 | xargs")
118
+ hw["cpu"] = cpu or "Unknown"
119
+
120
+ # Get cores
121
+ cores = self._run_cmd("nproc")
122
+ hw["cores"] = int(cores) if cores else 6
123
+
124
+ # Get memory
125
+ mem = self._run_cmd("free -g | grep Mem | awk '{print $2}'")
126
+ hw["memory_gb"] = int(mem) if mem else 32
127
+
128
+ # Get MACs (ensures PoA signal uses real hardware data)
129
+ macs = self._get_mac_addresses()
130
+ hw["macs"] = macs
131
+ hw["mac"] = macs[0]
132
+
133
+ self.hw_info = hw
134
+ return hw
135
+
136
+ def attest(self):
137
+ """Hardware attestation"""
138
+ print(f"\n🔐 [{datetime.now().strftime('%H:%M:%S')}] Attesting...")
139
+
140
+ self._get_hw_info()
141
+
142
+ try:
143
+ # Get challenge
144
+ resp = requests.post(f"{self.node_url}/attest/challenge", json={}, timeout=10)
145
+ if resp.status_code != 200:
146
+ print(f"❌ Challenge failed: {resp.status_code}")
147
+ return False
148
+
149
+ challenge = resp.json()
150
+ nonce = challenge.get("nonce")
151
+ print(f"✅ Got challenge nonce")
152
+
153
+ except Exception as e:
154
+ print(f"❌ Challenge error: {e}")
155
+ return False
156
+
157
+ # Collect entropy just before signing the report
158
+ entropy = self._collect_entropy()
159
+ self.last_entropy = entropy
160
+
161
+ # Submit attestation
162
+ attestation = {
163
+ "miner": self.wallet,
164
+ "miner_id": f"ryzen5-{self.hw_info['hostname']}",
165
+ "nonce": nonce,
166
+ "report": {
167
+ "nonce": nonce,
168
+ "commitment": hashlib.sha256(
169
+ (nonce + self.wallet + json.dumps(entropy, sort_keys=True)).encode()
170
+ ).hexdigest(),
171
+ "derived": entropy,
172
+ "entropy_score": entropy.get("variance_ns", 0.0)
173
+ },
174
+ "device": {
175
+ "family": self.hw_info["family"],
176
+ "arch": self.hw_info["arch"],
177
+ "model": "AMD Ryzen 5 5500",
178
+ "cpu": self.hw_info["cpu"],
179
+ "cores": self.hw_info["cores"],
180
+ "memory_gb": self.hw_info["memory_gb"]
181
+ },
182
+ "signals": {
183
+ "macs": self.hw_info.get("macs", [self.hw_info["mac"]]),
184
+ "hostname": self.hw_info["hostname"]
185
+ }
186
+ }
187
+
188
+ try:
189
+ resp = requests.post(f"{self.node_url}/attest/submit",
190
+ json=attestation, timeout=30)
191
+
192
+ if resp.status_code == 200:
193
+ result = resp.json()
194
+ if result.get("ok"):
195
+ self.attestation_valid_until = time.time() + 580
196
+ print(f"✅ Attestation accepted!")
197
+ print(f" CPU: {self.hw_info['cpu']}")
198
+ print(f" Family: x86/modern")
199
+ print(f" Expected Weight: 1.0x")
200
+ return True
201
+ else:
202
+ print(f"❌ Rejected: {result}")
203
+ else:
204
+ print(f"❌ HTTP {resp.status_code}: {resp.text[:200]}")
205
+
206
+ except Exception as e:
207
+ print(f"❌ Error: {e}")
208
+
209
+ return False
210
+
211
+ def enroll(self):
212
+ """Enroll in epoch"""
213
+ if time.time() >= self.attestation_valid_until:
214
+ print(f"📝 Attestation expired, re-attesting...")
215
+ if not self.attest():
216
+ return False
217
+
218
+ print(f"\n📝 [{datetime.now().strftime('%H:%M:%S')}] Enrolling...")
219
+
220
+ payload = {
221
+ "miner_pubkey": self.wallet,
222
+ "miner_id": f"ryzen5-{self.hw_info['hostname']}",
223
+ "device": {
224
+ "family": self.hw_info["family"],
225
+ "arch": self.hw_info["arch"]
226
+ }
227
+ }
228
+
229
+ try:
230
+ resp = requests.post(f"{self.node_url}/epoch/enroll",
231
+ json=payload, timeout=30)
232
+
233
+ if resp.status_code == 200:
234
+ result = resp.json()
235
+ if result.get("ok"):
236
+ self.enrolled = True
237
+ weight = result.get('weight', 1.0)
238
+ print(f"✅ Enrolled!")
239
+ print(f" Epoch: {result.get('epoch')}")
240
+ print(f" Weight: {weight}x")
241
+ return True
242
+ else:
243
+ print(f"❌ Failed: {result}")
244
+ else:
245
+ error_data = resp.json() if resp.headers.get('content-type') == 'application/json' else {}
246
+ print(f"❌ HTTP {resp.status_code}: {error_data.get('error', resp.text[:200])}")
247
+
248
+ except Exception as e:
249
+ print(f"❌ Error: {e}")
250
+
251
+ return False
252
+
253
+ def check_balance(self):
254
+ """Check balance"""
255
+ try:
256
+ resp = requests.get(f"{self.node_url}/balance/{self.wallet}", timeout=10)
257
+ if resp.status_code == 200:
258
+ result = resp.json()
259
+ balance = result.get('balance_rtc', 0)
260
+ print(f"\n💰 Balance: {balance} RTC")
261
+ return balance
262
+ except:
263
+ pass
264
+ return 0
265
+
266
+ def mine(self):
267
+ """Start mining"""
268
+ print(f"\n⛏️ Starting mining...")
269
+ print(f"Block time: {BLOCK_TIME//60} minutes")
270
+ print(f"Press Ctrl+C to stop\n")
271
+
272
+ # Save wallet
273
+ with open("/tmp/local_miner_wallet.txt", "w") as f:
274
+ f.write(self.wallet)
275
+ print(f"💾 Wallet saved to: /tmp/local_miner_wallet.txt\n")
276
+
277
+ cycle = 0
278
+
279
+ try:
280
+ while True:
281
+ cycle += 1
282
+ print(f"\n{'='*70}")
283
+ print(f"Cycle #{cycle} - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
284
+ print(f"{'='*70}")
285
+
286
+ if self.enroll():
287
+ print(f"⏳ Mining for {BLOCK_TIME//60} minutes...")
288
+
289
+ for i in range(BLOCK_TIME // 30):
290
+ time.sleep(30)
291
+ elapsed = (i + 1) * 30
292
+ remaining = BLOCK_TIME - elapsed
293
+ print(f" ⏱️ {elapsed}s elapsed, {remaining}s remaining...")
294
+
295
+ self.check_balance()
296
+
297
+ else:
298
+ print("❌ Enrollment failed. Retrying in 60s...")
299
+ time.sleep(60)
300
+
301
+ except KeyboardInterrupt:
302
+ print(f"\n\n⛔ Mining stopped")
303
+ print(f" Wallet: {self.wallet}")
304
+ self.check_balance()
305
+
306
+ if __name__ == "__main__":
307
+ import argparse
308
+ parser = argparse.ArgumentParser()
309
+ parser.add_argument("--wallet", help="Wallet address")
310
+ args = parser.parse_args()
311
+
312
+ miner = LocalMiner(wallet=args.wallet)
313
+ miner.mine()
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "clawrtc",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "ClawRTC — Let your AI agent mine RTC tokens on any modern hardware. 1x multiplier, built-in wallet, VM-penalized.",
5
5
  "bin": {
6
6
  "clawrtc": "./bin/clawrtc.js"
7
7
  },
8
+ "files": [
9
+ "bin/",
10
+ "data/"
11
+ ],
8
12
  "keywords": ["clawrtc", "ai-agent", "miner", "rustchain", "rtc", "openclaw", "proof-of-antiquity", "blockchain"],
9
13
  "author": "Elyan Labs <scott@elyanlabs.ai>",
10
14
  "license": "MIT",