Computing a request hash
Every request you submit to a head carries requestHash, a digest you compute over the payloads
you are sending. The head re-derives it from the bytes it received and refuses the request if the two
differ. This guide gives you the construction, so your client can produce a value the head accepts.
You need this for both request kinds — DEPOSIT.md and L2TXS.md — because
POST {headUri}/head/requests rejects a body without it.
Why the client computes it
A digest the head computes and hands back tells you what the head thinks it received, and you have
nothing to check it against except the same head’s word. A digest you compute and the head confirms
is an end-to-end check on the bytes: it fails exactly when the request the head holds is not the
request you built — a truncated payload, a client-side encoding change, a mangled l1Payload.
The same value identifies your request afterwards. It appears beside every request in a block
(requestHash in the block view, GET {headUri}/head/blocks/{n}), so you can recognize your own
request in a block by matching the digest you already hold, without trusting anyone’s arithmetic.
The construction
blake2b-256 over an ASCII domain tag, a one-byte variant tag, and the payloads:
requestHash = blake2b_256(
"gummiworm-request-v1"
|| u8(variant) -- 0x00 deposit, 0x01 transaction
|| deposit: blake2b_256(l1Payload) || blake2b_256(l2Payload)
|| transaction: l2Payload
)
| Piece | Bytes |
|---|---|
| domain tag | 67756d6d69776f726d2d726571756573742d7631 (20 bytes, ASCII, no terminator) |
| variant tag | one byte: 00 for a deposit, 01 for a transaction |
| deposit payload | two 32-byte digests, l1Payload’s then l2Payload’s |
| transaction payload | l2Payload verbatim, to the end |
Three rules the layout depends on:
- The variant tag leads the payload. Without it, a transaction whose
l2Payloadhappens to be a deposit’s 64-byte pair of digests would hash identically to that deposit. - A deposit’s payloads are hashed before being concatenated. Hashing the raw bytes instead would
collapse
hash(abc + def)andhash(ab + cdef), so two different deposits could share a digest. - The
requestIdis not in the preimage. You have no request id when you compute this, and the same bytes hash the same however the head sequences them.
l1Payload is the unsigned deposit-tx CBOR — the same bytes you put in the request body, not the
signed transaction you later submit to L1.
Worked examples
These are the vectors pinned in src/test/scala/hydrozoa/multisig/consensus/UserRequestTest.scala.
Reproduce them before trusting your implementation.
A transaction. With l2Payload =
7b226163636f756e744964223a226363613363636361373232636630666134353961316433343831376235353563616538333735383539383033383030613362363166316361222c2264656c6567617465644b6579223a223537633861663235366135643632616238643933396466613232613264373933623037623034306538613831656638633963333861333932227d:
requestHash = 58828159aaac6c4575395db0ea87f5e2a378c2e3f4c6e78b27d61eddbd2b1e85
A deposit. With that same l2Payload and the l1Payload from the same test, the two
intermediate digests are:
blake2b_256(l1Payload) = 75223e9148063e52d6ee97845ef1272936bb5abb9d3d3f33d39d5e50a32de00f
blake2b_256(l2Payload) = 0c0d70d38b29875722199523c082858278c6a2790faee29b6db6b16b06855790
requestHash = ac596c7fb689a6e4757fb6c782580d7cc2ca0ff812ae872753be1292063a28bb
If your transaction digest matches but your deposit digest does not, check the variant tag and the order of the two intermediate digests.
Submitting it
requestHash is a required field of the submit body, lowercase hex, 32 bytes:
{
"type": "transaction",
"l2Payload": "84a400d9010281825820…",
"requestHash": "58828159aaac6c4575395db0ea87f5e2a378c2e3f4c6e78b27d61eddbd2b1e85"
}
{
"type": "deposit",
"l1Payload": "84a600d9010281825820…",
"l2Payload": "a1024568656164…",
"requestHash": "ac596c7fb689a6e4757fb6c782580d7cc2ca0ff812ae872753be1292063a28bb"
}
When it does not match
The head refuses the request and names both digests:
requestHash does not match the submitted body: submitted=<yours>, derived=<the head's>
Nothing is stored and no request id is consumed, so fix the request and submit again. The head never corrects the digest for you: a mismatch means the two of you disagree about what was submitted, and there is no version of the request it would be safe to accept.
Worked implementation
hydrozoa submit-deposit and hydrozoa submit-l2-tx compute it through
UserRequestBody.mkHash (src/main/scala/hydrozoa/multisig/consensus/UserRequest.scala), which is the
same function the head runs to verify. Read it alongside this guide.