diff --git a/docs/plans/counter-asic-3-status.md b/docs/plans/counter-asic-3-status.md index 8f631ba8d..d58313059 100644 --- a/docs/plans/counter-asic-3-status.md +++ b/docs/plans/counter-asic-3-status.md @@ -423,7 +423,7 @@ Every failing seed is ONE low-entropy load site; the mechanism is lineage-blind Known-failed test class_v4_distinct_ratio_rejects_the_low_entropy_band green on box 2 (12.95 s): p15 attempt 3 (52638ea2e8b0fd68) site 2 at 0.943; p18 attempt 2 (9a37e9489d8ba698) site 6 at 0.927; p19 attempt 0 (79d7441de0689223) site 15 at 0.933; p56 attempt 2 (486a8ad2701ec3b5) site 2 at 0.965; p23 attempt 1 (d65122675f16a1c7) draws site 7 from r5 and passes, and with r6 put back is refused by (a') UnfreshLoadSource and, run anyway, by the ratio at 0.836. Stream unchanged from ddacfbd3 (re-export diff 0 on all eight packs): id a785001687d8688a, the seven fingerprints and the packs zip sha256 4f2445c5... as recorded. THE SUITE AT 017e7037 on box 2: 103 of 103 (64 lib + 7 derive + 4 mixer + 19 packs + 2 recheck + 7 scratch, 3 diag ignored), rc 0; the lib tests 140.8 s against the 41 s whole-suite line at ddacfbd3, because every class v4 draw in the tests pays the 2^20 ratio pass on its chosen candidate (2.2 s each): a CI-time cost, not a node cost (one pass per epoch draw). CI run 37639406567 on 017e7037 success. THE CENSUS AT 017e7037 (box 2, 4,096 chain-shaped seeds plus F8's p1 to p3, draws in parallel across the cores; tools/ca3-v4-uniform now draws across available_parallelism since the serial run became a four-hour job): 4,099 programs, 0 lossy-sourced load sites of 65,584 (57,322 injecting, 8,262 bijective), 0 exhaustions; attempt histogram 0:1297 1:899 2:609 3:416 4:298 5:197 6:125 7:92 8:54 9:46 10:21 11:14 12:13 13:9 14:6 16:2 17:1, mean 2.086, max 17 (ddacfbd3 read 1328/917/622/409/284..., mean 1.998, max 17): the ratio refuses about 4 percent of candidates that pass every other test, one extra attempt on about one seed in twelve, the worst case unchanged at 17; devnet epoch-0 at attempt 1, id a785001687d8688a; F8's p2 attempt 5, p3 attempt 1. The attack-pass lane's class check: ddacfbd3's shadow-executed verdicts against 8bdcbdd8 on 598,678 chain-shaped seeds move 11,990 (2.0 percent) to a different attempt, 0 exhausted, max 32; on 017e7037 the pairing holds and the 64-seed gate at 2^24 plus the 10^6 exhaustion count run to about 16:05Z. The hash lane's ledger lines 2a111fb1 on origin and build (the GitHub 500s cleared on their own), CI run 37642582140 success at 15:25Z; the hash lane has nothing in flight. Nothing on the hash side stops a cut of sub-version 3 at 017e7037 for 0.3.22; then the attack-pass lane's 64-seed gate at 2^24 and 10^6 exhaustion count. Owed as before: G2, G3, the ladder re-measure, AMD (PC 1), the 2019-class core, the fleet and PC 2 G1 on the sub-version 3 packs (a re-run of a known result). release-0.3.21-node STAGED (the node lane, 16:0xZ): 96161037 on the mirror, at the shipper's sweep-end word, in the final order on 55768f88 (c631c64b, 52e96c94, f067f7c1, b0444f51, 437f0438, 2e32d5f6, f95178a1 cherry-picked, a6864e36 and d8bceca5), pairing igneum-pow 8c728ca3 at byte 5, no re-pin. The whole set on the tip green: consensus-core 126, the consensus crate whole (lib 120, both integration targets), igneum-miner 25, kaspa-pow 17, igneum-exec 36, the three checks, 15:49 to 15:57Z on build-2. Binary built on build-1 at 15:58Z, igneumd sha256 f99340b3cf4ce32e, the string read back; the digest eada4bda on the previous live object and 4bbbe816 on the published floor file (so the 0.3.20 floor file is published), as the pin reads them. The node lane's digest and mixed-version gates on it from 16:00Z; the fleet's set from the same minute; plan 6.9 carries the steps and the two box-input rules the staging added. THE 0.3.20 RECORD (the shipper, UTC). Cases: the dc141409 run on c18-1 CASES END rc 0 at 12:37:18Z (both halves PASS); the c4459193 run on c20-1 CASES END rc 0 at 14:54:02Z with its relay half void (one RunPod host, no hairpin) and the poison half PASS (13 rejected, 0 accepted); main's (b): the publish on the dc141409 cases plus the diff argument (the class byte the only touch in the cases' territory) and c19-1's tip-following, with the separate-host relay re-run on c19-1 on the live digest as the halt condition and the Discord card's gate (CASES END about 16:25Z). PUBLISH 14:54:17Z (15:54 BST): manifest 0.3.20, mac DMG sha256 73796c5f..., Windows installer 45b2f3fb..., the hive tar d9dd12df... on the public alias. THE FLOOR FILE: program_class_v4_activation_daa 900,000 (the live DAA 294,073 at 14:51Z; 294,073 + 604,800 = 898,873 rounded up to the 3,600 boundary), window 86,400 unchanged, file sha256 294f1f80...; the digest 4bbbe8162ea9fff277aa5b16b4ffad9e2262ba5e6a5acac7b697ff77211e7328 read on c4459193's binary before the cut and on the hub's handshake line after. The pin c4459193, igneum-pow 8c728ca3, byte 5; the sweep's node pair a80ed39c / 70a5180f (the build-server lane's native build of the same tree). THE SWEEP complete 15:39Z (16:39 BST): wave 1 the hands (observer-node, node1 at 14:57Z), the Mac (15:23Z on its poller, interface 1.0.1), the hub, pool-1 and the eight heaviest voters (15:04 to 15:21Z); wave 2 the four lighter voters (15:23 to 15:26Z); wave 3 Devnet 2's four pods and bps-seed (digest 4a0b8726 unmoved); the first new-side lock 9313 at 15:28:08Z after a 23-minute split; every lock line "sweep complete before 13 October 09:00 UK (the margin; the floor is now DAA 900,000)". 0.3.17 nodes remaining: PC 1 and PC 2 (offline for the project lead's cable work; their apps update on their pollers on return) and the three testnet seeds (on the testnet, not on the devnet's floor); no devnet node of the fleet, the hands or the Mac on 0.3.17. EARLIEST FLIP: the floor at DAA 900,000 is about 7.0 days of DAA from the publish (605,927 DAA at about one a second), so about 14 October 23:00Z (15 October 00:00 BST) at the earliest, and only once the seven 86,400-DAA windows read 95 percent of blue weight signalling byte 5; the live floor of 831,600 (13 Oct 08:00Z) is replaced by the published file on every swept node, so the chain never flips to the 6 October stream. Per tier: a solo miner on the Mac or a swept box mines on; a PC miner on 0.3.17 is refused by digest when it next connects until its app updates (its poller does this on return; nothing by hand); the pool and the hub are on the pin; the auditor reads one object, one digest, one floor. -SUB-VERSION 3 PASSES BOTH GATES (the attack-pass lane, the close line dated 7 October 2026): class v4 sub-version 3, commit 017e70376489251e18564c0abce7e466e606c8b3 on ca3-v4-amend, pairing id a785001687d8688a (verified by the harness). F8's 64-seed census (p2 to p65) at 2^24 nonces each, chain path, window-model control, box 2, 14:51Z to 16:00:20Z: PASS, 60 of 64 under 1.2x (0.9915x to 1.144x); the only four over are the named tail, unattributed and chased: p10 1.5036x (identical to sub-versions 1 and 2, hottest item 0x4004da at 362 reads, no predicted source), p8 1.3776x (0x837de4, 420 reads), p34 1.2505x (0x800010, 541 reads, the one-bit value through sub at 5), p4 1.2167x (0x4000e7, 355 reads); every strong seed gone (p23 4.82x, p19 3.32x, p15 2.57x, p18 2.50x, p56 2.01x all under 1.2x); the worst ratio on the stream 1.50x; the tail's hottest items carry 355 to 541 reads of 2^31 (one to two per 2^22 items above the mean), no chip consequence. The exhaustion gate as it is: the chain-path count on 017e7037 runs slowly (the draw evaluates (c'') at 2^20 per candidate: 20,532 seeds in 59 minutes, 0 exhausted, 0 panics, max attempt 29), so a 10^6 count is two days away and is not waited on; the substance is met by construction (the 256 cap and the last resort unchanged from 8bdcbdd8, 0 of 10^6 there) and by measurement at 017e7037 on 24,631 seeds (0 exhausted, max 29; r about 0.68); the count runs on as a strengthening line. The node's epoch draw now costs about 2 attempts at 2.2 s each, 4 to 5 s per epoch on slower cores, once an hour. The hash lane's ledger at 6941da1d. SUB-VERSION 3 AT 017e7037 IS THE FROZEN GENERATOR FOR 0.3.22 (byte 7), THE TAIL RULED (main, 18:1x UK): the four seeds (p10 1.5036x with its hottest item at 362 reads, p8 1.3776x at 420, p34 1.2505x at 541, p4 1.2167x at 355, of 2^31 reads) are accepted as the window model's unattributed residue at 1.22x to 1.50x with nil chip consequence; the AP-F8-1 row CLOSES with that wording and the hottest-item counts beside it; the 10^6 count on 017e7037 runs on as the strengthening line. The attack-pass plan's start 15 October or the morning after. 0.3.21's FIRST CANDIDATE 96161037 (sha256 f99340b3cf4ce32e, string read back) passed the node lane's two gates on its own binary: the digest gate 15:59:54Z to 16:01:32Z PASS (a89be8a7 with the peers right; db9a85f9 refused, no peer); the mixed-version gate 16:01:53Z to 16:12:05Z PASS (one digest b0afb2ee on five; 252 new and 352 old accepted, 0 rejected; counts equal at 316, 493 and 604 through both clean joins and the restart step; no panic); the node side complete; the fleet's set on the same binary (the kept start with the ids gate, the wipe canary, the cases rerun, the 12 GB line, N15's restarts of p1-5090 and p2-3090-3) is what the shipper's pin word waits on; byte 5 throughout, nothing on the class v4 seam moved. NOTHING ELSE IS OPEN ON THE COUNTER ASIC LANE TONIGHT (main's word): the record lands and the lane stops. 0.3.21's STAGING (the node lane): the order dry-merges onto 55768f88 with nothing moving to 0.3.22; the late-join fix is 52e96c94 (70e4601e rebased onto 55768f88, exec suite 33 green with both new tests); f067f7c1, b0444f51 and 437f0438 merge clean in order; 2e32d5f6's one conflict (DST_ADDRESS beside pool-finish's DST_BINDING in consensus/core/src/finality.rs) kept both; the live-file digest eada4bda after each (every switch at never); the staging waits on the shipper's sweep-end word; the re-pin held. PC 2 DOWN AGAIN (main, 16:5x UK): the project lead takes PC 2 down for cable work (PC 1 back but his desk); both PCs out of the sweep's waves, each updates on its poller on return; no PC job to PC 1; the Windows G1 completed before the outage, nothing reruns. 0.3.21's SECOND GATE LINE on 55768f88 (sha256 279b1b690e854fc9): the ten-minute mixed-version gate beside the 5899f603 pair, 13:37:40Z to 13:47:52Z, SUMMARY PASS (one digest b0afb2ee on five nodes; 223 new and 381 old blocks accepted by the old hub, 0 rejected; counts equal at 319, 486 and 604 through both clean joins and the restart step at 13:45:22Z; no panic); the node lane's two lines on 0.3.21's first candidate complete, in plan 6.9 on ca3-v4-node; the fleet's set on it (the bare-child 12 GB line, the wipe, the kept read, the cases) is the fleet's. 0.3.21's FIRST GATE LINE on 55768f88 (sha256 279b1b690e854fc9, the string read back; pairing igneum-pow 8c728ca3 at byte 5): the digest gate 13:35:41Z to 13:37:19Z SUMMARY PASS (a89be8a7 on both binaries with the peers; db9a85f9 refused, no peer; the live file's eada4bda unmoved); the ten-minute mixed-version gate from 13:37:40Z, line about 13:50Z. The 0.3.21 order as the shipper sent it: 55768f88; f067f7c1 and 70e4601e; b0444f51; 6eb21fc9; db28d331; then the re-pin from 8bdcbdd8 on the coordinator's word; suites between, the digest read after every one; the mirror's release-0.3.20-node back at the pin c4459193, release-0.3.21-node open at 55768f88. THE LATE-JOIN COMMIT (N9's second half, the node lane): 70e4601e on the box mirror as branch proof-hold-fix, from c4459193, two files (igneum/exec/src/proving.rs, protocol/flows/src/v10/proving.rs); the gap was the fetch side on the joiner (the served record ran the native check against the joiner's trailing exec state before anything was stored, the check refused it, the proof was never held, the body rule read "not held" for 20 s and failed the IBD); the fix holds the proof by hash before the checks (the pool entry still needs them) and the serve side says when it holds fewer than asked; the exec suite 32 passed at 13:26Z with the known-failed shape first, the flows check green 13:28Z, igneumd on build-1 at the 0321 worktree path built 13:32Z, sha256 17649eeb2f7d1290, string read back; with the testnet lane (the resume form, B alone); it joins the 0.3.21 staging as its own commit. THE WIPE CANARY ON c19-1, c4459193 (sha 45be9b02d1b002f5, string read back): FORM END rc 0 at 13:50:53Z. Wipe synced 13:35:50Z (57 minutes, inside the 98-minute class); mining 13:36:00Z to 13:47:07Z, 66 mined, 66 accepted, 0 rejected, isSynced true at the tip throughout; the hub holds 41 of its blocks in its last 700 with 0 rejects (13:47:09Z); the restart on its kept datadir at 13:47:15Z: the old process stopped at once (the new process's first lock line seven seconds after the marker; the watchdog held nothing, the b7cc37e7 fault closed), synced again at 13:48:39Z after 84 s, 109 templates read with max 3,432 ms and 0 timeouts; the kept read on pool-1's 0.3.17 copy on the same pod passed at 13:38Z (the rewrite line once, a clean second start). The pin's set on c4459193: the digest gate PASS, the mixed-version gate PASS, the wipe canary PASS, the kept read PASS, the restart PASS, the 12 GB line proves and verifies (paid is a race, not a gate); CASES END from c20-1 (about 14:50Z) is the last pin line. THE INTEROP FACT stands from the void run: the 5899f603 hub accepted 235 object-byte-5 blocks from the 8097d600 node with 0 rejected, one digest on all five nodes on the live sixteen-field file. The gates: the digest test and the kaspa-pow vector test (the amended devnet epoch-0 id 1a4230699a6b9c60 must equal, c120d7963abdcd96 must differ, the v3 control unchanged) on the box; the mixed-version Devnet 2 gate (the amended 0.3.20 node beside a 5899f603 node for ten minutes on the live file without the v4 fields) after the Mac build; the fresh-join canary the 0.3.20 cut's | +SUB-VERSION 3 PASSES BOTH GATES (the attack-pass lane, the close line dated 7 October 2026): class v4 sub-version 3, commit 017e70376489251e18564c0abce7e466e606c8b3 on ca3-v4-amend, pairing id a785001687d8688a (verified by the harness). F8's 64-seed census (p2 to p65) at 2^24 nonces each, chain path, window-model control, box 2, 14:51Z to 16:00:20Z: PASS, 60 of 64 under 1.2x (0.9915x to 1.144x); the only four over are the named tail, unattributed and chased: p10 1.5036x (identical to sub-versions 1 and 2, hottest item 0x4004da at 362 reads, no predicted source), p8 1.3776x (0x837de4, 420 reads), p34 1.2505x (0x800010, 541 reads, the one-bit value through sub at 5), p4 1.2167x (0x4000e7, 355 reads); every strong seed gone (p23 4.82x, p19 3.32x, p15 2.57x, p18 2.50x, p56 2.01x all under 1.2x); the worst ratio on the stream 1.50x; the tail's hottest items carry 355 to 541 reads of 2^31 (one to two per 2^22 items above the mean), no chip consequence. The exhaustion gate as it is: the chain-path count on 017e7037 runs slowly (the draw evaluates (c'') at 2^20 per candidate: 20,532 seeds in 59 minutes, 0 exhausted, 0 panics, max attempt 29), so a 10^6 count is two days away and is not waited on; the substance is met by construction (the 256 cap and the last resort unchanged from 8bdcbdd8, 0 of 10^6 there) and by measurement at 017e7037 on 24,631 seeds (0 exhausted, max 29; r about 0.68); the count runs on as a strengthening line. The node's epoch draw now costs about 2 attempts at 2.2 s each, 4 to 5 s per epoch on slower cores, once an hour. The hash lane's ledger at 6941da1d. SUB-VERSION 3 AT 017e7037 IS THE FROZEN GENERATOR FOR 0.3.22 (byte 7), THE TAIL RULED (main, 18:1x UK): the four seeds (p10 1.5036x with its hottest item at 362 reads, p8 1.3776x at 420, p34 1.2505x at 541, p4 1.2167x at 355, of 2^31 reads) are accepted as the window model's unattributed residue at 1.22x to 1.50x with nil chip consequence; the AP-F8-1 row CLOSES with that wording and the hottest-item counts beside it; the 10^6 count on 017e7037 runs on as the strengthening line. The attack-pass plan's start 15 October or the morning after. 0.3.21's FIRST CANDIDATE 96161037 (sha256 f99340b3cf4ce32e, string read back) passed the node lane's two gates on its own binary: the digest gate 15:59:54Z to 16:01:32Z PASS (a89be8a7 with the peers right; db9a85f9 refused, no peer); the mixed-version gate 16:01:53Z to 16:12:05Z PASS (one digest b0afb2ee on five; 252 new and 352 old accepted, 0 rejected; counts equal at 316, 493 and 604 through both clean joins and the restart step; no panic); the node side complete; the fleet's set on the same binary (the kept start with the ids gate, the wipe canary, the cases rerun, the 12 GB line, N15's restarts of p1-5090 and p2-3090-3) is what the shipper's pin word waits on; byte 5 throughout, nothing on the class v4 seam moved. DEVNET 3 NOW (the project lead through main, 17:3x BST): 0.3.22 is the Devnet 3 release (a fresh network object igneum-devnet-3 with every activation at 0 and no override file, the era VDF fork, class v4 sub-version 3 at byte 7 from genesis), the node building on build-1, genesis by 17:30Z (18:30 BST). THE HANDOFF (17:3x BST, inside the 17:40 BST line, to the shipper and the node lane): igneum-pow 017e70376489251e18564c0abce7e466e606c8b3 (the audit-freeze-2026-10-07 tag; 2a111fb1 and 6941da1d above it docs only), object byte 7, PROGRAM_SUBVERSION_V4 = 3, devnet epoch-0 program id a785001687d8688a (must-differ c120d7963abdcd96, 1a4230699a6b9c60, a788661687db4bb3), kit packs-ca3-v4-sub3 zip sha256 4f2445c50c58d76a5544023492d8b858d0b07c5e372d31f9c90c4ce51f829154, the eight fingerprints as recorded (equal on Metal, Apple OpenCL, the fleet 5090 and PC 2's 5090), both attack-pass gates GREEN, suite 103 of 103, CI success; the open items stated as open: the four-seed tail under main's ruling (the attribution for 0.3.23), the 10^6 count as a strengthening line, the 4 to 5 s epoch draw, the owed measurements that do not gate Devnet 3. NO FURTHER HASH CHANGE RIDES ON 0.3.22; anything from the tail goes to 0.3.23. The node lane stages the re-pin on the 0.3.21 tip as its own commit (the fork commit and the kaspa-pow suite line owed to this record); the hash lane's sub-version 3 pairing follows it. THE PUBLIC CHIP TEXT REWRITTEN LAUNCH-FIRST (the project lead: "I thought we were making it 2.1 from launch?"; master 9b996d06, docs/plans/counter-asic-3-public-text-2026-10-07.md): the testnet and mainnet objects set program_class_v4_activation_daa 0, so class v4 is live from genesis and the launch number is 2.1x to 3.9x on day one; the three texts and evidence row 17 lead with that (labels kept), then class v5, then the 5x to 9x only as the class v3 baseline the work started from, the devnet's activation height a devnet fact only; no em dashes, no prize mention, eight columns; with the site lane to apply in the same deploy as the padding sweep (its commit and deploy time owed to this record). The lane carries on until the re-pin line, the site commit and the genesis record land. 0.3.21's STAGING (the node lane): the order dry-merges onto 55768f88 with nothing moving to 0.3.22; the late-join fix is 52e96c94 (70e4601e rebased onto 55768f88, exec suite 33 green with both new tests); f067f7c1, b0444f51 and 437f0438 merge clean in order; 2e32d5f6's one conflict (DST_ADDRESS beside pool-finish's DST_BINDING in consensus/core/src/finality.rs) kept both; the live-file digest eada4bda after each (every switch at never); the staging waits on the shipper's sweep-end word; the re-pin held. PC 2 DOWN AGAIN (main, 16:5x UK): the project lead takes PC 2 down for cable work (PC 1 back but his desk); both PCs out of the sweep's waves, each updates on its poller on return; no PC job to PC 1; the Windows G1 completed before the outage, nothing reruns. 0.3.21's SECOND GATE LINE on 55768f88 (sha256 279b1b690e854fc9): the ten-minute mixed-version gate beside the 5899f603 pair, 13:37:40Z to 13:47:52Z, SUMMARY PASS (one digest b0afb2ee on five nodes; 223 new and 381 old blocks accepted by the old hub, 0 rejected; counts equal at 319, 486 and 604 through both clean joins and the restart step at 13:45:22Z; no panic); the node lane's two lines on 0.3.21's first candidate complete, in plan 6.9 on ca3-v4-node; the fleet's set on it (the bare-child 12 GB line, the wipe, the kept read, the cases) is the fleet's. 0.3.21's FIRST GATE LINE on 55768f88 (sha256 279b1b690e854fc9, the string read back; pairing igneum-pow 8c728ca3 at byte 5): the digest gate 13:35:41Z to 13:37:19Z SUMMARY PASS (a89be8a7 on both binaries with the peers; db9a85f9 refused, no peer; the live file's eada4bda unmoved); the ten-minute mixed-version gate from 13:37:40Z, line about 13:50Z. The 0.3.21 order as the shipper sent it: 55768f88; f067f7c1 and 70e4601e; b0444f51; 6eb21fc9; db28d331; then the re-pin from 8bdcbdd8 on the coordinator's word; suites between, the digest read after every one; the mirror's release-0.3.20-node back at the pin c4459193, release-0.3.21-node open at 55768f88. THE LATE-JOIN COMMIT (N9's second half, the node lane): 70e4601e on the box mirror as branch proof-hold-fix, from c4459193, two files (igneum/exec/src/proving.rs, protocol/flows/src/v10/proving.rs); the gap was the fetch side on the joiner (the served record ran the native check against the joiner's trailing exec state before anything was stored, the check refused it, the proof was never held, the body rule read "not held" for 20 s and failed the IBD); the fix holds the proof by hash before the checks (the pool entry still needs them) and the serve side says when it holds fewer than asked; the exec suite 32 passed at 13:26Z with the known-failed shape first, the flows check green 13:28Z, igneumd on build-1 at the 0321 worktree path built 13:32Z, sha256 17649eeb2f7d1290, string read back; with the testnet lane (the resume form, B alone); it joins the 0.3.21 staging as its own commit. THE WIPE CANARY ON c19-1, c4459193 (sha 45be9b02d1b002f5, string read back): FORM END rc 0 at 13:50:53Z. Wipe synced 13:35:50Z (57 minutes, inside the 98-minute class); mining 13:36:00Z to 13:47:07Z, 66 mined, 66 accepted, 0 rejected, isSynced true at the tip throughout; the hub holds 41 of its blocks in its last 700 with 0 rejects (13:47:09Z); the restart on its kept datadir at 13:47:15Z: the old process stopped at once (the new process's first lock line seven seconds after the marker; the watchdog held nothing, the b7cc37e7 fault closed), synced again at 13:48:39Z after 84 s, 109 templates read with max 3,432 ms and 0 timeouts; the kept read on pool-1's 0.3.17 copy on the same pod passed at 13:38Z (the rewrite line once, a clean second start). The pin's set on c4459193: the digest gate PASS, the mixed-version gate PASS, the wipe canary PASS, the kept read PASS, the restart PASS, the 12 GB line proves and verifies (paid is a race, not a gate); CASES END from c20-1 (about 14:50Z) is the last pin line. THE INTEROP FACT stands from the void run: the 5899f603 hub accepted 235 object-byte-5 blocks from the 8097d600 node with 0 rejected, one digest on all five nodes on the live sixteen-field file. The gates: the digest test and the kaspa-pow vector test (the amended devnet epoch-0 id 1a4230699a6b9c60 must equal, c120d7963abdcd96 must differ, the v3 control unchanged) on the box; the mixed-version Devnet 2 gate (the amended 0.3.20 node beside a 5899f603 node for ten minutes on the live file without the v4 fields) after the Mac build; the fresh-join canary the 0.3.20 cut's | | Main's rulings (7 October, morning) | no generator change to v4 on the live devnet; the record's null is the window model with numbers, sent by the hash lane to the attack-pass lane so AP-F8-1 re-gates against it; a fault beyond the model (a low-entropy source at site 15) stops at the coordinator with the two options priced (a 0.3.19 class amendment before the flip, or the flip held at the floor), nothing shipping without the project lead's word; the tighter tail, an acceptance bound on the hot-set share, is a CLASS V5 item (sent to the v5 lane a6410f3b8abefb762 with the 64-seed census as its gate; the bound's number follows from the model) | ### AP-F4-1, the weak-day MUL draw (the attack-pass lane, 7 October, morning): PASS against v4, a class v5 rule diff --git a/infra/build-server/devnet3/devnet3.sh b/infra/build-server/devnet3/devnet3.sh new file mode 100755 index 000000000..d8a97319e --- /dev/null +++ b/infra/build-server/devnet3/devnet3.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Devnet 3 (0.3.22) on igneum-build-1: the seed as a bare process and the hands' second instances. Dry run by default, --go executes. +# infra/build-server/devnet3/devnet3.sh seed [--go] start the Devnet 3 seed (empty datadir, NET_FLAGS, the seed ports), read back +# infra/build-server/devnet3/devnet3.sh node1 [--go] a second node1 instance on Devnet 3 (own datadir and ports), read back +# infra/build-server/devnet3/devnet3.sh observer-node [--go] the same for the observer node (p2p on loopback, peers node1-dn3) +# infra/build-server/devnet3/devnet3.sh status pids, ports, digest and genesis lines of the three +# Read-back per instance: the first log line (igneumd/-), the commit string count in the running binary, the +# "Consensus params digest" line (the devnet-3 digest), the "[igneum-exec] genesis executed" line, the GRPC and P2P server +# lines. No unit yet: the processes are detached (setsid nohup) under the build user like the Devnet 2 seed; units follow once the +# network is green. The old devnet's node1 and observer-node are not touched. +set -euo pipefail +HERE=$(cd "$(dirname "$0")" && pwd); . "$HERE/devnet3.env" +KEY="${IGNEUM_BUILD_KEY:-$HOME/.ssh/igneum_ed25519}"; HOST=$(head -1 "${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" | tr -d '[:space:]') +SSH=(ssh -i "$KEY" -o BatchMode=yes -o ConnectTimeout=15 "$HOST") +say() { echo "$(TZ=Europe/London date '+%H:%M:%S %Z') devnet3: $*" >&2; } +mode="${1:-}"; shift || true; GO=0; [ "${1:-}" = --go ] && GO=1 +case "$NET_FLAGS" in *suffix=3*) say "NET_FLAGS is still the placeholder ($NET_FLAGS): the node lane's flag goes into devnet3.env first" ;; esac +case "$IGNEUMD" in *PLACEHOLDER*) [ "$mode" = status ] || { say "IGNEUMD is the placeholder: the 0.3.22 candidate's build fills it"; [ "$GO" = 0 ] || exit 1; } ;; esac +start_one() { # [extra args...] + local name="$1" appdir="$2" p2p="$3" rpc="$4" json="$5" evm="$6" log="$7"; shift 7 + local args="$NET_FLAGS --appdir=$appdir --rpclisten=127.0.0.1:$rpc --rpclisten-json=127.0.0.1:$json --evm-rpclisten=127.0.0.1:$evm --listen=$p2p --nodnsseed --disable-upnp --nologfiles --yes $*" + if [ "$GO" = 0 ]; then say "DRY $name: $IGNEUMD $args > $log"; return 0; fi + "${SSH[@]}" bash -s -- "$name" "$IGNEUMD" "$appdir" "$log" "$args" <<'REMOTE' +set -euo pipefail; name="$1"; bin="$2"; appdir="$3"; log="$4"; args="$5" +[ -x "$bin" ] || { echo "no binary $bin"; exit 1; } +mkdir -p "$appdir"; [ -z "$(ls -A "$appdir")" ] || echo "note: $appdir is not empty" +cd "$(dirname "$log")"; setsid nohup "$bin" $args > "$log" 2>&1 < /dev/null & pid=$!; sleep 8 +echo "$name pid $pid alive=$(kill -0 $pid 2>/dev/null && echo yes || echo NO) commit-strings=$(grep -a -c "$(echo "$bin" | grep -oE '[0-9a-f]{8}' | tail -1)" /proc/$pid/exe 2>/dev/null || echo ?)" +head -1 "$log" | cut -c1-120; grep -oE "Consensus params digest: [0-9a-f]+" "$log" | head -1 +for i in $(seq 1 20); do grep -qE "genesis [0-9a-f]{8,} executed|P2P Server starting" "$log" && break; sleep 1; done +grep -oE "genesis [0-9a-f]+ executed" "$log" | head -1; grep -E "GRPC Server starting|P2P Server starting" "$log" | head -2 | cut -c1-100 +grep -iE "panic|error" "$log" | head -2 | cut -c1-140 || true +REMOTE +} +case "$mode" in + seed) start_one dn3-seed "$DN3_SEED_APPDIR" "0.0.0.0:$DN3_SEED_P2P" "$DN3_SEED_RPC" "$DN3_SEED_JSON" "$DN3_SEED_EVM" "$DN3_SEED_LOG" --maxinpeers=128 --outpeers=8 ;; + node1) start_one node1-dn3 "$DN3_NODE1_APPDIR" "0.0.0.0:$DN3_NODE1_P2P" "$DN3_NODE1_RPC" "$DN3_NODE1_JSON" "$DN3_NODE1_EVM" /srv/hands/node1-dn3.log --enable-unsynced-mining --addpeer=127.0.0.1:$DN3_SEED_P2P --maxinpeers=128 --outpeers=8 ;; + observer-node) start_one observer-dn3 "$DN3_OBS_APPDIR" "127.0.0.1:$DN3_OBS_P2P" "$DN3_OBS_RPC" "$DN3_OBS_JSON" "$DN3_OBS_EVM" /srv/hands/observer-dn3.log --addpeer=127.0.0.1:$DN3_NODE1_P2P --addpeer=127.0.0.1:$DN3_SEED_P2P ;; + status) "${SSH[@]}" "for l in $DN3_SEED_LOG /srv/hands/node1-dn3.log /srv/hands/observer-dn3.log; do [ -f \$l ] && { echo \"== \$l\"; head -1 \$l | cut -c1-100; grep -oE 'Consensus params digest: [0-9a-f]+|genesis [0-9a-f]+ executed' \$l | head -2; tail -1 \$l | cut -c1-120; }; done; ss -ltn | awk '{print \$4}' | grep -E ':(26631|26651|26661|27630|26650|26660)\$' | tr '\n' ' '; echo" ;; + *) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; +esac diff --git a/infra/build-server/provision.sh b/infra/build-server/provision.sh index 37458e9ed..a40c9b948 100755 --- a/infra/build-server/provision.sh +++ b/infra/build-server/provision.sh @@ -55,7 +55,7 @@ SCCACHE_VERSION="${SCCACHE_VERSION:-}" NODE_MAJOR="${NODE_MAJOR:-22}" WORKTREES="${WORKTREES:-}" SLOTS_GIVEN="${SLOTS:-}"; SLOTS="${SLOTS:-2}" # 2 since main's ruling of 6 October 2026 (20:3x UK); remote-run.sh gives 90 jobs alone, 45 beside another -P2P_PORTS="${P2P_PORTS:-26611 26811 26621}" # 26621: the Devnet 2 seed on build-1 (blocked by ufw until 7 Oct 2026 16:40 BST; the fleet lane found it closed from outside) +P2P_PORTS="${P2P_PORTS:-26611 26811 26621 26631 26651}" # 26631 the Devnet 3 seed, 26651 the hands' Devnet 3 node1 (0.3.22, placeholders until the object lands) # 26621: the Devnet 2 seed on build-1 (blocked by ufw until 7 Oct 2026 16:40 BST; the fleet lane found it closed from outside) BOX_HOSTNAME="${BOX_HOSTNAME:-igneum-build-1}" case "$BOX_HOSTNAME" in *-2|*-4) [ -n "$SLOTS_GIVEN" ] || SLOTS=3 ;; esac # build-2 and build-4 (AX162-1, 96 threads): three slots (main, 7 Oct 2026; the bounded runs take one 32-core band each) WORKERS_HOST="${WORKERS_HOST:-build.igneum.network}" # the dashboard feed's HTTPS name (A record in deSEC, 6 Oct 2026) diff --git a/site/lib/money-regen.mjs b/site/lib/money-regen.mjs new file mode 100644 index 000000000..eb12337f5 --- /dev/null +++ b/site/lib/money-regen.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +// Rewrites the four tables in site/money.js from app/igneum-app/ui/app.js (View.REGIONS, ISO_CURRENCY, EU_MEMBERS, +// CURRENCY_CODES). Run after a change to the app's tables, then commit both; site/lib/money.test.mjs fails on drift. +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +const here = dirname(fileURLToPath(import.meta.url)); +const m = { exports: {} }; new Function('module', readFileSync(join(here, '../../app/igneum-app/ui/app.js'), 'utf8'))(m); +const V = m.exports.View, p = join(here, '../money.js'); +const regions = V.REGIONS.map(r => ({ code: r.code, name: r.name, cur: r.cur, price: r.price, source: r.source || '', restricted: !!r.restricted })); +let s = readFileSync(p, 'utf8'); +s = s.replace(/ M\.REGIONS = .*\n/, ' M.REGIONS = ' + JSON.stringify(regions) + ';\n'); +s = s.replace(/ M\.ISO_CURRENCY = .*\n/, ' M.ISO_CURRENCY = ' + JSON.stringify(V.ISO_CURRENCY) + ';\n'); +s = s.replace(/ M\.EU_MEMBERS = .*\n/, ' M.EU_MEMBERS = ' + JSON.stringify(V.EU_MEMBERS) + ';\n'); +s = s.replace(/ M\.CURRENCY_CODES = .*\n/, ' M.CURRENCY_CODES = ' + JSON.stringify(V.CURRENCY_CODES) + ';\n'); +writeFileSync(p, s); console.log('site/money.js tables regenerated'); diff --git a/site/lib/money.test.mjs b/site/lib/money.test.mjs new file mode 100644 index 000000000..6a42e2b8a --- /dev/null +++ b/site/lib/money.test.mjs @@ -0,0 +1,48 @@ +// node --test site/lib/money.test.mjs (the pre-push gate runs it beside the explorer and emission tests) +// site/money.js is the app's currency rules for the site: its tables must equal the app's, its formatter must place the +// symbol as the locale does, and the picker's choice must follow navigator.language with a saved override winning. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +const here = dirname(fileURLToPath(import.meta.url)); +const load = (p) => { const mod = { exports: {} }; new Function('module', 'window', readFileSync(p, 'utf8'))(mod, undefined); return mod.exports; }; +const M = load(join(here, '../money.js')); +const V = load(join(here, '../../app/igneum-app/ui/app.js')).View; + +test('the site tables equal the app tables (regenerate with node site/lib/money-regen.mjs)', (t) => { + // the app side rides release-0.3.21 first; on a tree whose app has no ISO_CURRENCY yet the drift check waits for it + if (!V.ISO_CURRENCY) { t.skip('the app on this tree carries no currency tables yet (currency-21 app side not merged here)'); return; } + assert.deepEqual(M.REGIONS, V.REGIONS.map(r => ({ code: r.code, name: r.name, cur: r.cur, price: r.price, source: r.source || '', restricted: !!r.restricted }))); + assert.deepEqual(M.ISO_CURRENCY, V.ISO_CURRENCY); assert.deepEqual(M.EU_MEMBERS, V.EU_MEMBERS); assert.deepEqual(M.CURRENCY_CODES, V.CURRENCY_CODES); + for (const c of ['GBP', 'EUR', 'JPY', 'CHF']) assert.deepEqual(M.minorOf(c), V.minorOf(c), c); +}); + +test('the picker follows navigator.language, a saved choice wins, no row falls back to USD and asks', () => { + assert.equal(M.currencyChoice('', 'de-DE').code, 'EUR'); assert.equal(M.currencyChoice('', 'en-GB').code, 'GBP'); assert.equal(M.currencyChoice('', 'fr-CA').code, 'CAD'); assert.equal(M.currencyChoice('', 'en-AU').code, 'AUD'); + assert.equal(M.currencyChoice('USD', 'de-DE').code, 'USD'); assert.equal(M.currencyChoice('USD', 'de-DE').source, 'override'); + const eo = M.currencyChoice('', 'eo'); assert.equal(eo.code, 'USD'); assert.equal(eo.prompt, true); + assert.equal(M.currencyChoice('zzz', 'eo').prompt, true, 'an unknown saved code is ignored'); +}); + +test('the default tariff is the region row, else the first table row in the currency, and the cost line uses one formatter', () => { + const de = M.tariffFor(M.currencyChoice('', 'de-DE')); assert.equal(de.price, 38.69); assert.match(de.source, /Eurostat/); assert.equal(de.region, 'Germany'); + const gbp = M.tariffFor(M.currencyChoice('GBP', 'de-DE')); assert.equal(gbp.price, 26.32); assert.equal(gbp.region, 'United Kingdom'); + const fr = M.tariffFor(M.currencyChoice('', 'fr-FR')); assert.equal(fr.price, 28.96, 'an EU member on the euro takes the EU average'); + assert.equal(M.tariffFor(M.currencyChoice('', 'sv-SE')), null, 'no table tariff in kronor: the picker says so'); + assert.equal(M.fmtMoney(M.costPerDay(227, 38.69), 'EUR', 'de-DE'), '2,11 €'); + assert.equal(M.fmtMoney(M.costPerDay(227, 26.32), 'GBP', 'en-GB'), '£1.43'); + assert.equal(M.fmtMoney(M.costPerDay(227, 17.7), 'USD', 'en-US'), '$0.96'); + assert.equal(M.costPerDay(null, 26.32), null, 'no watts, no cost'); + assert.equal(M.fmtMoney(1234.5, 'USD', 'en-US'), '$1,235'); assert.equal(M.fmtMoney(1.5, 'JPY', 'en-US'), '¥2'); +}); + +test('yourcard.js loads money.js and shows the electricity line with the currency switch; no hand-built money string', () => { + const yc = readFileSync(join(here, '../yourcard.js'), 'utf8'); + assert.ok(yc.includes("'/money.js'"), 'loads the module when the page did not'); + assert.ok(yc.includes('window.IgneumMoney') && yc.includes('.fmtMoney(') && yc.includes('.currencyChoice(') && yc.includes('.tariffFor('), 'the module is the only money path'); + assert.ok(yc.includes('igneum-currency'), 'the saved choice'); + assert.ok(yc.includes('class="cur"'), 'the switch'); + assert.equal(/'£'|"£"/.test(yc), false); +}); diff --git a/site/money.js b/site/money.js new file mode 100644 index 000000000..82bdf4f90 --- /dev/null +++ b/site/money.js @@ -0,0 +1,61 @@ +/* Igneum money (currency-21, 7 October 2026): the site's copy of the miner app's currency rules, for the card picker + (yourcard.js) and any page that shows a money figure. No IP lookup: navigator.language's region picks the currency and + the default tariff; a saved choice (localStorage igneum-currency) wins; every figure goes through one Intl.NumberFormat + call with the currency code and the narrow symbol, placed as the locale places it; the user's own tariff is the + number, so no exchange rate exists anywhere. The tables below are generated from app/igneum-app/ui/app.js (View.REGIONS, + View.ISO_CURRENCY, View.EU_MEMBERS, View.CURRENCY_CODES); site/lib/money.test.mjs fails when they drift, and + `node site/lib/money-regen.mjs` rewrites them. A classic script (window.IgneumMoney) so yourcard.js can load it on + pages that carry no module tag; under node it also exports through module.exports for the test. */ +(function (root) { + var M = {}; + M.REGIONS = [{"code":"gb","name":"United Kingdom","cur":"GBP","price":26.32,"source":"Ofgem cap, 1 October to 31 December 2026","restricted":false},{"code":"ie","name":"Ireland","cur":"EUR","price":40.42,"source":"Eurostat, second half of 2025","restricted":false},{"code":"de","name":"Germany","cur":"EUR","price":38.69,"source":"Eurostat, second half of 2025","restricted":false},{"code":"be","name":"Belgium","cur":"EUR","price":34.99,"source":"Eurostat, second half of 2025","restricted":false},{"code":"eu","name":"European Union, other","cur":"EUR","price":28.96,"source":"Eurostat, second half of 2025, the EU average","restricted":false},{"code":"hu","name":"Hungary","cur":"EUR","price":10.82,"source":"Eurostat, second half of 2025, in euro cents","restricted":false},{"code":"mt","name":"Malta","cur":"EUR","price":12.82,"source":"Eurostat, second half of 2025","restricted":false},{"code":"bg","name":"Bulgaria","cur":"EUR","price":13.55,"source":"Eurostat, second half of 2025, in euro cents","restricted":false},{"code":"us","name":"United States","cur":"USD","price":17.7,"source":"EIA average, second half of 2025","restricted":false},{"code":"kz","name":"Kazakhstan","cur":"USD","price":4,"source":"about 4 c, a secondary source, approximate","restricted":false},{"code":"py","name":"Paraguay","cur":"USD","price":4.4,"source":"ANDE crypto tariff, 4.4 to 6 c; every crypto contract ends 31 December 2027","restricted":false},{"code":"ir","name":"Iran","cur":"USD","price":1,"source":"licensed mining tariff, about 1 c, a secondary source, approximate","restricted":false},{"code":"et","name":"Ethiopia","cur":"USD","price":2,"source":"2 to 5.3 c, a secondary source","restricted":false},{"code":"ng","name":"Nigeria","cur":"USD","price":4.8,"source":"hosted, 4.8 c, a secondary source","restricted":false},{"code":"ru-mow","name":"Russia, Moscow region","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru-irk","name":"Russia, Irkutsk","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru-bur","name":"Russia, Buryatia","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru-zab","name":"Russia, Zabaikalsky","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru","name":"Russia, other region","cur":"USD","price":0,"source":"","restricted":true},{"code":"cn","name":"China","cur":"USD","price":0,"source":"","restricted":true},{"code":"other","name":"Somewhere else","cur":"USD","price":0,"source":"","restricted":false}]; + M.ISO_CURRENCY = {"at":"EUR","be":"EUR","cy":"EUR","de":"EUR","ee":"EUR","es":"EUR","fi":"EUR","fr":"EUR","gr":"EUR","hr":"EUR","ie":"EUR","it":"EUR","lt":"EUR","lu":"EUR","lv":"EUR","mt":"EUR","nl":"EUR","pt":"EUR","si":"EUR","sk":"EUR","gb":"GBP","us":"USD","ca":"CAD","au":"AUD","nz":"NZD","ch":"CHF","li":"CHF","se":"SEK","no":"NOK","dk":"DKK","is":"ISK","pl":"PLN","cz":"CZK","hu":"HUF","ro":"RON","bg":"BGN","rs":"RSD","tr":"TRY","ua":"UAH","ge":"GEL","kz":"KZT","ru":"RUB","jp":"JPY","kr":"KRW","cn":"CNY","hk":"HKD","tw":"TWD","sg":"SGD","my":"MYR","th":"THB","vn":"VND","ph":"PHP","id":"IDR","in":"INR","pk":"PKR","bd":"BDT","ae":"AED","sa":"SAR","il":"ILS","ir":"IRR","za":"ZAR","ng":"NGN","ke":"KES","eg":"EGP","ma":"MAD","et":"ETB","br":"BRL","mx":"MXN","ar":"ARS","cl":"CLP","co":"COP","pe":"PEN","py":"PYG"}; + M.EU_MEMBERS = ["at","be","bg","hr","cy","cz","dk","ee","fi","fr","de","gr","hu","ie","it","lv","lt","lu","mt","nl","pl","pt","ro","sk","si","es","se"]; + M.CURRENCY_CODES = ["GBP","EUR","USD","CAD","AUD","AED","ARS","BDT","BGN","BRL","CHF","CLP","CNY","COP","CZK","DKK","EGP","ETB","GEL","HKD","HUF","IDR","ILS","INR","IRR","ISK","JPY","KES","KRW","KZT","MAD","MXN","MYR","NGN","NOK","NZD","PEN","PHP","PKR","PLN","PYG","RON","RSD","RUB","SAR","SEK","SGD","THB","TRY","TWD","UAH","VND","ZAR"]; + var MINOR = { GBP: ['pence', 'p'], EUR: ['euro cents', 'c'], USD: ['cents', 'c'], CAD: ['cents', 'c'], AUD: ['cents', 'c'], NZD: ['cents', 'c'], CHF: ['rappen', 'rp'], SEK: ['öre', 'öre'], NOK: ['øre', 'øre'], DKK: ['øre', 'øre'], PLN: ['grosze', 'gr'], CZK: ['haléře', 'h'], INR: ['paise', 'p'], BRL: ['centavos', 'c'], MXN: ['centavos', 'c'], ZAR: ['cents', 'c'], JPY: ['yen', '¥'], KRW: ['won', '₩'] }; + M.minorOf = function (cur) { + cur = String(cur || '').toUpperCase(); var digits = 2; + try { digits = new Intl.NumberFormat('en', { style: 'currency', currency: cur }).resolvedOptions().maximumFractionDigits; } catch (e) { } + var mm = MINOR[cur] || (digits === 0 ? [cur.toLowerCase(), cur] : ['cents', 'c']); + return { word: mm[0], abbr: mm[1], digits: digits }; + }; + M.fmtMoney = function (v, cur, loc) { + if (v === null || v === undefined || isNaN(v)) return ''; + var d = v >= 100 ? 0 : M.minorOf(cur).digits; + try { return new Intl.NumberFormat(loc || 'en-GB', { style: 'currency', currency: cur, currencyDisplay: 'narrowSymbol', minimumFractionDigits: d, maximumFractionDigits: d }).format(v).replace(/[\u00a0\u202f]/g, ' '); } + catch (e) { return cur + ' ' + (d ? v.toFixed(d) : String(Math.round(v))); } + }; + M.regionFromLocale = function (lang) { + lang = String(lang || '').trim(); if (!lang) return ''; var reg = ''; + try { var l = new Intl.Locale(lang); reg = l.region || (l.maximize ? l.maximize().region : '') || ''; } catch (e) { var mm = /^[A-Za-z]{2,3}[-_]([A-Za-z]{2})\b/.exec(lang); reg = mm ? mm[1] : ''; } + return /^[A-Za-z]{2}$/.test(reg) ? reg.toLowerCase() : ''; + }; + var synth = {}; + function regionName(code) { try { var n = new Intl.DisplayNames(['en'], { type: 'region' }).of(code.toUpperCase()); if (n && n !== code.toUpperCase()) return n; } catch (e) { } return code.toUpperCase(); } + M.regionOf = function (code) { + for (var i = 0; i < M.REGIONS.length; i++) if (M.REGIONS[i].code === code) return M.REGIONS[i]; + if (!/^[a-z]{2}$/.test(code || '')) return null; + if (synth[code]) return synth[code]; + var cur = M.ISO_CURRENCY[code] || '', eu = M.EU_MEMBERS.indexOf(code) >= 0 && cur === 'EUR', avg = M.regionOf('eu'); + return (synth[code] = { code: code, name: regionName(code), cur: cur || 'USD', unknown: !cur, price: eu && avg ? avg.price : 0, source: eu && avg ? avg.source : '' }); + }; + // the currency in force: a saved choice, else the locale's region, else USD with the prompt + M.currencyChoice = function (saved, lang) { + var ov = String(saved || '').toUpperCase(), loc = M.regionFromLocale(lang); + if (ov && M.CURRENCY_CODES.indexOf(ov) >= 0) return { code: ov, source: 'override', region: loc, prompt: false }; + var r = loc ? M.regionOf(loc) : null; + if (r && !r.unknown) return { code: r.cur, source: 'locale', region: loc, prompt: false }; + return { code: 'USD', source: 'fallback', region: loc, prompt: true }; + }; + // the default tariff for a choice: the locale's region row when it carries the currency, else the first table row in that currency + M.tariffFor = function (choice) { + var r = choice.region ? M.regionOf(choice.region) : null; + if (r && r.cur === choice.code && r.price > 0) return { price: r.price, source: r.source, region: r.name, cur: choice.code }; + for (var i = 0; i < M.REGIONS.length; i++) if (M.REGIONS[i].cur === choice.code && M.REGIONS[i].price > 0 && !M.REGIONS[i].restricted) return { price: M.REGIONS[i].price, source: M.REGIONS[i].source, region: M.REGIONS[i].name, cur: choice.code }; + return null; + }; + // watts over a day at a tariff in hundredths of the unit per kWh: the unit amount + M.costPerDay = function (watts, pricePerKwh) { return watts > 0 && pricePerKwh > 0 ? watts * 24 / 1000 * pricePerKwh / 100 : null; }; + root.IgneumMoney = M; + if (typeof module === 'object' && module && module.exports) module.exports = M; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/site/yourcard.js b/site/yourcard.js index 4ad14adb7..5fb676d12 100644 --- a/site/yourcard.js +++ b/site/yourcard.js @@ -1,21 +1,42 @@ -/* The "your card" line: a measured card, and how often it finds a block at the network's hash rate right now. One block a - second is the target, so the wait is network hashes per second over the card's rate. The rates are the bench table's - measured numbers (site/miners.html); nothing here is a promise. Reads /api/live every 15 s. */ +/* The "your card" line: a measured card, how often it finds a block at the network's hash rate right now, and what its + electricity costs a day in your currency. One block a second is the target, so the wait is network hashes per second + over the card's rate. The rates are the bench table's measured numbers (site/miners.html); the watts are the tuned + draws from the same runs (app.html's Ember table: 5090 226.8 W, 4070 75.6 W; the 9070 XT 202 W from docs/bench-log.md, + 6 October 2026; Apple silicon reports no draw). The currency follows navigator.language (no IP lookup); the switch saves + a choice in localStorage igneum-currency; the tariff is the public table's default for that region or currency, so the + cost is arithmetic from a typical price, never a promise (currency-21, 7 October 2026). Reads /api/live every 15 s. */ (function () { - var CARDS = [['NVIDIA RTX 5090', 127.7], ['NVIDIA RTX 5060 Ti', 30.9], ['NVIDIA RTX 4070', 28.8], ['Apple M5 Max', 26.7], ['AMD RX 9070 XT', 17.8]]; + var CARDS = [['NVIDIA RTX 5090', 127.7, 226.8], ['NVIDIA RTX 4070', 28.8, 75.6], ['Apple M5 Max', 26.7, null], ['AMD RX 9070 XT', 17.8, 202]]; var els = document.querySelectorAll('[data-yourcard]'); if (!els.length) return; var net = null, chosen = (function () { try { return localStorage.getItem('igneum-card'); } catch (e) { return null; } })(); + var savedCur = (function () { try { return localStorage.getItem('igneum-currency') || ''; } catch (e) { return ''; } })(); function dur(sec) { if (!(sec > 0)) return 'pending'; if (sec < 90) return Math.round(sec) + ' s'; if (sec < 5400) return Math.round(sec / 60) + ' min'; if (sec < 172800) { var h = Math.floor(sec / 3600), m = Math.round((sec % 3600) / 60); return h + ' h' + (m ? ' ' + m + ' min' : ''); } return Math.round(sec / 86400) + ' days'; } - function render() { els.forEach(function (el) { var r = parseFloat(el.querySelector('select').value) * 1e6, eta = el.querySelector('.eta'); eta.textContent = net && r ? 'every ' + dur(net / r) : 'pending'; }); } - // the vendor badge beside the picker: the five mini marks the build stamped as JSON (site/lib/marks.mjs), chosen by the card's name - var marks = {}; try { var mj = document.getElementById('gpu-marks'); if (mj) marks = JSON.parse(mj.textContent); } catch (e) { marks = {}; } - function vendorOf(n) { return /nvidia|geforce|\bRTX\b|\bGTX\b/i.test(n) ? 'nvidia' : /\bamd\b|radeon/i.test(n) ? 'amd' : /intel|\barc\b/i.test(n) ? 'intel' : /apple|\bM\d (max|pro|ultra)\b/i.test(n) ? 'apple' : 'gpu'; } - function badge(el) { var sel = el.querySelector('select'), b = el.querySelector('.badge-slot'); if (!sel || !b) return; var name = sel.options[sel.selectedIndex].text.split(' \u00b7 ')[0]; b.innerHTML = marks[vendorOf(name)] || ''; } - els.forEach(function (el) { - el.innerHTML = 'Your cardfinds a block about pending at the network\u2019s hash rate right now.'; - el.querySelector('select').addEventListener('change', function (e) { var name = e.target.options[e.target.selectedIndex].text.split(' \u00b7 ')[0]; try { localStorage.setItem('igneum-card', name); } catch (x) {} document.querySelectorAll('[data-yourcard] select').forEach(function (s) { s.value = e.target.value; }); els.forEach(badge); render(); }); - badge(el); - }); + function costWords(watts) { + var M = window.IgneumMoney; if (!M) return ''; + var ch = M.currencyChoice(savedCur, navigator.language), t = M.tariffFor(ch); + if (watts === null) return 'no power reading on this card'; + if (!t) return 'no typical ' + ch.code + ' tariff in the table; the app uses the price you type'; + var c = M.costPerDay(watts, t.price); + return 'about ' + M.fmtMoney(c, ch.code, navigator.language) + ' a day in electricity at ' + t.price + ' ' + M.minorOf(ch.code).abbr + '/kWh (' + t.region + ', typical)'; + } + function render() { + els.forEach(function (el) { + var sel = el.querySelector('select.card'), r = parseFloat(sel.value) * 1e6, eta = el.querySelector('.eta'), cost = el.querySelector('.cost'); + eta.textContent = net && r ? 'every ' + dur(net / r) : 'pending'; + var row = CARDS[sel.selectedIndex]; if (cost) cost.textContent = costWords(row ? row[2] : null); + }); + } + function init() { + var M = window.IgneumMoney, ch = M ? M.currencyChoice(savedCur, navigator.language) : null; + els.forEach(function (el) { + el.innerHTML = 'Your cardfinds a block about pending at the network\u2019s hash rate right now.' + + (M ? '' : ''); + el.querySelector('select.card').addEventListener('change', function (e) { var name = e.target.options[e.target.selectedIndex].text.split(' \u00b7 ')[0]; try { localStorage.setItem('igneum-card', name); } catch (x) {} document.querySelectorAll('[data-yourcard] select.card').forEach(function (s) { s.value = e.target.value; }); render(); }); + var cur = el.querySelector('select.cur'); if (cur) cur.addEventListener('change', function (e) { savedCur = e.target.value; try { localStorage.setItem('igneum-currency', savedCur); } catch (x) {} document.querySelectorAll('[data-yourcard] select.cur').forEach(function (s) { s.value = savedCur; }); render(); }); + }); + render(); + } + if (window.IgneumMoney) init(); else { var sc = document.createElement('script'); sc.src = '/money.js'; sc.onload = init; sc.onerror = init; document.head.appendChild(sc); } function poll() { fetch('/api/live?window=30', { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (d) { if (d && d.ok && d.state && d.state.hashes_per_second_estimate > 0) { net = d.state.hashes_per_second_estimate; render(); } }).catch(function () {}); } poll(); setInterval(function () { if (!document.hidden) poll(); }, 15000); window.IgneumYourCard = { update: function (h) { if (h > 0) { net = h; render(); } } }; diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index cdc2d62e9..391b40955 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -118,7 +118,7 @@ tree_checks() { run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh run "no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs)" overlap_sweep - run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/api/public-stats.test.mjs + run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs run "ship tool self-test" node tools/ship-app.mjs --self-test run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs