clawrtc 1.1.0 → 1.2.0

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));
@@ -301,6 +270,7 @@ ${G}${B}════════════════════════
301
270
  Wallet: ${wallet}
302
271
  Location: ${INSTALL_DIR}
303
272
  Reward: 1x multiplier (modern hardware)
273
+ Node: ${NODE_URL} (CA-signed TLS)
304
274
 
305
275
  Next steps:
306
276
  clawrtc start Start mining (foreground)
@@ -317,7 +287,7 @@ ${G}${B}════════════════════════
317
287
 
318
288
  Verify & audit:
319
289
  * Source: https://github.com/Scottcjn/Rustchain
320
- * Explorer: https://50.28.86.131/explorer
290
+ * Explorer: ${NODE_URL}/explorer
321
291
  * clawrtc uninstall Remove everything cleanly
322
292
  ═══════════════════════════════════════════════════════════${NC}
323
293
  `);
@@ -404,7 +374,6 @@ function setupLaunchd(wallet) {
404
374
  function cmdStart(flags) {
405
375
  const plat = os.platform();
406
376
 
407
- // If --service flag, set up persistence
408
377
  if (flags.service) {
409
378
  const wf = path.join(INSTALL_DIR, '.wallet');
410
379
  const wallet = fs.existsSync(wf) ? fs.readFileSync(wf, 'utf8').trim() : 'agent';
@@ -413,36 +382,23 @@ function cmdStart(flags) {
413
382
  return;
414
383
  }
415
384
 
416
- // Try existing service first
417
385
  if (plat === 'linux') {
418
386
  const sf = path.join(os.homedir(), '.config', 'systemd', 'user', 'clawrtc-miner.service');
419
387
  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) {}
388
+ try { execSync('systemctl --user start clawrtc-miner', { stdio: 'inherit' }); ok('Miner started (systemd)'); return; } catch (e) {}
425
389
  }
426
390
  } else if (plat === 'darwin') {
427
391
  const pf = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.clawrtc.miner.plist');
428
392
  if (fs.existsSync(pf)) {
429
- try {
430
- execSync(`launchctl load "${pf}"`, { stdio: 'inherit' });
431
- ok('Miner started (launchd)');
432
- return;
433
- } catch (e) {}
393
+ try { execSync(`launchctl load "${pf}"`, { stdio: 'inherit' }); ok('Miner started (launchd)'); return; } catch (e) {}
434
394
  }
435
395
  }
436
396
 
437
- // Fallback: run in foreground
438
397
  const minerPy = path.join(INSTALL_DIR, 'miner.py');
439
398
  const pythonBin = path.join(VENV_DIR, 'bin', 'python');
440
399
  const wf = path.join(INSTALL_DIR, '.wallet');
441
400
 
442
- if (!fs.existsSync(minerPy)) {
443
- console.error(`${R}[ERROR]${NC} Miner not installed. Run: clawrtc install`);
444
- process.exit(1);
445
- }
401
+ if (!fs.existsSync(minerPy)) { console.error(`${R}[ERROR]${NC} Miner not installed. Run: clawrtc install`); process.exit(1); }
446
402
 
447
403
  const wallet = fs.existsSync(wf) ? fs.readFileSync(wf, 'utf8').trim() : '';
448
404
  const walletArgs = wallet ? ['--wallet', wallet] : [];
@@ -466,34 +422,24 @@ function cmdStatus() {
466
422
  const plat = os.platform();
467
423
  if (plat === 'linux') {
468
424
  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) {}
425
+ if (fs.existsSync(sf)) { try { execSync('systemctl --user status clawrtc-miner', { stdio: 'inherit' }); } catch (e) {} }
426
+ else log('No background service configured. Use: clawrtc start --service');
476
427
  }
477
428
 
478
429
  const wf = path.join(INSTALL_DIR, '.wallet');
479
430
  if (fs.existsSync(wf)) log(`Wallet: ${fs.readFileSync(wf, 'utf8').trim()}`);
480
431
 
481
- // File integrity
482
432
  for (const filename of ['miner.py', 'fingerprint_checks.py']) {
483
433
  const fp = path.join(INSTALL_DIR, filename);
484
- if (fs.existsSync(fp)) {
485
- log(`${filename} SHA256: ${sha256File(fp).slice(0, 16)}...`);
486
- }
434
+ if (fs.existsSync(fp)) log(`${filename} SHA256: ${sha256File(fp).slice(0, 16)}...`);
487
435
  }
488
436
 
489
- https.get(`${NODE_URL}/health`, { rejectUnauthorized: false }, (res) => {
437
+ https.get(`${NODE_URL}/health`, (res) => {
490
438
  let d = '';
491
439
  res.on('data', c => d += c);
492
440
  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'); }
441
+ try { const h = JSON.parse(d); log(`Network: ${h.ok ? 'online' : 'offline'} (v${h.version || '?'})`); }
442
+ catch (e) { warn('Could not parse network status'); }
497
443
  });
498
444
  }).on('error', () => warn('Could not reach network'));
499
445
  }
@@ -501,17 +447,11 @@ function cmdStatus() {
501
447
  function cmdLogs() {
502
448
  if (os.platform() === 'linux') {
503
449
  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
- }
450
+ if (fs.existsSync(sf)) { spawn('journalctl', ['--user', '-u', 'clawrtc-miner', '-f', '--no-pager', '-n', '50'], { stdio: 'inherit' }); }
451
+ 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
452
  } else {
512
453
  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');
454
+ if (fs.existsSync(lf)) spawn('tail', ['-f', lf], { stdio: 'inherit' }); else warn('No log file found');
515
455
  }
516
456
  }
517
457
 
@@ -545,13 +485,12 @@ Commands:
545
485
  clawrtc uninstall Remove everything cleanly
546
486
 
547
487
  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
488
+ clawrtc install --dry-run Preview without installing
489
+ clawrtc install --verify Show SHA256 hashes of bundled files
551
490
  clawrtc install -y Skip consent prompt (for CI/automation)
552
491
 
553
- Modern hardware gets 1x multiplier. VMs are detected and penalized.
554
- Vintage hardware (PowerPC G4/G5) gets up to 2.5x bonus.
492
+ All miner code is bundled in the package. No external downloads.
493
+ Network endpoint: ${NODE_URL} (CA-signed TLS certificate)
555
494
 
556
495
  Source: https://github.com/Scottcjn/Rustchain
557
496
  `);
@@ -563,7 +502,6 @@ const cmd = args[0];
563
502
  const flags = {
564
503
  wallet: null,
565
504
  dryRun: args.includes('--dry-run'),
566
- showUrls: args.includes('--show-urls'),
567
505
  verify: args.includes('--verify'),
568
506
  service: args.includes('--service'),
569
507
  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.0",
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",