From 58464384155a50e0e2caa431abced61d89cf7f3e Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 18 Jul 2026 01:50:59 +0000 Subject: [PATCH 1/2] test: pin mask constants by re-derivation, add lone-high-bit case, use relative src import Each CTPOP mask constant is asserted against an independently loop-built value so a corrupted constant cannot hide behind ctpop and ctpopSlow sharing it; a deterministic 1<<255 case pins the top of the range; the test imports src via a relative path so the suite survives consumption as a soldeer dependency. Appends the adversarial-mutation-test scan record. Closes #24 Closes #25 Closes #26 Co-Authored-By: Claude --- audit/mutation-test-scans.json | 17 +++++++++++ test/src/lib/LibCtPop.ctpop.t.sol | 51 ++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 audit/mutation-test-scans.json diff --git a/audit/mutation-test-scans.json b/audit/mutation-test-scans.json new file mode 100644 index 0000000..2a30516 --- /dev/null +++ b/audit/mutation-test-scans.json @@ -0,0 +1,17 @@ +[ + { + "timestamp": "2026-07-18T01:50:46Z", + "commit": "208336a29fc53b74226e385594f02703336974d5", + "publishedTag": "0.1.3", + "commitsAheadOfTag": 0, + "scope": "change-only: LibCtPop test hardening (mask-constant pins, lone-high-bit) + NatSpec/README docs", + "tool": "adversarial-mutation-test", + "skillVersion": "0.27.0", + "summary": { + "behaviours": 11, + "candidates": 0, + "confirmed": 0, + "filed": [] + } + } +] diff --git a/test/src/lib/LibCtPop.ctpop.t.sol b/test/src/lib/LibCtPop.ctpop.t.sol index 7b4d2e3..e73efb6 100644 --- a/test/src/lib/LibCtPop.ctpop.t.sol +++ b/test/src/lib/LibCtPop.ctpop.t.sol @@ -3,7 +3,18 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibCtPop} from "src/lib/LibCtPop.sol"; +import { + LibCtPop, + CTPOP_M1, + CTPOP_M2, + CTPOP_M4, + CTPOP_M8, + CTPOP_M16, + CTPOP_M32, + CTPOP_M64, + CTPOP_M128, + CTPOP_H01 +} from "../../../src/lib/LibCtPop.sol"; /// @title LibCtPopTest /// CTPOP (count population) is a function that counts the number of bits set in @@ -72,4 +83,42 @@ contract LibCtPopTest is Test { uint256 ctSlow = LibCtPop.ctpopSlow(x); assertEq(ct, ctSlow); } + + /// A lone bit in the highest position is counted, deterministically + /// pinning the top of the range rather than relying on fuzz coverage. + function testCtPopLoneHighBit() external pure { + uint256 x = 1 << 255; + assertEq(1, LibCtPop.ctpop(x)); + assertEq(1, LibCtPop.ctpopSlow(x)); + } + + /// Build the alternating mask with `width` bits set then `width` bits + /// clear, starting from the low bit, across the whole word. + function altMask(uint256 width) internal pure returns (uint256 mask) { + unchecked { + uint256 chunk = (1 << width) - 1; + for (uint256 i = 0; i < 256; i += 2 * width) { + mask |= chunk << i; + } + } + } + + /// Every mask constant is pinned against an independent re-derivation so + /// a corrupted constant cannot hide behind ctpop and ctpopSlow sharing it. + function testMaskConstantsRederived() external pure { + assertEq(altMask(1), CTPOP_M1); + assertEq(altMask(2), CTPOP_M2); + assertEq(altMask(4), CTPOP_M4); + assertEq(altMask(8), CTPOP_M8); + assertEq(altMask(16), CTPOP_M16); + assertEq(altMask(32), CTPOP_M32); + assertEq(altMask(64), CTPOP_M64); + assertEq(altMask(128), CTPOP_M128); + + uint256 h01 = 0; + for (uint256 i = 0; i < 256; i += 8) { + h01 |= uint256(1) << i; + } + assertEq(h01, CTPOP_H01); + } } From 578de9f95f647d0eef531cc30da18284a18ef64f Mon Sep 17 00:00:00 2001 From: David Meister Date: Sun, 19 Jul 2026 18:43:26 +0000 Subject: [PATCH 2/2] Drop audit/mutation-test-scans.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file is unrelated to the three issues this PR closes (#24 bare src/ import, #25 mask-constant pins, #26 lone-high-bit case) and sits in the wrong place: `audit/` is the Protofire PDF location by org convention — roh-scan scopes its external-audit coverage to `audit/protofire/` — while the audit skill's own run stamps live under `.audit/`. A mutation-test record is neither, and no other repo in the org carries this filename, so committing it here would establish a new path by accident. Harmless but out of scope: verified it does not affect roh-scan (which globs `audit/protofire/` only) and that REUSE legal passes either way. The durable record of this work is the tests themselves and the closed issues. Co-Authored-By: Claude Opus 4.8 --- audit/mutation-test-scans.json | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 audit/mutation-test-scans.json diff --git a/audit/mutation-test-scans.json b/audit/mutation-test-scans.json deleted file mode 100644 index 2a30516..0000000 --- a/audit/mutation-test-scans.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "timestamp": "2026-07-18T01:50:46Z", - "commit": "208336a29fc53b74226e385594f02703336974d5", - "publishedTag": "0.1.3", - "commitsAheadOfTag": 0, - "scope": "change-only: LibCtPop test hardening (mask-constant pins, lone-high-bit) + NatSpec/README docs", - "tool": "adversarial-mutation-test", - "skillVersion": "0.27.0", - "summary": { - "behaviours": 11, - "candidates": 0, - "confirmed": 0, - "filed": [] - } - } -]