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 +65 -127
- package/data/fingerprint_checks.py +450 -0
- package/data/miner.py +313 -0
- package/package.json +5 -1
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
|
|
10
|
-
* clawrtc install --
|
|
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.
|
|
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://
|
|
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.
|
|
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
|
-
|
|
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 --
|
|
104
|
+
clawrtc install --verify Show SHA256 hashes of bundled files
|
|
132
105
|
Source code: https://github.com/Scottcjn/Rustchain
|
|
133
|
-
Block 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
|
-
// --
|
|
162
|
-
if (flags.
|
|
163
|
-
log('
|
|
164
|
-
for (const [
|
|
165
|
-
|
|
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
|
|
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
|
-
║
|
|
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}
|
|
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
|
-
//
|
|
246
|
-
log('
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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`,
|
|
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:
|
|
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
|
-
|
|
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`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
549
|
-
clawrtc install --
|
|
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
|
-
|
|
554
|
-
|
|
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.
|
|
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",
|