Skip to content

keccak: document that an unconstrained len_bytes admits prefix hashes; add negative tests#1758

Open
pauldelucia wants to merge 2 commits into
binius-zk:mainfrom
pauldelucia:keccak-len-bytes-contract
Open

keccak: document that an unconstrained len_bytes admits prefix hashes; add negative tests#1758
pauldelucia wants to merge 2 commits into
binius-zk:mainfrom
pauldelucia:keccak-len-bytes-contract

Conversation

@pauldelucia

@pauldelucia pauldelucia commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Keccak256::new takes len_bytes as an arbitrary Wire and proves digest == keccak256(message[..len_bytes]) for a prover-chosen length — not digest == keccak256(message). The only constraint on the wire is len_bytes <= max_len_bytes.

If a caller leaves len_bytes a free witness and the digest is a prover-influenced / internally-consumed value (rather than a verifier-fixed public input), the constraint system is satisfiable for any prefix. Concretely: for a 100-byte message, a prover can claim len = 50, supply digest = keccak256(message[..50]) with hand-built padding, and it verifies.

If the digest is a public input, a free len_bytes is harmless. But a circuit that consumes the digest internally has no such protection — which is exactly the shape the in-tree ethsign circuit has, and it stays sound only because it pins the length (a public input for the message hash, add_constant_64 for the address hash). So this is a caller obligation rather than a gadget bug — but it was undocumented, and the gadget had no negative tests.

This PR:

  1. documents the len_bytes contract on Keccak256::new and the field, notes the safe ethsign pattern, and points constant-length callers at fixed_length::keccak256;
  2. fixes the new doc's stale argument list; and
  3. adds characterization tests: prefix acceptance under a free len_bytes, over-capacity rejection, and a pinned-length control that rejects the same payload.

Happy to adjust the framing or add an explicit API guard if you'd prefer. New tests pass via cargo test -p binius-circuits --lib keccak::tests.

@pauldelucia pauldelucia requested a review from jimpo as a code owner July 10, 2026 07:46
@pauldelucia pauldelucia force-pushed the keccak-len-bytes-contract branch 2 times, most recently from f61f849 to 119e5f4 Compare July 10, 2026 08:19
gopikannappan added a commit to gopikannappan/binius64 that referenced this pull request Jul 10, 2026
… + add negative tests

blake3_variable hashes the prover-claimed length: it enforces
digest == blake3(message[..len_bytes]) and len_bytes <= max_len_bytes, but does
not constrain the len_bytes value itself. With a free len_bytes and an
internally-consumed digest, a prover can substitute the digest of any prefix.
This mirrors the keccak len_bytes contract documented in binius-zk#1758.

- document the len_bytes caller obligation on blake3_variable (safe patterns:
  pin to a public input / derived wire, or use blake3_fixed for constant length)
- add variable_unconstrained_len_admits_prefix_hash: honest-length control,
  the prefix-hash characterization, and a positive-binding control showing
  message[..len_bytes] is bound (full-message digest rejected under a short len)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keccak256's variable-length gadget proves digest == keccak256(message[..len_bytes])
for a prover-chosen len_bytes, constraining only len_bytes <= max_len_bytes. When a
caller leaves len_bytes a free witness and consumes the digest internally, the
constraint system is satisfiable for any prefix of the message.

Document the caller obligation on Keccak256::new and the len_bytes field, note the
safe ethsign pattern, point constant-length callers at fixed_length::keccak256, fix
the stale doc argument list, and add negative tests (prefix acceptance under a free
len wire, over-capacity rejection, and a pinned-length control that rejects the same
payload).
@pauldelucia pauldelucia force-pushed the keccak-len-bytes-contract branch from 119e5f4 to ecbd9f2 Compare July 10, 2026 08:38
@pauldelucia

Copy link
Copy Markdown
Contributor Author

While writing this up I noticed the same len_bytes-selected-digest shape in two other hash gadgets on main: Sha256::new (which asserts the digest against a caller-supplied arg) and sha512_varlen (which returns the digest, so there's no pin point at all). Same caller obligation, and both undocumented. Happy to mirror this PR's doc block + negative tests to both if that'd be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant