diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index fe287df57..0f81a0148 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -124,6 +124,28 @@ impl Default for Settings { } impl Settings { + /// What a measurement engine (`--sweep`, started by a job beside the installed app) runs with, whatever the copied + /// file says: no remote jobs (run 4, 6 October 2026: the second engine fetched the jobs file and ran 96 old jobs + /// inside its scratch root), no updates, no proving, not paused, the tune on, Power control off (only an engine + /// that is itself elevated controls NVIDIA, through the probe's `direct`), every card due and unpinned. The file + /// on disk is never changed: the playbook copies the installed app's settings verbatim (a PowerShell JSON round + /// trip rewrote big integers as doubles and the engine read the whole file as defaults: no payout address, every + /// card off). + pub fn for_measurement(mut self) -> Settings { + self.remote_jobs = false; + self.auto_update = false; + self.prove = false; + self.paused = false; + self.sweep = true; + self.power_control = false; + self.setup_done = true; + for p in self.cards.values_mut() { + p.sweep_at = 0; + p.pinned = false; + } + self + } + pub fn load(path: &Path) -> Settings { let mut s: Settings = std::fs::read_to_string(path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default(); let mut dirty = false; @@ -374,6 +396,18 @@ mod tests { out } + #[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() }; + s.cards.insert("nvidia:0:x".into(), CardPref { enabled: true, identities: 8, sweep_at: 1_791_000_000, pinned: true, power_pct: 70, ..Default::default() }); + let m = s.for_measurement(); + assert!(!m.remote_jobs && !m.auto_update && !m.prove && !m.paused && m.sweep && !m.power_control && m.setup_done); + assert_eq!(m.address, "0xabc", "the payout address is the installed app's"); + let c = &m.cards["nvidia:0:x"]; + assert!(c.enabled && c.identities == 8 && c.power_pct == 70, "the card's choices stay"); + assert!(c.sweep_at == 0 && !c.pinned, "every card is due and unpinned"); + } + #[test] fn manifest_url_round_trips_through_the_token() { assert_eq!(manifest_url_for_token("abc123"), "https://dl.igneum.network/dl/abc123/igneum-app-latest.json"); diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index bbb24485d..14b96c8d1 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -101,6 +101,8 @@ fn main() { } let packaged = config::Packaged::load(&candidates).with_env_overrides(); let settings = config::Settings::load(&runtime.app_dir.join("settings.json")); + // a measurement engine runs with the installed app's choices and its own switches (config.rs for_measurement) + let settings = if sweep { settings.for_measurement() } else { settings }; // the per-launch token: 32 hex characters from the OS let mut raw = [0u8; 16]; diff --git a/relay/playbooks/ember-tune-pc1.ps1 b/relay/playbooks/ember-tune-pc1.ps1 index d498f652b..0fecf74b3 100644 --- a/relay/playbooks/ember-tune-pc1.ps1 +++ b/relay/playbooks/ember-tune-pc1.ps1 @@ -47,7 +47,11 @@ $installedVer = (& (Join-Path $installDir 'igneum-app.exe') --version 2>&1 | Out $installedHasEmber = $false if ($installedVer -match 'igneum-app (\d+)\.(\d+)\.(\d+)') { $installedHasEmber = ([int]$Matches[1] -gt 0) -or ([int]$Matches[2] -gt 3) -or (([int]$Matches[2] -eq 3) -and ([int]$Matches[3] -ge 12)) } $ember = $null -if (-not $installedHasEmber) { foreach ($cand in @((Join-Path $appDir 'jobs\ember-kit-2\igneum-app-ember.exe'), (Join-Path $appDir 'jobs\ember-kit-1\igneum-app-ember.exe'))) { if (Test-Path $cand) { $ember = $cand; break } } } +# ember-kit-3 (the engine with Settings::for_measurement) is preferred when present, whatever the installed version; +# older kits only when the installed app predates Ember Tune +$k3 = Join-Path $appDir 'jobs\ember-kit-3\igneum-app-ember.exe' +if (Test-Path $k3) { $ember = $k3 } +elseif (-not $installedHasEmber) { foreach ($cand in @((Join-Path $appDir 'jobs\ember-kit-2\igneum-app-ember.exe'), (Join-Path $appDir 'jobs\ember-kit-1\igneum-app-ember.exe'))) { if (Test-Path $cand) { $ember = $cand; break } } } if ($ember) { Copy-Item -LiteralPath $ember -Destination (Join-Path $bin 'igneum-app.exe') -Force Say ("engine: the Ember build from " + $ember) @@ -71,33 +75,18 @@ foreach ($f in @('settings.json', 'machine-id', 'wallet.json', 'tuning.json')) { $src = Join-Path $appDir $f if (Test-Path $src) { Copy-Item -LiteralPath $src -Destination (Join-Path $sApp $f) -Force } } -# the second engine must not poll jobs (it would see this one), update itself, or prove; the tune is on +# the copies are VERBATIM (run 4, 6 October 2026: a PowerShell ConvertFrom-Json | ConvertTo-Json round trip rewrote big +# integers as doubles, the engine read the file as defaults, no payout address, every card off, 96 old jobs run in +# the scratch root); the engine itself switches remote jobs, updates, proving and Power control off under --sweep +# (Settings::for_measurement) and makes every card due. Only a report line is read here. $sj = Join-Path $sApp 'settings.json' -if (Test-Path $sj) { - try { - $j = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json - $j.remote_jobs = $false; $j.auto_update = $false; $j.prove = $false; $j.paused = $false; $j.setup_done = $true; $j.sweep = $true - # the project lead, 6 October 2026, 07:20Z: never raise an administrator prompt. The installed app's Power control is READ and - # reported, but the copy runs with it OFF so the second engine can never start the elevated helper; the 5090 is - # measured as it runs either way (the two-knob tune is the installed engine's job once it carries Ember Tune) - $installedPowerControl = $false - try { $installedPowerControl = [bool]$j.power_control } catch { } - Write-Output ('RESULT TUNE installed_power_control=' + $installedPowerControl.ToString().ToLower() + ' (the copy runs with it off: no prompt)') - if ($j.PSObject.Properties.Name -contains 'power_control') { $j.power_control = $false } else { $j | Add-Member -NotePropertyName power_control -NotePropertyValue $false } - # every card is due: the stored results are cleared in the COPY only - if ($j.cards) { foreach ($p in $j.cards.PSObject.Properties) { $p.Value.sweep_at = 0; $p.Value.pinned = $false } } - # PowerShell 5.1's Set-Content -Encoding utf8 writes a BOM, which the engine's JSON parser refuses: the copy then - # read as defaults (no payout address, no cards) and the miners never started (runs 1 and 2, 5 and 6 October 2026) - [IO.File]::WriteAllText($sj, ($j | ConvertTo-Json -Depth 8), (New-Object System.Text.UTF8Encoding $false)) - } catch { Say ("settings.json: " + $_.Exception.Message) } - $back = $null - try { $back = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json } catch { } - $addr = ''; if ($back) { $addr = [string]$back.address } - $bom = (Get-Content -LiteralPath $sj -Encoding Byte -TotalCount 3 -ErrorAction SilentlyContinue) -join ',' - Write-Output ('RESULT TUNE scratch settings: address ' + $(if ($addr) { $addr.Substring(0, [Math]::Min(10, $addr.Length)) + '...' } else { 'EMPTY' }) + ', cards ' + $(if ($back -and $back.cards) { @($back.cards.PSObject.Properties).Count } else { 0 }) + ', first bytes ' + $bom) - if (-not $addr) { Write-Output 'RESULT TUNE error=no_address reason=the_copied_settings_carry_no_payout_address'; exit 2 } - if ($bom -eq '239,187,191') { Write-Output 'RESULT TUNE error=bom reason=settings.json_starts_with_a_BOM'; exit 2 } -} else { Write-Output 'RESULT TUNE error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 } +if (-not (Test-Path -LiteralPath $sj)) { Write-Output 'RESULT TUNE error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 } +$installedPowerControl = 'unknown'; $addr = ''; $ncards = 0 +try { $back = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json; $addr = [string]$back.address; if ($back.cards) { $ncards = @($back.cards.PSObject.Properties).Count }; if ($back.PSObject.Properties.Name -contains 'power_control') { $installedPowerControl = ([bool]$back.power_control).ToString().ToLower() } } catch { } +$bom = (Get-Content -LiteralPath $sj -Encoding Byte -TotalCount 3 -ErrorAction SilentlyContinue) -join ',' +Write-Output ('RESULT TUNE installed_power_control=' + $installedPowerControl + ' (the tune engine runs with it off: no prompt unless the job itself is elevated)') +Write-Output ('RESULT TUNE scratch settings (verbatim copy): address ' + $(if ($addr) { $addr.Substring(0, [Math]::Min(10, $addr.Length)) + '...' } else { 'EMPTY' }) + ', cards ' + $ncards + ', first bytes ' + $bom + ', ' + (Get-Item -LiteralPath $sj).Length + ' bytes') +if (-not $addr -and -not (Test-Path (Join-Path $sApp 'wallet.json'))) { Write-Output 'RESULT TUNE error=no_address reason=the_copied_settings_carry_no_payout_address_and_no_wallet.json'; exit 2 } Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue # the state before, for the report diff --git a/relay/playbooks/sweep-5090.ps1 b/relay/playbooks/sweep-5090.ps1 index 29a9c7c02..846feeaf5 100644 --- a/relay/playbooks/sweep-5090.ps1 +++ b/relay/playbooks/sweep-5090.ps1 @@ -37,15 +37,8 @@ foreach ($f in @('settings.json', 'machine-id', 'wallet.json')) { $src = Join-Path $appDir $f if (Test-Path $src) { Copy-Item -LiteralPath $src -Destination (Join-Path $sApp $f) -Force } } -# the second engine must not poll jobs (it would see this one), update itself, or prove -$sj = Join-Path $sApp 'settings.json' -if (Test-Path $sj) { - try { - $j = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json - $j.remote_jobs = $false; $j.auto_update = $false; $j.prove = $false; $j.paused = $false; $j.setup_done = $true - [IO.File]::WriteAllText($sj, ($j | ConvertTo-Json -Depth 8), (New-Object System.Text.UTF8Encoding $false)) # no BOM: the engine's JSON parser refuses one (C35, runs 1 and 2) - } catch { Say ("settings.json: " + $_.Exception.Message) } -} else { Write-Output 'RESULT SWEEP error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 } +# the copies are verbatim: the engine switches jobs, updates and proving off itself under --sweep (Settings::for_measurement, 0.3.13) +if (-not (Test-Path (Join-Path $sApp 'settings.json'))) { Write-Output 'RESULT SWEEP error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 } Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue # the cap state before, for the report diff --git a/tools/ci/second-engine-check.sh b/tools/ci/second-engine-check.sh index da127dd0a..8f378b082 100755 --- a/tools/ci/second-engine-check.sh +++ b/tools/ci/second-engine-check.sh @@ -23,6 +23,9 @@ while IFS= read -r f; do if grep -qE 'settings\.json|\.json' "$f" && grep -vE '^\s*#' "$f" | grep -qE 'Set-Content[^\n]*-Encoding +utf8'; then echo "second-engine: $f writes JSON with Set-Content -Encoding utf8 (a BOM the engine refuses: the copy read as defaults, no payout address, nothing mined); use [IO.File]::WriteAllText with UTF8Encoding(\$false)"; fail=1 fi + if grep -vE '^\s*#' "$f" | grep -qE 'ConvertTo-Json' && grep -qE 'settings\.json' "$f"; then + echo "second-engine: $f rewrites settings.json through ConvertTo-Json (a lossy round trip: big integers become doubles and the engine reads the whole file as defaults; run 4, 6 October 2026); copy the file verbatim, the engine applies Settings::for_measurement under --sweep"; fail=1 + fi if ! grep -qE "IGNEUM_APP_NO_OTA *= *'1'" "$f"; then echo "second-engine: $f starts an engine without IGNEUM_APP_NO_OTA = '1' (its updater would run the installer, which quits the installed app: PC 1, 5 October 2026, 22:31 UTC)"; fail=1 fi