Expand description
Pack 18-byte chain nodes into 64-byte words.
§The problem
Hash chains produce 18-byte nodes (ELEMENT_BYTES), but the rest of the
hardware moves 64-byte words: a BLAKE3 block is 64 bytes and an HBM beat
(one transfer on the memory bus) is 64 bytes. Somebody has to glue a
stream of 18-byte pieces into a stream of 64-byte words, with no gaps.
Two places in a slice turn a stream of 18-byte nodes into 64-byte words:
| Stream | Prefix | Then | Total |
|---|---|---|---|
| Endpoint commitment (BLAKE3 blocks) | pk_nonce, 16 B | 68 chain ends | 1,240 B |
| Signature frame tail (HBM beats) | last 34 header bytes | 68 revealed nodes | 1,258 B |
The first is used by the key factory to hash the
68 chain ends into pk_hash (the key’s public commitment). The second is
used by the signer to lay the 68 revealed nodes
into the signature frame right after the header. Both are the same circuit,
ElementPacker, with a different prefix length P.
§Why it is tricky
18 does not divide 64, so each node lands at a different byte offset. The
sequence of offsets repeats every 32 nodes (32 × 18 = 9 × 64).
word 0: [ prefix (P bytes) | node 0 | node 1 | node 2 | node 3 (part) ]
word 1: [ node 3 (rest) | node 4 | node 5 | node 6 (part) ] …For the signature stream (P = 34) the first words look like this:
byte: 0 34 52 64
word 0: [ header tail | node 0 | node 1: 12 B ]
word 1: [ node 1: 6 B | node 2 | node 3 | node 4 | node 5: 4 B ] ...
last: [ ... node 67 ends at byte 42 | zero padding ] (1,258 = 19 × 64 + 42)The packer keeps a fill counter (bytes already used in the current
word). Each new node is placed at offset fill. If it fits, fill grows
by 18. If it crosses the 64-byte edge, the full word is emitted and the
0 to 17 overflow bytes (the remainder) start the next word.
ElementPacker accepts at most one node per cycle and emits at most one
word per cycle. Both streams have the property that the 68th node never
completes a word and leaves a remainder, so a single output suffices. A
compile-time assertion checks this for each prefix length.
§Why the kernel looks unusual
RHDL lowers every array-element assignment into a copy of the whole array,
so “for each offset, for each byte, maybe write it” would emit thousands
of 512-bit copies. Instead each output byte is one OR of ANDs over a
zero-padded copy of the node (place_kernel). Every index is a
compile-time constant in range, and synthesis folds the padding zeros away,
leaving an 18-way select per byte.
§Timing
One register stage: a node presented with valid in cycle t produces
its word (if any) on the output in cycle t + 1.
Structs§
- Element
Packer - Registered packer for a stream with a
P-byte prefix and 68 nodes. - Packer
Input - Packer input;
startmust accompany node 0 of each stream. - Packer
Output - One output word; bytes at and after
bytesare zero.
Constants§
- COMMITMENT_
PREFIX - Prefix length of the endpoint-commitment stream (
pk_nonce). - SIGNATURE_
PREFIX - Prefix length of the signature-frame tail: the 1,642-byte wire format puts
418 header bytes before the nodes, and
418 = 6 × 64 + 34.
Functions§
- element_
packer_ kernel - Wiring for
ElementPacker. - place_
kernel - Bytes of
nodeshifted to start at offsetfill, over 82 positions. - single_
flush - Whether
prefixkeeps the 68th node from overflowing into a second word.