Hash records through the data model, not around it #638

Merged ngerakines.me
targeting main
Merged opened by and assigned to ngerakines.me
targeting main

atpcid and atproto-record-cid both encoded a serde_json::Value straight to DAG-CBOR. AT Protocol's JSON spells a link as {"$link": "..."} and a byte string as {"$bytes": "..."} because JSON has no syntax for either, so encoding structurally writes a two-key map where a link belongs and hashes to a CID nothing else on the network computes.

Every record carrying a blob has a $link in it, so both tools were wrong for a large share of real records while looking right for the rest -- which is how this survived. Against a post captured from the network, both returned bafyreihxokfjjnh2xo23jlgrnshwhajgafyt3cesfmadub6zcxjonjl3gu where the network published bafyreicupfyigj4xq5cwa4q7pt2mg5lx2ct7luuwevw7n3nhutbhhx2ra4. Both now return the latter.

The correct encoders already existed, and atproto_json's module docs already warned about exactly this failure. They were simply unreachable without knowing the module path: the crate root exported drisl's to_vec and compute_cid_for, so the structural encoder was the discoverable one and the sentinel-aware one was not. That is the actual defect, and the fix is to export them as to_vec_from_json and compute_cid_from_json -- named rather than shadowing, since both compile against a record and only one is right.

atproto-jetstream had a third copy of the translate-then-hash sequence; it now calls the shared helper.

Tests are anchored to the captured record and the CID the network gave it, and one asserts the structural encoding still differs -- without that, the first would pass for the wrong reason. A third records why the bug was survivable: with no sentinel anywhere, the two paths agree.

atproto_json::compute_cid returned cid::Cid, which reads as the crate-local newtype but resolves to the extern crate. It says CidCore now.

This pull request has no versions submitted yet.