From 3c30c17ab01b6a8ce0cd746b7410141b17c4916a Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sun, 4 Oct 2026 11:07:31 +0000 Subject: [PATCH] Igneum Miner: identity labels and vote keys come from a per-install machine id, never the hostname (two cloned PCs shared COMPUTERNAME and signed with the same keys); hostname is a display label only, editable in settings; upload fields carry the id Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/config.rs | 35 +++++++++++++++++++++++++++++++++-- app/igneum-app/src/engine.rs | 29 +++++++++++++++++------------ app/igneum-app/src/server.rs | 3 ++- app/igneum-app/src/state.rs | 4 +++- app/igneum-app/ui/app.js | 7 ++++++- app/igneum-app/ui/index.html | 8 ++++++++ 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 72cc64a03..fb864e846 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -32,6 +32,9 @@ pub struct Settings { pub identities: u32, #[serde(default)] pub cards: HashMap, + /// A friendly name for this machine (defaults to the hostname); display only. + #[serde(default)] + pub display_name: String, #[serde(default = "yes")] pub vote: bool, #[serde(default)] @@ -50,7 +53,7 @@ fn yes() -> bool { impl Default for Settings { fn default() -> Settings { - Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), vote: true, paused: false, accepted_total: 0 } + Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0 } } } @@ -68,6 +71,24 @@ impl Settings { } } +/// Reads the machine id, or makes one on the first run (16 hex from the OS) and locks the file to the user. +fn machine_id(app_dir: &Path) -> String { + let path = app_dir.join("machine-id"); + if let Some(id) = std::fs::read_to_string(&path).ok().map(|s| s.trim().to_ascii_lowercase()) { + if id.len() == 16 && id.chars().all(|c| c.is_ascii_hexdigit()) { + return id; + } + } + let mut raw = [0u8; 8]; + getrandom::getrandom(&mut raw).expect("os randomness"); + let id = crate::keys::hex(&raw); + let _ = std::fs::create_dir_all(app_dir); + crate::platform::lock_permissions(app_dir, true); + let _ = std::fs::write(&path, format!("{id}\n")); + crate::platform::lock_permissions(&path, false); + id +} + /// Written by the packager (build-dmg.sh, make-payload.sh). Missing fields disable the feature. #[derive(Clone, Serialize, Deserialize, Default)] pub struct Packaged { @@ -109,7 +130,12 @@ pub struct Runtime { pub app_dir: PathBuf, pub log_dir: PathBuf, pub status_secs: u32, + /// The machine's hostname: a friendly label only, never part of a key or a payout address. pub host: String, + /// Per-install random id (16 hex, app data dir/machine-id, locked to the user). Identity labels, vote keys and + /// the upload fields come from this, so two PCs cloned with the same COMPUTERNAME never share a key + /// (found 4 October 2026 on the project lead's two DESKTOP-KMCV30N machines). + pub machine_id: String, } impl Runtime { @@ -136,7 +162,12 @@ impl Runtime { let app_dir = root.join("app"); let log_dir = crate::platform::log_root(); let status_secs = env("IGNEUM_APP_STATUS_SECS").and_then(|v| v.parse().ok()).unwrap_or(30); - Runtime { network, rpc_port, p2p_port, peers, unsynced_mining, devnet_suffix, node_dir, app_dir, log_dir, status_secs, host: crate::platform::host_label() } + let machine_id = machine_id(&app_dir); + Runtime { network, rpc_port, p2p_port, peers, unsynced_mining, devnet_suffix, node_dir, app_dir, log_dir, status_secs, host: crate::platform::host_label(), machine_id } + } + /// The first 8 hex of the machine id: what goes into labels. + pub fn id8(&self) -> String { + self.machine_id.chars().take(8).collect() } pub fn rpc_url(&self) -> String { format!("grpc://127.0.0.1:{}", self.rpc_port) diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 010fe679a..cb74b9c49 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -61,6 +61,8 @@ impl Shared { st.network = runtime.network.clone(); st.chain = if runtime.network == "devnet" { "devnet v4".into() } else { format!("{} (private)", runtime.network) }; st.host = runtime.host.clone(); + st.display_name = if settings.display_name.is_empty() { runtime.host.clone() } else { settings.display_name.clone() }; + st.machine_id = runtime.machine_id.clone(); st.node_dir = runtime.node_dir.display().to_string(); st.log_dir = runtime.log_dir.display().to_string(); st.setup_done = settings.setup_done; @@ -187,10 +189,15 @@ impl Shared { Ok(json!({ "ok": true, "address": w.address, "display": keys::checksum(&w.address), "private_key": w.private_key, "wallet_file": self.wallet_path.display().to_string() })) } - pub fn apply_settings(&self, identities: Option, vote: Option, login: Option, address: Option<&str>) -> Result { + pub fn apply_settings(&self, identities: Option, vote: Option, login: Option, address: Option<&str>, display_name: Option<&str>) -> Result { let mut restart = Vec::new(); { let mut s = self.settings.lock().unwrap(); + if let Some(n) = display_name { + let n: String = n.trim().chars().take(40).collect(); + s.display_name = n.clone(); + self.state.lock().unwrap().display_name = if n.is_empty() { self.runtime.host.clone() } else { n }; + } if let Some(n) = identities { if n != s.identities { s.identities = n; @@ -338,7 +345,8 @@ impl Engine { let (y, m, d) = civil_from_days(days as i64); format!("{y:04}{m:02}{d:02}-{:02}{:02}{:02}", t % 86400 / 3600, t % 3600 / 60, t % 60) }; - let label_base = format!("{}-{}", if cfg!(target_os = "macos") { "mac" } else { "win" }, shared.runtime.host); + // labels never carry the hostname: two cloned PCs with the same COMPUTERNAME would sign with the same keys + let label_base = format!("{}-{}", if cfg!(target_os = "macos") { "mac" } else { "win" }, shared.runtime.id8()); Engine { shared, bins, @@ -385,9 +393,10 @@ impl Engine { pub fn run(mut self) { let setup_done = self.shared.settings.lock().unwrap().setup_done; self.shared.log(&format!( - "Igneum Miner {} on {}, {} (run {}-{})", + "Igneum Miner {} on {} (machine id {}), {} (run {}-{})", VERSION, self.shared.runtime.host, + self.shared.runtime.machine_id, self.st().chain, self.label_base, self.stamp @@ -653,22 +662,17 @@ impl Engine { /// One slot per enabled card. Called once the cards are known; re-planned after a settings change. fn plan_miners(&mut self) { let cards = self.st().mining.cards.clone(); - let mut per_vendor = std::collections::HashMap::::new(); - for c in cards.iter().filter(|c| c.enabled) { - *per_vendor.entry(c.vendor.clone()).or_default() += 1; - } - let mut seen = std::collections::HashMap::::new(); + let id8 = self.shared.runtime.id8(); for c in cards.iter() { if !c.enabled || self.miners.iter().any(|m| m.card == c.index) { continue; } - let n = seen.entry(c.vendor.clone()).or_default(); - *n += 1; let prefix = match c.vendor.as_str() { "apple" => "mac".to_string(), v => v.to_string(), }; - let label = if per_vendor.get(&c.vendor).copied().unwrap_or(1) > 1 { format!("{prefix}-{}-{n}", self.shared.runtime.host) } else { format!("{prefix}-{}", self.shared.runtime.host) }; + // --: the miner derives the vote keys from this label (and -1..N per identity) + let label = format!("{prefix}-{id8}-{}", c.index + 1); let worker = match c.worker.as_str() { "Metal" => self.bins.metal.clone(), "CUDA" => self.bins.cuda.clone(), @@ -1382,7 +1386,8 @@ impl Engine { files.push((format!("miner-{}", m.label), pr.log_path.clone())); } } - let (url, key, machine, run_id) = (p.log_intake_url.clone(), p.log_intake_key.clone(), self.shared.runtime.host.clone(), format!("{}-{}", self.label_base, self.stamp)); + // machine = "-": readable in tools/logs.mjs and distinct for cloned PCs + let (url, key, machine, run_id) = (p.log_intake_url.clone(), p.log_intake_key.clone(), format!("{}-{}", self.shared.runtime.host, self.shared.runtime.id8()), format!("{}-{}", self.label_base, self.stamp)); let t = std::thread::spawn(move || { for (label, path) in files { crate::update::upload_log(&url, &key, &label, &machine, &run_id, &path); diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index 3d6c77ea2..5cbe8c3d3 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -214,7 +214,8 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { shared.send(Cmd::CheckUpdate); diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index e7d3c1773..bfa1bf94c 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -136,7 +136,9 @@ pub struct State { pub setup_done: bool, pub network: String, pub chain: String, - pub host: String, + pub host: String, // the hostname, a friendly label only + pub display_name: String, // the user's name for this machine (settings), defaults to the hostname + pub machine_id: String, // per-install id; labels and vote keys come from its first 8 hex pub node_dir: String, pub log_dir: String, pub detecting: bool, diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 929db1a5c..ea3c81f0b 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -97,6 +97,8 @@ if (!state) return; $('s-address').textContent = state.address.display || 'not set'; renderCardRows($('s-cards'), state.mining.cards); + $('s-name').value = state.display_name || ''; + $('s-mid').textContent = (state.machine_id || '').slice(0, 8); $('s-vote').checked = state.settings.vote; $('s-login').checked = state.settings.start_at_login; $('s-version').textContent = 'Igneum Miner ' + state.version; @@ -115,6 +117,9 @@ $('s-reveal').addEventListener('click', function () { api('api/key/reveal', {}).then(function (r) { if (r.ok) { $('s-key').textContent = r.private_key; $('s-key-box').hidden = false; } else toast(r.error || 'no key'); }); }); + $('s-name-save').addEventListener('click', function () { + api('api/settings', { display_name: $('s-name').value }).then(function (r) { if (r.ok) toast('Renamed'); }); + }); $('s-cards-save').addEventListener('click', function () { var choices = readCardRows($('s-cards')); api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast('Applied; only the changed workers restart'); }); @@ -308,7 +313,7 @@ } // bottom $('btn-pause').textContent = m.paused ? 'Resume' : 'Pause'; - $('foot-status').textContent = (s.quitting ? 'stopping: miners first, then the node' : (m.state === 'mining' ? 'mining' : m.state) + ' · node ' + nodeWord + ' · up ' + uptime(s.uptime_s) + (s.address.display ? ' · rewards to ' + s.address.display.slice(0, 8) + '…' + s.address.display.slice(-4) : '')); + $('foot-status').textContent = (s.quitting ? 'stopping: miners first, then the node' : (s.display_name ? s.display_name + ' · ' : '') + (m.state === 'mining' ? 'mining' : m.state) + ' · node ' + nodeWord + ' · up ' + uptime(s.uptime_s) + (s.address.display ? ' · rewards to ' + s.address.display.slice(0, 8) + '…' + s.address.display.slice(-4) : '')); $('foot-version').textContent = 'v' + s.version; } function renderPill(s) { diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index d24801581..26855c8e3 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -229,6 +229,14 @@ +
+
this machine
+
+ + +
+

A label for you only. Keys and labels come from the machine id , never from the computer name.

+
cards