Expand description
A Haraka lane specialised to exactly one DSig chain step.
§The problem
A DSig key is 68 W-OTS+ chains (w = 4). Making a key walks every chain
from its secret node three steps to its public end, and every step is one
Haraka-512 hash of a 64-byte chain block. That is 204 chain steps per
signature. The signer wants one chain step per clock per lane at
205 MHz, so the chain-step lane is replicated many times, and its size
decides how many signers fit on the chip.
Most of a lane’s area is AES S-boxes. A plain Haraka-512 has 640 of them (10 AES rounds × 64 bytes). This module asks: how many of those 640 does a DSig chain step really need? The answer is 494, and this file is the hardware that uses only those.
§The reasoning, step by step
- The chain block is mostly fixed. Only the 16-byte salted nonce and the 18-byte node change; bytes 34..64 are always zero.
- An S-box whose input is a known constant has a known constant output. It needs no hardware at all.
- Haraka’s AES lanes do not talk to each other until the first word mix, after the second AES round. So the constants survive for two rounds.
- DSig keeps only 18 of the 32 digest bytes. Any S-box that cannot reach those 18 bytes is wasted work.
The decision: compute, at compile time, which bytes are constant (going forwards) and which bytes are needed (going backwards), then build a pipeline in which the constant bytes are literal values. Synthesis does the rest: it deletes logic that drives nothing and folds logic whose inputs are constants.
§How the constants fold away
live = varies per chain step · = compile-time constant
AES lane: 0 1 2 3
block bytes: 0..16 16..32 32..48 48..64
round 0 in: live×16 live×16 live×2 (32,33) ·×16
(salted nonce) (node 0..16) ·×14 (all zero)
│ │ │ │
rounds 0-1: all S-boxes all S-boxes only the S-boxes every S-box
live live fed by bytes 32, constant:
33 and what they 32 boxes fold
spread to
└─────────────────┴─── word MIX after round 1 ──┴───────────────┘
round 2: the mix has moved lane 3's still-constant words into other
lanes; S-boxes on those 16 bytes still fold
rounds 3-6: every byte varies and every byte is needed
rounds 7-8: working backwards from the output, one lane's worth of
bytes (16) can no longer reach the 18 kept bytes
round 9: only the columns that feed the 18 kept bytes matter:
5 of 16 columns × 4 bytes = 20 of 64 S-boxes
│
output: 18 bytes = mid_haraka512(block)
live S-boxes per AES round (constant ones folded, unobserved ones deleted):
round: 0 1 2 3 4 5 6 7 8 9 total
live: 34 40 48 64 64 64 64 48 48 20 = 494 (of 640)The per-round detail is not written down by hand in the code:
LIVE_SBOX records it, live_sbox_evaluations counts it, and a test
pins the total of 494. The per-round row above is that same analysis
re-run by hand for this explanation; only the total is checked by a test.
§Why a second Haraka pipeline?
Haraka512Pipeline hashes any 64-byte
block and returns the full 32-byte digest. DSig never asks for that. Every
chain step hashes a block with a fixed shape
(dsig_chain_block) and keeps only 18
bytes of the answer:
byte 0 16 34 63
├──────┼─────────┼────────────────────────────────────┤
block │nonce │ secret │ thirty zero bytes │
└──────┴─────────┴────────────────────────────────────┘
AES lane 0 = bytes 0..16 AES lane 2 = bytes 32..48
AES lane 1 = bytes 16..32 AES lane 3 = bytes 48..64 (all zero)Haraka runs four independent AES lanes and only mixes them after every second AES round. So for the first two rounds AES lane 3 sees nothing but zeros: its 32 S-box evaluations always produce the same bytes. Lane 2 only has two live bytes, so most of its first-round S-boxes are also constant. At the far end, the 18 kept output bytes depend on only five of the sixteen 32-bit columns, so most of the final round is never observed.
This module turns those facts into hardware savings:
- A
const fn(KNOWN) symbolically runs the pipeline over “known constant” / “live” bytes and records, for every stage, which state bytes are compile-time constants and what their values are. - The RHDL kernel pins those bytes to literals before each stage. Synthesis then folds their S-boxes into constants and deletes the registers that would have carried them.
- The lane outputs only the 18 DSig bytes, so synthesis also deletes every S-box whose result can never reach an output.
Counted by live_sbox_evaluations, one chain step needs 494 S-box
evaluations instead of Haraka’s 640, about 23% less of the dominant area.
The count is a structural planning number. Only an implemented-design report
measures LUTs (see the validation ladder in the crate documentation).
§Where this sits
ChainEngine ── three steps in series (depth 0 → 1 → 2) ──▶ four cache nodes
│
└── each step is one ChainStepLane: 20 registered stages, II = 1“ChainEngine” is SliceChainEngine
in crate::slice_chain. “II = 1” (initiation interval of one) means a
new token can enter every clock. Which chain hash is compiled in, and so
whether this lane is used at all, is decided in crate::chain_lane.
The neighbours this file leans on:
| Module | What it gives this file |
|---|---|
crate::haraka | round keys, word mix, truncation, mix_key512_kernel, the dsig_chain oracle |
crate::aes_table | the S-box stages (SboxStages), the UltraRAM loader, the T-table ROMs |
crate::sbox_placement | which of the 494 live S-boxes (LIVE_SBOX) go to UltraRAM or block RAM |
§Reading order
KnownBytes,KNOWN: the forward constant analysis.live_sbox_evaluations,LIVE_SBOX: the backward “is it needed” analysis.ChainStepInput,ChainStepOutput: the lane’s ports.ChainStepLane,chain_step_kernel: the all-S-box lane.TableChainStepLane,table_chain_step_kernel: the same lane with some rounds done by T-table lookups instead.
The lane has the same timing contract as the general pipeline: it accepts a
token every cycle, has no backpressure, and returns it after exactly
CHAIN_STEP_LATENCY registered transfers with its tag unchanged. Reset
clears only the valid bits, so all in-flight tokens are discarded.
Compare it bit-for-bit with dsig_chain using
tests/chain_step.rs.
Structs§
- Chain
Step Input - One chain-step request; an asserted
validis always accepted. - Chain
Step Lane - Twenty-stage, II = 1 DSig chain step with compile-time constant folding.
- Chain
Step Output - The next chain node; all fields except
validare meaningless when invalid. - Known
Bytes - Compile-time knowledge about the state entering one stage.
- Table
Chain Step Lane ChainStepLanewith selected AES rounds moved from LUTs into T-table ROMs.
Constants§
- CHAIN_
STEP_ LATENCY - Registered stages per chain step (two per AES round, as in Haraka).
- KNOWN
KNOWN[s]describes the state entering stages;KNOWN[20]is the final state before feed-forward. Index 0 is the DSig chain block itself.- KNOWN_
MASK - RHDL-typed copy of the masks in
KNOWN, indexed[stage][byte]. - KNOWN_
VALUE - RHDL-typed copy of the constant values in
KNOWN. - LIVE_
SBOX - Per-position live S-boxes (computed by the private
live_sboxes). - ROMS_
PER_ TABLE_ ROUND - Dual-port ROMs per table round: 64 S-boxes, two per ROM.
- STEP_
OUTPUT_ BYTES - DSig keeps this many leading bytes of the standard 32-byte digest.
- TABLE_
ROUND_ FIRST - AES rounds that may use block-RAM T-tables: every byte is live and used.
- TABLE_
ROUND_ SLOTS - Number of table-capable rounds (rounds 3 through 8).
- ZERO_
TAIL_ START - First block byte that is always zero in a DSig chain block.
Functions§
- chain_
step_ block_ kernel - Builds the zero-tailed DSig chain block from its two live fields.
- chain_
step_ digest_ kernel - Final feed-forward: the 18 kept digest bytes of a finished state.
- chain_
step_ feed_ kernel - The 18 input bytes that the feed-forward XORs back into the digest.
- chain_
step_ kernel - Stage wiring for
ChainStepLane. - live_
sbox_ evaluations - Count the S-box evaluations that are neither constant nor unobserved.
- pin_
known_ kernel - Replace the bytes
maskmarks as compile-time constants byvalue. - table_
addresses_ kernel - Present ShiftRows-ordered state bytes to a round’s 32 dual-port ROMs.
- table_
chain_ step_ kernel - Stage wiring for
TableChainStepLane. - table_
combine_ kernel - Combine one round’s 64 table words: rotated XOR per column plus the key.
- valid_
table_ mask - Whether
maskis a legalTableChainStepLanetable selection.