Collapse the mirror: LibUint256Array/Matrix become cast wrappers over the bytes32 libraries - #149
Conversation
`uint256[]` and `bytes32[]` are the same structure in memory, so the two library pairs were identical modulo the element type and were kept in sync by hand. Nothing detected them drifting, and they had drifted twice. `LibBytes32Array`/`LibBytes32Matrix` keep the implementation; the uint256 libraries relabel and delegate. bytes32 is the hot path in this stack and the opaque-bits type, with uint256 as the numeric view over it, so the wrapper surcharge lands on the numeric side. The public surface is unchanged and the whole existing test suite, including all 14 mirrored bytes32 test files, passes untouched. Refs #128 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review. Walkthrough
ChangesUint256 delegation
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR replaces the uint256 mirror implementations with private cast wrappers while preserving the public API and tested behavior; no actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The two `private` cast helpers in each wrapper had a body of `relabelled := array` and nothing else, so they bought a call frame to move a pointer between two types that are the same structure in memory. A jump is worth paying for real logic, not for a relabel. Each delegation now does its own relabel in an inline assembly block where it needs one. Dropping the helpers also drops the two named returns they declared, which are not this repo's convention (#135). No public signature, no behaviour and no test changes. All 14 mirrored test files are still untouched and the whole suite still passes unchanged. Refs #128 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs #128 — option 1, the source collapse.
Direction
bytes32keeps the implementation;LibUint256ArrayandLibUint256Matrixbecome cast wrappers overLibBytes32Array/LibBytes32Matrix. That is the reverse of #128's wording, for two reasons:bytes32is the hot path in this stack, so the wrapper surcharge lands on the colder side, andbytes32is the opaque-bits type withuint256as the numeric view over it rather than the other way round. It also leaves the audited assembly where Protofire read it.Each delegation does its own relabel in an inline assembly block at the point it needs one. There are no
privatecast helpers: a helper whose whole body isrelabelled := arraybuys a call frame to move a pointer between two types that are the same structure in memory, and a jump is worth paying for real logic. Dropping the helpers also drops the two named returns they declared, which are not this repo's convention (#135).The diff is two files, +174/−296.
LibBytes32Array.sol,LibBytes32Matrix.soland all 14 mirrored test files are untouched.Measured, not asserted
Pinned toolchain (forge 1.7.2-nightly
43923a4, solc 0.8.25, optimizer on at 100000 runs,evm_version = "cancun"),bytecode_hash = "none"andcbor_metadata = falseso no metadata trailer lands in the compared bytes.The rig is a pair of mirrored harness contracts, one per element type, with one
externalfunction per library entry point — 26 each (17 array, 9 matrix) — so nothing is inlined into a caller and every entry point is separately measurable. A probe is thegasleft()span across astaticcallinto one harness function. Calldata is built fromuint256values for both element types, because the ABI encoding ofbytes32[]anduint256[]is byte identical, so the two sides receive the same bytes modulo the four byte selector. 16 of the 26 entry points vary with length and are swept at 0, 1, 2, 8 and 32; the other 10 get one probe each: 90 probes per element type.All three builds —
main, the private-helper spelling, and this inline-assembly spelling — were rebuilt from clean and re-measured against that one harness, so the figures below are comparable to each other. They are not comparable to the absolute numbers in the previous revision of this description, which came from a different rig and have been replaced rather than carried over. Every build and every probe run was repeated a second time from clean: byte-identical bytecode and byte-identical probe output both times.Bytes32Harnessdeployed bytesUint256Harnessdeployed bytesBytes32Harnessruntime bytecode is byte-identical across all three builds — sha2566fec3349256ddbd9cda812b7a10b4663db3de13ec3a06891fe247af5f014a78f— and its 90 gas probes are identical, gas for gas, on every one. The hot path is provably unmoved.The uint256 surcharge is a flat constant per entry point: for all 16 size-varying entry points the delta against
mainis the same at every one of the five lengths, in both spellings. It is the wrapper failing to inline, not a copy.Inline assembly against the private helpers, head to head
The spelling this branch ships is the dearer of the two on this rig, and the numbers are reported as they came out.
Uint256Harnessdeployed bytesThe mean is a wash — 1.4 gas on entry points costing 1.0k to 37k — but the distribution is lopsided the other way from an earlier measurement of an earlier rig, which had inline cheaper on 50 of 90. On this rig it is dearer on 61 of 90. The cheaper side is concentrated in the wide
arrayFromoverloads (arrayFrom8−129,arrayFrom7−81,arrayFrom5−71); the dearer side is a uniform +4…+8 across the pointer,truncate,reverseanditemCountentry points, plus +23/+28 onmatrixFrom2/matrixFrom3. The relabel itself is a pointer move in both spellings, and the delta againstmainis flat across the whole length sweep for every size-varying entry point, so what differs is how the legacy pipeline lays out the surrounding jumps rather than any work either spelling does.The surcharge is accepted deliberately. rainlanguage/rainix#328 tracks the pipeline fix: under
via_irthe wrapper compiles away entirely and the collapse is byte-identical to main.The harness is a measurement rig and is not committed — the diff stays source-only. It is described above in enough detail to rebuild.
Behaviour is unchanged
mainruns 391 tests, 0 failed; this branch runs 391 tests, 0 failed. The entire mirrored suite passing without a line changed is the evidence that the delegation preserves behaviour.The mirrored
uint256suite stays exactly as it is. Its behavioural assertions are what would catch a future change to the wrapper — a delegation check would not — and tests are only ever for the future.forge build --deny warnings,forge fmt --checkandforge lintare all clean.Public surface
Unchanged: same names, parameter types, return types and mutability on every function in both rewritten files. Nothing is added to the surface at all — there are no longer any
privatefunctions in either file. The release stays a patch bump.Audit scope
All four library files are inside the Protofire scope recorded in
audit/audits.json. This changes two of them structurally; thebytes32pair — where the assembly under review lives — is untouched, and its compiled output is byte-identical.QA
mainand on this branch, and is what a behaviour-changing delegation would break.adversarial-mutation-testskill and its bundledmutation-probe— 26/26 killed, 0 survived, 0 no-run, 0 harness errors. Each targets one delegation or one relabel (M04 unsafeAsUint256Array relabel is an offset view not the identity,M18 matrix startPointer delegates to dataPointer,M24 matrixFrom3 drops its third array,M26 flatten returns an empty array, …) and the probe names the tests that killed it. Five of the 26 attack the relabel itself by making it an offset view rather than the identity, which is the failure mode this spelling opens up: 37 relabel assignments across 33 assembly blocks, where the helper spelling had 6 helper bodies.bytes32implementation being delegated to, plus the deployed-bytecode comparison —Bytes32Harnessbyte-identical across main and both spellings, so the hot path is proven unmoved rather than argued. The adversarial question this respelling raises is whether the 21 uninitialised memory locals the relabels are assigned into allocate: if any did,unsafeExtendwould silently lose its in-place path and no content assertion would notice. Falsified against the existing allocator tests, not argued —LibUint256Array.allocation.t.sol::testExtendInlineAllocatesExactlypins the free memory pointer to the exact end of the extended array andLibUint256Array.extend.t.sol::testExtendInlineEmptyExtendKeepsBasePointerpins the in-place branch, and both pass unchanged.src/. The mirrored test suite stays as it is, so this isRefs #128, notCloses.