Expand description
A slice’s chain engine: one chain per cycle through three folded lanes.
§Why this block exists
A W-OTS+ chain (here with w = 4) is a short hash chain: secret →
hash → hash → hash. A one-time key has 68 of them, and the
slice_factory makes one key every 68 cycles. So
the slice must finish one chain per cycle, which means three chain
hashes per cycle. Instead of one hash core used three times in a loop
(which would accept a new chain only every third pass), we build three
pipelined lanes and put them in series. A lane is a fully pipelined
hash: it accepts a new input every cycle (initiation interval, II = 1) and
returns each result a fixed number of cycles later.
Every DSig chain starts from an 18-byte secret and is hashed three times,
at depths 0, 1 and 2. The slice keeps all four positions in its cache,
because the signer later reveals whichever position each message digit
selects (reference::PreparedKey::sign).
secret ─▶ [lane depth 0] ─▶ node1 ─▶ [lane depth 1] ─▶ node2 ─▶ [lane depth 2] ─▶ node3
│ 20 cycles │ 20 cycles │ 20 cycles │
└────────── carried beside the hashes, so all four leave together ──────────────────┘Each lane is a ChainLane, whose hash is
chosen at compile time by a chain-* feature (see crate::chain_lane).
With the default Haraka-512 hash it is a
TableChainStepLane (the
T-table form of ChainStepLane),
shaped by the TABLE_MASK area knob. The diagram shows the Haraka lane
latency of 20 cycles; BLAKE3 lanes take 28 and SHA-512 lanes 82.
Each depth salts the nonce differently: the low
32 bits of pk_nonce gain index + 68·depth
(dsig_chain_block). The engine
computes the three salted nonces itself, so the caller supplies only the
raw pk_nonce.
Timing contract: one chain may enter every cycle (II = 1), there is no
backpressure, and results appear exactly SLICE_CHAIN_LATENCY cycles
later in issue order with the tag unchanged. Reset discards every in-flight
chain. The caller must keep the index below 68; the slice controller does
this structurally by counting 0..=67.
§How the earlier nodes keep up
The cache wants all four positions of a chain at once, but each lane only
outputs the newest node. So a carry shift register, one stage per
pipeline cycle, runs beside the lanes. It holds the chain’s earlier nodes,
its pk_nonce and its index. When a lane’s result emerges, it is written
into the carry at that point, so the carry always holds every node
computed so far:
carry stage: 0 ……… L−1 │ L ……… 2L−1 │ 2L ……… 3L−1 │ output
nodes[0]: secret │ secret │ secret │ position 0
nodes[1]: (secret) │ node1 ◀─────┤ node1 │ position 1
nodes[2]: (secret) │ (secret) │ node2 ◀──────┤ position 2
depth-2 lane output │ position 3
(L = LANE_LATENCY; "(secret)" = placeholder, overwritten later)The carry also gives depth 1 and depth 2 the pk_nonce and chain index
they need for their salt, exactly when their input arrives.
Structs§
- Slice
Chain Engine - Three lanes in series plus the metadata that travels beside them.
- Slice
Chain Input - One chain to expand.
- Slice
Chain Output - All four positions of one finished chain.
Constants§
- SLICE_
CHAIN_ LATENCY - Cycles from a chain’s issue until its four nodes are presented.
Functions§
- salt_
kernel - Add
index + 68·depthto the nonce’s low 32 bits (no carry into byte 4). - slice_
chain_ kernel - Wiring for
SliceChainEngine: salt, cascade, and carry earlier nodes.