mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-14 10:49:54 +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>
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"PULL_THRESHOLD_PX": "readonly",
|
||||
"PacketFilter": "readonly",
|
||||
"PathInspector": "readonly",
|
||||
"PrefixReserved": "readonly",
|
||||
"QRCode": "readonly",
|
||||
"ROLE_COLORS": "readonly",
|
||||
"ROLE_EMOJI": "readonly",
|
||||
|
||||
+57
-9
@@ -1615,6 +1615,14 @@
|
||||
html += hashMatrixLegendHtml(legendLabels);
|
||||
el.innerHTML = html;
|
||||
initMatrixTooltip(el);
|
||||
// #1473 — Grey out cells whose first byte the MeshCore firmware keygen
|
||||
// routine avoids (pub_key[0] in {0x00, 0xFF}). This is a keygen
|
||||
// CONVENTION, not a protocol-level rejection — see firmware
|
||||
// examples/simple_repeater/main.cpp:83 (HEAD 8ede7641). Must run BEFORE
|
||||
// we wire click handlers so .hash-active is stripped first.
|
||||
if (typeof PrefixReserved !== 'undefined' && PrefixReserved && typeof PrefixReserved.markReservedCells === 'function') {
|
||||
PrefixReserved.markReservedCells(el);
|
||||
}
|
||||
el.querySelectorAll('.hash-active').forEach(td => {
|
||||
td.addEventListener('click', () => {
|
||||
clickHandlerFn(td);
|
||||
@@ -2992,6 +3000,12 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
||||
<div class="analytics-card" id="ptGenerator">
|
||||
<h3 style="margin-top:0">Generate Available Prefix</h3>
|
||||
<p class="text-muted" style="margin-top:0;font-size:0.9em">Find a prefix with zero current collisions.</p>
|
||||
<p class="text-muted" style="margin:4px 0 10px;font-size:0.82em">
|
||||
<span aria-hidden="true">🚫</span>
|
||||
<strong>0x00 and 0xFF excluded</strong> as a first byte — the MeshCore firmware keygen routine re-rolls identities whose <code>pub_key[0]</code> is <code>00</code> or <code>FF</code>, so by convention you should not see those prefixes on real nodes (see
|
||||
<a href="https://github.com/meshcore-dev/MeshCore/blob/8ede7641/examples/simple_repeater/main.cpp#L83"
|
||||
target="_blank" rel="noopener noreferrer" style="color:var(--accent)">simple_repeater/main.cpp:83</a>).
|
||||
</p>
|
||||
<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-bottom:12px">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="ptGenSize" value="1" ${initGenerate === '1' ? 'checked' : ''}> 1-byte
|
||||
@@ -3052,6 +3066,19 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
||||
: [{ b: input.length / 2, prefix: input }];
|
||||
|
||||
let html = '';
|
||||
// #1473 — Warn when the user pastes a prefix or full pubkey whose
|
||||
// first byte is one the MeshCore firmware keygen routine avoids
|
||||
// (pub_key[0] in {0x00, 0xFF}). Firmware keygen CONVENTION, not a
|
||||
// protocol-level rejection — see simple_repeater/main.cpp:83.
|
||||
if (typeof PrefixReserved !== 'undefined' && PrefixReserved &&
|
||||
PrefixReserved.isReservedPrefix(input)) {
|
||||
html += `<div role="alert" style="margin-bottom:10px;padding:10px 14px;border:1px solid var(--status-yellow);border-radius:6px;background:var(--bg-secondary,var(--bg))">
|
||||
<strong style="color:var(--status-yellow)">⚠️ Firmware avoids this first byte</strong>
|
||||
<div class="text-muted" style="font-size:0.85em;margin-top:4px">
|
||||
<code class="mono">${input.slice(0,2)}</code> as the first byte of a node pubkey is avoided by the MeshCore firmware keygen convention (the standard repeater re-rolls identities whose <code class="mono">pub_key[0]</code> is <code class="mono">00</code> or <code class="mono">FF</code>). You generally shouldn't see this on real nodes.
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
if (isFullKey) {
|
||||
const inNetwork = nodes.some(n => n.public_key.toUpperCase() === input);
|
||||
html += `<p class="text-muted" style="font-size:0.85em;margin:0 0 10px">Derived prefixes: <code class="mono">${input.slice(0,2)}</code> / <code class="mono">${input.slice(0,4)}</code> / <code class="mono">${input.slice(0,6)}</code>${!inNetwork ? ' — <em>this node is not yet in the network</em>' : ''}</p>`;
|
||||
@@ -3085,34 +3112,55 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
||||
const b = sizeInput ? parseInt(sizeInput.value) : 2;
|
||||
const hexLen = b * 2;
|
||||
const totalSpace = spaceSizes[b];
|
||||
const available = totalSpace - idx[b].size;
|
||||
// #1473 — Reserved prefixes (first byte 0x00 / 0xFF) are dropped from
|
||||
// the candidate pool because the MeshCore firmware keygen routine
|
||||
// re-rolls identities whose pub_key[0] is 0x00 or 0xFF — a keygen
|
||||
// CONVENTION (not a protocol rejection). See firmware
|
||||
// examples/simple_repeater/main.cpp:83 (HEAD 8ede7641).
|
||||
// Available = space - used - reserved.
|
||||
const reservedTotal = (typeof PrefixReserved !== 'undefined' && PrefixReserved)
|
||||
? PrefixReserved.reservedCount(b)
|
||||
: 0;
|
||||
// Count reserved prefixes that are ALREADY used so we don't subtract them twice.
|
||||
let reservedUsed = 0;
|
||||
if (typeof PrefixReserved !== 'undefined' && PrefixReserved) {
|
||||
for (const p of idx[b].keys()) {
|
||||
if (PrefixReserved.isReservedPrefix(p)) reservedUsed++;
|
||||
}
|
||||
}
|
||||
const available = totalSpace - idx[b].size - (reservedTotal - reservedUsed);
|
||||
|
||||
if (available === 0) {
|
||||
if (available <= 0) {
|
||||
const next = b < 3 ? (b + 1) + '-byte' : 'a different size';
|
||||
genResultEl.innerHTML = `<p style="color:var(--status-red);margin:0">No collision-free ${b}-byte prefixes available. Try ${next}.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const isReserved = (p) =>
|
||||
(typeof PrefixReserved !== 'undefined' && PrefixReserved)
|
||||
? PrefixReserved.isReservedPrefix(p)
|
||||
: false;
|
||||
|
||||
let prefix;
|
||||
if (b === 1) {
|
||||
// Enumerate all 256 options
|
||||
// Enumerate all 256 options, skipping used + reserved.
|
||||
const free = [];
|
||||
for (let i = 0; i < totalSpace; i++) {
|
||||
const p = i.toString(16).toUpperCase().padStart(hexLen, '0');
|
||||
if (!idx[b].has(p)) free.push(p);
|
||||
if (!idx[b].has(p) && !isReserved(p)) free.push(p);
|
||||
}
|
||||
prefix = free[Math.floor(Math.random() * free.length)];
|
||||
} else {
|
||||
// Random sampling — with 2K used / 65K space, hit rate >96%
|
||||
// Random sampling — with 2K used / 65K space, hit rate >96%.
|
||||
let attempts = 0;
|
||||
do {
|
||||
prefix = Math.floor(Math.random() * totalSpace).toString(16).toUpperCase().padStart(hexLen, '0');
|
||||
} while (idx[b].has(prefix) && ++attempts < 500);
|
||||
// Fallback to enumeration if sampling kept hitting used prefixes
|
||||
if (idx[b].has(prefix)) {
|
||||
} while ((idx[b].has(prefix) || isReserved(prefix)) && ++attempts < 500);
|
||||
// Fallback to enumeration if sampling kept hitting used/reserved prefixes.
|
||||
if (idx[b].has(prefix) || isReserved(prefix)) {
|
||||
for (let i = 0; i < totalSpace; i++) {
|
||||
const p = i.toString(16).toUpperCase().padStart(hexLen, '0');
|
||||
if (!idx[b].has(p)) { prefix = p; break; }
|
||||
if (!idx[b].has(p) && !isReserved(p)) { prefix = p; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
<script src="packet-filter.js?v=__BUST__"></script>
|
||||
<script src="filter-ux.js?v=__BUST__"></script>
|
||||
<script src="hash-color.js?v=__BUST__"></script>
|
||||
<script src="prefix-reserved.js?v=__BUST__"></script>
|
||||
<script src="packet-helpers.js?v=__BUST__"></script>
|
||||
<script src="vendor/aes-ecb.js?v=__BUST__"></script>
|
||||
<script src="vendor/sha256-hmac.js?v=__BUST__"></script>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* === CoreScope — prefix-reserved.js =====================================
|
||||
*
|
||||
* Issue #1473 — Flag prefixes that the MeshCore firmware keygen routine
|
||||
* avoids by convention.
|
||||
*
|
||||
* Scope (narrow, per meshcore-protocol-expert review):
|
||||
* - This is a FIRMWARE KEYGEN CONVENTION, not a protocol-level rule.
|
||||
* The standard repeater example re-rolls any new identity whose
|
||||
* public-key FIRST BYTE is 0x00 or 0xFF, so in practice you should
|
||||
* never see a node prefix of 00 or FF in the wild.
|
||||
* - We only check the FIRST byte. Other bytes 00/FF inside a pubkey are
|
||||
* perfectly normal (~96% of pubkeys contain a 00 or FF byte somewhere).
|
||||
* - There is NO protocol rejection of such pubkeys and NO routing-level
|
||||
* wildcard semantics tied to dest_hash == 0xFF.
|
||||
*
|
||||
* Firmware citation (HEAD 8ede7641, examples/simple_repeater/main.cpp:83):
|
||||
*
|
||||
* 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++;
|
||||
* }
|
||||
*
|
||||
* https://github.com/meshcore-dev/MeshCore/blob/8ede7641/examples/simple_repeater/main.cpp#L83
|
||||
*
|
||||
* Surfaces that consume this helper:
|
||||
* - Prefix matrix (analytics.js → renderHashMatrixFromServer, 1-byte view):
|
||||
* grey 00 / FF cells and disable click, tooltip explains the convention.
|
||||
* - Prefix generator (analytics.js → renderPrefixTool.doGenerate):
|
||||
* never suggest a prefix whose first byte is 00 / FF; visible note.
|
||||
*
|
||||
* Reporter: @halo779 (community).
|
||||
* ========================================================================= */
|
||||
'use strict';
|
||||
|
||||
(function (root) {
|
||||
// First-byte reservations as uppercase 2-char hex strings.
|
||||
var RESERVED_FIRST_BYTES = ['00', 'FF'];
|
||||
var RESERVED_CLASS = 'prefix-reserved';
|
||||
var RESERVED_NOTE = '0x00 and 0xFF excluded — the MeshCore firmware keygen routine avoids these as the first byte of a node pubkey.';
|
||||
var RESERVED_TITLE =
|
||||
'0x00 and 0xFF as a first byte are avoided by the MeshCore firmware keygen convention (the standard repeater re-rolls identities whose pub_key[0] is 0x00 or 0xFF), so you should not pick them as a node prefix.';
|
||||
|
||||
function isReservedPrefix(prefix) {
|
||||
if (prefix == null) return false;
|
||||
var s = String(prefix);
|
||||
if (s.length < 2) return false;
|
||||
var head = s.slice(0, 2).toUpperCase();
|
||||
for (var i = 0; i < RESERVED_FIRST_BYTES.length; i++) {
|
||||
if (head === RESERVED_FIRST_BYTES[i]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function filterReserved(prefixes) {
|
||||
var out = [];
|
||||
for (var i = 0; i < prefixes.length; i++) {
|
||||
if (!isReservedPrefix(prefixes[i])) out.push(prefixes[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// How many prefixes of `bytes` length the reservation removes from the
|
||||
// total space of 256^bytes. (For each reserved first byte the entire
|
||||
// 256^(bytes-1) tail is reserved.)
|
||||
function reservedCount(bytes) {
|
||||
var b = Number(bytes) || 1;
|
||||
if (b < 1) return 0;
|
||||
return RESERVED_FIRST_BYTES.length * Math.pow(256, b - 1);
|
||||
}
|
||||
|
||||
// Given a DOM root (or any object exposing querySelectorAll), find
|
||||
// hash-matrix cells whose data-hex first byte is reserved, mark them
|
||||
// .prefix-reserved + aria-disabled, strip .hash-active so the matrix's
|
||||
// click wiring skips them, and set a tooltip explaining why.
|
||||
// Returns the count of cells marked.
|
||||
function markReservedCells(root) {
|
||||
if (!root || typeof root.querySelectorAll !== 'function') return 0;
|
||||
var cells = root.querySelectorAll('[data-hex]');
|
||||
var n = 0;
|
||||
for (var i = 0; i < cells.length; i++) {
|
||||
var td = cells[i];
|
||||
var hex = (typeof td.getAttribute === 'function')
|
||||
? td.getAttribute('data-hex')
|
||||
: (td.dataset && td.dataset.hex);
|
||||
if (!isReservedPrefix(hex)) continue;
|
||||
if (td.classList && typeof td.classList.add === 'function') {
|
||||
td.classList.add(RESERVED_CLASS);
|
||||
td.classList.remove('hash-active');
|
||||
}
|
||||
if (typeof td.setAttribute === 'function') {
|
||||
td.setAttribute('aria-disabled', 'true');
|
||||
td.setAttribute('title', RESERVED_TITLE);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
var api = {
|
||||
RESERVED_FIRST_BYTES: RESERVED_FIRST_BYTES.slice(),
|
||||
RESERVED_CLASS: RESERVED_CLASS,
|
||||
RESERVED_NOTE: RESERVED_NOTE,
|
||||
RESERVED_TITLE: RESERVED_TITLE,
|
||||
isReservedPrefix: isReservedPrefix,
|
||||
filterReserved: filterReserved,
|
||||
reservedCount: reservedCount,
|
||||
markReservedCells: markReservedCells,
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||
if (root) root.PrefixReserved = api;
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
@@ -2407,6 +2407,26 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
|
||||
.hash-cell-taken { background: var(--status-green); color: #fff; }
|
||||
.hash-cell-possible { background: var(--status-yellow); color: #fff; }
|
||||
.hash-cell-collision { color: #fff; }
|
||||
/* #1473 — First-byte 0x00 / 0xFF pubkeys are avoided by the MeshCore
|
||||
firmware keygen convention (NOT a protocol-level rejection):
|
||||
https://github.com/meshcore-dev/MeshCore/blob/8ede7641/examples/simple_repeater/main.cpp#L83
|
||||
Visually grey + strike, kill pointer events so the matrix click handler
|
||||
skips them. Applied by public/prefix-reserved.js → markReservedCells(). */
|
||||
.hash-cell.prefix-reserved,
|
||||
.prefix-reserved {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
var(--card-bg),
|
||||
var(--card-bg) 4px,
|
||||
var(--border) 4px,
|
||||
var(--border) 8px
|
||||
) !important;
|
||||
color: var(--text-muted) !important;
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
.hash-matrix-tooltip {
|
||||
position: fixed; z-index: var(--z-tooltip); background: var(--surface-1); border: 1px solid var(--border);
|
||||
border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.25); padding: 8px 12px;
|
||||
|
||||
@@ -48,6 +48,8 @@ node test-issue-1456-score-labels.js
|
||||
# #1461 mobile UX overhaul + #1470 node-detail tile helper (#1468 covered by E2E)
|
||||
node test-issue-1461-mobile-page-actions.js
|
||||
node test-issue-1470-node-tile-helper.js
|
||||
node test-issue-1473-reserved-prefixes.js
|
||||
node test-issue-1473-prefix-generator.js
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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);
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Issue #1473 — The MeshCore firmware keygen routine re-rolls any new
|
||||
* identity whose public-key first byte is 0x00 or 0xFF — a KEYGEN
|
||||
* CONVENTION (not a protocol-level rule). Firmware citation (HEAD
|
||||
* 8ede7641, examples/simple_repeater/main.cpp:83):
|
||||
*
|
||||
* while (... && (pub_key[0] == 0x00 || pub_key[0] == 0xFF)) { ... }
|
||||
*
|
||||
* Only the FIRST byte is checked — internal 00 / FF bytes are normal.
|
||||
*
|
||||
* This test pins:
|
||||
* - isReservedPrefix() semantics (case-insensitive, first byte only)
|
||||
* - markReservedCells() applies .prefix-reserved + disables click on matrix
|
||||
*
|
||||
* Reporter: @halo779 (community).
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
const assert = require('assert');
|
||||
|
||||
// Load prefix-reserved.js as a CommonJS module under vm.
|
||||
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: isReservedPrefix() ===');
|
||||
test('"00" is reserved', () => assert.strictEqual(PR.isReservedPrefix('00'), true));
|
||||
test('"FF" is reserved', () => assert.strictEqual(PR.isReservedPrefix('FF'), true));
|
||||
test('lowercase "ff" is reserved', () => assert.strictEqual(PR.isReservedPrefix('ff'), true));
|
||||
test('lowercase "00" is reserved', () => assert.strictEqual(PR.isReservedPrefix('00'), true));
|
||||
test('"01" is NOT reserved', () => assert.strictEqual(PR.isReservedPrefix('01'), false));
|
||||
test('"FE" is NOT reserved', () => assert.strictEqual(PR.isReservedPrefix('FE'), false));
|
||||
test('"A3" is NOT reserved', () => assert.strictEqual(PR.isReservedPrefix('A3'), false));
|
||||
test('2-byte "0042" reserved (first byte = 00)', () => assert.strictEqual(PR.isReservedPrefix('0042'), true));
|
||||
test('2-byte "FF01" reserved (first byte = FF)', () => assert.strictEqual(PR.isReservedPrefix('FF01'), true));
|
||||
test('2-byte "01FF" NOT reserved (first byte = 01)', () => assert.strictEqual(PR.isReservedPrefix('01FF'), false));
|
||||
test('3-byte "001234" reserved', () => assert.strictEqual(PR.isReservedPrefix('001234'), true));
|
||||
test('3-byte "FFAABB" reserved', () => assert.strictEqual(PR.isReservedPrefix('FFAABB'), true));
|
||||
test('empty string NOT reserved', () => assert.strictEqual(PR.isReservedPrefix(''), false));
|
||||
|
||||
console.log('\n=== #1473: markReservedCells() on a mock 1-byte matrix ===');
|
||||
function mkCell(hex) {
|
||||
const classes = new Set(['hash-cell']);
|
||||
const attrs = { 'data-hex': hex };
|
||||
return {
|
||||
getAttribute: k => attrs[k],
|
||||
setAttribute: (k, v) => { attrs[k] = String(v); },
|
||||
classList: {
|
||||
add: c => classes.add(c),
|
||||
remove: c => classes.delete(c),
|
||||
contains: c => classes.has(c),
|
||||
},
|
||||
_attrs: attrs,
|
||||
_classes: classes,
|
||||
};
|
||||
}
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const hex = i.toString(16).toUpperCase().padStart(2, '0');
|
||||
cells.push(mkCell(hex));
|
||||
}
|
||||
cells[0].classList.add('hash-active');
|
||||
cells[255].classList.add('hash-active');
|
||||
cells[10].classList.add('hash-active');
|
||||
|
||||
const root = { querySelectorAll: () => cells };
|
||||
const marked = PR.markReservedCells(root);
|
||||
|
||||
test('markReservedCells returned 2 (00 and FF)', () => assert.strictEqual(marked, 2));
|
||||
test('cell 00 has .prefix-reserved', () => assert.strictEqual(cells[0]._classes.has('prefix-reserved'), true));
|
||||
test('cell FF has .prefix-reserved', () => assert.strictEqual(cells[255]._classes.has('prefix-reserved'), true));
|
||||
test('cell 01 does NOT have .prefix-reserved', () => assert.strictEqual(cells[1]._classes.has('prefix-reserved'), false));
|
||||
test('cell A3 does NOT have .prefix-reserved', () => assert.strictEqual(cells[0xA3]._classes.has('prefix-reserved'), false));
|
||||
test('cell FE does NOT have .prefix-reserved', () => assert.strictEqual(cells[0xFE]._classes.has('prefix-reserved'), false));
|
||||
test('cell 00 had .hash-active removed (no click handler attached)', () => assert.strictEqual(cells[0]._classes.has('hash-active'), false));
|
||||
test('cell FF had .hash-active removed', () => assert.strictEqual(cells[255]._classes.has('hash-active'), false));
|
||||
test('cell 10 still has .hash-active (untouched)', () => assert.strictEqual(cells[10]._classes.has('hash-active'), true));
|
||||
test('cell 00 has aria-disabled=true', () => assert.strictEqual(cells[0]._attrs['aria-disabled'], 'true'));
|
||||
test('cell FF has aria-disabled=true', () => assert.strictEqual(cells[255]._attrs['aria-disabled'], 'true'));
|
||||
test('cell 00 title cites MeshCore firmware keygen', () => assert.ok(/MeshCore firmware keygen/i.test(cells[0]._attrs.title)));
|
||||
test('cell FF title cites MeshCore firmware keygen', () => assert.ok(/MeshCore firmware keygen/i.test(cells[255]._attrs.title)));
|
||||
|
||||
console.log('\n=== #1473: prefix-reserved.js loaded by index.html ===');
|
||||
const indexHtml = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
|
||||
test('index.html includes prefix-reserved.js script', () => assert.ok(/prefix-reserved\.js/.test(indexHtml)));
|
||||
const styleCss = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
|
||||
test('style.css defines .prefix-reserved', () => assert.ok(/\.prefix-reserved\b/.test(styleCss)));
|
||||
test('style.css disables pointer events on reserved cell',
|
||||
() => assert.ok(/\.prefix-reserved[\s\S]{0,400}pointer-events:\s*none/i.test(styleCss)));
|
||||
|
||||
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
||||
if (failed) process.exit(1);
|
||||
Reference in New Issue
Block a user