test: pin the CertManager verified-cert packed layout - #82
Conversation
`_saveVerified` stores each verified cert as `abi.encodePacked(ca, notAfter, maxPathLen, subjectHash, pubKey)` and `_loadVerified` reads the four fixed-width fields back with `mload(add(packed, D + N))`, which lands an N-byte field at payload offset D in the low bytes of the loaded word. That gives 0x1, 0x9, 0x11 and 0x31 for fields whose payload offsets are 0, 1, 9 and 17. The two sets of numbers do not match by design, and the layout had no direct test: the warm-cache path was only covered indirectly through full attestation fixtures, which do not isolate a decoding fault. The offsets have now twice been reported as off-by-one (base#37, base#50). Add tests that assert the stored encoding field by field, round-trip every field through save/load including a `ca == false` entry and a fuzzed case, check the constructor-written root entry against the pinned ROOT_CA_* constants, and cover the empty-cache branch. Verified these have teeth: rewriting the offsets to the values proposed in base#50 fails four of the six, including `ca` flipping to true for a non-CA entry.
|
Non-blocking integration data point from a delayed-settlement consumer: pinning this packed layout is valuable because warm validation is part of the authorization boundary, and the explicit The additional property we had to enforce downstream is that a successful That is outside this PR’s intentionally narrow layout scope, and main already tests revocation during validator reuse. I’m mentioning it because this test correctly protects the storage primitive while consumers still need to keep |
Adds direct coverage for the
verified[certHash]packed encoding and the assembly offsets_loadVerifieduses to read it back. Test-only — no production code is touched.Why
_saveVerifiedstoresabi.encodePacked(ca, notAfter, maxPathLen, subjectHash, pubKey), so the payload is:_loadVerifiedreads the four fixed-width fields withmload(add(packed, D + N)). Reading the 32-byte word that ends at the end of a field puts that field in the low bytes, so the constant ispayload offset + field width:mloadconstant (D + N)ca0x1notAfter0x9maxPathLen0x11subjectHash0x31The constants therefore look shifted by one field against the payload offsets, which is exactly what you would expect an off-by-one to look like. It has been reported as one twice — #37 and #50 — and both proposed changing the constants to
0x0 / 0x1 / 0x9 / 0x11, which would read the length slot instead of the payload.The offsets on
mainare correct. What was missing is a test that says so: the warm-cache path was only exercised through full attestation fixtures, which fail as a whole and do not isolate a decoding fault.What this adds
test/CertManagerPackedLayout.t.sol, six tests over aCertManagerharness that exposes_saveVerified/_loadVerified:test_saveVerified_writesDocumentedByteLayout— asserts total length and each field at its documented payload offset.test_loadVerified_roundTripsEveryField— save/load round trip on concrete values.test_loadVerified_roundTripsNonCaEntry—ca == falsespecifically.cais read out of a word whose upper bytes hold the tail of the length slot, so this is the field most likely to silently pick them up.testFuzz_loadVerified_roundTripsEveryField— same round trip, fuzzed over all five fields.test_loadVerified_returnsRootCaConstants— the constructor-written root entry against the pinnedROOT_CA_*constants, so the assertion is not only against test-authored values.test_loadVerified_returnsEmptyCertForUnknownHash— thepacked.length == 0branch.These have teeth
I rewrote the offsets in
_loadVerifiedto the values #50 proposes and re-ran the file:24833is0x6101— the0x61length byte shifted in over thecabyte.mainwas then restored, and the suite passes.If this lands it gives #50 a definitive answer and stops the same report coming back a third time.
Checks
forge test— 204 tests, 203 passed, 1 skipped (197 before this PR, +6 here), 0 failed.forge fmt --check test/CertManagerPackedLayout.t.sol— clean. (Repo-wideforge fmt --checkalready reports diffs on every source file with stock Foundry 1.7.1; this file is not part of that.)