Summary
The Poseidon2 and Skyscraper duplex-sponge adapters decode each 32-byte lane via from_le_bytes_mod_order (or equivalent). This reduces lane values ≥ p mod p, so the bytes → Fr → bytes round-trip is only byte-injective when every absorbed lane is already < p.
All absorb sites write canonical encodings:
- Field elements serialized via
field_to_bytes_le (always < p by construction)
- WHIR engine outputs (themselves canonical field elements)
- ASCII domain-separator bytes (every byte
< 0x80, so any 32-byte lane is trivially < p)
Why it's fragile
Nothing at the sponge layer enforces this invariant. spongefish::DuplexSponge::absorb takes &[u8] and writes raw bytes straight into permutation_state — the Permutation::permute impl has no way to reject non-canonical input.
Any of the following would silently break byte-injectivity:
- A raw SHA-256 / Keccak-256 digest (~25% of outputs ≥
p)
- 32 bytes from a randomness beacon or
/dev/urandom
- A non-ASCII DS that lands above
0x80 in the MSB of a lane
Affected files
Both sponges share the same adapter (utils::bytes_to_field / utils::field_to_bytes_le)
Summary
The Poseidon2 and Skyscraper duplex-sponge adapters decode each 32-byte lane via
from_le_bytes_mod_order(or equivalent). This reduces lane values ≥pmodp, so the bytes → Fr → bytes round-trip is only byte-injective when every absorbed lane is already< p.All absorb sites write canonical encodings:
field_to_bytes_le(always< pby construction)< 0x80, so any 32-byte lane is trivially< p)Why it's fragile
Nothing at the sponge layer enforces this invariant.
spongefish::DuplexSponge::absorbtakes&[u8]and writes raw bytes straight intopermutation_state— thePermutation::permuteimpl has no way to reject non-canonical input.Any of the following would silently break byte-injectivity:
p)/dev/urandom0x80in the MSB of a laneAffected files
provekit/common/src/poseidon2/sponge.rsprovekit/common/src/skyscraper/sponge.rsBoth sponges share the same adapter (
utils::bytes_to_field/utils::field_to_bytes_le)