Skip to main content

Module slice_chain

Module slice_chain 

Source
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§

SliceChainEngine
Three lanes in series plus the metadata that travels beside them.
SliceChainInput
One chain to expand.
SliceChainOutput
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·depth to the nonce’s low 32 bits (no carry into byte 4).
slice_chain_kernel
Wiring for SliceChainEngine: salt, cascade, and carry earlier nodes.