Counter ASIC 3.0 node: the amended class v4 (AP-F8-1) is object 5; the split mechanism, the earliest flip arithmetic (section 6.6) and the signal harness stamping 5

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 09:46:35 +00:00
parent a2782e74ea
commit e40608c3e3
2 changed files with 18 additions and 6 deletions

View file

@ -142,6 +142,16 @@ The rule changed on the 0.3.16 branch (Horizon consensus-security): 95 percent i
Fork suites on the signalling change (0562a7f2): kaspa-consensus-core 110 + 7 (the signal-rule test, the window in the params, digest and fast-time file tests; the pinned devnet digest re-pinned from `3c505021...` to `7f2e49beabc253f327c5ac6bb457a674ea7f527af2971c95d3bdf65ef8bcf977`), kaspa-consensus lib 98 (the template test now reads the low byte and the signal byte), kaspa-pow with the feature 15; PC 2: section G6 of `node-gates.md`.
### 6.6 The amended class v4 (AP-F8-1, 7 October 2026): object 5, the split protection and the earliest flip
the project lead's word (15:2x UK, through the coordinator): option A, the load-source rule in class v4's chain draw, on 0.3.20 (the feature node, mirror branch `release-0.3.20-node`). The hash lane's stamp (`ca3-v4-amend` a0aaca92): generator stays 4 and every generator-4 program id carries `"sub/" || 1u16` little-endian inside `igneum_pow::generator::program_id` (`PROGRAM_SUBVERSION_V4` = 1), so a binary from before the rule and one after it never share an id for one seed; packs carry `IGNEUM_PROGRAM_SUBVERSION 1` and packcheck refuses a generator-4 pack without it. The node side on `release-0.3.20-node`: kaspa-pow pins the devnet epoch-0 vectors (the amended id 1a4230699a6b9c60 must equal, the 6 October stream's c120d7963abdcd96 must differ, the v3 control 73bcbfe8ccf988f1 unchanged); the daemon's window line names the object and the sub-version.
**The mechanism: a fresh object byte, 5.** `CLASS_SIGNAL_V4` is 5: the amended binary stamps 5 and the tally counts a block when its byte is at least 5. Object 4 was stamped only by the unpublished dc141409 canary (c18-1), so no published block carries it; a byte-4 block never counts towards the amended flip. A node of the 6 October stream that sees byte 5 counts it as v4 (its rule is "at least 4"), flips to ITS stream at the same epoch and forks alone: every block it makes fails the amended node's id check and every amended block fails its own, so it is alone either way, and it is ours to upgrade in the sweep. Object 6 is class v5's. The two holds already in the rule stand and are what keep the window from opening early: a node stamps and tallies only when both v4 fields are in its file (the 0.3.15 canary rule), and the fields are published only after the one-sweep rollout of section 2, so the first signalling block comes after every node is on 0.3.20. The worker: the release is one bundle per machine (node and worker), and a worker that lags refuses the amended pack at packcheck and mines nothing after the flip; it costs that box, never the chain. Holding the window to the rollout length is not needed on top: seven full windows of a day each are the hold, 604,800 DAA, and the sweep takes about 40 minutes.
**The arithmetic (approximate, measured 7 October 09:40Z).** DAA 270,659 at the hub's tip; 1.095 DAA/s over the last 25 hours (1.165 over the last 4); epoch 3,600 DAA with a lead of 600; window 86,400 DAA, seven windows 604,800 DAA. From a publish of the two fields at T, after the sweep (about 40 minutes: 32 minutes for the 14 standing boxes one at a time, the hands and the seed 3 minutes, the Mac and PCs by update-now), the seven windows must lie wholly after the sweep's end `D_s`, so the flip epoch is the first `e` with `3600 e - 600 >= D_s + 604,800`: the flip lands between `D_s + 605,400` and `D_s + 609,000` DAA, 6 days 9.6 to 10.5 hours later at 1.095 DAA/s (6 days 0 hours at 1.165). **Earliest flip: T + 6 days 10 hours to T + 6 days 11 hours UK clock at the 25-hour rate; for a publish at 12:00 UK on 7 October, 13 October between 22:20 and 23:10 UK (about 13:00 UK at the 4-hour rate).** The floor as it stands, 831,600, is 560,941 DAA away, about 13 October 09:00 UK at 1.095 DAA/s: it would fire BEFORE any seven-window signal from a publish today can complete, so it moves, per the 0.3.16 rule, to the publish DAA plus 604,800 rounded up to the epoch boundary: 882,000 for a 12:00 UK publish (DAA about 275,900 then), which fires about 30 minutes before the earliest signal flip. Either way the class flips about 6 days 10 hours after the publish, and never before every node has had the sweep plus a week.
**Testing, limited by the project lead's word:** the digest test (the thirteen-field file b18ed271 unchanged, the sixteen-field object re-read on the amended binary; the sub-version is not a params field, so no digest moves), the mixed-version Devnet 2 gate on `node-compat.mjs` (an amended 0.3.20 node mining beside a 5899f603 node for ten minutes on the live file without the v4 fields, the old node accepting every block), and the fresh-join canary the 0.3.20 cut runs anyway; G4 to G6 recorded as owed. The fast-time signal harness (`class-v4-signal.mjs`) now stamps 5 for a signaller and 4 for the non-signaller, so its no-flip case also shows a byte-4 node not counting.
## 7. 0.3.17 node items after the 3.0 lane (Horizon queue, 6 October 2026, evening)
Numbering (the shipper, 6 October 2026, 22:4xZ): this tree ships as 0.3.17. 0.3.16 became the corrective cut of 0.3.15's tree the same night (the Mac and PC 1 had taken early 0.3.15 builds), node f1ea7a38 unchanged. The branch keeps its name `ca3-v4-0316` and the commit messages their "0.3.16"; every "0.3.16" in the fork's code comments means this tree.

