Windows node package: igneumd cross-compiled for the PC, START-NODE and BUILD-NODE launchers

proto-cuda/windows-node/: START-NODE.bat and start-node.ps1 (igneumd
--devnet with --addpeer to the Mac, status line every 30 s through
igneum-miner watch, keep-awake, random-delay restart, Ctrl+C),
BUILD-NODE.bat and build-node.ps1 (builds on the PC from the src.zip
snapshot; winget for Rustup, LLVM and protoc; MSVC default toolset),
ALLOW-FIREWALL.bat and allow-firewall.ps1, README.txt, cross-build.sh
(the mingw-w64 recipe that built igneumd.exe in 8 min 25 s; the exe
ships with the three mingw runtime DLLs) and make-package.sh.
WINDOWS-MINER.md gains "Run your own node"; bench-log records the
cross-compile and the two-peer sync test on the Mac (ports 27000 and
27010, live node untouched). The mining launcher's NODE_HOST=auto
edit stays uncommitted for the agent that owns start-mining.ps1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-03 20:54:27 +00:00
parent 31ccd2752b
commit 3e1ecab245
11 changed files with 842 additions and 0 deletions

View file

@ -233,3 +233,10 @@ Metal GPU worker (`proto-metal/igneum-bench --serve`, runtime-compiled `igneum_h
Overnight devnet (started 20:07 BST): genesis bits 0x1d100000 = 2^28 expected hashes per block, sized for RTX 5090 229 + M5 Max 45 + gfx1036 4 = 278 MH/s (1.04 blocks/s); genesis hash edc4fa84...fb07; epoch 0 program 136 loads/hash compiled in 69.5 ms on Metal; node 1 alone (0.0.0.0:26610/26611, `caffeinate -dims`, logs /tmp/igneum-devnet/node1.log) with the Metal worker (`/tmp/igneum-devnet/metal-worker.log`): 31 blocks in 273 s = 0.11 blocks/s at 31.1 MH/s, as expected for 2^28 until the PC joins.
Worker implementations, all bit-exact with `igneum-pow hash-bound` for a 96-nonce job across the 2^32 lane boundary (nonces 4294967264 to 4294967359, prehash ab x 32, devnet seeds): Metal (M5 Max GPU), OpenCL (`proto-opencl/host.c --serve`, Apple OpenCL 1.2 on the M5 Max, local-memory exchange, `--vendor` device filter added), CUDA (`proto-cuda/host.cu --serve` with the pack's `kernel_bound.cu`, through the clang emulation shim: bench PASS on the Rust-exported devnet pack, serve job bit-exact, seed-mismatch error exercised). CUDA and OpenCL are built ahead of time per pack; the Windows launcher re-exports and rebuilds at each epoch or day change (miner exit 42). `igneum-miner.exe` cross-compiled for x86_64-pc-windows-gnu with Homebrew mingw-w64 (9.4 MB; no rocksdb in the miner's closure).
Not done: no NVIDIA or AMD run of the bound kernels yet (the Windows package `~/Desktop/igneum-mine-test.zip` is the next step); NVRTC and runtime OpenCL rebuilds inside the workers; the block level for pruning proofs on a lane-only pow value; 8 vs 18 decimals; the VDF seeds and finality.
## 3 October 2026, Windows node package: igneumd cross-compiled for x86_64-pc-windows-gnu, two-peer sync test (consensus-engineer)
Machine: Apple M5 Max, load average 60 to 110 (three other agents building), rustc 1.99.0, Homebrew mingw-w64 (gcc 16.2.0), Homebrew llvm (libclang). Worktree `vendor/igneum-node-win`, branch `windows-node` at `352495f6` (the rename commit `d62708a8` plus one miner change: `watch` prints `blue=` and `synced=` after `sink=`).
Cross-compile: `cargo build --release -j 6 -p kaspad -p igneum-miner --features igneum-pow --target x86_64-pc-windows-gnu` at `nice -n 19`, 8 min 25 s from a clean target directory (recipe in `proto-cuda/windows-node/cross-build.sh`). `librocksdb-sys` (bindgen plus the bundled rocksdb and snappy C++) built with `LIBCLANG_PATH=/opt/homebrew/opt/llvm/lib`, `BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw32 --sysroot=<mingw sysroot> -I<mingw include>"` and `CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++`; zstd, lz4, bzip2, zlib, secp256k1 and mimalloc built with the mingw gcc. `igneumd.exe` 43,172,352 bytes, `igneum-miner.exe` 9,391,104 bytes. The exe imports `libstdc++-6.dll`: `-static-libstdc++` is ignored by the gcc driver rustc links with, and `-C link-arg=-static` (second full build, 8 min) does not cover a library rustc passes as `-Bdynamic`; the package ships `libstdc++-6.dll`, `libgcc_s_seh-1.dll` and `libwinpthread-1.dll` next to the exe (fix for next time: empty `CXXSTDLIB_x86_64_pc_windows_gnu` plus `-Wl,-Bstatic -lstdc++`). Not run on Windows yet.
Sync test (native build of the same worktree, 21:44 to 21:46 BST, live node on 26610/26611 untouched): node A on 27000/27001 with START-NODE.bat's flags (`--addpeer=192.168.68.64:26611 --listen=0.0.0.0:27001 --nodnsseed --disable-upnp --nologfiles`) connected and started IBD within 20 ms, had 5,144 headers and 1,386 blocks at 7 s, and from 17 s on matched the live node's block count, DAA score, blue score and sink at every 10-s sample over 60 s (5,157 to 5,175 blocks, sink identical 5 of 6 samples, `synced=true`); every header checked by `igneum-lottery-v1-bound`. Live node `peers` 1 -> 2 -> 1. Node B on 27010/27011 dialled A's listener, synced to the same sink within 25 s and learnt the live node from A (outbound 2): a node with `--addpeer` plus `--listen` accepts inbound connections (`--connect` would set the inbound limit to 0, `kaspad/src/daemon.rs`). SIGINT stopped both cleanly.
Package: `~/Desktop/igneum-node-windows.zip` from `proto-cuda/windows-node/make-package.sh` (exe, miner exe, src.zip snapshot of the worktree HEAD plus `igneum-pow/`, scripts, README). Build on the PC (BUILD-NODE.bat) estimated at 15 to 25 minutes on the 9800X3D, approximate, untested.

View file

@ -129,3 +129,120 @@ start-mining.ps1 (no PowerShell on this Mac), the dashboard drawing (cursor posi
console font, the window-height fallback), and running the .exe files on Windows. The 3 October 2026 dashboard version
was checked only with a bracket and quote balance pass and a read-through; the first Windows run is the real test, and
`PLAIN=1` is the way back if the window misbehaves.
## Run your own node (igneum-node-windows.zip, 3 October 2026, evening)
The PC runs its own `igneumd` and the miners mine against localhost; the PC and the Mac are two peers of the same
devnet. Package built by `windows-node/make-package.sh` from `windows-node/` (START-NODE.bat, start-node.ps1,
BUILD-NODE.bat, build-node.ps1, ALLOW-FIREWALL.bat, allow-firewall.ps1, README.txt), `windows-miner/upload-log.bat`,
this file, a cross-compiled `igneumd.exe` and `igneum-miner.exe`, and `src.zip`: a `git archive` of the node worktree's
HEAD (branch `windows-node`, commit `352495f6`, the rename commit plus one miner change) under `vendor/igneum-node/`
with `igneum-pow/` from this repository's HEAD next to it, the layout the fork's relative path dependency needs. No
GitHub access from the PC; the repository stays private.
Steps on the PC: extract to a short path (C:\igneum-node), double-click START-NODE.bat, click "Allow access" on the
firewall prompt (tick Private networks), then START-MINING.bat as before. BUILD-NODE.bat is only for the case where
igneumd.exe is missing or has to be rebuilt from the snapshot.
### The exe: cross-compiled on the Mac (the path that worked)
`windows-node/cross-build.sh`: `cargo build --release -p kaspad -p igneum-miner --features igneum-pow --target
x86_64-pc-windows-gnu` with Homebrew mingw-w64 (gcc 16.2.0) and Homebrew llvm's libclang. The feared blocker,
`librocksdb-sys`, built: bindgen parsed `rocksdb/c.h` once `LIBCLANG_PATH` pointed at `/opt/homebrew/opt/llvm/lib` and
`BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu` gave clang the mingw target and sysroot
(`--target=x86_64-w64-mingw32 --sysroot=<mingw>/x86_64-w64-mingw32 -I<mingw>/x86_64-w64-mingw32/include`); the bundled
rocksdb and snappy C++ sources compiled with `CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++` through the cc crate
(the crate's own `x86_64-pc-windows-gnu` branch defines `_POSIX_C_SOURCE` and `_WIN32_WINNT_VISTA`); zstd, lz4, bzip2,
zlib, secp256k1 and mimalloc (no malloc override on Windows, the crate builds without it) compiled with the mingw gcc.
First build 8 min 25 s with `-j 6` at `nice -n 19` on the loaded M5 Max; `igneumd.exe` 43.2 MB (thin LTO, stripped).
One real problem: the exe imports `libstdc++-6.dll`, which Windows does not have. `-C link-arg=-static-libstdc++` means
nothing to the gcc (C) driver rustc links with, and `-C link-arg=-static` does not reach it either: rustc passes the cc
crate's `-lstdc++` as `-Wl,-Bdynamic -lstdc++`, so ld takes the import library whatever comes later (both tried, two
full rebuilds, the second 8 min). The package therefore ships the three mingw runtime DLLs next to the exe
(`libstdc++-6.dll`, which itself needs `libgcc_s_seh-1.dll` and `libwinpthread-1.dll`, from the Homebrew toolchain;
GCC runtime library exception). The clean fix for next time is `CXXSTDLIB_x86_64_pc_windows_gnu=` (empty, so the cc
crate emits no `-lstdc++`) plus `-C link-arg=-Wl,-Bstatic -C link-arg=-lstdc++` so the archive is taken; not done
tonight because each flag change is a full rebuild and the project lead wanted the package. The other imports are Windows' own
(kernel32, ws2_32, bcrypt, the `api-ms-win-crt-*` UCRT forwarders the miner already imports, rpcrt4 and shlwapi for
rocksdb, pdh, powrprof, psapi and iphlpapi for sysinfo).
Not tested here: running igneumd.exe on Windows (the miner exe from the same toolchain ran on the PC on 3 October). If
the exe fails to start, BUILD-NODE.bat is the fallback; the PC build is upstream's own release path (rusty-kaspa ships
Windows binaries built with MSVC).
### BUILD-NODE.bat (the fallback, builds on the PC)
`build-node.ps1` unpacks `src.zip` to `src\`, checks the tools and installs the missing ones with winget, silent:
`Rustlang.Rustup` (stable, MSVC host), `LLVM.LLVM` (libclang.dll for the rocksdb bindings; `LIBCLANG_PATH` is set to
`C:\Program Files\LLVM\bin`), `Google.Protobuf` for protoc (the gRPC and p2p crates run protoc from prost-build, no
vendored copy; if winget has no such package the script downloads `protoc-33.1-win64.zip` from the protobuf releases
into `tools\protoc`), and the MSVC toolset through `vcvarsall.bat x64` with its default version (14.5x is fine for Rust
and the cc crate; the CUDA 14.30 trick is not used). Then `cargo build --release -p kaspad --features igneum-pow` (the
crate keeps its upstream name kaspad, the binary is igneumd.exe; without the feature the node would check blocks with
the kHeavyHash stub and reject every devnet block) with a progress line every minute (elapsed, crates compiled, the
crate being compiled; cargo's output in `build-<stamp>.cargo.log`), then copies `igneumd.exe` next to the bat. No CMake:
every native dependency builds through the cc crate. Estimate on the 9800X3D: 15 to 25 minutes for the first build
(approximate; about 500 crates plus the rocksdb C++ sources, thin LTO at the end, Defender's scan of `target\`), under a
minute to rebuild with nothing changed. Checked only by read-through and the bracket and quote balance pass (no
PowerShell on this Mac).
### START-NODE.bat
Settings at the top: `MAC_NODE` (192.168.68.64:26611), `RPC_LISTEN` (127.0.0.1:26610, so the miner launcher's default
port is the same on either host), `P2P_LISTEN` (0.0.0.0:26611), `APPDIR` (`%LOCALAPPDATA%\igneum\devnet`),
`UNSYNCED_MINING` (0: the node hands out no template until synced, `rpc/service/src/service.rs` refuses
`get_block_template` without `--enable-unsynced-mining`; the Mac node runs with 1 because it mines alone),
`STATUS_SECS`, `EXTRA_ARGS`. The node starts as `igneumd --devnet --appdir=... --rpclisten=... --listen=...
--addpeer=<MAC_NODE> --nodnsseed --disable-upnp --nologfiles`. It is `--addpeer`, not `--connect`:
`kaspad/src/daemon.rs` sets the inbound limit to 0 and the outbound target to 0 when `--connect` is given, so the Mac
could never dial the PC. `--addpeer` is a permanent connection request retried with backoff up to 8 minutes
(`components/connectionmanager/src/lib.rs`), and the listener stays open.
The launcher (`start-node.ps1`): refuses to start when 26610 or 26611 is already in use (names the process), prints the
node version, tests the Mac's p2p port once, keeps the PC awake (SetThreadExecutionState), logs the node's output to
`node-<stamp>.log` next to the bat (a new segment per start) and its own lines to `node-launcher-<stamp>.log`
(uploaded every 5 min with upload-log.bat under `node-<PC>` and `nodelog-<PC>`), prints a status line every 30 s read
with `igneum-miner.exe watch 1 grpc://127.0.0.1:26610` in the background (blocks, headers, peers, blue score, DAA, tips,
difficulty, synced yes or no; the miner's `watch` line gained `blue=` and `synced=` after `sink=` in commit `352495f6`,
appended last so the mining launcher's regex still matches), restarts the node after a crash with a random 5 to 60 s
delay (exit code and the last stderr lines logged), and stops it with taskkill on Ctrl+C (rocksdb recovers from its WAL;
no graceful signal reaches a console child while the launcher owns Ctrl+C). The "extract the zip first" check in the
bats escapes its brackets (`^(` and `^)`), as cmd ends a parenthesised block at a bare `)`.
Firewall: the first time igneumd.exe listens on 0.0.0.0:26611, Windows Defender Firewall shows "Windows Defender
Firewall has blocked some features of this app". Tick "Private networks", click "Allow access". That rule is what lets
the Mac dial the PC; the PC dials the Mac anyway, so the chain syncs even without it. "Cancel" creates a block rule:
ALLOW-FIREWALL.bat removes it and adds an inbound allow rule for the exe (private and domain profiles, elevated once).
The gRPC port is loopback only and never prompts.
### The mining launcher: NODE_HOST=auto
`START-MINING.bat` now sets `NODE_HOST=auto` and `start-mining.ps1` resolves it before anything else: a TCP connect to
127.0.0.1:NODE_PORT with a 1.5 s timeout picks the node on the PC, otherwise the Mac at 192.168.68.64; the choice is on
the launcher log's first line ("NODE_HOST=auto: using the node on this PC" or "nothing listens on 127.0.0.1:26610, using
the Mac node"). An explicit NODE_HOST still wins. Nothing else in the launcher changed (the hot-swap work is another
agent's).
### Sync test on the Mac
The Windows scripts cannot run here, so the flags and the sync were tested with the worktree's native `igneumd`
(`target/release`, built from the same commit) as a stand-in for the PC, 3 October 2026, 21:44 to 21:46 BST, against
the live node (gRPC 26610, p2p 26611, which stayed up throughout): node A `--devnet --appdir=/tmp/igneum-win-test/nodeA
--rpclisten=127.0.0.1:27000 --listen=0.0.0.0:27001 --addpeer=192.168.68.64:26611 --nodnsseed --disable-upnp
--nologfiles`, exactly START-NODE.bat's flags with the test ports. Banner `igneumd/2.1.0-2873fed`; "P2P Connected to
outgoing peer 192.168.68.64:26611", "IBD started" within 20 ms of the start; every header checked by
`igneum-lottery-v1-bound` ("PoW accepted ... daa 0, 1, 2 ..."). `igneum-miner watch 60` on A and the live node: at
the first sample (7 s in) A had 5,144 headers and 1,386 blocks, `synced=false`; from the second sample on (17 s) A and
the live node reported the same block count, DAA score, blue score and sink at every reading (5,157 to 5,175 blocks,
blue 5,126 to 5,143, `synced=true`), sink identical at 5 of 6 samples (the first was mid-IBD). The live node showed
`peers=2` while A ran and `peers=1` after it stopped. Inbound: a third node B on 27010/27011 with `--addpeer=127.0.0.1:27001`
connected to A's listener (A `peers=2`), synced to the same sink within 25 s (B also picked up the live node from A's
address exchange, "outbound: 2"), so a node started with `--addpeer` plus `--listen` accepts connections, which is what
the Mac needs when it dials the PC. SIGINT stopped B and A cleanly ("P2P Server stopped", "GRPC Server stopped",
"igneumd has stopped"); `/tmp/igneum-win-test` removed afterwards. The `blue=` and `synced=` fields on the `watch` line
are from the new miner build and parsed as the launcher does.
Not tested here: everything Windows (winget, vcvarsall, MSVC, the firewall prompt, Get-NetTCPConnection, taskkill,
Ctrl+C, the random restart delay, upload-log.bat from the node launcher, running the cross-compiled exe). The scripts
passed the bracket and quote balance pass (`windows-node/` and the two edited lines of `start-mining.ps1`).

