From 3e1ecab2457e89a2e7af5072a868aa4ba29a75f4 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:54:27 +0000 Subject: [PATCH] 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 --- docs/bench-log.md | 7 + proto-cuda/WINDOWS-MINER.md | 117 ++++++++ proto-cuda/windows-node/ALLOW-FIREWALL.bat | 12 + proto-cuda/windows-node/BUILD-NODE.bat | 28 ++ proto-cuda/windows-node/README.txt | 9 + proto-cuda/windows-node/START-NODE.bat | 45 ++++ proto-cuda/windows-node/allow-firewall.ps1 | 26 ++ proto-cuda/windows-node/build-node.ps1 | 212 +++++++++++++++ proto-cuda/windows-node/cross-build.sh | 36 +++ proto-cuda/windows-node/make-package.sh | 57 ++++ proto-cuda/windows-node/start-node.ps1 | 293 +++++++++++++++++++++ 11 files changed, 842 insertions(+) create mode 100644 proto-cuda/windows-node/ALLOW-FIREWALL.bat create mode 100644 proto-cuda/windows-node/BUILD-NODE.bat create mode 100644 proto-cuda/windows-node/README.txt create mode 100644 proto-cuda/windows-node/START-NODE.bat create mode 100644 proto-cuda/windows-node/allow-firewall.ps1 create mode 100644 proto-cuda/windows-node/build-node.ps1 create mode 100755 proto-cuda/windows-node/cross-build.sh create mode 100755 proto-cuda/windows-node/make-package.sh create mode 100644 proto-cuda/windows-node/start-node.ps1 diff --git a/docs/bench-log.md b/docs/bench-log.md index dfb5ee833..92db67d6e 100644 --- a/docs/bench-log.md +++ b/docs/bench-log.md @@ -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= -I"` 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. diff --git a/proto-cuda/WINDOWS-MINER.md b/proto-cuda/WINDOWS-MINER.md index ec1126ba0..9654245f0 100644 --- a/proto-cuda/WINDOWS-MINER.md +++ b/proto-cuda/WINDOWS-MINER.md @@ -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=/x86_64-w64-mingw32 -I/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-.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= --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-.log` next to the bat (a new segment per start) and its own lines to `node-launcher-.log` +(uploaded every 5 min with upload-log.bat under `node-` and `nodelog-`), 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`). diff --git a/proto-cuda/windows-node/ALLOW-FIREWALL.bat b/proto-cuda/windows-node/ALLOW-FIREWALL.bat new file mode 100644 index 000000000..b85215df3 --- /dev/null +++ b/proto-cuda/windows-node/ALLOW-FIREWALL.bat @@ -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 diff --git a/proto-cuda/windows-node/BUILD-NODE.bat b/proto-cuda/windows-node/BUILD-NODE.bat new file mode 100644 index 000000000..87f8dfe24 --- /dev/null +++ b/proto-cuda/windows-node/BUILD-NODE.bat @@ -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-.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 diff --git a/proto-cuda/windows-node/README.txt b/proto-cuda/windows-node/README.txt new file mode 100644 index 000000000..69b40b569 --- /dev/null +++ b/proto-cuda/windows-node/README.txt @@ -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-.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"). diff --git a/proto-cuda/windows-node/START-NODE.bat b/proto-cuda/windows-node/START-NODE.bat new file mode 100644 index 000000000..d0489a95a --- /dev/null +++ b/proto-cuda/windows-node/START-NODE.bat @@ -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-.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 diff --git a/proto-cuda/windows-node/allow-firewall.ps1 b/proto-cuda/windows-node/allow-firewall.ps1 new file mode 100644 index 000000000..154b312cb --- /dev/null +++ b/proto-cuda/windows-node/allow-firewall.ps1 @@ -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 diff --git a/proto-cuda/windows-node/build-node.ps1 b/proto-cuda/windows-node/build-node.ps1 new file mode 100644 index 000000000..aa1290655 --- /dev/null +++ b/proto-cuda/windows-node/build-node.ps1 @@ -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')) diff --git a/proto-cuda/windows-node/cross-build.sh b/proto-cuda/windows-node/cross-build.sh new file mode 100755 index 000000000..f08328f2c --- /dev/null +++ b/proto-cuda/windows-node/cross-build.sh @@ -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 diff --git a/proto-cuda/windows-node/make-package.sh b/proto-cuda/windows-node/make-package.sh new file mode 100755 index 000000000..82716a256 --- /dev/null +++ b/proto-cuda/windows-node/make-package.sh @@ -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' diff --git a/proto-cuda/windows-node/start-node.ps1 b/proto-cuda/windows-node/start-node.ps1 new file mode 100644 index 000000000..fa06e9014 --- /dev/null +++ b/proto-cuda/windows-node/start-node.ps1 @@ -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=, --nodnsseed, --disable-upnp and +# --nologfiles; the node's own output goes to node-.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-.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://` run in the background, the same tool and +# line the mining launcher reads (vendor/igneum-node/igneum/miner/src/main.rs, watch): +# 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" +}