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 <noreply@anthropic.com>
This commit is contained in:
parent
fcdb04bb41
commit
3c30c17ab0
6 changed files with 69 additions and 17 deletions
|
|
@ -32,6 +32,9 @@ pub struct Settings {
|
|||
pub identities: u32,
|
||||
#[serde(default)]
|
||||
pub cards: HashMap<String, CardPref>,
|
||||
/// 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)
|
||||
|
|
|
|||
|
|
@ -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<u32>, vote: Option<bool>, login: Option<bool>, address: Option<&str>) -> Result<Value, String> {
|
||||
pub fn apply_settings(&self, identities: Option<u32>, vote: Option<bool>, login: Option<bool>, address: Option<&str>, display_name: Option<&str>) -> Result<Value, String> {
|
||||
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::<String, usize>::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::<String, usize>::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) };
|
||||
// <platform>-<machine id8>-<card n>: 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 = "<hostname>-<id8>": 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);
|
||||
|
|
|
|||
|
|
@ -214,7 +214,8 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
let vote = body.get("vote").and_then(|v| v.as_bool());
|
||||
let login = body.get("start_at_login").and_then(|v| v.as_bool());
|
||||
let address = s("address");
|
||||
shared.apply_settings(identities, vote, login, address.as_deref())
|
||||
let display_name = s("display_name");
|
||||
shared.apply_settings(identities, vote, login, address.as_deref(), display_name.as_deref())
|
||||
}
|
||||
"/api/update/check" => {
|
||||
shared.send(Cmd::CheckUpdate);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -229,6 +229,14 @@
|
|||
<button class="btn small ghost" id="s-reveal" hidden>Show my key</button>
|
||||
<div class="box mono key" id="s-key-box" hidden><span id="s-key"></span><button class="btn tiny" data-copy="s-key">Copy</button></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">this machine</div>
|
||||
<div class="row">
|
||||
<input type="text" class="addr-input" id="s-name" placeholder="a name for this machine" maxlength="40" spellcheck="false">
|
||||
<button class="btn small" id="s-name-save">Rename</button>
|
||||
</div>
|
||||
<p class="note">A label for you only. Keys and labels come from the machine id <span class="mono" id="s-mid"></span>, never from the computer name.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">cards</div>
|
||||
<div class="cards compact" id="s-cards"></div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue