2.0.2: PRODUCT=public|lab packaging switch, the kill-by-name refusal, the prove host's lease, the ten-minute check, the synced apply bound
PRODUCT=public|lab (the founder's word at 20:21 UK): one variable in packaging/mac/packaged-config.sh that every packager reads: Igneum-Miner.iss AppId, AppName, folder and file name (/DProduct from build-installer.ps1 -Product), make-payload.sh and make-hive-package.sh names (payload folder, Hive CUSTOM_NAME and archive), build-dmg.sh bundle id, app and dmg names, the channel (igneum-2.0-devnet stays the public string), the manifest name and the signing root; the packager writes edition, channel and ota_root_hex into igneum-app.json and the engine names a mismatch on its update card. The job runner refuses a run-script body that ends a process by name (Stop-Process -Name, taskkill /IM, Get-Process | Stop-Process, pkill, killall): exit 77 with the matched line, in the signer and in the runner; a pid is the only way. Every igneum-prove-host spawn carries IGNEUM_PROVE_DEVICE, IGNEUM_PROVE_WORKLOAD, IGNEUM_PROVE_MEM_FREE_MB, IGNEUM_PROVE_MEM_BUDGET_MB and IGNEUM_PROVE_DEADLINE_S (the V6-07 contract); exit 78 reads "proving needs N GB free". The manifest check runs every ten minutes. A staged update applies at once on a synced node with an idle miner and within two minutes of the sync otherwise; Install now passes the finality guard; publish-manifest.sh --urgent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
af4302002f
commit
af5ade4339
16 changed files with 476 additions and 63 deletions
|
|
@ -344,6 +344,14 @@ fn machine_id(app_dir: &Path) -> String {
|
|||
/// Written by the packager (build-dmg.sh, make-payload.sh). Missing fields disable the feature.
|
||||
#[derive(Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Packaged {
|
||||
/// PRODUCT=public|lab (packaging/mac/packaged-config.sh, 8 October 2026): the edition the package was made for, its
|
||||
/// channel and the signing root its engine must carry; empty in a package from before the switch.
|
||||
#[serde(default)]
|
||||
pub edition: String,
|
||||
#[serde(default)]
|
||||
pub channel: String,
|
||||
#[serde(default)]
|
||||
pub ota_root_hex: String,
|
||||
#[serde(default)]
|
||||
pub update_manifest: String,
|
||||
#[serde(default)]
|
||||
|
|
@ -450,6 +458,22 @@ impl Packaged {
|
|||
self
|
||||
}
|
||||
|
||||
/// The words when the package was made for the other edition or another signing root (a public engine in a lab
|
||||
/// package would never verify a lab manifest, and the other way round); None when the fields match or are absent.
|
||||
pub fn edition_mismatch(&self) -> Option<String> {
|
||||
let mut faults = Vec::new();
|
||||
if !self.edition.is_empty() && self.edition != crate::edition::name() {
|
||||
faults.push(format!("the package is the {} edition and this engine is the {} build", self.edition, crate::edition::name()));
|
||||
}
|
||||
if !self.channel.is_empty() && !crate::edition::channel_accepted(&self.channel) {
|
||||
faults.push(format!("the package's channel {} is not this build's ({})", self.channel, crate::edition::channel()));
|
||||
}
|
||||
if !self.ota_root_hex.is_empty() && self.ota_root_hex != crate::edition::ota_key() {
|
||||
faults.push(format!("the package's signing root {} is not this build's root {}", fingerprint8(&self.ota_root_hex), fingerprint8(crate::edition::ota_key())));
|
||||
}
|
||||
if faults.is_empty() { None } else { Some(format!("package mismatch: {}; updates will not verify until the matching build is installed", faults.join("; "))) }
|
||||
}
|
||||
|
||||
/// The log header line: the intake URL with the key's fingerprint and the manifest URL with the folder's
|
||||
/// fingerprint, each with its source; the values themselves never appear (the log is uploaded).
|
||||
pub fn describe(&self) -> String {
|
||||
|
|
@ -612,6 +636,21 @@ mod tests {
|
|||
out
|
||||
}
|
||||
|
||||
/// Known failed first (PRODUCT=public|lab, 8 October 2026): the packager writes the edition, the channel and the signing
|
||||
/// root into igneum-app.json; an engine of the other build reports the mismatch in words (a public engine in a lab
|
||||
/// package would never verify a lab manifest, and the other way round), and an older package without the fields says nothing.
|
||||
#[test]
|
||||
fn a_package_of_the_other_edition_is_named() {
|
||||
let mine = Packaged { edition: crate::edition::name().into(), channel: crate::edition::channel().into(), ota_root_hex: crate::edition::ota_key().into(), ..Default::default() };
|
||||
assert_eq!(mine.edition_mismatch(), None);
|
||||
assert_eq!(Packaged::default().edition_mismatch(), None, "a package without the fields says nothing");
|
||||
let other = Packaged { edition: "beta".into(), channel: "igneum-2.0-devnet-beta".into(), ota_root_hex: "ab".repeat(32), ..Default::default() };
|
||||
let w = other.edition_mismatch().expect("named");
|
||||
assert!(w.contains("beta") && w.contains(crate::edition::name()) && w.contains("root"), "{w}");
|
||||
let root_only = Packaged { edition: crate::edition::name().into(), ota_root_hex: "cd".repeat(32), ..Default::default() };
|
||||
assert!(root_only.edition_mismatch().unwrap().contains("signing root"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_measurement_engine_overrides_the_copied_settings_in_memory() {
|
||||
let mut s = Settings { remote_jobs: true, auto_update: true, prove: true, paused: true, sweep: false, power_control: true, address: "0xabc".into(), ..Default::default() };
|
||||
|
|
|
|||
|
|
@ -136,6 +136,67 @@ impl Budget {
|
|||
}
|
||||
}
|
||||
|
||||
/// The job the host is admitted for (one per spawn): a shard proof, a segment aggregation, a whole chain run.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Workload {
|
||||
Shard,
|
||||
Aggregate,
|
||||
Chain,
|
||||
}
|
||||
|
||||
impl Workload {
|
||||
pub fn word(self) -> &'static str {
|
||||
match self {
|
||||
Workload::Shard => "shard",
|
||||
Workload::Aggregate => "aggregate",
|
||||
Workload::Chain => "chain",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The exit code the host answers with when the budget is under its profile's floor (the V6-07 sub-lane's contract,
|
||||
/// 8 October 2026): the engine records it on the lease and the card reads "proving needs N GB free".
|
||||
pub const HOST_FLOOR_EXIT: i32 = 78;
|
||||
|
||||
/// The five variables every igneum-prove-host spawn carries, exactly as the host reads them (the shipper's contract with
|
||||
/// the V6-07 sub-lane, 8 October 2026): the card's ordinal as the host enumerates it, the one workload admitted, the free
|
||||
/// memory the engine read at admission, the lease's grant (the host's hard ceiling) and the lease's deadline.
|
||||
pub fn host_env(device: u32, workload: Workload, free_mib: u64, budget_mib: u64, deadline_s: u64) -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
("IGNEUM_PROVE_DEVICE", device.to_string()),
|
||||
("IGNEUM_PROVE_WORKLOAD", workload.word().to_string()),
|
||||
("IGNEUM_PROVE_MEM_FREE_MB", free_mib.to_string()),
|
||||
("IGNEUM_PROVE_MEM_BUDGET_MB", budget_mib.to_string()),
|
||||
("IGNEUM_PROVE_DEADLINE_S", deadline_s.to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
/// The lease's grant for a proof on a card of `vram_mib` under `mode`: beside the miner the measured peak plus the
|
||||
/// headroom; alone on the card everything above the free floor; nothing on a card that only mines.
|
||||
pub fn proof_budget_mib(vram_mib: u64, mode: Mode) -> u64 {
|
||||
match mode {
|
||||
Mode::Simultaneous => PROOF_PEAK_MIB + PROOF_PEAK_MIB * HEADROOM_PCT / 100,
|
||||
Mode::TimeShare | Mode::ProveOnly => vram_mib.saturating_sub(FREE_MIB),
|
||||
Mode::MiningOnly => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The card's words after exit 78: the figure the host printed ("needs N GB" or "needs N MB"/"MiB"; the host's own line
|
||||
/// is "RESULT memory_profile refused: proving needs N GB free on the card for the <workload> workload (<floor> MiB
|
||||
/// floor); <free> MiB free") when it did, else the budget it was offered, in whole GB rounded up.
|
||||
pub fn floor_refusal_words(out: &str, budget_mib: u64) -> String {
|
||||
let printed = out.lines().rev().find_map(|l| {
|
||||
let i = l.find("needs ")?;
|
||||
let rest = &l[i + 6..];
|
||||
let n: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
let n = n.parse::<u64>().ok().filter(|n| *n > 0)?;
|
||||
let unit = rest[n.to_string().len()..].trim_start();
|
||||
Some(if unit.starts_with("GB") || unit.starts_with("GiB") { n } else { n.div_ceil(1024) })
|
||||
});
|
||||
let gb = printed.unwrap_or(budget_mib.div_ceil(1024));
|
||||
format!("proving needs {gb} GB free")
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Coordinator {
|
||||
leases: HashMap<String, Vec<Lease>>,
|
||||
|
|
@ -229,6 +290,40 @@ impl Coordinator {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Known failed first (the shipper's contract with the V6-07 sub-lane, 8 October 2026): every igneum-prove-host spawn
|
||||
/// carries the five names, exactly as written, with the lease's figures; a spawn without them is the old shape.
|
||||
#[test]
|
||||
fn the_host_reads_its_lease_from_five_named_variables() {
|
||||
let env = host_env(1, Workload::Shard, 11_800, 7_532, 600);
|
||||
let names: Vec<&str> = env.iter().map(|(k, _)| *k).collect();
|
||||
assert_eq!(names, ["IGNEUM_PROVE_DEVICE", "IGNEUM_PROVE_WORKLOAD", "IGNEUM_PROVE_MEM_FREE_MB", "IGNEUM_PROVE_MEM_BUDGET_MB", "IGNEUM_PROVE_DEADLINE_S"]);
|
||||
let values: Vec<&str> = env.iter().map(|(_, v)| v.as_str()).collect();
|
||||
assert_eq!(values, ["1", "shard", "11800", "7532", "600"]);
|
||||
assert_eq!(host_env(0, Workload::Aggregate, 1, 2, 3)[1].1, "aggregate");
|
||||
assert_eq!(host_env(0, Workload::Chain, 1, 2, 3)[1].1, "chain");
|
||||
}
|
||||
|
||||
/// The lease's grant per mode: the measured proof peak with headroom beside the miner; the card less the free floor
|
||||
/// when the prover has the card to itself; nothing on a mining-only card.
|
||||
#[test]
|
||||
fn the_budget_is_the_grant_the_mode_allows() {
|
||||
assert_eq!(proof_budget_mib(24_576, Mode::Simultaneous), PROOF_PEAK_MIB + PROOF_PEAK_MIB * HEADROOM_PCT / 100);
|
||||
assert_eq!(proof_budget_mib(12_288, Mode::TimeShare), 12_288 - FREE_MIB);
|
||||
assert_eq!(proof_budget_mib(8_192, Mode::ProveOnly), 8_192 - FREE_MIB);
|
||||
assert_eq!(proof_budget_mib(8_192, Mode::MiningOnly), 0);
|
||||
}
|
||||
|
||||
/// Exit 78 from the host is the floor refusal: the card's words name the floor the host printed, else the budget.
|
||||
#[test]
|
||||
fn the_floor_refusal_names_the_memory_proving_needs() {
|
||||
assert_eq!(HOST_FLOOR_EXIT, 78);
|
||||
assert_eq!(floor_refusal_words("RESULT refused: profile needs 9216 MB free, 7532 offered", 7_532), "proving needs 9 GB free");
|
||||
// the host's own line (the V6-07 sub-lane, 8 October 2026): the GB figure is read as GB, not as MiB
|
||||
assert_eq!(floor_refusal_words("RESULT memory_profile refused: proving needs 8 GB free on the card for the shard workload (7700 MiB floor); 6100 MiB free", 7_532), "proving needs 8 GB free");
|
||||
assert_eq!(floor_refusal_words("nothing useful", 7_532), "proving needs 8 GB free");
|
||||
assert_eq!(floor_refusal_words("", 12_000), "proving needs 12 GB free");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_modes_follow_the_measured_rows() {
|
||||
// nvidia-smi's figures: 32 GB 32,607; 24 GB 24,564; 16 GB 16,376; 12 GB 12,208; 8 GB 8,188; 6 GB 6,144
|
||||
|
|
|
|||
|
|
@ -1042,6 +1042,12 @@ impl Engine {
|
|||
// which intake and which downloads folder this build reports to and checks (fingerprints, never the values;
|
||||
// rotation phase 2 reads this line from every machine's upload: docs/plans/rotation-phase-2.md)
|
||||
self.shared.log(&self.shared.packaged.describe());
|
||||
if let Some(w) = self.shared.packaged.edition_mismatch() {
|
||||
self.shared.log(&w);
|
||||
let mut st = self.shared.state.lock().unwrap();
|
||||
st.update.error = w;
|
||||
st.update.status = "error".into();
|
||||
}
|
||||
// the prover service (proving v0): its own thread, idle until the setting is on
|
||||
crate::prover::start(self.shared.clone(), self.bins.dir.clone());
|
||||
self.shared.log(&format!("node binary: {} ({v})", self.bins.node.display()));
|
||||
|
|
@ -4174,6 +4180,8 @@ impl Engine {
|
|||
boundary_eta_s: if st.node.daa > 0 && st.program.boundary_daa > 0 { Some(st.program.eta_s) } else { None },
|
||||
// a remote job in progress counts as busy: no update applies under it (src/jobrun.rs)
|
||||
miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting"),
|
||||
// 2.0.2: no card mining or starting and nothing building: the staged update applies at once on a synced node
|
||||
miner_idle: !self.miners.iter().any(|m| m.building) && !st.mining.cards.iter().any(|c| c.enabled && (c.state == "mining" || c.state == "starting")),
|
||||
// a remote job in progress holds the update, urgent or not (src/manifest.rs safe_to_apply; PC 1, 6 October 2026)
|
||||
job_active: self.jobs.active(),
|
||||
daa: st.node.daa,
|
||||
|
|
|
|||
|
|
@ -1332,6 +1332,12 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
|||
let dir = jobs_dir.join(&job.id);
|
||||
let shell = shell_for(job);
|
||||
let body = job.str_param("script").replace("\r\n", "\n");
|
||||
// the founder's word at 20:21 UK (8 October 2026): a body that ends a process by name never runs (the relay agent's
|
||||
// Check-Task carries the same refusal, exit 77 with the matched line)
|
||||
if let Some(line) = jobs::kill_by_name_line(&body) {
|
||||
sink.line(&format!("refused: the script ends a process by name ({line}); a pid is the only way"));
|
||||
return Ok(Done { status: "failed".into(), exit: 77, summary: format!("refused: the script ends a process by name ({line}); a pid is the only way"), extra: json!({ "refused": "kill_by_name", "line": line }) });
|
||||
}
|
||||
let script = dir.join(if shell == "powershell" { "script.ps1" } else { "script.sh" });
|
||||
let text = if shell == "powershell" { body.replace('\n', "\r\n") } else { body };
|
||||
std::fs::write(&script, if shell == "powershell" { [b"\xEF\xBB\xBF".as_slice(), text.as_bytes()].concat() } else { text.into_bytes() }).map_err(|e| format!("cannot write the script: {e}"))?;
|
||||
|
|
|
|||
|
|
@ -373,12 +373,36 @@ fn sha_ok(s: &str) -> bool {
|
|||
}
|
||||
|
||||
/// Per-kind checks of the params, so a job that cannot run is refused at signing time.
|
||||
/// The founder's word at 20:21 UK, 8 October 2026 (fifteen Mac processes killed by a grep that evening; by construction,
|
||||
/// not by rule): a run-script body that ends a process by NAME is refused before it runs; a pid is the only way. The
|
||||
/// shapes: `Stop-Process -Name`, `taskkill /IM`, `Get-Process -Name … | Stop-Process`, `pkill`, `killall`, as commands
|
||||
/// (the first word of a line or of a pipeline stage), never as a word inside a string. Returns the matched line.
|
||||
pub fn kill_by_name_line(script: &str) -> Option<&str> {
|
||||
for raw in script.lines() {
|
||||
let line = raw.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let lower = line.to_ascii_lowercase();
|
||||
let stage_starts = |word: &str| lower.split(['|', ';', '&']).any(|stage| { let st = stage.trim_start(); st == word || st.starts_with(&format!("{word} ")) || st.starts_with(&format!("{word}\t")) });
|
||||
let taskkill_im = lower.contains("taskkill") && lower.split_whitespace().any(|w| w == "/im");
|
||||
let stop_by_name = lower.contains("stop-process") && (lower.contains("-name") || lower.contains("get-process -name") || lower.contains("get-process "));
|
||||
if stage_starts("pkill") || stage_starts("killall") || taskkill_im || stop_by_name {
|
||||
return Some(raw.trim());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn validate_params(job: &Job) -> Result<(), String> {
|
||||
match job.kind.as_str() {
|
||||
"run" => {
|
||||
if job.str_param("script").trim().is_empty() {
|
||||
return Err("run: params.script is empty".into());
|
||||
}
|
||||
if let Some(line) = kill_by_name_line(&job.str_param("script")) {
|
||||
return Err(format!("run: the script ends a process by name ({line}); a pid is the only way (Stop-Process -Id, taskkill /PID, kill <pid>)"));
|
||||
}
|
||||
let sh = job.str_param("shell");
|
||||
if !sh.is_empty() && !["powershell", "bash"].contains(&sh.as_str()) {
|
||||
return Err(format!("run: shell '{sh}' is not powershell or bash"));
|
||||
|
|
@ -860,6 +884,15 @@ mod tests {
|
|||
assert!(j("shard-benchmark", r#"{}"#).unwrap_err().contains("sha256"));
|
||||
assert!(j("shard-benchmark", &format!(r#"{{"sha256":"{}","fixtures":["../x"]}}"#, "b".repeat(64))).unwrap_err().contains("fixture"));
|
||||
assert!(j("run", r#"{"script":"ls","shell":"zsh"}"#).unwrap_err().contains("shell"));
|
||||
// the founder's word at 20:21 UK, 8 October 2026 (by construction, not by rule): a run-script body that ends a
|
||||
// process by name is refused before it runs; a pid is the only way. Known-failed first: every shape ran.
|
||||
for body in ["Stop-Process -Name igneum-app -Force", "taskkill /IM igneum-app.exe /F", "Get-Process -Name igneumd | Stop-Process", "pkill -f igneumd", "killall igneum-worker", "echo ok\ntaskkill /f /im node.exe\necho done"] {
|
||||
let e = j("run", &format!(r#"{{"script":{}}}"#, serde_json::Value::String(body.into()))).unwrap_err();
|
||||
assert!(e.contains("ends a process by name") && e.contains("pid"), "{body}: {e}");
|
||||
}
|
||||
assert!(j("run", r#"{"script":"Stop-Process -Id 4242; taskkill /PID 4242 /F; kill -TERM $(cat run.pid)"}"#).is_ok(), "a pid is the way");
|
||||
assert_eq!(kill_by_name_line("echo one\nGet-Process -Name igneumd | Stop-Process\necho two"), Some("Get-Process -Name igneumd | Stop-Process"));
|
||||
assert_eq!(kill_by_name_line("echo pkill is a word in a string, not a command: 'the pkill rule'"), None, "the shape, not the word");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -410,8 +410,18 @@ pub struct Moment {
|
|||
/// not, until the user presses Install now (`install_asked`)
|
||||
pub auto_update_off: bool,
|
||||
pub install_asked: bool,
|
||||
/// 2.0.2 (the shipper, the founder's mini, 8 October 2026): no card is mining or starting; an idle or refused miner
|
||||
/// is the strongest reason to apply, never a reason to wait.
|
||||
pub miner_idle: bool,
|
||||
/// How long the node has read synced, in seconds (0 when it does not).
|
||||
pub synced_for_s: u64,
|
||||
}
|
||||
|
||||
/// With automatic updates on, a staged update applies within this many seconds of the node reading synced (and of the
|
||||
/// staging, whichever is later), whatever the miner is doing: the machine's slot minute, the boundary guard and a
|
||||
/// starting worker hold it no longer than this (2.0.2; the mini held 2.0.1 for its slot minute with an idle miner).
|
||||
pub const SYNCED_APPLY_BOUND_S: u64 = 120;
|
||||
|
||||
/// No checkpoint lock for this long on a synced node = finality paused (the devnet locks every few minutes).
|
||||
pub const FINALITY_PAUSE_S: f64 = 15.0 * 60.0;
|
||||
|
||||
|
|
@ -430,19 +440,27 @@ pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
|||
if m.auto_update_off && !m.install_asked {
|
||||
return Err("automatic updates are off: waiting for Install now".into());
|
||||
}
|
||||
// the finality rule next: a fork-close or unsupported urgency does not pass it, only the manifest's flag
|
||||
if m.finality_paused && !m.manifest_urgent {
|
||||
// the finality rule next: a fork-close or unsupported urgency does not pass it, only the manifest's flag or the
|
||||
// operator's hand (Install now, 2.0.2: the held update can be the fix that restores the locks; the rule is for
|
||||
// unattended machines)
|
||||
if m.finality_paused && !m.manifest_urgent && !m.install_asked {
|
||||
return Err("waiting for finality: the network has not locked a checkpoint for 15 min; nothing installs on a chain that cannot lock".into());
|
||||
}
|
||||
if m.job_active {
|
||||
return Err("a remote job is running; installing when it closes".into());
|
||||
}
|
||||
if m.urgent {
|
||||
// the operator's Install now is urgent in its own right (the engine folds it into `urgent` too)
|
||||
if m.urgent || m.install_asked {
|
||||
return Ok(());
|
||||
}
|
||||
if m.network_drop_pct > NETWORK_DROP_HOLD_PCT {
|
||||
return Err(format!("the network lost {:.0}% of its identities in the last 10 minutes; holding the update", m.network_drop_pct));
|
||||
}
|
||||
// 2.0.2: a synced node with an idle miner applies at once; a mining one within SYNCED_APPLY_BOUND_S of the sync
|
||||
// and the staging, slot or no slot, boundary or no boundary
|
||||
if m.node_synced && (m.miner_idle || (m.synced_for_s >= SYNCED_APPLY_BOUND_S && m.ready_for_s >= SYNCED_APPLY_BOUND_S)) {
|
||||
return Ok(());
|
||||
}
|
||||
if !m.slot_ok {
|
||||
return Err("waiting for this machine's own minute of the hour (machines take turns)".into());
|
||||
}
|
||||
|
|
@ -656,9 +674,37 @@ mod tests {
|
|||
assert!(!newer("0.3.1.2", "0.3.0"));
|
||||
}
|
||||
|
||||
/// Known failed first (the shipper, the founder's mini, 8 October 2026 20:30 to 20:40 UK): 2.0.1 staged at 20:30:22,
|
||||
/// the node synced from 20:35, the miner idle the whole time, and nothing installed by 20:40 because the machine's
|
||||
/// own minute of the hour had not come. The rule: with automatic updates on, a staged update applies within
|
||||
/// SYNCED_APPLY_BOUND_S of the node reading synced, whatever the miner is doing; an idle or refused miner is the
|
||||
/// strongest reason to apply, never a reason to wait.
|
||||
#[test]
|
||||
fn a_staged_update_applies_within_two_minutes_of_sync_and_at_once_on_an_idle_miner() {
|
||||
let staged = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, job_active: false, ready_for_s: 300, urgent: false, slot_ok: false, network_drop_pct: 0.0, finality_paused: false, manifest_urgent: false, auto_update_off: false, install_asked: false, miner_idle: false, synced_for_s: 0 };
|
||||
assert_eq!(SYNCED_APPLY_BOUND_S, 120);
|
||||
// the mini's case: node synced, miner idle, outside the slot: applies at once
|
||||
assert!(safe_to_apply(&Moment { miner_idle: true, synced_for_s: 1, ..staged.clone() }).is_ok());
|
||||
// a mining miner: within the bound of the sync, outside the slot and inside the boundary guard, it still applies
|
||||
assert!(safe_to_apply(&Moment { synced_for_s: SYNCED_APPLY_BOUND_S, boundary_eta_s: Some(30), ..staged.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { synced_for_s: SYNCED_APPLY_BOUND_S - 1, ..staged.clone() }).unwrap_err().contains("own minute"));
|
||||
// the bound counts from the later of the sync and the staging
|
||||
assert!(safe_to_apply(&Moment { synced_for_s: 3600, ready_for_s: SYNCED_APPLY_BOUND_S - 1, ..staged.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { synced_for_s: 3600, ready_for_s: SYNCED_APPLY_BOUND_S, ..staged.clone() }).is_ok());
|
||||
// an unsynced node still waits, idle miner or not; a job, paused finality and updates-off still hold
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, miner_idle: true, synced_for_s: 0, ..staged.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { miner_idle: true, synced_for_s: 500, job_active: true, ..staged.clone() }).unwrap_err().contains("remote job"));
|
||||
assert!(safe_to_apply(&Moment { miner_idle: true, synced_for_s: 500, finality_paused: true, ..staged.clone() }).unwrap_err().contains("finality"));
|
||||
assert!(safe_to_apply(&Moment { miner_idle: true, synced_for_s: 500, auto_update_off: true, ..staged.clone() }).unwrap_err().contains("Install now"));
|
||||
// 20:45 on the mini: the finality guard held the update that was the fix for the locks, and Install now could not
|
||||
// pass it; the operator's hand outranks a rule for unattended machines (a fork-close urgency alone still does not)
|
||||
assert!(safe_to_apply(&Moment { finality_paused: true, install_asked: true, ..staged.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { finality_paused: true, urgent: true, install_asked: false, ..staged.clone() }).unwrap_err().contains("finality"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_moments() {
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, job_active: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0, finality_paused: false, manifest_urgent: false, auto_update_off: false, install_asked: false };
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, job_active: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0, finality_paused: false, manifest_urgent: false, auto_update_off: false, install_asked: false, miner_idle: false, synced_for_s: 60 };
|
||||
assert!(safe_to_apply(&base).is_ok());
|
||||
// the finality rule (Horizon frontier lane, 6 October 2026), the known-failed case: paused, no install; active, install;
|
||||
// paused with the manifest's urgent flag, install; paused with only a fork-close urgency, no install; patience never passes it
|
||||
|
|
@ -687,7 +733,8 @@ mod tests {
|
|||
// patience: an unsynced node for 6 h applies anyway
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S - 1, ..base.clone() }).is_err());
|
||||
// the machine's slot: outside it nothing applies, not even with patience; urgent ignores it
|
||||
// the machine's slot: outside it nothing applies in the first SYNCED_APPLY_BOUND_S of a synced node (2.0.2; before
|
||||
// that nothing applied outside it at all, not even with patience); urgent ignores it
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, ..base.clone() }).unwrap_err().contains("own minute"));
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { slot_ok: false, urgent: true, ..base.clone() }).is_ok());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
//! rule such as difficulty v2) reaches every node before its activation height.
|
||||
//!
|
||||
//! The loop, driven from the engine's tick:
|
||||
//! check (on start, then hourly with jitter): fetch igneum-app-latest.json and its .sig, verify the Ed25519
|
||||
//! check (on start, then every ten minutes with jitter (2.0.2; hourly before)): fetch igneum-app-latest.json and its .sig, verify the Ed25519
|
||||
//! signature with the key compiled into src/manifest.rs, parse, compare versions
|
||||
//! -> download (curl with resume into <app data>/app/updates/, then size and sha256 against the manifest)
|
||||
//! -> stage (macOS: mount the DMG or unpack the zip, copy the new bundle next to the running one, check its
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
//! the previous version (macOS: the .previous bundle; Windows: the previous installer kept in updates/).
|
||||
//!
|
||||
//! Environment (tests): IGNEUM_APP_UPDATE_MANIFEST overrides the manifest URL from igneum-app.json,
|
||||
//! IGNEUM_APP_UPDATE_CHECK_SECS the hourly interval, IGNEUM_APP_UPDATE_FIRST_SECS the delay of the first check.
|
||||
//! IGNEUM_APP_UPDATE_CHECK_SECS the check interval (600 s), IGNEUM_APP_UPDATE_FIRST_SECS the delay of the first check.
|
||||
|
||||
use crate::engine::{Cmd, Shared};
|
||||
use crate::manifest::{self, Manifest, Moment, PlatformEntry};
|
||||
|
|
@ -37,7 +37,7 @@ use std::time::{Duration, Instant};
|
|||
/// A manifest published this long before the engine started is a catch-up: the hourly rollout slot does not apply.
|
||||
const CATCH_UP_AFTER_S: u64 = 3600;
|
||||
const HEALTHY_AFTER_S: u64 = 90;
|
||||
const CHECK_EVERY_S: u64 = 3600;
|
||||
const CHECK_EVERY_S: u64 = 600;
|
||||
const RETRY_AFTER_ERROR_S: u64 = 600;
|
||||
|
||||
pub enum Event {
|
||||
|
|
@ -72,6 +72,8 @@ pub struct Ctx {
|
|||
pub finality_paused: bool,
|
||||
pub boundary_eta_s: Option<i64>,
|
||||
pub miner_busy: bool,
|
||||
/// No card mines or starts (2.0.2): a staged update applies at once on a synced node.
|
||||
pub miner_idle: bool,
|
||||
/// A remote job is running (src/jobrun.rs): the install holds, urgent or not.
|
||||
pub job_active: bool,
|
||||
pub daa: u64,
|
||||
|
|
@ -102,6 +104,8 @@ pub struct Updater {
|
|||
busy: bool,
|
||||
next_check: Instant,
|
||||
ready_since: Option<Instant>,
|
||||
/// when the node last went from unsynced to synced (None while it is not): manifest::SYNCED_APPLY_BOUND_S counts from it
|
||||
synced_since: Option<Instant>,
|
||||
last_safe_check: Instant,
|
||||
install_asked: bool,
|
||||
pending: Option<Pending>,
|
||||
|
|
@ -165,6 +169,7 @@ impl Updater {
|
|||
busy: false,
|
||||
next_check: now + Duration::from_secs(first),
|
||||
ready_since: None,
|
||||
synced_since: None,
|
||||
last_safe_check: now,
|
||||
install_asked: false,
|
||||
pending: None,
|
||||
|
|
@ -614,8 +619,14 @@ impl Updater {
|
|||
}
|
||||
let slot_ok = minute == self.slot || catch_up || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
||||
let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
||||
if ctx.node_synced {
|
||||
self.synced_since.get_or_insert(now);
|
||||
} else {
|
||||
self.synced_since = None;
|
||||
}
|
||||
let synced_for = self.synced_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
||||
let manifest_urgent = self.manifest.as_ref().map(|m| m.urgent).unwrap_or(false);
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, job_active: ctx.job_active, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct , auto_update_off: !self.auto, install_asked: self.install_asked, finality_paused: ctx.finality_paused, manifest_urgent };
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, job_active: ctx.job_active, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct , auto_update_off: !self.auto, install_asked: self.install_asked, finality_paused: ctx.finality_paused, manifest_urgent, miner_idle: ctx.miner_idle, synced_for_s: synced_for };
|
||||
if !self.auto && !self.install_asked {
|
||||
// F14: off stays off, urgent or not; an unsupported version pauses mining (the engine reads update.hold_mining)
|
||||
let mut st = shared.state.lock().unwrap();
|
||||
|
|
@ -1248,6 +1259,14 @@ pub fn legacy_return_sequence(installer_exit: i32) -> Vec<ReturnStep> {
|
|||
#[cfg(test)]
|
||||
mod return_tests {
|
||||
use super::*;
|
||||
/// Known failed first (8 October 2026, 2.0.2): the hourly check made a user wait up to an hour for an entry the
|
||||
/// fleet published (the 2.0.1 entry reached a machine after 14 minutes only because its api was poked).
|
||||
/// Ten minutes, the first check after start unchanged, the staged update still applied at the next safe moment.
|
||||
#[test]
|
||||
fn the_manifest_check_runs_every_ten_minutes() {
|
||||
assert_eq!(CHECK_EVERY_S, 600);
|
||||
assert_eq!(CHECK_EVERY_S / 6 + 1, 101, "the jitter window stays a sixth of the interval");
|
||||
}
|
||||
fn done(steps: &[ReturnStep]) -> &ReturnStep {
|
||||
steps.last().unwrap()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ use std::process::Command;
|
|||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// The lease deadline a shard proof is given (the F07 budget row's clock: transfer, start, prove and rebuild inside
|
||||
/// the exclusive window); a chain run and an aggregation carry their run limits.
|
||||
const SHARD_DEADLINE_S: u64 = 600;
|
||||
|
||||
/// One shard the node lists for this machine's keys (`igneum_getAssignedShards`).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Work {
|
||||
|
|
@ -247,6 +251,42 @@ fn read_verifier(shared: &Shared) {
|
|||
/// every second and killed when the app quits, the setting goes off or `limit` passes (a proof must never outlive
|
||||
/// the app). Returns (exit ok, output).
|
||||
fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&str, &str)], limit: Duration, log: &Path) -> (bool, String) {
|
||||
let (code, out) = run_tool_code(shared, t, exe, args, env, limit, log);
|
||||
(code == Some(0), out)
|
||||
}
|
||||
|
||||
/// The lease every igneum-prove-host spawn carries (the V6-07 sub-lane's contract, src/device.rs host_env): the first
|
||||
/// NVIDIA card as the host enumerates it, the free memory read now, the grant the card's mode allows, the deadline.
|
||||
/// (free, budget, env) so the refusal words can name the budget; a machine without an NVIDIA card gets device 0 and
|
||||
/// no grant, which the host reads as the CPU shape.
|
||||
fn host_lease(shared: &Shared, workload: crate::device::Workload, deadline_s: u64) -> (u64, u64, Vec<(&'static str, String)>) {
|
||||
let (index, vram, mining) = {
|
||||
let st = shared.state.lock().unwrap();
|
||||
st.mining.cards.iter().filter(|c| c.vendor == "nvidia").min_by_key(|c| c.index).map(|c| (c.index as u32, c.vram_mb, c.enabled && c.state == "mining")).unwrap_or((0, 0, false))
|
||||
};
|
||||
let used = crate::detect::nvidia_memory_used().get(&index.to_string()).copied().unwrap_or(0);
|
||||
let free = vram.saturating_sub(used);
|
||||
let budget = crate::device::proof_budget_mib(vram, crate::device::mode(vram, mining));
|
||||
let env = crate::device::host_env(index, workload, free, budget, deadline_s);
|
||||
shared.log(&format!("LEASE holder=prover device=nvidia:{index} workload={} free_mib={free} budget_mib={budget} deadline_s={deadline_s}", workload.word()));
|
||||
(free, budget, env)
|
||||
}
|
||||
|
||||
/// The host's floor refusal (exit 78): the card's words, the LEASE line with the refusal, Some(words); None otherwise.
|
||||
fn host_floor_refusal(shared: &Shared, code: Option<i32>, out: &str, budget: u64) -> Option<String> {
|
||||
if code != Some(crate::device::HOST_FLOOR_EXIT) {
|
||||
return None;
|
||||
}
|
||||
let words = crate::device::floor_refusal_words(out, budget);
|
||||
shared.log(&format!("LEASE holder=prover event=refused exit={} budget_mib={budget} words=\"{words}\"", crate::device::HOST_FLOOR_EXIT));
|
||||
set(shared, |p| {
|
||||
p.status = "waiting".into();
|
||||
p.message = words.clone();
|
||||
});
|
||||
Some(words)
|
||||
}
|
||||
|
||||
fn run_tool_code(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&str, &str)], limit: Duration, log: &Path) -> (Option<i32>, String) {
|
||||
// Windows: a script file exports the environment and execs the host; the arguments travel as $1, $2... (the
|
||||
// single-quoted rule of src/wslhost.rs). The file lives until the run ends.
|
||||
let (mut cmd, _script) = if t.wsl {
|
||||
|
|
@ -254,7 +294,7 @@ fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&st
|
|||
body.push_str(&format!("exec {} \"$@\"", crate::wslhost::sq(&exe.display().to_string())));
|
||||
let file = match crate::wslhost::write_script("prove-run", &body) {
|
||||
Ok(f) => f,
|
||||
Err(e) => return (false, format!("cannot write the WSL run script: {e}")),
|
||||
Err(e) => return (None, format!("cannot write the WSL run script: {e}")),
|
||||
};
|
||||
let argv: Vec<&str> = args.iter().map(|a| a.as_str()).collect();
|
||||
let c = crate::wslhost::command(&crate::platform::tool("wsl"), crate::wslhost::DISTRO, None, &file.path, true, &argv);
|
||||
|
|
@ -268,12 +308,12 @@ fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&st
|
|||
(c, None)
|
||||
};
|
||||
crate::platform::quiet(&mut cmd);
|
||||
let Ok(file) = std::fs::File::create(log) else { return (false, format!("cannot write {}", log.display())) };
|
||||
let Ok(err) = file.try_clone() else { return (false, "cannot clone the log handle".into()) };
|
||||
let Ok(file) = std::fs::File::create(log) else { return (None, format!("cannot write {}", log.display())) };
|
||||
let Ok(err) = file.try_clone() else { return (None, "cannot clone the log handle".into()) };
|
||||
cmd.stdin(std::process::Stdio::null()).stdout(file).stderr(err);
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return (false, format!("{}: {e}", exe.display())),
|
||||
Err(e) => return (None, format!("{}: {e}", exe.display())),
|
||||
};
|
||||
let started = Instant::now();
|
||||
let status = loop {
|
||||
|
|
@ -282,7 +322,7 @@ fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&st
|
|||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
let _ = child.kill();
|
||||
return (false, format!("{}: {e}", exe.display()));
|
||||
return (None, format!("{}: {e}", exe.display()));
|
||||
}
|
||||
}
|
||||
let stop = {
|
||||
|
|
@ -298,8 +338,8 @@ fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&st
|
|||
};
|
||||
let out = std::fs::read_to_string(log).unwrap_or_default();
|
||||
match status {
|
||||
Some(st) => (st.success(), out),
|
||||
None => (false, format!("{} stopped after {} s (quit, proving switched off, or the {} s limit)", exe.display(), started.elapsed().as_secs(), limit.as_secs())),
|
||||
Some(st) => (st.code().or(Some(-1)), out),
|
||||
None => (None, format!("{} stopped after {} s (quit, proving switched off, or the {} s limit)", exe.display(), started.elapsed().as_secs(), limit.as_secs())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -774,7 +814,14 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
}
|
||||
set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "CPU prover: about five minutes a shard, 30 GB of RAM, paid only when no card proves first".into() });
|
||||
let prover_env = if t.cuda { "cuda" } else { "cpu" };
|
||||
let (ok, out) = run_tool(&shared, t, &t.host, &[fix_p, "--mode".into(), "compressed".into(), "--shard".into(), w.shard.to_string(), "--prover".into(), payout.clone(), "--out".into(), res_p], &[("SP1_PROVER", prover_env), ("RUST_LOG", "off")], Duration::from_secs(3 * 3600), &dir.join(format!("prove-{}-{}.log", w.number, w.shard)));
|
||||
let (_free, budget, lease) = host_lease(&shared, crate::device::Workload::Shard, SHARD_DEADLINE_S);
|
||||
let mut env: Vec<(&str, &str)> = vec![("SP1_PROVER", prover_env), ("RUST_LOG", "off")];
|
||||
env.extend(lease.iter().map(|(k, v)| (*k, v.as_str())));
|
||||
let (code, out) = run_tool_code(&shared, t, &t.host, &[fix_p, "--mode".into(), "compressed".into(), "--shard".into(), w.shard.to_string(), "--prover".into(), payout.clone(), "--out".into(), res_p], &env, Duration::from_secs(3 * 3600), &dir.join(format!("prove-{}-{}.log", w.number, w.shard)));
|
||||
if let Some(words) = host_floor_refusal(&shared, code, &out, budget) {
|
||||
return Err(format!("prover: {words}"));
|
||||
}
|
||||
let ok = code == Some(0);
|
||||
if !ok || !results.exists() {
|
||||
let last = out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed").to_string();
|
||||
// the root-socket class (5 October 2026, PC 2 at 20:00Z and 21:25Z): a job that ran the host as root
|
||||
|
|
@ -975,7 +1022,14 @@ fn prove_segment(shared: &Shared, t: &Tools, seg: &crate::segments::SegmentWork,
|
|||
args.push("--prev".into());
|
||||
args.push(pf.to_string());
|
||||
}
|
||||
let (ok, out) = run_tool(shared, t, &t.host, &args, &[("SP1_PROVER", if t.cuda { "cuda" } else { "cpu" }), ("RUST_LOG", "off")], Duration::from_secs(3 * 3600), &dir.join("chain.log"));
|
||||
let (_free, budget, lease) = host_lease(shared, crate::device::Workload::Chain, 3 * 3600);
|
||||
let mut env: Vec<(&str, &str)> = vec![("SP1_PROVER", if t.cuda { "cuda" } else { "cpu" }), ("RUST_LOG", "off")];
|
||||
env.extend(lease.iter().map(|(k, v)| (*k, v.as_str())));
|
||||
let (code, out) = run_tool_code(shared, t, &t.host, &args, &env, Duration::from_secs(3 * 3600), &dir.join("chain.log"));
|
||||
if let Some(words) = host_floor_refusal(shared, code, &out, budget) {
|
||||
return Err(format!("chain: {words}"));
|
||||
}
|
||||
let ok = code == Some(0);
|
||||
if !ok || !results.exists() {
|
||||
let last_line = out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed").to_string();
|
||||
let hint = if last_line.contains("PermissionDenied") { " (a GPU-server socket /tmp/sp1-cuda-*.sock owned by another user: the root-socket class)" } else { "" };
|
||||
|
|
@ -1139,7 +1193,14 @@ fn aggregate_once(shared: &Shared, t: &Tools, label: &str, payout: &str, attempt
|
|||
args.push("--prev".into());
|
||||
args.push(pf);
|
||||
}
|
||||
let (ok, out) = run_tool(shared, t, &t.host, &args, &[("SP1_PROVER", if t.cuda { "cuda" } else { "cpu" }), ("RUST_LOG", "off")], Duration::from_secs(2 * 3600), &dir.join("aggregate.log"));
|
||||
let (_free, budget, lease) = host_lease(shared, crate::device::Workload::Aggregate, 2 * 3600);
|
||||
let mut env: Vec<(&str, &str)> = vec![("SP1_PROVER", if t.cuda { "cuda" } else { "cpu" }), ("RUST_LOG", "off")];
|
||||
env.extend(lease.iter().map(|(k, v)| (*k, v.as_str())));
|
||||
let (code, out) = run_tool_code(shared, t, &t.host, &args, &env, Duration::from_secs(2 * 3600), &dir.join("aggregate.log"));
|
||||
if let Some(words) = host_floor_refusal(shared, code, &out, budget) {
|
||||
return Err(format!("segment {first}..{last}: {words}"));
|
||||
}
|
||||
let ok = code == Some(0);
|
||||
if !ok || !results.exists() {
|
||||
return Err(format!("segment {first}..{last}: aggregator: {}", out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,3 +135,24 @@ One tree, two builds, chosen by the cargo feature `lab` on `app/igneum-app` (`sr
|
|||
|
||||
Each build refuses a manifest of the other channel before staging it. The engine reports `edition`, `product` and `channel` in `api/state` and `channel=`/`edition=` on the IGNEUM-APP intake line, so the relay agent and the jobs publisher refuse a public engine by what it says about itself. `tools/build-remote.sh` passes `--features lab` through its cargo arguments; the variable travels with the shipper's build environment (`bs_repro_env`).
|
||||
|
||||
|
||||
### PRODUCT=public|lab (the founder's word at 20:21 UK, 8 October 2026)
|
||||
|
||||
One variable, read by every packager through `packaging/mac/packaged-config.sh` (the table lives there and nowhere else):
|
||||
|
||||
| | public (default) | lab |
|
||||
|---|---|---|
|
||||
| name | Igneum Miner | Igneum Miner Lab |
|
||||
| channel | igneum-2.0-devnet | igneum-2.0-devnet-lab |
|
||||
| manifest | dl/<token>/igneum-app-latest.json | dl/<token>/igneum-app-lab-latest.json |
|
||||
| signing root | the release root (src/manifest.rs) | IGNEUM_LAB_PUBLIC_KEY or ~/.config/igneum/lab-signing/lab-signing-key.pub (refused when neither holds 64 hex) |
|
||||
| Mac | Igneum Miner.app, network.igneum.miner, Igneum-Miner-<v>.dmg | Igneum Miner Lab.app, network.igneum.miner.lab, Igneum-Miner-Lab-<v>.dmg |
|
||||
| Windows | AppId {A4C1F0E2-...}, Igneum-Miner-Setup-<v>.exe, payload igneum-windows-app | AppId {5E2B7C41-...}, Igneum-Miner-Lab-Setup-<v>.exe, payload igneum-windows-app-lab |
|
||||
| Hive | igneum-hive-<v>.tar.gz, CUSTOM_NAME=igneum | igneum-lab-hive-<v>.tar.gz, CUSTOM_NAME=igneum-lab |
|
||||
|
||||
`PRODUCT=lab packaging/mac/build-dmg.sh`, `PRODUCT=lab packaging/windows/make-payload.sh`, `PRODUCT=lab packaging/hive/make-hive-package.sh`,
|
||||
and on the PC `build-installer.ps1 -Product lab` (or `$env:PRODUCT`), which passes `/DProduct=lab` to Inno. The engine inside a
|
||||
lab package must be the lab build (`--features lab`): the packager writes `edition`, `channel` and `ota_root_hex` into
|
||||
igneum-app.json and the engine reports a mismatch on its update card (`Packaged::edition_mismatch`), since the other
|
||||
build would never verify that package's manifest. The two products share %LOCALAPPDATA%\igneum and the engine port on
|
||||
one PC: a lab install belongs on a lab machine, not beside the public one. Hive has no update manifest (no OTA on a rig).
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ NODE_OUT="${NODE_OUT:-$REPO/infra/cross/out-v2}"
|
|||
WORKERS_OUT="${WORKERS_OUT:-$REPO/infra/cross/out-workers}"
|
||||
OUT="${OUT:-$HERE/build}"
|
||||
FAKE=0; [[ "${1:-}" == "--fake" ]] && FAKE=1
|
||||
# PRODUCT=public|lab (packaging/mac/packaged-config.sh): the Hive custom-miner name is the slug (igneum or igneum-lab), so
|
||||
# the archive, the directory inside it, the config and the log paths are the product's own
|
||||
. "$REPO/packaging/mac/packaged-config.sh"
|
||||
log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*"; }
|
||||
die() { log "ERROR: $*" >&2; exit 1; }
|
||||
stage="$OUT/igneum"; rm -rf "$stage"; mkdir -p "$stage/bin"
|
||||
stage="$OUT/$PRODUCT_SLUG"; rm -rf "$stage"; mkdir -p "$stage/bin"
|
||||
if [[ $FAKE == 1 ]]; then
|
||||
VERSION="${VERSION:-0.0.0-fake}"
|
||||
for b in igneumd igneum-miner igneum-worker-cuda igneum-worker-opencl; do printf '#!/bin/sh\necho fake %s "$@"\n' "$b" > "$stage/bin/$b"; chmod +x "$stage/bin/$b"; done
|
||||
|
|
@ -35,17 +38,17 @@ else
|
|||
fi
|
||||
[[ -n "$VERSION" ]] || die "no version (VERSION=... or a version.txt with 'igneumd <version>')"
|
||||
cp "$HERE/h-config.sh" "$HERE/h-run.sh" "$HERE/h-stats.sh" "$HERE/README.md" "$stage/"
|
||||
sed "s/^CUSTOM_VERSION=.*/CUSTOM_VERSION=$VERSION/" "$HERE/h-manifest.conf" > "$stage/h-manifest.conf"
|
||||
sed -e "s/^CUSTOM_VERSION=.*/CUSTOM_VERSION=$VERSION/" -e "s/^CUSTOM_NAME=.*/CUSTOM_NAME=$PRODUCT_SLUG/" "$HERE/h-manifest.conf" > "$stage/h-manifest.conf"
|
||||
chmod +x "$stage"/h-*.sh "$stage"/bin/*
|
||||
for f in "$stage"/h-*.sh; do bash -n "$f"; done
|
||||
tgz="$OUT/igneum-hive-$VERSION.tar.gz"
|
||||
COPYFILE_DISABLE=1 tar -C "$OUT" --no-xattrs --no-mac-metadata -czf "$tgz" igneum 2>/dev/null || COPYFILE_DISABLE=1 tar -C "$OUT" -czf "$tgz" igneum
|
||||
tgz="$OUT/$PRODUCT_SLUG-hive-$VERSION.tar.gz"
|
||||
COPYFILE_DISABLE=1 tar -C "$OUT" --no-xattrs --no-mac-metadata -czf "$tgz" "$PRODUCT_SLUG" 2>/dev/null || COPYFILE_DISABLE=1 tar -C "$OUT" -czf "$tgz" "$PRODUCT_SLUG"
|
||||
rm -rf "$stage"
|
||||
sum="$(shasum -a 256 "$tgz" 2>/dev/null | awk '{print $1}' || sha256sum "$tgz" | awk '{print $1}')"
|
||||
log "wrote $tgz ($(du -h "$tgz" | cut -f1), sha256 $sum)"
|
||||
cat <<TXT
|
||||
Flight Sheet (HiveOS): Miner = Custom, then Setup Miner Config:
|
||||
Installation URL https://<your host>/igneum-hive-$VERSION.tar.gz (publish the archive on the download host)
|
||||
Installation URL https://<your host>/$PRODUCT_SLUG-hive-$VERSION.tar.gz (publish the archive on the download host)
|
||||
Miner name igneum
|
||||
Wallet and worker 0x<your 40-hex payout address>.%WORKER_NAME%
|
||||
Pool URL grpc://<your node>:26610 or local (the bundled node on the rig)
|
||||
|
|
|
|||
|
|
@ -35,12 +35,13 @@ REBUILD_WORKER="${REBUILD_WORKER:-1}"
|
|||
ICONS="$ROOT/brand/icons"
|
||||
BUILD="$HERE/build"
|
||||
DIST="$HERE/dist"
|
||||
DMG="$DIST/Igneum-Miner-$VERSION.dmg"
|
||||
STAGE="$BUILD/dmg"
|
||||
APP="$STAGE/Igneum Miner.app"
|
||||
STAMP="$(date -u +%Y%m%d%H%M)"
|
||||
export PATH="$HOME/.cargo/bin:/opt/homebrew/bin:$PATH"
|
||||
# PRODUCT=public|lab (packaged-config.sh): the app name, the dmg name, the volume, the bundle id
|
||||
. "$HERE/packaged-config.sh"
|
||||
DMG="$DIST/$PRODUCT_FILE-$VERSION.dmg"
|
||||
STAGE="$BUILD/dmg"
|
||||
APP="$STAGE/$PRODUCT_NAME.app"
|
||||
STAMP="$(date -u +%Y%m%d%H%M)"
|
||||
|
||||
if [ ! -x "$NODE" ]; then
|
||||
if [ -x "$ROOT/vendor/igneum-node/target/release/kaspad" ]; then
|
||||
|
|
@ -94,6 +95,9 @@ sed -e "s/VERSION_STAMP/$STAMP/" "$HERE/app/Info.plist" > "$APP/Contents/Info.pl
|
|||
# the short version is $VERSION whatever the template says (tools/ship-app.mjs keeps the template equal to Cargo.toml;
|
||||
# the sed that replaced a literal 0.3.0 here stopped matching at 0.3.1 and the bundles said 0.3.2 after that)
|
||||
plutil -replace CFBundleShortVersionString -string "$VERSION" "$APP/Contents/Info.plist"
|
||||
plutil -replace CFBundleIdentifier -string "$PRODUCT_BUNDLE_ID" "$APP/Contents/Info.plist"
|
||||
plutil -replace CFBundleName -string "$PRODUCT_NAME" "$APP/Contents/Info.plist"
|
||||
plutil -replace CFBundleDisplayName -string "$PRODUCT_NAME" "$APP/Contents/Info.plist"
|
||||
plutil -lint "$APP/Contents/Info.plist" >/dev/null
|
||||
printf 'APPL????' > "$APP/Contents/PkgInfo"
|
||||
cp "$BUILD/window/Igneum Miner" "$APP/Contents/MacOS/Igneum Miner"
|
||||
|
|
@ -130,17 +134,17 @@ v="$("$APP/Contents/MacOS/igneum-app" --version 2>&1 || true)"; case "$v" in "ig
|
|||
|
||||
# the rest of the image
|
||||
cp "$HERE/dmg/README.txt" "$STAGE/README.txt"
|
||||
cp "$HERE/app/Stop Igneum Miner.command" "$STAGE/Stop Igneum Miner.command"
|
||||
chmod 755 "$STAGE/Stop Igneum Miner.command"
|
||||
cp "$HERE/app/Stop Igneum Miner.command" "$STAGE/Stop $PRODUCT_NAME.command"
|
||||
chmod 755 "$STAGE/Stop $PRODUCT_NAME.command"
|
||||
|
||||
rm -f "$DMG"
|
||||
if command -v dmgbuild >/dev/null 2>&1; then
|
||||
dmgbuild -s "$HERE/dmg/settings.py" -D "app=$APP" -D "stage=$STAGE" -D "icons=$ICONS" "Igneum Miner" "$DMG"
|
||||
dmgbuild -s "$HERE/dmg/settings.py" -D "app=$APP" -D "stage=$STAGE" -D "icons=$ICONS" "$PRODUCT_NAME" "$DMG"
|
||||
else
|
||||
echo "note: dmgbuild is not installed (pip3 install dmgbuild); building a plain image without icon positions or background"
|
||||
ln -s /Applications "$STAGE/Applications"
|
||||
cp "$ICONS/igneum-volume.icns" "$STAGE/.VolumeIcon.icns"
|
||||
hdiutil create -volname "Igneum Miner" -srcfolder "$STAGE" -ov -format ULFO -fs HFS+ "$DMG" >/dev/null
|
||||
hdiutil create -volname "$PRODUCT_NAME" -srcfolder "$STAGE" -ov -format ULFO -fs HFS+ "$DMG" >/dev/null
|
||||
fi
|
||||
hdiutil verify "$DMG" >/dev/null
|
||||
echo "built $DMG ($(du -h "$DMG" | cut -f1), $(stat -f %z "$DMG") bytes, version $VERSION build $STAMP)"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,38 @@ LOG_URL="${IGNEUM_INTAKE_URL:-https://igneum-six.vercel.app/api/log}"
|
|||
LIVE_PAGE="https://igneum.network/live"
|
||||
DOWNLOAD_PAGE="https://igneum.network/#mine"
|
||||
DL_HOST="https://dl.igneum.network"
|
||||
|
||||
# ---- PRODUCT=public|lab (the founder's word at 20:21 UK, 8 October 2026): ONE variable the packagers read -------------
|
||||
# public: "Igneum Miner", the release root, channel igneum-2.0-devnet, manifest igneum-app-latest.json.
|
||||
# lab: "Igneum Miner Lab", the lab signing root (IGNEUM_LAB_PUBLIC_KEY or ~/.config/igneum/lab-signing/lab-signing-key.pub,
|
||||
# the hex only, never the private key), channel igneum-2.0-devnet-lab, manifest igneum-app-lab-latest.json in the
|
||||
# same downloads folder, its own bundle id, installer AppId, install folder and package names, so a lab install
|
||||
# never updates from, or over, the public one. The engine built for the package must be the matching build
|
||||
# (cargo --features lab for lab): the engine reads the edition and the root hex back from igneum-app.json and
|
||||
# refuses a mismatch (src/config.rs Packaged::edition_mismatch).
|
||||
PRODUCT="${PRODUCT:-public}"
|
||||
# the release root the engine compiles in (app/igneum-app/src/manifest.rs OTA_PUBLIC_KEY_HEX); the lab root is read, never written here
|
||||
PUBLIC_OTA_ROOT_HEX="b3c9c5bd144e9d246dc0edf897387d4f3f7ca494cd47457123d1c2f892cabddd"
|
||||
case "$PRODUCT" in
|
||||
public)
|
||||
PRODUCT_NAME="Igneum Miner"; PRODUCT_FILE="Igneum-Miner"; PRODUCT_SLUG="igneum"
|
||||
PRODUCT_BUNDLE_ID="network.igneum.miner"; PRODUCT_CHANNEL="igneum-2.0-devnet"; PRODUCT_MANIFEST_NAME="igneum-app-latest.json"
|
||||
PRODUCT_WINDOWS_APPID="{A4C1F0E2-6B8D-4E7A-9F31-2C5D8E7B9A01}"; PRODUCT_PAYLOAD_DIR="igneum-windows-app" ;;
|
||||
lab)
|
||||
PRODUCT_NAME="Igneum Miner Lab"; PRODUCT_FILE="Igneum-Miner-Lab"; PRODUCT_SLUG="igneum-lab"
|
||||
PRODUCT_BUNDLE_ID="network.igneum.miner.lab"; PRODUCT_CHANNEL="igneum-2.0-devnet-lab"; PRODUCT_MANIFEST_NAME="igneum-app-lab-latest.json"
|
||||
PRODUCT_WINDOWS_APPID="{5E2B7C41-9D3A-4F06-B8E7-61C4A2D9F0B3}"; PRODUCT_PAYLOAD_DIR="igneum-windows-app-lab" ;;
|
||||
*) echo "PRODUCT must be public or lab (got '$PRODUCT')" >&2; exit 2 ;;
|
||||
esac
|
||||
# igneum_ota_root_hex -> the signing root the package's engine must carry: the release root, or the lab root from the
|
||||
# variable or the key file (64 hex); empty with a note when a lab package has no root to read
|
||||
igneum_ota_root_hex() {
|
||||
if [ "$PRODUCT" = "public" ]; then printf '%s' "$PUBLIC_OTA_ROOT_HEX"; return 0; fi
|
||||
local hex="${IGNEUM_LAB_PUBLIC_KEY:-}"
|
||||
[ -n "$hex" ] || hex="$(igneum_read_trimmed "${IGNEUM_CONFIG_DIR:-$HOME/.config/igneum}/lab-signing/lab-signing-key.pub")"
|
||||
printf '%s' "$hex" | grep -qE '^[0-9a-f]{64}$' || { echo "lab package: no lab root (IGNEUM_LAB_PUBLIC_KEY or ~/.config/igneum/lab-signing/lab-signing-key.pub, 64 hex)" >&2; return 1; }
|
||||
printf '%s' "$hex"
|
||||
}
|
||||
# Consensus parameters pinned into the package for the bundled node: igneum-app.json "node_override_params"; the engine
|
||||
# writes them to <app data>/override-params.json and starts igneumd with --override-params-file. Empty = no override
|
||||
# file, the node runs the network's defaults. Difficulty v2 (4 Oct 2026): the version that bundles igneumd v2 must
|
||||
|
|
@ -55,7 +87,7 @@ igneum_read_trimmed() {
|
|||
}
|
||||
# igneum_manifest_url <token> -> the manifest URL for that downloads folder; nothing for an empty token
|
||||
igneum_manifest_url() {
|
||||
[ -n "$1" ] && printf '%s/dl/%s/igneum-app-latest.json' "$DL_HOST" "$1" || true
|
||||
[ -n "$1" ] && printf '%s/dl/%s/%s' "$DL_HOST" "$1" "$PRODUCT_MANIFEST_NAME" || true
|
||||
}
|
||||
# igneum_fingerprint <value> -> the first 8 hex of sha256 over the value, for logs and the app's header (never the value)
|
||||
igneum_fingerprint() {
|
||||
|
|
@ -64,7 +96,9 @@ igneum_fingerprint() {
|
|||
|
||||
# writes the JSON to $1; prints which files were used (names), the lengths and the fingerprints, never the values
|
||||
write_packaged_config() {
|
||||
local out="$1" token="" manifest="" key="" key_file="" token_file=""
|
||||
local out="$1" token="" manifest="" key="" key_file="" token_file="" root=""
|
||||
root="$(igneum_ota_root_hex)" || return 1
|
||||
echo "product: $PRODUCT ($PRODUCT_NAME, channel $PRODUCT_CHANNEL, root fingerprint $(igneum_fingerprint "$root"))"
|
||||
key_file="$(igneum_secret_file IGNEUM_INTAKE_KEY_FILE log-intake-key)"
|
||||
token_file="$(igneum_secret_file IGNEUM_DL_TOKEN_FILE dl-token)"
|
||||
key="$(igneum_read_trimmed "$key_file")"
|
||||
|
|
@ -83,6 +117,10 @@ write_packaged_config() {
|
|||
{
|
||||
$override_line
|
||||
$network_lines
|
||||
"edition": "$PRODUCT",
|
||||
"product": "$PRODUCT_NAME",
|
||||
"channel": "$PRODUCT_CHANNEL",
|
||||
"ota_root_hex": "$root",
|
||||
"update_manifest": "$manifest",
|
||||
"log_intake_url": "$LOG_URL",
|
||||
"log_intake_key": "$key",
|
||||
|
|
@ -95,6 +133,8 @@ JSON
|
|||
# ---- self-test: packaging/mac/packaged-config.sh --test (temporary files only; nothing under ~/.config is read) -----
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
set -euo pipefail
|
||||
# --lab-probe <out>: writes one config with the environment's PRODUCT (the self-test's child for the lab rows)
|
||||
if [ "${1:-}" = "--lab-probe" ]; then write_packaged_config "$2"; exit $?; fi
|
||||
[ "${1:-}" = "--test" ] || { echo "usage: $0 --test (otherwise source this file)" >&2; exit 2; }
|
||||
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||
PY="$(command -v python3 || command -v python)" # the GitHub Windows runner's Git Bash may only have python
|
||||
|
|
@ -146,5 +186,13 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|||
# 8. the override line
|
||||
NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa": 123456}' write_packaged_config "$T/d.json" >/dev/null
|
||||
check "override params land" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["node_override_params"]["difficulty_v2_activation_daa"])' "$T/d.json")" "123456"
|
||||
# 9. PRODUCT=public|lab (8 October 2026): the table, the manifest name, the root, the refusal without a lab root
|
||||
check "public edition lands" "$("$PY" -c 'import json,sys; j=json.load(open(sys.argv[1])); print(j["edition"], j["product"], j["channel"], j["ota_root_hex"])' "$T/a.json")" "public Igneum Miner igneum-2.0-devnet $PUBLIC_OTA_ROOT_HEX"
|
||||
lab_out="$(PRODUCT=lab IGNEUM_LAB_PUBLIC_KEY="$(printf 'ab%.0s' $(seq 32))" bash "${BASH_SOURCE[0]}" --lab-probe "$T/lab.json" 2>&1)"
|
||||
check "lab edition lands" "$("$PY" -c 'import json,sys; j=json.load(open(sys.argv[1])); print(j["edition"], j["product"], j["channel"], j["ota_root_hex"][:6])' "$T/lab.json")" "lab Igneum Miner Lab igneum-2.0-devnet-lab ababab"
|
||||
check "lab manifest has its own name" "$("$PY" -c 'import json,sys; print(json.load(open(sys.argv[1]))["update_manifest"])' "$T/lab.json")" "$DL_HOST/dl/tok2new/igneum-app-lab-latest.json"
|
||||
check "lab output never carries the root" "$(printf '%s' "$lab_out" | grep -c 'abababab')" "0"
|
||||
check "lab without a root is refused" "$(PRODUCT=lab IGNEUM_LAB_PUBLIC_KEY= IGNEUM_CONFIG_DIR="$T/cfg" bash "${BASH_SOURCE[0]}" --lab-probe "$T/lab2.json" >/dev/null 2>&1; echo $?)" "1"
|
||||
check "a bad PRODUCT is refused" "$(PRODUCT=beta bash "${BASH_SOURCE[0]}" --lab-probe "$T/lab3.json" >/dev/null 2>&1; echo $?)" "2"
|
||||
if [ "$fails" = 0 ]; then echo "packaged-config: all checks passed"; else echo "packaged-config: $fails check(s) failed"; exit 1; fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
# packaging/ota/publish-manifest.sh --version 0.3.1 --mac packaging/mac/dist/Igneum-Miner-0.3.1.dmg \
|
||||
# [--win packaging/windows/dist/Igneum-Miner-Setup-0.3.1.exe] --notes "one line of what changed" \
|
||||
# [--activation-height 120000 --deadline-note "difficulty v2"] [--min-supported 0.3.0] [--channel devnet] [--deploy]
|
||||
# [--urgent] the manifest's own urgent flag (2.0.2, 8 October 2026): the apps install at the first
|
||||
# safe moment, through the finality guard too (manifest::safe_to_apply); for the entry
|
||||
# that IS the fix for a chain that cannot lock; beside, not instead of, --min-supported
|
||||
# [--allow-passed-activation] (Horizon polish Q4: an activation height at or below the live DAA is refused otherwise)
|
||||
# --self-test-height proves the height check on known values (33000 against 201776 refused; 300000 passes) and exits
|
||||
# [--override '{"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":120000}']
|
||||
|
|
@ -48,7 +51,7 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
|||
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
|
||||
|
||||
VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
|
||||
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 UI_FILE="" NO_UI=0 DRIVERS_FILE="" NO_DRIVERS=0
|
||||
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 UI_FILE="" NO_UI=0 DRIVERS_FILE="" NO_DRIVERS=0 URGENT=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
|
|
@ -61,6 +64,7 @@ while [ $# -gt 0 ]; do
|
|||
--deadline-note) DEADLINE="$2"; shift 2 ;;
|
||||
--override) OVERRIDE="$2"; shift 2 ;; # consensus.override: the exact JSON object every app writes to its override.json (all height switches, not just the new one)
|
||||
--min-supported) MIN_SUPPORTED="$2"; shift 2 ;;
|
||||
--urgent) URGENT=1; shift ;;
|
||||
--channel) CHANNEL="$2"; shift 2 ;;
|
||||
--tuning) TUNING_FILE="$2"; shift 2 ;;
|
||||
--no-tuning) NO_TUNING=1; shift ;;
|
||||
|
|
@ -258,9 +262,10 @@ if [ -n "$ACTIVATION" ]; then
|
|||
fi
|
||||
# the version pair: the UI tree this publish builds from stamps its interface version on every new app entry
|
||||
UI_TREE_VERSION="$(tr -d '[:space:]' < "$ROOT/app/igneum-app/ui/VERSION" 2>/dev/null || true)"
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" "${UI_TREE_VERSION:-}" <<'PY'
|
||||
[ "$URGENT" = 1 ] && echo "urgent: the apps install this entry at the first safe moment, finality guard included"
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" "${UI_TREE_VERSION:-}" "$URGENT" <<'PY'
|
||||
import json, sys, datetime
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers, ui_tree = sys.argv[1:15]
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers, ui_tree, urgent = sys.argv[1:16]
|
||||
override = json.loads(override) if override else None
|
||||
if override is not None and (not isinstance(override, dict) or not override): raise SystemExit("--override must be a non-empty JSON object")
|
||||
def entry(s, ui_version=None):
|
||||
|
|
@ -282,6 +287,8 @@ m = {
|
|||
"notes": notes,
|
||||
"consensus": {"activation_height": int(activation) if activation else None, "deadline_note": deadline, **({"override": override} if override is not None else {})},
|
||||
}
|
||||
if urgent == "1":
|
||||
m["urgent"] = True
|
||||
if tuning:
|
||||
m["tuning"] = json.loads(tuning)
|
||||
if ui:
|
||||
|
|
|
|||
|
|
@ -11,12 +11,28 @@
|
|||
#ifndef AppVersion
|
||||
#define AppVersion "2.0.1"
|
||||
#endif
|
||||
#define AppName "Igneum Miner"
|
||||
; PRODUCT=public|lab (the founder's word, 8 October 2026; packaging/mac/packaged-config.sh is the table): build-installer.ps1
|
||||
; passes /DProduct=<public|lab>. The lab product has its own AppId, name, install folder and Start Menu group, so a lab
|
||||
; install never lands on top of a public one and neither updates the other.
|
||||
#ifndef Product
|
||||
#define Product "public"
|
||||
#endif
|
||||
#if Product == "lab"
|
||||
#define AppName "Igneum Miner Lab"
|
||||
#define AppIdGuid "{5E2B7C41-9D3A-4F06-B8E7-61C4A2D9F0B3}"
|
||||
#define SetupBase "Igneum-Miner-Lab-Setup"
|
||||
#elif Product == "public"
|
||||
#define AppName "Igneum Miner"
|
||||
#define AppIdGuid "{A4C1F0E2-6B8D-4E7A-9F31-2C5D8E7B9A01}"
|
||||
#define SetupBase "Igneum-Miner-Setup"
|
||||
#else
|
||||
#error Product must be public or lab
|
||||
#endif
|
||||
#define Publisher "Igneum"
|
||||
#define Url "https://igneum.network"
|
||||
|
||||
[Setup]
|
||||
AppId={{A4C1F0E2-6B8D-4E7A-9F31-2C5D8E7B9A01}
|
||||
AppId={{#AppIdGuid}
|
||||
AppName={#AppName}
|
||||
AppVersion={#AppVersion}
|
||||
AppVerName={#AppName} {#AppVersion}
|
||||
|
|
@ -34,7 +50,7 @@ DefaultGroupName={#AppName}
|
|||
DisableProgramGroupPage=yes
|
||||
LicenseFile=LICENSE.txt
|
||||
OutputDir=dist
|
||||
OutputBaseFilename=Igneum-Miner-Setup-{#AppVersion}
|
||||
OutputBaseFilename={#SetupBase}-{#AppVersion}
|
||||
SetupIconFile={#ArtDir}\igneum.ico
|
||||
UninstallDisplayIcon={app}\igneum.ico
|
||||
UninstallDisplayName={#AppName}
|
||||
|
|
@ -61,7 +77,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
|||
|
||||
[Messages]
|
||||
english.WelcomeLabel2=This will install [name/ver] on your computer.%n%nOne window runs your node and the GPU miner, and shows your hash rate, your blocks and the chain. Nothing is bought or sold on this network.
|
||||
english.FinishedHeadingLabel=Igneum Miner is installed
|
||||
english.FinishedHeadingLabel={#AppName} is installed
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
|
||||
|
|
@ -77,16 +93,16 @@ Name: "autoupdate"; Description: "Update automatically (installs new versions at
|
|||
Source: "{#Payload}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion; Excludes: "*.log,*.seeds,*.DS_Store,packs\*,build\*,dist\*"
|
||||
Source: "{#ArtDir}\igneum.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#Payload}\stop-igneum.ps1"; Flags: dontcopy
|
||||
Source: "wrappers\Stop Igneum Miner.cmd"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "wrappers\Stop Igneum Miner.cmd"; DestDir: "{app}"; DestName: "Stop {#AppName}.cmd"; Flags: ignoreversion
|
||||
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
; igneum-app.exe --launch hands over to "Igneum Miner.exe" (the window) when it is installed, else opens the dashboard in the browser.
|
||||
Name: "{group}\Igneum Miner"; Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "Start the node and mine"
|
||||
Name: "{group}\Stop Igneum Miner"; Filename: "{app}\Stop Igneum Miner.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "Stop the miner and the node"
|
||||
Name: "{group}\Igneum Miner logs"; Filename: "{localappdata}\igneum\logs"; IconFilename: "{app}\igneum.ico"; Comment: "The folder the log files land in"
|
||||
Name: "{group}\Uninstall Igneum Miner"; Filename: "{uninstallexe}"; IconFilename: "{app}\igneum.ico"
|
||||
Name: "{autodesktop}\Igneum Miner"; Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Tasks: desktopicon
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "Start the node and mine"
|
||||
Name: "{group}\Stop {#AppName}"; Filename: "{app}\Stop {#AppName}.cmd"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Comment: "Stop the miner and the node"
|
||||
Name: "{group}\{#AppName} logs"; Filename: "{localappdata}\igneum\logs"; IconFilename: "{app}\igneum.ico"; Comment: "The folder the log files land in"
|
||||
Name: "{group}\Uninstall {#AppName}"; Filename: "{uninstallexe}"; IconFilename: "{app}\igneum.ico"
|
||||
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; WorkingDir: "{app}"; IconFilename: "{app}\igneum.ico"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
; 0.3.22 (the project lead, 7 October 2026: "all the rights need to be done on install, and then on update if anything new"): the app's own
|
||||
|
|
@ -96,7 +112,7 @@ Name: "{autodesktop}\Igneum Miner"; Filename: "{app}\igneum-app.exe"; Parameters
|
|||
Filename: "{app}\igneum-app.exe"; Parameters: "--rights"; Flags: waituntilterminated runasoriginaluser
|
||||
; The inbound firewall rule needs an administrator and is asked for once by the app on its first run (declined = the node dials out and mines without it).
|
||||
; Started as the signed-in user, not as administrator (the data lands in that user's %LOCALAPPDATA%).
|
||||
Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; Description: "Start Igneum Miner now"; Flags: postinstall nowait skipifsilent runasoriginaluser
|
||||
Filename: "{app}\igneum-app.exe"; Parameters: "--launch"; Description: "Start {#AppName} now"; Flags: postinstall nowait skipifsilent runasoriginaluser
|
||||
; The over-the-air updater (packaging/ota, src/ota.rs) runs this installer /VERYSILENT /IGNOTA=1 and the app must come back by itself.
|
||||
; /IGNOTA=2 (helpers from 0.3.22 on, MF-11): the helper starts the app itself, polls the new engine and restores the kept
|
||||
; exe set when nothing answers; this entry stays silent so the two never race for the single-instance window.
|
||||
|
|
@ -170,7 +186,7 @@ end;
|
|||
// without touching the app; the detached run installs and removes the task at its end. The job's proof is the --version
|
||||
// wait and the detached log, not this process's exit code (1: "Setup failed to initialize").
|
||||
const
|
||||
DetachTask = 'Igneum Miner install';
|
||||
DetachTask = '{#AppName} install';
|
||||
|
||||
function DetachedRun: Boolean;
|
||||
begin
|
||||
|
|
@ -352,7 +368,7 @@ begin
|
|||
Exec('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -File "' + StopScript + '" -Install "' + OldDir + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
if (OldDir <> '') and (not WizardSilent) and FileExists(OldDir + '\unins000.exe') then
|
||||
begin
|
||||
if MsgBox('Igneum Miner now installs in your user folder, so updates need no administrator prompt.' + #13#10#13#10 +
|
||||
if MsgBox('{#AppName} now installs in your user folder, so updates need no administrator prompt.' + #13#10#13#10 +
|
||||
'An older copy is still in ' + OldDir + '. Remove it now? (one administrator prompt; your chain data, address and settings stay)',
|
||||
mbConfirmation, MB_YESNO) = IDYES then
|
||||
ShellExec('runas', OldDir + '\unins000.exe', '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ param(
|
|||
[string]$Payload = '',
|
||||
[string]$PayloadUrl = $(if ($env:IGNEUM_PAYLOAD_URL) { $env:IGNEUM_PAYLOAD_URL } else { 'https://dl.igneum.network/igneum-windows-app.zip' }),
|
||||
[string]$Version = '0.3.0',
|
||||
# PRODUCT=public|lab (packaging/mac/packaged-config.sh, 8 October 2026): the installer's AppId, AppName, folder and file name
|
||||
[ValidateSet('public', 'lab')][string]$Product = $(if ($env:PRODUCT) { $env:PRODUCT } else { 'public' }),
|
||||
[switch]$NoRcedit,
|
||||
[switch]$NoWinget
|
||||
)
|
||||
|
|
@ -101,12 +103,15 @@ function Test-PayloadDir([string]$dir) {
|
|||
}
|
||||
return $true
|
||||
}
|
||||
$productName = $(if ($Product -eq 'lab') { 'Igneum Miner Lab' } else { 'Igneum Miner' })
|
||||
$payloadName = $(if ($Product -eq 'lab') { 'igneum-windows-app-lab' } else { 'igneum-windows-app' })
|
||||
Say "product: $Product ($productName; payload folder $payloadName)"
|
||||
$source = $null
|
||||
if ($Payload) {
|
||||
if (-not (Test-PayloadDir $Payload)) { throw "-Payload $Payload does not hold START-IGNEUM.bat, the .ps1 files and both exes." }
|
||||
$source = (Resolve-Path $Payload).Path
|
||||
} else {
|
||||
foreach ($dir in @((Join-Path $here 'igneum-windows-app'), (Join-Path $here '..\igneum-windows-app'), (Join-Path $here 'payload'), (Join-Path $here '..\..\igneum-windows-app'))) {
|
||||
foreach ($dir in @((Join-Path $here $payloadName), (Join-Path $here "..\$payloadName"), (Join-Path $here 'payload'), (Join-Path $here "..\..\$payloadName"))) {
|
||||
if (Test-PayloadDir $dir) { $source = (Resolve-Path $dir).Path; break }
|
||||
}
|
||||
}
|
||||
|
|
@ -152,7 +157,7 @@ foreach ($exeName in @('igneum-app.exe', 'igneumd.exe', 'igneum-miner.exe')) {
|
|||
$exe = Join-Path $payloadDir $exeName
|
||||
if (-not (Test-Path $exe)) { continue }
|
||||
$info = (Get-Item $exe).VersionInfo
|
||||
if ($info.ProductName -eq 'Igneum Miner') { Say "$exeName already carries the Igneum Miner version block (relinked on the Mac)"; continue }
|
||||
if ($info.ProductName -eq $productName) { Say "$exeName already carries the $productName version block (relinked on the Mac)"; continue }
|
||||
if ($NoRcedit) { Say "$exeName has no version block; -NoRcedit given, so Explorer shows it without the coin icon"; continue }
|
||||
if (-not $rcedit) {
|
||||
if ($env:RCEDIT -and (Test-Path $env:RCEDIT)) { $rcedit = $env:RCEDIT }
|
||||
|
|
@ -161,10 +166,10 @@ foreach ($exeName in @('igneum-app.exe', 'igneumd.exe', 'igneum-miner.exe')) {
|
|||
if (-not (Test-Path $rcedit)) { Say "downloading rcedit from $rceditUrl"; Invoke-WebRequest -Uri $rceditUrl -OutFile $rcedit -UseBasicParsing }
|
||||
}
|
||||
}
|
||||
$desc = switch ($exeName) { 'igneumd.exe' { 'Igneum Miner node' } 'igneum-app.exe' { 'Igneum Miner engine' } default { 'Igneum Miner' } }
|
||||
$desc = switch ($exeName) { 'igneumd.exe' { "$productName node" } 'igneum-app.exe' { "$productName engine" } default { $productName } }
|
||||
$rcArgs = @($exe, '--set-icon', (Join-Path $art 'igneum.ico'),
|
||||
'--set-version-string', 'CompanyName', 'Igneum',
|
||||
'--set-version-string', 'ProductName', 'Igneum Miner',
|
||||
'--set-version-string', 'ProductName', $productName,
|
||||
'--set-version-string', 'FileDescription', $desc,
|
||||
'--set-version-string', 'OriginalFilename', $exeName,
|
||||
'--set-version-string', 'LegalCopyright', 'Igneum. Nothing is bought or sold.',
|
||||
|
|
@ -177,7 +182,7 @@ foreach ($exeName in @('igneum-app.exe', 'igneumd.exe', 'igneum-miner.exe')) {
|
|||
|
||||
# ---- 6. compile ------------------------------------------------------------------------------------------------------
|
||||
$iss = Join-Path $here 'Igneum-Miner.iss'
|
||||
$isccArgs = @('/Qp', "/DPayload=$payloadDir", "/DArtDir=$art", "/DAppVersion=$Version", "/O$dist", $iss)
|
||||
$isccArgs = @('/Qp', "/DPayload=$payloadDir", "/DArtDir=$art", "/DAppVersion=$Version", "/DProduct=$Product", "/O$dist", $iss)
|
||||
Say "compiling $iss"
|
||||
& $iscc @isccArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ISCC failed (exit $LASTEXITCODE)" }
|
||||
|
|
|
|||
|
|
@ -26,15 +26,16 @@ set -euo pipefail
|
|||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "$ROOT/app/igneum-app/Cargo.toml" | head -1)"
|
||||
OUT="${1:-$HOME/Desktop/igneum-windows-app.zip}"
|
||||
# the packaged configuration (the manifest URL from the token file, the intake key from the key file, NODE_OVERRIDE_PARAMS)
|
||||
# and the PRODUCT=public|lab table (the payload folder igneum-windows-app or igneum-windows-app-lab, the product name)
|
||||
. "$ROOT/packaging/mac/packaged-config.sh"
|
||||
OUT="${1:-$HOME/Desktop/$PRODUCT_PAYLOAD_DIR.zip}"
|
||||
case "$OUT" in /*) ;; *) OUT="$PWD/$OUT" ;; esac
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
REL="${IGNEUM_WIN_RELEASE:-$ROOT/vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release}"
|
||||
ENGINE="${IGNEUM_APP_EXE:-$ROOT/app/igneum-app/target/x86_64-pc-windows-gnu/release/igneum-app.exe}"
|
||||
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
|
||||
STAGE="$HERE/igneum-windows-app"
|
||||
# the packaged configuration (the manifest URL from the token file, the intake key from the key file, NODE_OVERRIDE_PARAMS)
|
||||
. "$ROOT/packaging/mac/packaged-config.sh"
|
||||
STAGE="$HERE/$PRODUCT_PAYLOAD_DIR"
|
||||
|
||||
[ -f "$ENGINE" ] || { echo "no $ENGINE: build it first (cd app/igneum-app && cargo build --release --target x86_64-pc-windows-gnu, see proto-cuda/windows-node/cross-build.sh for the environment)" >&2; exit 1; }
|
||||
[ -f "$REL/igneumd.exe" ] || { echo "no $REL/igneumd.exe; cross-compile the node first" >&2; exit 1; }
|
||||
|
|
@ -128,19 +129,19 @@ cp "$ROOT"/proving/fixtures/*.json "$STAGE/wsl2/fixtures/"
|
|||
# the fingerprints, never the values)
|
||||
write_packaged_config "$STAGE/igneum-app.json"
|
||||
cat > "$STAGE/README.txt" <<TXT
|
||||
Igneum Miner $VERSION for Windows (payload). Devnet v4. Test network; nothing is bought or sold.
|
||||
$PRODUCT_NAME $VERSION for Windows (payload). Devnet v4. Test network; nothing is bought or sold.
|
||||
Nobody from Igneum will ever ask for your seed.
|
||||
This folder is what the installer packs. To build the installer on a PC:
|
||||
1. app\\windows\\BUILD-APP.bat builds "Igneum Miner.exe" (the window; needs Visual Studio; optional)
|
||||
2. packaging\\windows\\BUILD-INSTALLER.bat builds Igneum-Miner-Setup-$VERSION.exe (Inno Setup via winget)
|
||||
2. packaging\\windows\\BUILD-INSTALLER.bat builds $PRODUCT_FILE-Setup-$VERSION.exe (Inno Setup via winget; PRODUCT=$PRODUCT)
|
||||
To run without the installer: double-click igneum-app.exe (the dashboard opens in your browser).
|
||||
TXT
|
||||
for f in "$STAGE"/*.bat "$STAGE/README.txt" "$STAGE/stop-igneum.ps1" "$STAGE/app/windows/BUILD-APP.bat" "$STAGE/proto-cuda/build.bat" "$STAGE/proto-opencl/build.bat"; do
|
||||
[ -f "$f" ] && perl -pi -e 's/\r?\n/\r\n/' "$f"
|
||||
done
|
||||
rm -f "$OUT"
|
||||
if command -v zip >/dev/null 2>&1; then (cd "$HERE" && zip -qr "$OUT" "igneum-windows-app" -x '*.DS_Store')
|
||||
elif command -v 7z >/dev/null 2>&1; then (cd "$HERE" && 7z a -tzip -bso0 -bsp0 "$OUT" "igneum-windows-app" '-xr!.DS_Store')
|
||||
if command -v zip >/dev/null 2>&1; then (cd "$HERE" && zip -qr "$OUT" "$PRODUCT_PAYLOAD_DIR" -x '*.DS_Store')
|
||||
elif command -v 7z >/dev/null 2>&1; then (cd "$HERE" && 7z a -tzip -bso0 -bsp0 "$OUT" "$PRODUCT_PAYLOAD_DIR" '-xr!.DS_Store')
|
||||
else echo "neither zip nor 7z is on PATH" >&2; exit 1; fi
|
||||
echo "staged $STAGE"
|
||||
ls -la "$OUT"
|
||||
|
|
|
|||
Loading…
Reference in a new issue