Hashing several values in SimplicityHL currently means threading a SHA-256 context through a chain of jets:
let ctx1: Ctx8 = jet::sha_256_ctx_8_init();
let ctx2: Ctx8 = jet::sha_256_ctx_8_add_2(ctx1, 0x5120);
let ctx3: Ctx8 = jet::sha_256_ctx_8_add_32(ctx2, tweaked_key);
let hash: u256 = jet::sha_256_ctx_8_finalize(ctx3);
Every hash of a compound value spans several lines of ctx-threading, which is noisy in application code.
PR #423 adds an unstable raw_hash builtin that does the same in one call:
let hash: u256 = raw_hash::<(u16, u256)>((0x5120, tweaked_key));
How it works
It accepts a tuple of unsigned integers: u8, u16, u32, u64, u128, u256. It compiles to init, then one sha_256_ctx_8_add_N jet per element in source order, then finalize. It’s unstable, so it’s enabled with the -Z raw_hash flag.
Why it’s “raw”
It hashes the concatenated bytes with no type or length information. Different inputs can therefore produce the same hash: for example, (u8, u8) holding (0x51, 0x20) and (u16,) holding (0x5120,) hash identically. Use it only where the layout of the hashed data is fixed and agreed on, and add your own tag or domain separation where it matters. A hash that’s unambiguous across types needs a more complete type system in SimplicityHL first, so that’s planned as a separate, later step (discussion in #374).
Thanks! I took a quick look at your PR. chain! looks useful, because it solves the reading order in general, while raw_hash is specifically for hashing a tuple of values.
I think they fit different cases: chain! works for any sequence of steps, for example when the context starts from a tag, or when a step is a helper that adds several fields, or for types that have no add_N jet. raw_hash covers the simple case where you just want the digest of a flat tuple of integers, and the types pick the jets for you.
We could say this in the documentation once both land
I was having a discussion with the developers of the SHRINCS verifier implementation about some inefficiencies in calling hash updates this way. An AI pointed out that if we used structural typecasts we can often reduce the overhead by combining multiple hash updates into fewer.
I think that specific optimization works very well with raw_hash! The compiler can notice the fewest number of updates that would apply to the overall data in the individual raw_hash call, and use the structural typecast to combine the input data appropriately.
As a trivial example, raw_hash::<u8, u8, u8, u8>((a, b, c, d)) could emit a single jet::sha_256_ctx_8_add_4 call instead of four jet::sha_256_ctx_8_add_1 calls. The caller could have done the typecast into a u32 itself, but the compiler can be helpful and take care of this for the caller.