SequenceHash lands in C2SP: hash-agnostic multihashing with length-encoded inputs
Trail of Bits published SequenceHash v1.0.0 — a hash-agnostic multihashing construction with 128-bit length encoding, specified in C2SP with Rust, Go and Python implementations.
Trail of Bits published SequenceHash v1.0.0 on October 2, a hash-agnostic multihashing construction that resolves one of the most common foot-guns in protocol crypto: how to deterministically hash multiple values together without accidentally letting an attacker shift bytes between inputs. The spec is in the Community Cryptography Specification Project at C2SP/C2SP/sequencehash.md. The announcement sits on the Trail of Bits blog as SequenceHash: multihashing for the rest of us.
The news peg
Multihashing bugs keep showing up in blockchain-adjacent code. Signature-replay primitives, Merkle-tree aggregations, Fiat–Shamir transcripts in zk-SNARK rollups and transaction-batching commitments all boil down to H(a || b || c), which is unsafe when any of a, b or c has variable length — an attacker can rebalance the boundaries and get the same digest from a different triple.
NIST's answer, TupleHash, is pinned to Keccak-f and only ships inside SHA-3 extensions, so protocols running SHA-256 or BLAKE have been rolling their own. SequenceHash is the first widely-reviewed drop-in that works with any underlying hash: SHA-256, SHA-384, SHA-512, BLAKE2, SHA-3 variants, RIPEMD and WHIRLPOOL are all in-scope.
How it works
The construction encodes each input's length as a 128-bit little-endian prefix (EncodeLSBF) and concatenates the resulting length-tagged byte strings, then double-hashes the result in an HMAC-style outer/inner pattern. Two choices matter for protocol designers:
- Length prefixes are 128 bits. That removes practical length-collision concerns for anything short of a 2^128-byte input, and keeps the construction fast — one extra 16-byte prefix per input, no padding, no block alignment.
- Domain separation is explicit. Function tags (
F_SEQHSH = 2,F_SEQMAC = 1) and user-supplied customization strings prevent cross-protocol collisions between the plain and keyed variants and between protocols sharing the same hash.
SequenceMAC — the keyed variant — requires ≥32-byte keys and the double-hash construction neutralizes length-extension attacks that otherwise bite Merkle–Damgård constructions like SHA-256 and SHA-512.
The authors are Opal Wright and Scott Arciszewski, published under C2SP v1.0.0 (stable, not draft).
For builders
Reference implementations are up in three languages:
- Rust:
trailofbits/sequencehash-rs - Go:
trailofbits/sequencehash-go - Python:
trailofbits/sequencehash-py
Where it will meet blockchain code
The obvious adopters are:
- Rollup transcripts. Any Fiat–Shamir-based zk prover that commits to a sequence of public inputs — Boojum, Noir, Halo2 with a SHA-256 transcript, STARK verifiers wiring to a non-Keccak hash — can use SequenceHash to replace ad hoc concatenation.
- Transaction-batching hashes. Rollup inboxes, aggregator contracts and multi-send commitments where "commit to this list of transactions" currently uses
H(len(txs) || tx1 || tx2 || ...)with hand-rolled length guards. - Merkle-leaf disambiguation. Not a replacement for a Merkle construction, but a drop-in when leaves themselves bundle multiple fields of variable length (e.g., ERC-721 metadata, Solana account layouts).
Protocols reaching for TupleHash purely to avoid rolling their own now have a hash-agnostic alternative under the same security assumptions.
Limits and caveats
- The spec is new; while C2SP review is strong, protocols with existing on-chain verifiers or audited transcripts cannot switch without a hard-fork or a migration proof.
- The 128-bit length prefix is 16 bytes per input — negligible off-chain, but non-trivial inside zk circuits where every byte costs constraints. ZK adopters should measure before swapping.
- No formal verification of the construction itself has been published; the double-hash pattern mirrors HMAC and inherits its heuristic argument against length-extension and collision pathologies.
How to try it
Clone the Rust reference, run cargo test, and compare its output against the test vectors in C2SP's sequencehash.md. For zk work, start by replacing a single H(a || b) call in a Fiat–Shamir transcript and measure the constraint delta — the cost is linear in the number of inputs, not quadratic, so most transcripts will absorb it with margin.