View file

@ -0,0 +1,12 @@
@echo off
rem Adds a Windows Firewall rule so the Mac node can connect to igneumd.exe on this PC (inbound, private and domain
rem networks). Only needed if you clicked Cancel on the firewall prompt the first time START-NODE.bat ran, or if the
rem status line keeps saying peers 0 while the Mac node is up. Asks for administrator rights once.
cd /d "%~dp0"
if not exist "%~dp0igneumd.exe" (
echo igneumd.exe is not in this folder yet ^(run BUILD-NODE.bat first^); the rule needs the exe path.
pause
exit /b 1
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0allow-firewall.ps1"
pause

View file

@ -0,0 +1,28 @@
@echo off
rem Builds igneumd.exe (the Igneum node) on this PC from the source snapshot in src.zip. Double-click this file.
rem Only needed when igneumd.exe is not already in this folder. It installs Rust, LLVM and protoc with winget when they
rem are missing, uses the Visual Studio C++ toolset already on the PC, and runs cargo build --release -p kaspad
rem --features igneum-pow (about 15 to 25 minutes the first time on a 9800X3D, approximate). A progress line prints
rem every minute; cargo's own output goes to build-<stamp>.cargo.log next to this file.
rem ---- settings -----------------------------------------------------------------------------------
rem Visual Studio: only the path is needed; the default toolset of that installation is used.
set "VCVARS=C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat"
rem -------------------------------------------------------------------------------------------------
cd /d "%~dp0"
if not exist "%~dp0build-node.ps1" (
echo Please extract the whole zip first ^(right-click, Extract All^), then run BUILD-NODE.bat from the extracted folder.
pause
exit /b 1
)
where powershell >nul 2>nul
if errorlevel 1 (
echo PowerShell was not found. Windows 10 and 11 ship it; nothing else can be done here.
pause
exit /b 1
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-node.ps1"
echo.
echo The build has finished ^(see the lines above^). If igneumd.exe is now in this folder, run START-NODE.bat.
pause

View file

@ -0,0 +1,9 @@
Igneum devnet node for Windows: your own igneumd on the PC, so the miners mine against localhost and the PC peers with the Mac node (192.168.68.64:26611).
Extract the whole zip to a short path such as C:\igneum-node first (the scripts refuse to run from inside the zip).
1. BUILD-NODE.bat, once, only if igneumd.exe is NOT in this folder (the package ships one cross-compiled on the Mac; if that exe will not start, run this). It unpacks src.zip, installs Rust, LLVM and protoc with winget when missing, builds with cargo and the Visual Studio C++ toolset (any version), and copies igneumd.exe here. About 15 to 25 minutes the first time (approximate), a progress line every minute.
2. START-NODE.bat. Starts igneumd --devnet with the database under %LOCALAPPDATA%\igneum\devnet, RPC on 127.0.0.1:26610, p2p on 0.0.0.0:26611, peered to the Mac. One status line every 30 s (blocks, headers, peers, blue score, DAA, synced yes or no); the node's output is in node-<stamp>.log next to the bat; the PC stays awake; a crash restarts the node after 5 to 60 s; Ctrl+C stops it.
Firewall: the first time, Windows Defender Firewall asks about igneumd.exe. Tick Private networks and click Allow access (that lets the Mac dial this PC). If you clicked Cancel, run ALLOW-FIREWALL.bat once.
3. In the mining folder, START-MINING.bat: NODE_HOST=auto (the new default) picks 127.0.0.1:26610 when this node is running and falls back to the Mac; with an older copy of the mining package set NODE_HOST=127.0.0.1 at the top of the bat. Restart START-MINING.bat. The miners wait ("waiting") until this node is synced, usually under a minute.
Settings at the top of START-NODE.bat: MAC_NODE, RPC_LISTEN, P2P_LISTEN, APPDIR, UNSYNCED_MINING (1 = templates before sync), STATUS_SECS, EXTRA_ARGS. The node runs with --addpeer (not --connect: --connect would stop the Mac connecting in).
If the node exits at once with a database error (an older build's database), delete %LOCALAPPDATA%\igneum\devnet and start again; it resyncs from the Mac in seconds.
Files: START-NODE.bat and start-node.ps1, BUILD-NODE.bat and build-node.ps1, ALLOW-FIREWALL.bat and allow-firewall.ps1, igneumd.exe with libstdc++-6.dll, libgcc_s_seh-1.dll and libwinpthread-1.dll (its C++ runtime, keep them next to it), igneum-miner.exe (status reader), src.zip (source snapshot), upload-log.bat, WINDOWS-MINER.md (full write-up, section "Run your own node").

View file

@ -0,0 +1,45 @@
@echo off
rem Igneum devnet node for Windows. Double-click this file.
rem Runs igneumd.exe (the Igneum node) on this PC: the miners mine against it on localhost and it peers with the Mac
rem node. The window shows one status line every 30 seconds; the node's own output goes to node-<stamp>.log next to
rem this file. Stop with Ctrl+C; the window stays open at the end.
rem ---- settings: edit these lines only ----------------------------------------------------------
rem The Mac node (its p2p port). This node dials it at start and keeps retrying; the Mac can dial back as well.
set "MAC_NODE=192.168.68.64:26611"
rem gRPC for the miners on this PC (START-MINING.bat with NODE_HOST=auto looks here first).
set "RPC_LISTEN=127.0.0.1:26610"
rem p2p on all interfaces, so the Mac node can connect in. Windows Firewall asks once: tick Private networks, Allow access.
set "P2P_LISTEN=0.0.0.0:26611"
rem Where the chain database lives.
set "APPDIR=%LOCALAPPDATA%\igneum\devnet"
rem 1 = hand out block templates before this node is synced (the Mac node runs like this because it mines alone).
rem 0 = the miners wait until this node has caught up with the Mac.
set "UNSYNCED_MINING=0"
rem Seconds between status lines.
set "STATUS_SECS=30"
rem Anything else to pass to igneumd (for example --loglevel=debug).
set "EXTRA_ARGS="
rem -------------------------------------------------------------------------------------------------
cd /d "%~dp0"
if not exist "%~dp0start-node.ps1" (
echo Please extract the whole zip first ^(right-click, Extract All^), then run START-NODE.bat from the extracted folder.
pause
exit /b 1
)
if not exist "%~dp0igneumd.exe" (
echo igneumd.exe is not in this folder yet. Run BUILD-NODE.bat first ^(it builds the node from the source in src.zip^).
pause
exit /b 1
)
where powershell >nul 2>nul
if errorlevel 1 (
echo PowerShell was not found. Windows 10 and 11 ship it; nothing else can be done here.
pause
exit /b 1
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0start-node.ps1"
echo.
echo The node has stopped. The logs are next to this file.
pause

View file

@ -0,0 +1,26 @@
# Adds the inbound Windows Firewall rule for igneumd.exe (started by ALLOW-FIREWALL.bat). Runs itself again elevated,
# removes any block rule Windows made for the exe (the "Cancel" button on the prompt creates one), then adds an allow
# rule for the exe on the private and domain profiles. 3 October 2026.
param([switch]$Elevated)
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
$exe = Join-Path $root 'igneumd.exe'
$rule = 'Igneum node (igneumd)'
if (-not $Elevated) {
Write-Host "asking for administrator rights to add the firewall rule for $exe"
$psArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"' + $PSCommandPath + '"'), '-Elevated')
try { Start-Process -FilePath powershell.exe -Verb RunAs -Wait -ArgumentList $psArgs } catch { Write-Host "not elevated ($_); nothing changed"; exit 1 }
Write-Host 'done. Start the node again with START-NODE.bat.'
exit 0
}
try {
$blocked = @(Get-NetFirewallApplicationFilter -ErrorAction SilentlyContinue | Where-Object { $_.Program -and ($_.Program -ieq $exe) } | Get-NetFirewallRule | Where-Object { $_.Action -eq 'Block' })
foreach ($b in $blocked) { Write-Host "removing block rule '$($b.DisplayName)'"; $b | Remove-NetFirewallRule }
Get-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue | Remove-NetFirewallRule
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Program $exe -Action Allow -Profile Private,Domain -Protocol TCP | Out-Null
Write-Host "added inbound allow rule '$rule' for $exe (private and domain networks)"
} catch {
Write-Host "could not change the firewall: $_"
Start-Sleep -Seconds 5
exit 1
}
Start-Sleep -Seconds 2

View file

@ -0,0 +1,212 @@
# Builds igneumd.exe (the Igneum node) on this PC from the source snapshot in src.zip. Started by BUILD-NODE.bat.
# 3 October 2026.
#
# Steps:
# 1. Unpacks src.zip into src\ (src\vendor\igneum-node is the node fork, src\igneum-pow the lottery-hash crate the fork
# depends on by relative path; the layout must stay as it is). Nothing is cloned: the snapshot is a `git archive`
# of the fork's HEAD made on the Mac, so no GitHub access is needed and the repository stays private.
# 2. Makes sure the tools are present, installing the missing ones with winget (silent):
# Rust Rustlang.Rustup (stable, MSVC host). cargo lands in %USERPROFILE%\.cargo\bin.
# LLVM LLVM.LLVM, for libclang.dll: the rocksdb crate generates its bindings with bindgen (LIBCLANG_PATH).
# protoc Google.Protobuf, or a direct download of the protoc release zip: the gRPC and p2p crates run protoc
# from their build scripts (prost-build, no vendored protoc).
# MSVC the toolset of the installed Visual Studio, through vcvarsall x64 (the default 14.5x is fine for
# Rust and for the C and C++ crates: rocksdb, zstd, lz4, bzip2, zlib, secp256k1, mimalloc). The CUDA
# 14.30 trick of the mining launcher is not used here.
# 3. Runs `cargo build --release -p kaspad --features igneum-pow` in src\vendor\igneum-node (the package keeps its
# upstream name kaspad; the binary is igneumd.exe; the feature selects the real lottery-hash engine, without it the
# node would check blocks with the kHeavyHash stub and reject every devnet block). A progress line prints every
# minute: elapsed time, crates compiled, the crate being compiled.
# 4. Copies target\release\igneumd.exe next to BUILD-NODE.bat (and igneum-miner.exe if it was missing).
#
# Time: approximate 15 to 25 minutes on a Ryzen 7 9800X3D for the first build (the rocksdb C++ sources and about 500
# Rust crates; thin LTO at the end), under a minute for a rebuild with nothing changed. Windows Defender's real-time
# scan of target\ can add a few minutes. Extract the zip to a short path (C:\igneum-node) so no build path reaches the
# 260-character limit.
$ProgressPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Continue'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $root
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$buildLog = Join-Path $root "build-$stamp.log"
$started = Get-Date
$vcvars = if ($env:VCVARS) { $env:VCVARS } else { 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat' }
function Log([string]$msg) {
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg"
Write-Host $line
Add-Content -Path $buildLog -Value $line
}
function Refresh-Path {
$m = [Environment]::GetEnvironmentVariable('PATH', 'Machine')
$u = [Environment]::GetEnvironmentVariable('PATH', 'User')
$env:PATH = "$env:PATH;$m;$u"
}
function Invoke-Winget([string]$id) {
if (-not (Get-Command winget.exe -ErrorAction SilentlyContinue)) { Log "winget is not available, cannot install $id by itself"; return }
Log "installing $id with winget (silent)"
$out = & winget.exe install --id $id -e --silent --accept-package-agreements --accept-source-agreements 2>&1
$out | ForEach-Object { Add-Content -Path $buildLog -Value " winget: $_" }
Refresh-Path
}
Log "igneum node build on $env:COMPUTERNAME (run $stamp), folder $root"
if ($root.Length -gt 80) { Log "WARNING: this folder path is $($root.Length) characters long; cargo's build paths may pass the 260-character limit. C:\igneum-node is a safer place." }
# ---- 1. the source -----------------------------------------------------------------------------------------------------
$src = Join-Path $root 'src'
$nodeDir = Join-Path $src 'vendor\igneum-node'
$srcZip = Join-Path $root 'src.zip'
if (-not (Test-Path (Join-Path $nodeDir 'Cargo.toml'))) {
if (-not (Test-Path $srcZip)) { Log 'neither src\vendor\igneum-node nor src.zip is here. Extract the whole package again.'; exit 1 }
Log "unpacking src.zip into $src"
try { Expand-Archive -Path $srcZip -DestinationPath $src -Force } catch { Log "could not unpack src.zip: $_"; exit 1 }
}
if (-not (Test-Path (Join-Path $src 'igneum-pow\Cargo.toml'))) { Log 'src\igneum-pow is missing (the fork depends on it by relative path); the snapshot is incomplete.'; exit 1 }
$snapshotNote = Join-Path $src 'SNAPSHOT.txt'
if (Test-Path $snapshotNote) { Get-Content $snapshotNote | ForEach-Object { Log "source: $_" } }
# ---- 2. tools -------------------------------------------------------------------------------------------------------------
# Rust
$cargoBin = Join-Path $env:USERPROFILE '.cargo\bin'
if (Test-Path $cargoBin) { $env:PATH = "$cargoBin;$env:PATH" }
if (-not (Get-Command cargo.exe -ErrorAction SilentlyContinue)) {
Invoke-Winget 'Rustlang.Rustup'
if (Test-Path $cargoBin) { $env:PATH = "$cargoBin;$env:PATH" }
}
if (-not (Get-Command cargo.exe -ErrorAction SilentlyContinue)) { Log 'cargo is still not found. Install Rust from https://rustup.rs (default options) and run BUILD-NODE.bat again.'; exit 1 }
$toolchains = @(& rustup.exe toolchain list 2>&1)
if (-not ($toolchains | Where-Object { $_ -match '^stable' })) {
Log 'no stable toolchain yet: rustup toolchain install stable'
& rustup.exe toolchain install stable --profile minimal 2>&1 | ForEach-Object { Add-Content -Path $buildLog -Value " rustup: $_" }
& rustup.exe default stable 2>&1 | ForEach-Object { Add-Content -Path $buildLog -Value " rustup: $_" }
}
$active = (& rustup.exe show active-toolchain 2>&1 | Select-Object -First 1)
if ($active -notmatch 'msvc') { Log "the active toolchain is '$active'; the MSVC host toolchain is the tested one (rustup default stable-msvc)" }
Log "rust: $(& rustc.exe --version 2>&1) / $(& cargo.exe --version 2>&1) ($active)"
# LLVM (libclang for bindgen)
$libclang = $null
foreach ($d in @($env:LIBCLANG_PATH, 'C:\Program Files\LLVM\bin', 'C:\Program Files\LLVM\lib')) {
if ($d -and (Test-Path (Join-Path $d 'libclang.dll'))) { $libclang = $d; break }
}
if (-not $libclang) {
Invoke-Winget 'LLVM.LLVM'
foreach ($d in @('C:\Program Files\LLVM\bin', 'C:\Program Files\LLVM\lib')) { if (Test-Path (Join-Path $d 'libclang.dll')) { $libclang = $d; break } }
}
if (-not $libclang) { Log 'libclang.dll not found (LLVM). Install LLVM from https://releases.llvm.org (the Windows installer) and run BUILD-NODE.bat again.'; exit 1 }
$env:LIBCLANG_PATH = $libclang
Log "libclang: $libclang"
# protoc
$protoc = $null
$cmd = Get-Command protoc.exe -ErrorAction SilentlyContinue
if ($cmd) { $protoc = $cmd.Source }
$localProtoc = Join-Path $root 'tools\protoc\bin\protoc.exe'
if (-not $protoc -and (Test-Path $localProtoc)) { $protoc = $localProtoc }
if (-not $protoc) {
Invoke-Winget 'Google.Protobuf'
$cmd = Get-Command protoc.exe -ErrorAction SilentlyContinue
if ($cmd) { $protoc = $cmd.Source }
}
if (-not $protoc) {
# Fallback: the release zip from the protobuf project (public), unpacked under tools\protoc.
$ver = '33.1'
$url = "https://github.com/protocolbuffers/protobuf/releases/download/v$ver/protoc-$ver-win64.zip"
$zip = Join-Path $root 'tools\protoc.zip'
Log "protoc not found: downloading $url"
try {
New-Item -ItemType Directory -Force -Path (Join-Path $root 'tools') | Out-Null
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
Expand-Archive -Path $zip -DestinationPath (Join-Path $root 'tools\protoc') -Force
} catch { Log "download failed: $_" }
if (Test-Path $localProtoc) { $protoc = $localProtoc }
}
if (-not $protoc) { Log 'protoc is still missing. Install it (winget install Google.Protobuf, or the protoc-*-win64.zip from the protobuf releases page, unpacked to tools\protoc) and run BUILD-NODE.bat again.'; exit 1 }
$env:PROTOC = $protoc
Log "protoc: $protoc ($(& $protoc --version 2>&1))"
# MSVC: import the toolset environment (default version). rustc and the cc crate find MSVC through vswhere by themselves,
# so a missing vcvarsall is a warning, not a stop.
function Find-VcVarsAll {
if ($vcvars -and (Test-Path $vcvars)) { return $vcvars }
foreach ($pf in @('C:\Program Files', 'C:\Program Files (x86)')) {
$hits = Get-ChildItem -Path (Join-Path $pf 'Microsoft Visual Studio') -Recurse -Filter 'vcvarsall.bat' -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\VC\\Auxiliary\\Build\\vcvarsall\.bat$' } | Sort-Object FullName -Descending
if ($hits) { return $hits[0].FullName }
}
return $null
}
$bat = Find-VcVarsAll
if ($bat) {
Log "importing the MSVC environment: `"$bat`" x64"
$out = cmd /s /c "`"$bat`" x64 >nul 2>&1 && set" 2>$null
foreach ($l in @($out)) {
$i = $l.IndexOf('=')
if ($i -gt 0) { [Environment]::SetEnvironmentVariable($l.Substring(0, $i), $l.Substring($i + 1), 'Process') }
}
}
$cl = Get-Command cl.exe -ErrorAction SilentlyContinue
if ($cl) { Log "MSVC: $($cl.Source)" } else { Log 'cl.exe is not on PATH (no vcvarsall found); cargo will look for MSVC by itself. If the build stops with "linker link.exe not found", install the "Desktop development with C++" workload in the Visual Studio Installer.' }
# ---- 3. the build ------------------------------------------------------------------------------------------------------
$needMiner = -not (Test-Path (Join-Path $root 'igneum-miner.exe'))
$cargoArgs = @('build', '--release', '-p', 'kaspad', '--features', 'igneum-pow')
if ($needMiner) { $cargoArgs += @('-p', 'igneum-miner') }
$cargoOut = Join-Path $root "build-$stamp.cargo.log"
Log "cargo $($cargoArgs -join ' ') in $nodeDir (cargo's own output: $cargoOut)"
Log 'this takes about 15 to 25 minutes the first time; a line prints every minute'
$t0 = Get-Date
$proc = Start-Process -FilePath (Get-Command cargo.exe).Source -ArgumentList $cargoArgs -WorkingDirectory $nodeDir -RedirectStandardOutput ($cargoOut + '.out') -RedirectStandardError $cargoOut -NoNewWindow -PassThru
if ($proc) { $null = $proc.Handle }
function Get-CargoProgress {
# cargo prints " Compiling name vX.Y.Z" lines to stderr; count them and show the last one.
try {
$fs = New-Object IO.FileStream($cargoOut, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
try { $sr = New-Object IO.StreamReader($fs); $text = $sr.ReadToEnd(); $sr.Close() } finally { $fs.Close() }
} catch { return 'waiting for cargo' }
$lines = @($text -split "`n" | ForEach-Object { $_.TrimEnd("`r") })
$compiling = @($lines | Where-Object { $_ -match '^\s*Compiling ' })
$last = if ($compiling.Count -gt 0) { ($compiling[-1] -replace '^\s*Compiling ', '') -replace ' \(.*\)$', '' } else { 'resolving and downloading crates' }
$errors = @($lines | Where-Object { $_ -match '^error' }).Count
$txt = "$($compiling.Count) crates compiled, now $last"
if ($errors -gt 0) { $txt += ", $errors error line(s) so far" }
return $txt
}
$lastLine = Get-Date
while (-not $proc.HasExited) {
Start-Sleep -Seconds 5
if (((Get-Date) - $lastLine).TotalSeconds -ge 60) {
$lastLine = Get-Date
Log (" {0:N0} min: {1}" -f ((Get-Date) - $t0).TotalMinutes, (Get-CargoProgress))
}
}
$took = (Get-Date) - $t0
$rc = $proc.ExitCode
Log ("cargo finished with exit code {0} after {1} ({2})" -f $rc, $took.ToString('hh\:mm\:ss'), (Get-CargoProgress))
# ---- 4. the result ----------------------------------------------------------------------------------------------------------
$built = Join-Path $nodeDir 'target\release\igneumd.exe'
if ($rc -ne 0 -or -not (Test-Path $built)) {
Log "the build FAILED. The last error lines from $cargoOut :"
try { Get-Content $cargoOut -ErrorAction SilentlyContinue | Where-Object { $_ -match '^(error|warning: unused| = note: )|LNK|link\.exe|protoc|libclang|bindgen' } | Select-Object -Last 15 | ForEach-Object { Log " $_" } } catch { }
Log 'Common causes: no MSVC (install the Desktop development with C++ workload), protoc or libclang not found (see the lines above), a path over 260 characters (move the folder to C:\igneum-node), Defender holding a file (run it again).'
exit 1
}
Copy-Item $built (Join-Path $root 'igneumd.exe') -Force
$size = (Get-Item (Join-Path $root 'igneumd.exe')).Length
Log ("igneumd.exe copied next to BUILD-NODE.bat ({0:N1} MB)" -f ($size / 1MB))
if ($needMiner) {
$m = Join-Path $nodeDir 'target\release\igneum-miner.exe'
if (Test-Path $m) { Copy-Item $m (Join-Path $root 'igneum-miner.exe') -Force; Log 'igneum-miner.exe copied too (the status reader of START-NODE.bat)' }
}
Log ("done in {0}. Now run START-NODE.bat." -f ((Get-Date) - $started).ToString('hh\:mm\:ss'))

View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Cross-compiles igneumd.exe and igneum-miner.exe for x86_64-pc-windows-gnu on the Mac (Homebrew mingw-w64 and llvm).
# Worked on 3 October 2026 (8 min 25 s with -j 6 at nice 19 on a loaded M5 Max); see WINDOWS-MINER.md, "Run your own node".
# What each variable is for:
# CC/CXX/AR_x86_64_pc_windows_gnu the cc crate builds rocksdb (C++), snappy, zstd, lz4, bzip2, zlib, secp256k1 and
# mimalloc with the mingw compilers
# LIBCLANG_PATH librocksdb-sys runs bindgen on rocksdb/c.h; Homebrew llvm's libclang
# BINDGEN_EXTRA_CLANG_ARGS_... bindgen's clang must parse the mingw headers for the Windows target, not the Mac's
# CARGO_TARGET_..._RUSTFLAGS -static: ld takes libstdc++.a, libgcc.a and libwinpthread.a instead of the import
# libraries, so the exe does not need libstdc++-6.dll, libgcc_s_seh-1.dll or
# libwinpthread-1.dll on the PC (the first build linked libstdc++-6.dll because
# -static-libstdc++ means nothing to the gcc driver rustc links with)
# Usage: windows-node/cross-build.sh [node worktree, default vendor/igneum-node-win] [cargo jobs, default 6]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
NODE="${1:-$ROOT/vendor/igneum-node-win}"
JOBS="${2:-6}"
MINGW="/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32"
[ -d "$MINGW" ] || { echo "no mingw-w64 at $MINGW (brew install mingw-w64)" >&2; exit 1; }
[ -f /opt/homebrew/opt/llvm/lib/libclang.dylib ] || { echo "no libclang at /opt/homebrew/opt/llvm/lib (brew install llvm)" >&2; exit 1; }
export PATH="$HOME/.cargo/bin:/opt/homebrew/bin:$PATH"
rustup target list --installed | grep -q x86_64-pc-windows-gnu || rustup target add x86_64-pc-windows-gnu
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-target}"
export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc
export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++
export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar
export LIBCLANG_PATH=/opt/homebrew/opt/llvm/lib
export BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw32 --sysroot=$MINGW -I$MINGW/include"
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc"
cd "$NODE"
nice -n 19 cargo build --release -j "$JOBS" -p kaspad -p igneum-miner --features igneum-pow --target x86_64-pc-windows-gnu
for exe in igneumd igneum-miner; do
f="$CARGO_TARGET_DIR/x86_64-pc-windows-gnu/release/$exe.exe"
echo "$f: $(stat -f %z "$f") bytes; DLLs: $(x86_64-w64-mingw32-objdump -p "$f" | grep 'DLL Name' | awk '{print $3}' | sort -u | tr '\n' ' ')"
done

View file

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Builds igneum-node-windows.zip (run your own Igneum node on a Windows PC) on the Mac.
# Contents: START-NODE.bat, start-node.ps1, BUILD-NODE.bat, build-node.ps1, ALLOW-FIREWALL.bat, allow-firewall.ps1,
# README.txt, upload-log.bat (from windows-miner/), WINDOWS-MINER.md, igneum-miner.exe (cross-compiled, the status
# reader), igneumd.exe when a cross-compiled one exists, and src.zip: a `git archive` of the node worktree's HEAD under
# vendor/igneum-node/ plus igneum-pow/ from this repository's HEAD (the fork depends on it by relative path), so the PC
# can build the node with BUILD-NODE.bat without any GitHub access.
# Usage: windows-node/make-package.sh [node worktree, default vendor/igneum-node-win] [output zip, default ~/Desktop/igneum-node-windows.zip]
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
NODE="${1:-$ROOT/vendor/igneum-node-win}"
OUT="${2:-$HOME/Desktop/igneum-node-windows.zip}"
[ -f "$NODE/Cargo.toml" ] || { echo "no node worktree at $NODE" >&2; exit 1; }
EXE="$NODE/target/x86_64-pc-windows-gnu/release/igneumd.exe"
MINER="$NODE/target/x86_64-pc-windows-gnu/release/igneum-miner.exe"
STAGE="$(mktemp -d)/igneum-node-windows"
mkdir -p "$STAGE/srcstage"
cp "$HERE/START-NODE.bat" "$HERE/start-node.ps1" "$HERE/BUILD-NODE.bat" "$HERE/build-node.ps1" \
"$HERE/ALLOW-FIREWALL.bat" "$HERE/allow-firewall.ps1" "$HERE/README.txt" "$STAGE/"
cp "$ROOT/proto-cuda/windows-miner/upload-log.bat" "$STAGE/"
cp "$ROOT/proto-cuda/WINDOWS-MINER.md" "$STAGE/"
MINGW=/opt/homebrew/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32
if [ -f "$EXE" ]; then
cp "$EXE" "$STAGE/igneumd.exe"; echo "igneumd.exe: $(ls -la "$EXE" | awk '{print $5}') bytes (cross-compiled)"
# The exe imports libstdc++-6.dll (rustc passes the cc crate's -lstdc++ as -Bdynamic, so -static does not cover it);
# that DLL needs libgcc_s_seh-1.dll and libwinpthread-1.dll. Ship the three next to the exe (GCC runtime exception).
IMPORTS="$(x86_64-w64-mingw32-objdump -p "$EXE" 2>/dev/null | grep 'DLL Name: lib' || true)" # no grep -q: pipefail would turn objdump's SIGPIPE into a miss
if [ -n "$IMPORTS" ]; then
for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do
[ -f "$MINGW/$dll" ] || { echo "missing $MINGW/$dll" >&2; exit 1; }
cp "$MINGW/$dll" "$STAGE/"
x86_64-w64-mingw32-strip "$STAGE/$(basename "$dll")" 2>/dev/null || true # the toolchain's libstdc++-6.dll carries 27 MB of debug symbols
done
echo "mingw runtime DLLs added next to the exe: libstdc++-6.dll libgcc_s_seh-1.dll libwinpthread-1.dll"
fi
else echo "no cross-compiled igneumd.exe at $EXE; the package builds on the PC"; fi
if [ -f "$MINER" ]; then cp "$MINER" "$STAGE/igneum-miner.exe"; else echo "warning: no cross-compiled igneum-miner.exe at $MINER (BUILD-NODE.bat builds it too)"; fi
# the source snapshot
NODE_HEAD="$(git -C "$NODE" rev-parse HEAD)"
ROOT_HEAD="$(git -C "$ROOT" rev-parse HEAD)"
git -C "$NODE" archive --format=tar --prefix=vendor/igneum-node/ HEAD | tar -x -C "$STAGE/srcstage"
git -C "$ROOT" archive --format=tar HEAD igneum-pow | tar -x -C "$STAGE/srcstage"
{
echo "igneum node source snapshot, made $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "vendor/igneum-node: branch $(git -C "$NODE" rev-parse --abbrev-ref HEAD) commit $NODE_HEAD ($(git -C "$NODE" log -1 --format=%s))"
echo "igneum-pow: igneum commit $ROOT_HEAD"
echo "build: cd vendor/igneum-node && cargo build --release -p kaspad --features igneum-pow (binary: target/release/igneumd)"
} > "$STAGE/srcstage/SNAPSHOT.txt"
(cd "$STAGE/srcstage" && zip -qr "../src.zip" .)
rm -rf "$STAGE/srcstage"
# CRLF for the files Windows tools read as text
for f in "$STAGE"/*.bat "$STAGE/README.txt"; do perl -pi -e 's/\r?\n/\r\n/' "$f"; done
rm -f "$OUT"
(cd "$(dirname "$STAGE")" && zip -qr "$OUT" "$(basename "$STAGE")")
ls -la "$OUT"
unzip -l "$OUT" | tail -n +4 | awk '{print $1, $4}' | sed '/^ *$/d'

View file

@ -0,0 +1,293 @@
# Igneum devnet node, Windows launcher. Started by START-NODE.bat, which sets the settings as environment variables
# (MAC_NODE, RPC_LISTEN, P2P_LISTEN, APPDIR, UNSYNCED_MINING, STATUS_SECS, EXTRA_ARGS). 3 October 2026.
#
# What it does, in order:
# 1. Checks igneumd.exe and the ports, prints the node version, tests whether the Mac node answers on its p2p port.
# 2. Starts igneumd.exe --devnet with --appdir, --rpclisten, --listen, --addpeer=<Mac>, --nodnsseed, --disable-upnp and
# --nologfiles; the node's own output goes to node-<stamp>.log next to the bat (a new segment per start, because
# Start-Process truncates its redirect target). The launcher's own lines go to node-launcher-<stamp>.log.
# 3. Keeps the PC awake (SetThreadExecutionState, no power plan change).
# 4. Prints one status line every STATUS_SECS: blocks, headers, peers, blue score, DAA score, tips, synced yes or no.
# The reading comes from `igneum-miner.exe watch 1 grpc://<RPC_LISTEN>` run in the background, the same tool and
# line the mining launcher reads (vendor/igneum-node/igneum/miner/src/main.rs, watch):
# <unix> node1 blocks=N headers=N daa=N tips=N peers=N difficulty=X sink=<12 hex> blue=N synced=true|false
# 5. Restarts the node after a crash with a random delay of 5 to 60 s; uploads the launcher log every 5 minutes with
# upload-log.bat when it is present; Ctrl+C stops the node and prints a summary.
#
# Why --addpeer and not --connect: in igneumd (kaspad/src/daemon.rs) --connect sets the inbound limit to 0 and skips
# outbound peers, so the Mac could never dial this PC. --addpeer keeps the Mac as a permanent peer (retried with a
# backoff of up to 8 minutes, components/connectionmanager/src/lib.rs) and leaves the listener open for the Mac to
# connect in. The Mac node's inbound limit is the default 128.
$ProgressPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Continue'
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $root
$macNode = if ($env:MAC_NODE) { $env:MAC_NODE } else { '192.168.68.64:26611' }
$rpcListen = if ($env:RPC_LISTEN) { $env:RPC_LISTEN } else { '127.0.0.1:26610' }
$p2pListen = if ($env:P2P_LISTEN) { $env:P2P_LISTEN } else { '0.0.0.0:26611' }
$appDir = if ($env:APPDIR) { $env:APPDIR } else { Join-Path $env:LOCALAPPDATA 'igneum\devnet' }
$unsyncedMining = ($env:UNSYNCED_MINING -eq '1')
$statusSecs = 30
if ($env:STATUS_SECS -and [int]$env:STATUS_SECS -ge 5) { $statusSecs = [int]$env:STATUS_SECS }
$extraArgs = $env:EXTRA_ARGS
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$machine = $env:COMPUTERNAME
$launcherLog = Join-Path $root "node-launcher-$stamp.log"
$uploader = Join-Path $root 'upload-log.bat'
$nodeExe = Join-Path $root 'igneumd.exe'
$miner = Join-Path $root 'igneum-miner.exe'
$started = Get-Date
# The RPC address the status reader dials: the listen address, with an unspecified interface mapped to loopback.
$rpcHost = ($rpcListen -split ':')[0]
$rpcPort = ($rpcListen -split ':')[-1]
if ($rpcHost -eq '0.0.0.0' -or $rpcHost -eq '') { $rpcHost = '127.0.0.1' }
$rpcUrl = "grpc://${rpcHost}:${rpcPort}"
$p2pPort = ($p2pListen -split ':')[-1]
$macHost = ($macNode -split ':')[0]
$macPort = ($macNode -split ':')[-1]
function Log([string]$msg) {
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $msg"
Write-Host $line
Add-Content -Path $launcherLog -Value $line
}
# The last lines of a file another process is still writing (FileShare ReadWrite).
function Get-TailLines([string]$path, [int]$count) {
if (-not $path -or -not (Test-Path $path)) { return @() }
try {
$fs = New-Object IO.FileStream($path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
try {
$len = $fs.Length; $n = [Math]::Min($len, 16384); [void]$fs.Seek(-$n, [IO.SeekOrigin]::End)
$b = New-Object byte[] $n; [void]$fs.Read($b, 0, $n)
} finally { $fs.Close() }
$lines = @(([Text.Encoding]::UTF8.GetString($b) -split "`n") | ForEach-Object { $_.TrimEnd("`r") } | Where-Object { $_ -ne '' })
if ($lines.Count -le $count) { return $lines }
return @($lines[($lines.Count - $count)..($lines.Count - 1)])
} catch { return @() }
}
Log "igneum node launcher on $machine (run $stamp)"
Log "settings: Mac node $macNode, RPC $rpcListen, p2p $p2pListen, appdir $appDir, unsynced mining $(if ($unsyncedMining) { 'on' } else { 'off' }), status every $statusSecs s"
# ---- 1. checks ------------------------------------------------------------------------------------------------------
if (-not (Test-Path $nodeExe)) { Log 'igneumd.exe is missing. Run BUILD-NODE.bat first.'; exit 1 }
$ver = ''
try { $ver = (& $nodeExe --version 2>&1 | Select-Object -First 1) } catch { $ver = "could not run igneumd.exe: $_" }
Log "node binary: $nodeExe ($ver)"
if (-not (Test-Path $miner)) { Log 'igneum-miner.exe is not in this folder: the status line will only say whether the node process is alive' }
# Another node on the same ports (a second START-NODE window, or a stale process)
foreach ($port in @([int]$rpcPort, [int]$p2pPort)) {
try {
$busy = @(Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue)
if ($busy.Count -gt 0) {
$owner = ''
try { $owner = (Get-Process -Id $busy[0].OwningProcess -ErrorAction SilentlyContinue).ProcessName } catch { }
Log "port $port is already in use by $owner (pid $($busy[0].OwningProcess)). Is a node already running? Close it first."
exit 1
}
} catch { }
}
try { $macUp = Test-NetConnection -ComputerName $macHost -Port ([int]$macPort) -WarningAction SilentlyContinue -InformationLevel Quiet } catch { $macUp = $false }
if ($macUp) { Log "the Mac node answers on $macNode" } else { Log "the Mac node at $macNode does not answer right now; igneumd will keep retrying (backoff up to 8 minutes)" }
Log "If Windows Firewall asks about igneumd.exe: tick Private networks and click Allow access (the Mac node dials this PC on port $p2pPort). If you clicked Cancel once, run ALLOW-FIREWALL.bat."
New-Item -ItemType Directory -Force -Path $appDir | Out-Null
# ---- 2. the node ---------------------------------------------------------------------------------------------------
# Start-Process joins the argument list with spaces and does not quote, so values that may contain spaces are quoted here.
$nodeArgs = @('--devnet', ('"--appdir=' + $appDir + '"'), "--rpclisten=$rpcListen", "--listen=$p2pListen", "--addpeer=$macNode", '--nodnsseed', '--disable-upnp', '--nologfiles')
if ($unsyncedMining) { $nodeArgs += '--enable-unsynced-mining' }
if ($extraArgs) { $nodeArgs += ($extraArgs -split '\s+' | Where-Object { $_ -ne '' }) }
$script:node = @{ proc = $null; log = $null; err = $null; starts = 0; startedAt = $null; restartAt = $null; restarts = 0; exitCode = $null }
function Start-Node {
$n = $script:node
$n.starts += 1
$seg = if ($n.starts -gt 1) { "-r$($n.starts)" } else { '' }
$n.log = Join-Path $root "node-$stamp$seg.log"
$n.err = $n.log + '.err'
$n.restartAt = $null
$n.proc = Start-Process -FilePath $nodeExe -ArgumentList $nodeArgs -WorkingDirectory $root -RedirectStandardOutput $n.log -RedirectStandardError $n.err -NoNewWindow -PassThru
if ($n.proc) { $null = $n.proc.Handle } # without touching Handle, ExitCode comes back empty after the process ends
$n.startedAt = Get-Date
Log "igneumd started (pid $($n.proc.Id)): `"$nodeExe`" $($nodeArgs -join ' ') -> $($n.log)"
}
function Stop-Node {
$n = $script:node
if ($n.proc -and -not $n.proc.HasExited) { & taskkill.exe /T /F /PID $n.proc.Id 2>$null | Out-Null }
}
# ---- 4. status, read with igneum-miner watch in the background ----------------------------------------------------------
$script:watch = @{ proc = $null; log = (Join-Path $root "status-$stamp.tmp"); err = (Join-Path $root "status-$stamp.tmp.err"); lastStart = $null; fails = 0; last = $null }
$script:statusCount = 0
function Start-Watch {
if (-not (Test-Path $miner)) { return }
$w = $script:watch
if ($w.proc -and -not $w.proc.HasExited) {
if ($w.lastStart -and ((Get-Date) - $w.lastStart).TotalSeconds -gt 90) { & taskkill.exe /T /F /PID $w.proc.Id 2>$null | Out-Null; $w.proc = $null; $w.fails += 1 }
else { return }
}
try {
$w.proc = Start-Process -FilePath $miner -ArgumentList @('watch', '1', $rpcUrl) -WorkingDirectory $root -RedirectStandardOutput $w.log -RedirectStandardError $w.err -NoNewWindow -PassThru
if ($w.proc) { $null = $w.proc.Handle }
$w.lastStart = Get-Date
} catch { $w.proc = $null; $w.fails += 1 }
}
# Returns the parsed reading when the watch process has finished, else $null. Sets $script:watch.last.
function Read-Watch {
$w = $script:watch
if (-not $w.proc -or -not $w.proc.HasExited) { return $null }
$w.proc = $null
$line = $null
try {
$text = ''
$fs = New-Object IO.FileStream($w.log, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
try { $sr = New-Object IO.StreamReader($fs); $text = $sr.ReadToEnd(); $sr.Close() } finally { $fs.Close() }
foreach ($l in ($text -split "`n")) { if ($l -match ' node1 blocks=') { $line = $l; break } }
} catch { }
if ($line -and $line -match 'blocks=([0-9]+) headers=([0-9]+) daa=([0-9]+) tips=([0-9]+) peers=([0-9]+) difficulty=([0-9.]+) sink=([0-9a-f]+)(?: blue=([0-9]+))?(?: synced=(\w+))?') {
$r = @{ at = (Get-Date); blocks = [long]$Matches[1]; headers = [long]$Matches[2]; daa = [long]$Matches[3]; tips = [int]$Matches[4]; peers = [int]$Matches[5];
difficulty = [double]$Matches[6]; sink = $Matches[7]; blue = $(if ($Matches[8]) { [long]$Matches[8] } else { -1 }); synced = $(if ($Matches[9]) { ($Matches[9] -eq 'true') } else { $null }) }
$w.fails = 0
$w.last = $r
return $r
}
$w.fails += 1
return $null
}
function Print-Status($r) {
$script:statusCount += 1
$n = $script:node
$up = ((Get-Date) - $started).ToString('hh\:mm\:ss')
$alive = ($n.proc -and -not $n.proc.HasExited)
$tail = "up $up, restarts $($n.restarts)"
if (-not $alive) { Log "status: the node process is not running ($tail)"; return }
if ($r) {
$syncTxt = if ($r.synced -eq $null) { 'n/a' } elseif ($r.synced) { 'yes' } else { 'no' }
$blueTxt = if ($r.blue -ge 0) { [string]$r.blue } else { 'n/a' }
$peerTxt = if ($r.peers -eq 0) { '0 (the Mac node is not connected yet)' } else { [string]$r.peers }
Log ("status: blocks {0}, headers {1}, peers {2}, blue score {3}, DAA {4}, tips {5}, difficulty {6:N0}, synced {7} ({8})" -f $r.blocks, $r.headers, $peerTxt, $blueTxt, $r.daa, $r.tips, $r.difficulty, $syncTxt, $tail)
} elseif (-not (Test-Path $miner)) {
Log "status: igneumd is running (pid $($n.proc.Id)), no reader for the chain state without igneum-miner.exe ($tail)"
} else {
$age = [int]((Get-Date) - $n.startedAt).TotalSeconds
$why = if ($age -lt 60) { 'it is still starting, opening the database' } else { "no answer from the RPC at $rpcUrl for $($script:watch.fails) readings; see $($n.log)" }
Log "status: igneumd is running (pid $($n.proc.Id)) but gave no reading yet: $why ($tail)"
}
}
# ---- upload (optional) --------------------------------------------------------------------------------------------------
function Upload-Logs {
if (-not (Test-Path $uploader)) { return }
$o = cmd /s /c "`"$uploader`" `"$launcherLog`" node-$machine node-$machine-$stamp" 2>&1
Add-Content -Path $launcherLog -Value (" upload launcher log: " + ($o -join ' '))
$n = $script:node
if ($n.log -and (Test-Path $n.log)) {
$o = cmd /s /c "`"$uploader`" `"$($n.log)`" nodelog-$machine nodelog-$machine-$stamp" 2>&1
Add-Content -Path $launcherLog -Value (" upload node log: " + ($o -join ' '))
}
}
# ---- 3. keep awake ---------------------------------------------------------------------------------------------------
$keepAwake = $null
try {
$keepAwake = Add-Type -Name 'KeepAwake' -Namespace 'Igneum' -PassThru -MemberDefinition @'
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint SetThreadExecutionState(uint esFlags);
'@
[void]$keepAwake::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001) # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
Log 'keeping the PC awake while the node runs'
} catch { Log "could not set the keep-awake state: $_ (set Sleep to Never for the night)"; $keepAwake = $null }
$lastAwake = Get-Date
# ---- 5. loop ----------------------------------------------------------------------------------------------------------
Start-Node
Start-Sleep -Seconds 3
if ($script:node.proc.HasExited) {
Log "igneumd exited at once with code $($script:node.proc.ExitCode). Last lines:"
foreach ($l in (Get-TailLines $script:node.err 8)) { Log " stderr: $l" }
foreach ($l in (Get-TailLines $script:node.log 8)) { Log " stdout: $l" }
Log "Nothing to restart. Fix the cause above (a wrong --appdir, a port in use, a database from an older build that needs deleting: $appDir) and start again."
if ($keepAwake) { [void]$keepAwake::SetThreadExecutionState([uint32]2147483648) }
exit 1
}
foreach ($l in (Get-TailLines $script:node.log 3)) { Log " node: $l" }
[Console]::TreatControlCAsInput = $true
Log "running. Press Ctrl+C to stop. Status every $statusSecs s; the node's full output is in $($script:node.log)"
$lastStatus = (Get-Date).AddSeconds($statusSecs - 10) # first reading soon after the start
$lastUpload = Get-Date
$watchPending = $false
$stop = $false
try {
while (-not $stop) {
Start-Sleep -Milliseconds 500
if ($keepAwake -and ((Get-Date) - $lastAwake).TotalSeconds -ge 60) {
[void]$keepAwake::SetThreadExecutionState([uint32]2147483648 -bor [uint32]0x00000001)
$lastAwake = Get-Date
}
while ([Console]::KeyAvailable) {
$k = [Console]::ReadKey($true)
if ($k.Key -eq 'C' -and ($k.Modifiers -band [ConsoleModifiers]::Control)) { $stop = $true }
}
if ($stop) { break }
$n = $script:node
if ($n.proc -and $n.proc.HasExited -and -not $n.restartAt) {
$n.restarts += 1
$n.exitCode = $null
try { $n.exitCode = $n.proc.ExitCode } catch { }
$delay = Get-Random -Minimum 5 -Maximum 61
$n.restartAt = (Get-Date).AddSeconds($delay)
Log "igneumd exited with code $($n.exitCode) after $([int]((Get-Date) - $n.startedAt).TotalSeconds) s; restarting in $delay s (restart $($n.restarts))"
foreach ($l in (Get-TailLines $n.err 5)) { Log " stderr: $l" }
foreach ($l in (Get-TailLines $n.log 5)) { Log " stdout: $l" }
}
if ($n.restartAt -and (Get-Date) -ge $n.restartAt) { Start-Node }
# one reading per status period: start the reader, print when it has finished
if (((Get-Date) - $lastStatus).TotalSeconds -ge $statusSecs) {
$lastStatus = Get-Date
if ($n.proc -and -not $n.proc.HasExited -and (Test-Path $miner)) { Start-Watch; $watchPending = $true }
else { Print-Status $null }
}
if ($watchPending) {
$w = $script:watch
if (-not $w.proc) { Print-Status $null; $watchPending = $false }
elseif ($w.proc.HasExited) { $r = Read-Watch; Print-Status $r; $watchPending = $false }
elseif ($w.lastStart -and ((Get-Date) - $w.lastStart).TotalSeconds -gt 60) { Start-Watch } # Start-Watch kills a reader older than 90 s
}
if (((Get-Date) - $lastUpload).TotalSeconds -ge 300) { Upload-Logs; $lastUpload = Get-Date }
}
} finally {
[Console]::TreatControlCAsInput = $false
if ($keepAwake) { [void]$keepAwake::SetThreadExecutionState([uint32]2147483648); Log 'released the keep-awake state' } # ES_CONTINUOUS alone clears it
Log 'stopping the node'
Stop-Node
if ($script:watch.proc -and -not $script:watch.proc.HasExited) { & taskkill.exe /T /F /PID $script:watch.proc.Id 2>$null | Out-Null }
Start-Sleep -Seconds 2
$last = $script:watch.last
$lastTxt = if ($last) { "last reading at $($last.at.ToString('HH:mm:ss')): blocks $($last.blocks), headers $($last.headers), peers $($last.peers), DAA $($last.daa)" } else { 'no reading from the node in this run' }
Log ('SUMMARY after ' + ((Get-Date) - $started).ToString('hh\:mm\:ss') + ": node started $($script:node.starts) time(s), restarts $($script:node.restarts), $($script:statusCount) status lines; $lastTxt")
Remove-Item -Path $script:watch.log, $script:watch.err -ErrorAction SilentlyContinue
Upload-Logs
Log "the node's output is in $($script:node.log); the database stays in $appDir"
}