View file

@ -12,10 +12,11 @@
// epoch whose seed block (at 60 e - 10) has 210 DAA below it is epoch 4 (seed at DAA 230), so the flip is at DAA 240.
//
// The three cases and the known-failed case:
// --signal 4,4,3 --floor 100000 --expect no-flip two of three miners signal: 67 percent, the class must stay v3 (run 7 epochs)
// --signal 4,4,4 --floor 100000 --expect flip all three: it flips at epoch 4, the first boundary with seven full windows
// --signal 3,3,3 --floor 360 --expect floor nobody signals: it flips at the floor (epoch 6, DAA 360) and not before
// --signal 4,4,3 --floor 100000 --expect flip the known-failed case: the harness must report FAIL (no flip happened)
// --signal 5,5,4 --floor 100000 --expect no-flip two of three miners signal: 67 percent, the class must stay v3 (run 7 epochs);
// the third stamps 4, the 6 October stream's byte, which the amended rule does not count
// --signal 5,5,5 --floor 100000 --expect flip all three: it flips at epoch 4, the first boundary with seven full windows
// --signal 4,4,4 --floor 360 --expect floor nobody signals the amended object: it flips at the floor (epoch 6, DAA 360) and not before
// --signal 5,5,4 --floor 100000 --expect flip the known-failed case: the harness must report FAIL (no flip happened)
// The floor is REQUIRED for a signalling case: since the 0.3.15 canary rule a node stamps and reads the signal only with
// both v4 fields set (the window and a floor), so a floor at never means nobody signals (the first 0.3.16 run of this
// harness, 20:56Z: chain bytes all 0); a far floor (100,000 DAA, 27 hours of fast-time chain) keeps it out of the run.
@ -50,7 +51,8 @@ const FLOOR = actflag('floor', null);
const V3_ACTIVATION = actflag('v3-activation', 60);
const WINDOW = flag('window', 30);
const WINDOWS = 7; // CLASS_SIGNAL_WINDOWS in the node
const SIGNAL = (sflag('signal') || '4,4,4').split(',').map(Number);
// the amended class v4 is object 5 (AP-F8-1, 7 October 2026); 4 is the 6 October stream's byte and does not count
const SIGNAL = (sflag('signal') || '5,5,5').split(',').map(Number);
const EXPECT = sflag('expect') || 'flip';
if (!['flip', 'no-flip', 'floor'].includes(EXPECT) || SIGNAL.length !== 3) { console.error('usage: --signal a,b,c --expect flip|no-flip|floor'); process.exit(2); }
const started = [];
@ -184,7 +186,7 @@ const BOUNDARY = firstV4 ? firstV4.epoch * EPOCH : Infinity;
const before = blocks.filter(b => b.daa < BOUNDARY), after = blocks.filter(b => b.daa >= BOUNDARY);
// the signal bytes on the chain: the share of blocks whose version high byte is 4
const versionBytes = blocks.reduce((m, b) => { const v = b.version >> 8; m[v] = (m[v] || 0) + 1; return m; }, {});
const signalShareOnChain = blocks.length ? Math.round(10000 * (blocks.filter(b => (b.version >> 8) >= 4).length) / blocks.length) : 0;
const signalShareOnChain = blocks.length ? Math.round(10000 * (blocks.filter(b => ((b.version >> 8) & 0x3f) >= 5).length) / blocks.length) : 0;
const programs = new Map();
for (const i of [0, 1, 2]) for (const l of minerLog(i)) {