mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 19:27:58 +00:00
## Summary Two CoreScope surfaces treated `0x00` and `0xFF` as ordinary node prefixes, but the MeshCore firmware actively rerolls any identity whose public-key first byte is `0x00` or `0xFF` (see [`examples/simple_repeater/main.cpp:64`](https://github.com/meshcore-dev/MeshCore/blob/6b52fb32301c273fc78d96183501eb23ad33c5bb/examples/simple_repeater/main.cpp#L64)): ```cpp while (count < 10 && (the_mesh.self_id.pub_key[0] == 0x00 || the_mesh.self_id.pub_key[0] == 0xFF)) { // reserved id hashes the_mesh.self_id = radio_new_identity(); count++; } ``` As a result the analyzer was steering new operators toward identities the firmware will silently refuse — `0xFF` is also used as a wildcard flood marker in parts of the routing flow, so this isn't cosmetic. Reporter: **@halo779** (community). ## What this PR does * **`public/prefix-reserved.js`** — small new module, single source of truth. Exposes `isReservedPrefix`, `filterReserved`, `reservedCount`, `markReservedCells`. Firmware citation lives in the file header. * **Hash matrix (1-byte view)** — cells `00` and `FF` get the `.prefix-reserved` class, lose `.hash-active` so the matrix click handler skips them, and pick up an `aria-disabled` + a tooltip explaining why. * **Prefix generator** — random sampling, enumeration fallback, and the "available count" all filter out reserved prefixes. A visible note under the generator card cites `simple_repeater/main.cpp:64` directly. * **Prefix checker** — pasting a reserved prefix or full pubkey now surfaces a red `⚠️ Reserved prefix` alert above the per-tier breakdown. * **`public/style.css`** — `.prefix-reserved` greys + strikes through the cell and sets `pointer-events: none`. * **`public/index.html`** — loads `prefix-reserved.js` before `analytics.js`. ## Tests Red-then-green visible in commit history: * `test-issue-1473-reserved-prefixes.js` — `isReservedPrefix()` semantics (case + multi-byte) and `markReservedCells()` behavior on a mock 256-cell matrix. * `test-issue-1473-prefix-generator.js` — `filterReserved`, `reservedCount` per byte length, RNG-bias simulator showing the generator never returns a reserved prefix, enumeration-first-free skips `00`, and an assertion that `analytics.js` actually wires `PrefixReserved` into the generator. Both added to `test-all.sh`. Fixes #1473 --------- Co-authored-by: clawbot <bot@openclaw.invalid>
111 lines
4.9 KiB
JavaScript
111 lines
4.9 KiB
JavaScript
/**
|
|
* Issue #1473 — Prefix generator (#/analytics?tab=prefix-tool) must NOT
|
|
* suggest any prefix whose FIRST byte is 0x00 or 0xFF, since the MeshCore
|
|
* firmware keygen routine re-rolls such identities by convention (HEAD
|
|
* 8ede7641, examples/simple_repeater/main.cpp:83).
|
|
*
|
|
* Reporter: @halo779 (community).
|
|
*/
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
const assert = require('assert');
|
|
|
|
const src = fs.readFileSync(path.join(__dirname, 'public', 'prefix-reserved.js'), 'utf8');
|
|
const sandbox = { module: { exports: {} }, exports: {}, window: {} };
|
|
vm.createContext(sandbox);
|
|
vm.runInContext(src, sandbox);
|
|
const PR = (sandbox.module.exports && sandbox.module.exports.isReservedPrefix)
|
|
? sandbox.module.exports
|
|
: sandbox.window.PrefixReserved;
|
|
|
|
let passed = 0, failed = 0;
|
|
function test(name, fn) {
|
|
try { fn(); passed++; console.log(' \u2713 ' + name); }
|
|
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
|
|
}
|
|
|
|
console.log('\n=== #1473: filterReserved() drops 00 and FF from 1-byte space ===');
|
|
const all1byte = [];
|
|
for (let i = 0; i < 256; i++) all1byte.push(i.toString(16).toUpperCase().padStart(2, '0'));
|
|
const filtered = PR.filterReserved(all1byte);
|
|
|
|
test('filtered length is 254 (256 - 2 reserved)', () => assert.strictEqual(filtered.length, 254));
|
|
test('"00" not in filtered output', () => assert.strictEqual(filtered.indexOf('00'), -1));
|
|
test('"FF" not in filtered output', () => assert.strictEqual(filtered.indexOf('FF'), -1));
|
|
test('"01" still present', () => assert.ok(filtered.indexOf('01') >= 0));
|
|
test('"FE" still present', () => assert.ok(filtered.indexOf('FE') >= 0));
|
|
test('"A3" still present', () => assert.ok(filtered.indexOf('A3') >= 0));
|
|
|
|
console.log('\n=== #1473: reservedCount() per byte length ===');
|
|
test('1-byte reserved count = 2', () => assert.strictEqual(PR.reservedCount(1), 2));
|
|
test('2-byte reserved count = 512', () => assert.strictEqual(PR.reservedCount(2), 512));
|
|
test('3-byte reserved count = 131072', () => assert.strictEqual(PR.reservedCount(3), 131072));
|
|
|
|
console.log('\n=== #1473: simulated generator never returns a reserved prefix ===');
|
|
// Mirrors the production generator loop: random sampling + reserved filter.
|
|
// We pre-seed the RNG to return the reserved hex values up front; the loop
|
|
// MUST iterate past them and end on a non-reserved value.
|
|
function generateOne(bytes, usedSet, rng) {
|
|
const totalSpace = Math.pow(256, bytes);
|
|
const hexLen = bytes * 2;
|
|
let attempts = 0, prefix;
|
|
do {
|
|
prefix = Math.floor(rng() * totalSpace).toString(16).toUpperCase().padStart(hexLen, '0');
|
|
} while ((usedSet.has(prefix) || PR.isReservedPrefix(prefix)) && ++attempts < 2000);
|
|
return prefix;
|
|
}
|
|
|
|
// Sequence that biases towards reserved first, then a valid prefix.
|
|
const seq = [0, 255, 0, 255, 0, 0x42, 0x77, 0x10];
|
|
let rngIdx = 0;
|
|
const biasedRng = () => seq[rngIdx++ % seq.length] / 256;
|
|
for (let k = 0; k < 20; k++) {
|
|
const out = generateOne(1, new Set(), biasedRng);
|
|
test('1-byte gen run #' + k + ' (' + out + ') is not reserved',
|
|
() => assert.strictEqual(PR.isReservedPrefix(out), false));
|
|
}
|
|
|
|
// Enumeration-style generator (fallback path in production) — first available
|
|
// non-reserved.
|
|
function enumerateFirstFree(bytes, usedSet) {
|
|
const totalSpace = Math.pow(256, bytes);
|
|
const hexLen = bytes * 2;
|
|
for (let i = 0; i < totalSpace; i++) {
|
|
const p = i.toString(16).toUpperCase().padStart(hexLen, '0');
|
|
if (!usedSet.has(p) && !PR.isReservedPrefix(p)) return p;
|
|
}
|
|
return null;
|
|
}
|
|
test('enumerate-first-free (1-byte, empty used) returns "01" (skips 00)',
|
|
() => assert.strictEqual(enumerateFirstFree(1, new Set()), '01'));
|
|
|
|
// Used = everything except 00, FE, FF → must return FE, NOT 00 or FF.
|
|
const usedAllBut3 = new Set();
|
|
for (let i = 0; i < 256; i++) {
|
|
if (i !== 0x00 && i !== 0xFE && i !== 0xFF) {
|
|
usedAllBut3.add(i.toString(16).toUpperCase().padStart(2, '0'));
|
|
}
|
|
}
|
|
test('with only {00, FE, FF} free, generator returns "FE" (NOT 00 or FF)',
|
|
() => assert.strictEqual(enumerateFirstFree(1, usedAllBut3), 'FE'));
|
|
|
|
console.log('\n=== #1473: analytics.js wires reserved filter into the generator ===');
|
|
const analyticsSrc = fs.readFileSync(path.join(__dirname, 'public', 'analytics.js'), 'utf8');
|
|
test('analytics.js references PrefixReserved (generator wiring)',
|
|
() => assert.ok(/PrefixReserved/.test(analyticsSrc)));
|
|
test('analytics.js mentions the reserved-excluded note in the generator card',
|
|
() => assert.ok(/0x00 and 0xFF[\s\S]{0,200}excluded/i.test(analyticsSrc)));
|
|
test('analytics.js loads prefix-reserved before analytics in index.html',
|
|
() => {
|
|
const html = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
|
|
const pri = html.indexOf('prefix-reserved.js');
|
|
const ani = html.indexOf('analytics.js');
|
|
assert.ok(pri > 0 && ani > 0 && pri < ani, 'prefix-reserved.js must load before analytics.js');
|
|
});
|
|
|
|
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
|
if (failed) process.exit(1);
|