rhdl_dsig_scalable_core/slice_chain.rs
1//! A slice's chain engine: one chain per cycle through three folded lanes.
2//!
3//! # Why this block exists
4//!
5//! A W-OTS+ chain (here with `w = 4`) is a short hash chain: secret →
6//! hash → hash → hash. A one-time key has 68 of them, and the
7//! [`slice_factory`](crate::slice_factory) makes one key every 68 cycles. So
8//! the slice must finish **one chain per cycle**, which means three chain
9//! hashes per cycle. Instead of one hash core used three times in a loop
10//! (which would accept a new chain only every third pass), we build three
11//! pipelined *lanes* and put them in series. A *lane* is a fully pipelined
12//! hash: it accepts a new input every cycle (initiation interval, II = 1) and
13//! returns each result a fixed number of cycles later.
14//!
15//! Every DSig chain starts from an 18-byte secret and is hashed three times,
16//! at depths 0, 1 and 2. The slice keeps **all four** positions in its cache,
17//! because the signer later reveals whichever position each message digit
18//! selects ([`reference::PreparedKey::sign`](crate::reference::PreparedKey)).
19//!
20//! ```text
21//! secret ─▶ [lane depth 0] ─▶ node1 ─▶ [lane depth 1] ─▶ node2 ─▶ [lane depth 2] ─▶ node3
22//! │ 20 cycles │ 20 cycles │ 20 cycles │
23//! └────────── carried beside the hashes, so all four leave together ──────────────────┘
24//! ```
25//!
26//! Each lane is a [`ChainLane`](crate::chain_lane::ChainLane), whose hash is
27//! chosen at compile time by a `chain-*` feature (see [`crate::chain_lane`]).
28//! With the default Haraka-512 hash it is a
29//! [`TableChainStepLane`](crate::chain_step::TableChainStepLane) (the
30//! T-table form of [`ChainStepLane`](crate::chain_step::ChainStepLane)),
31//! shaped by the `TABLE_MASK` area knob. The diagram shows the Haraka lane
32//! latency of 20 cycles; BLAKE3 lanes take 28 and SHA-512 lanes 82.
33//!
34//! Each depth salts the nonce differently: the low
35//! 32 bits of `pk_nonce` gain `index + 68·depth`
36//! ([`dsig_chain_block`](crate::haraka::dsig_chain_block)). The engine
37//! computes the three salted nonces itself, so the caller supplies only the
38//! raw `pk_nonce`.
39//!
40//! Timing contract: one chain may enter every cycle (II = 1), there is no
41//! backpressure, and results appear exactly [`SLICE_CHAIN_LATENCY`] cycles
42//! later in issue order with the tag unchanged. Reset discards every in-flight
43//! chain. The caller must keep the index below 68; the slice controller does
44//! this structurally by counting 0..=67.
45//!
46//! # How the earlier nodes keep up
47//!
48//! The cache wants all four positions of a chain at once, but each lane only
49//! outputs the newest node. So a *carry* shift register, one stage per
50//! pipeline cycle, runs beside the lanes. It holds the chain's earlier nodes,
51//! its `pk_nonce` and its index. When a lane's result emerges, it is written
52//! into the carry at that point, so the carry always holds every node
53//! computed so far:
54//!
55//! ```text
56//! carry stage: 0 ……… L−1 │ L ……… 2L−1 │ 2L ……… 3L−1 │ output
57//! nodes[0]: secret │ secret │ secret │ position 0
58//! nodes[1]: (secret) │ node1 ◀─────┤ node1 │ position 1
59//! nodes[2]: (secret) │ (secret) │ node2 ◀──────┤ position 2
60//! depth-2 lane output │ position 3
61//! (L = LANE_LATENCY; "(secret)" = placeholder, overwritten later)
62//! ```
63//!
64//! The carry also gives depth 1 and depth 2 the `pk_nonce` and chain index
65//! they need for their salt, exactly when their input arrives.
66
67use crate::chain_lane::{ChainLane, LANE_LATENCY as CHAIN_STEP_LATENCY};
68use crate::chain_step::ChainStepInput;
69use crate::{CHAINS, ELEMENT_BYTES};
70use rhdl::prelude::*;
71use rhdl_fpga::core::dff::DFF;
72
73/// Cycles from a chain's issue until its four nodes are presented.
74///
75/// Three lanes in series: 3 × 20 = 60 with Haraka, 3 × 28 = 84 with BLAKE3,
76/// 3 × 82 = 246 with SHA-512. The factory adds this to its
77/// [`END_DELAY`](crate::slice_factory::END_DELAY).
78pub const SLICE_CHAIN_LATENCY: usize = 3 * CHAIN_STEP_LATENCY;
79
80/// One chain to expand.
81///
82/// The factory presents one of these per cycle (or `valid = false`).
83#[derive(Clone, Copy, Debug, PartialEq, Digital)]
84pub struct SliceChainInput {
85 /// Position-0 secret for this chain (bytes `18i..18i+18` of the key's XOF).
86 pub secret: [b8; ELEMENT_BYTES],
87 /// The key's public 16-byte nonce, unsalted.
88 pub pk_nonce: [b8; 16],
89 /// Chain number `0..68` within the key.
90 pub index: b7,
91 /// Opaque routing tag (key slot and chain), returned unchanged.
92 pub tag: b32,
93 /// A chain enters this cycle.
94 pub valid: bool,
95}
96
97/// An idle input: zeros and `valid = false`.
98impl Default for SliceChainInput {
99 /// Returns the idle input.
100 fn default() -> Self {
101 Self {
102 secret: [b8(0); ELEMENT_BYTES],
103 pk_nonce: [b8(0); 16],
104 index: b7(0),
105 tag: b32(0),
106 valid: false,
107 }
108 }
109}
110
111/// All four positions of one finished chain.
112///
113/// Appears [`SLICE_CHAIN_LATENCY`] cycles after the matching input.
114#[derive(Clone, Copy, Debug, PartialEq, Digital)]
115pub struct SliceChainOutput {
116 /// Positions 0 (secret) through 3 (public chain end).
117 pub nodes: [[b8; ELEMENT_BYTES]; 4],
118 /// The input tag.
119 pub tag: b32,
120 /// Four valid nodes are presented this cycle.
121 pub valid: bool,
122 /// All three lanes have loaded their UltraRAM S-boxes.
123 pub ready: bool,
124}
125
126/// One stage of the carry shift register: what travels beside the lanes.
127#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
128#[doc(hidden)] // Public only for the derived Synchronous associated state.
129pub struct ChainCarry {
130 /// Positions 0, 1 and 2 of the chain so far (18 bytes each). Positions
131 /// not computed yet hold a copy of the secret as a placeholder.
132 nodes: [[b8; ELEMENT_BYTES]; 3],
133 /// The key's unsalted `pk_nonce`, for the salts of depths 1 and 2.
134 pk_nonce: [b8; 16],
135 /// Chain number 0..68, also for the salts.
136 index: b7,
137}
138
139/// Three lanes in series plus the metadata that travels beside them.
140///
141/// `TABLE_MASK` is passed to each lane unchanged (see
142/// [`crate::chain_lane::ChainLane`]).
143#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
144pub struct SliceChainEngine<const TABLE_MASK: usize> {
145 /// First hash: secret (position 0) → position 1.
146 depth0: ChainLane<TABLE_MASK>,
147 /// Second hash: position 1 → position 2.
148 depth1: ChainLane<TABLE_MASK>,
149 /// Third hash: position 2 → position 3 (the public chain end).
150 depth2: ChainLane<TABLE_MASK>,
151 /// The carry shift register, [`SLICE_CHAIN_LATENCY`] stages long. Not
152 /// reset: its contents matter only where `valid` says so.
153 carry: [DFF<ChainCarry>; SLICE_CHAIN_LATENCY],
154 /// A valid bit per carry stage, cleared by reset.
155 valid: [DFF<bool>; SLICE_CHAIN_LATENCY],
156}
157
158/// An empty engine: no chains in flight.
159impl<const TABLE_MASK: usize> Default for SliceChainEngine<TABLE_MASK> {
160 /// Builds the three lanes, a don't-care carry and all-false valid bits.
161 fn default() -> Self {
162 Self {
163 depth0: ChainLane::default(),
164 depth1: ChainLane::default(),
165 depth2: ChainLane::default(),
166 carry: core::array::from_fn(|_| DFF::resetless()),
167 valid: core::array::from_fn(|_| DFF::new(false)),
168 }
169 }
170}
171
172/// Connects [`SliceChainEngine`] to its port types and wiring kernel.
173impl<const TABLE_MASK: usize> SynchronousIO for SliceChainEngine<TABLE_MASK> {
174 /// One chain per cycle.
175 type I = SliceChainInput;
176 /// One finished chain per cycle.
177 type O = SliceChainOutput;
178 /// The wiring, [`slice_chain_kernel`].
179 type Kernel = slice_chain_kernel<TABLE_MASK>;
180}
181
182/// Add `index + 68·depth` to the nonce's low 32 bits (no carry into byte 4).
183///
184/// Why salt: every hash call in the scheme gets a distinct input, so an
185/// attacker cannot attack all chains at once. The value `index + 68·depth`
186/// is unique for each of the 68 × 3 hash calls of a key.
187///
188/// Inputs: the 16-byte `pk_nonce`, the chain index (7 bits) and the depth
189/// (2 bits, 0..=2). Output: the salted nonce. Combinational (a 32-bit adder).
190#[kernel]
191pub fn salt_kernel(pk_nonce: [b8; 16], index: b7, depth: b2) -> [b8; 16] {
192 // Bytes 4..16 pass through unchanged.
193 let mut salted = pk_nonce;
194 // Bytes 0..4 as a little-endian 32-bit number.
195 let low: b32 = pk_nonce[0].resize()
196 | (pk_nonce[1].resize::<32>() << 8)
197 | (pk_nonce[2].resize::<32>() << 16)
198 | (pk_nonce[3].resize::<32>() << 24);
199 let offset: b32 = index.resize::<32>() + b32(CHAINS as u128) * depth.resize::<32>();
200 // Wraps modulo 2^32: the carry never reaches byte 4.
201 let sum = low + offset;
202 salted[0] = sum.resize();
203 salted[1] = (sum >> 8).resize();
204 salted[2] = (sum >> 16).resize();
205 salted[3] = (sum >> 24).resize();
206 salted
207}
208
209/// Wiring for [`SliceChainEngine`]: salt, cascade, and carry earlier nodes.
210///
211/// Timing, for a chain presented at cycle `t` (L = lane latency):
212///
213/// | Cycle | Event |
214/// |---|---|
215/// | `t` | enters depth 0 with salt `index`; carry stage 0 loaded |
216/// | `t + L` | depth 0 result enters depth 1 (salt `index + 68`) and carry stage L |
217/// | `t + 2L` | depth 1 result enters depth 2 (salt `index + 136`) and carry stage 2L |
218/// | `t + 3L` | depth 2 result and the carried nodes 0..2 leave together |
219///
220/// Output `valid` is forced low during reset; `ready` is the AND of the
221/// three lanes' ready flags (UltraRAM S-boxes loaded).
222#[allow(clippy::needless_range_loop)] // Compile-time pipeline elaboration.
223#[kernel]
224pub fn slice_chain_kernel<const TABLE_MASK: usize>(
225 clock_reset: ClockReset,
226 input: SliceChainInput,
227 q: SliceChainEngineQ<TABLE_MASK>,
228) -> (SliceChainOutput, SliceChainEngineD<TABLE_MASK>) {
229 let resetting = clock_reset.reset.any();
230 // Metadata leaving each lane boundary, aligned with that lane's output.
231 let at1 = q.carry[CHAIN_STEP_LATENCY - 1];
232 let at2 = q.carry[2 * CHAIN_STEP_LATENCY - 1];
233 let at3 = q.carry[SLICE_CHAIN_LATENCY - 1];
234 // Next inputs of the three lanes and the shift registers.
235 let mut d = SliceChainEngineD::<TABLE_MASK> {
236 // Depth 0 takes the new chain straight from the input.
237 depth0: ChainStepInput {
238 salted_nonce: salt_kernel(input.pk_nonce, input.index, b2(0)),
239 secret: input.secret,
240 tag: input.tag,
241 valid: input.valid,
242 },
243 // Depth 1 takes depth 0's output, with metadata from the carry.
244 depth1: ChainStepInput {
245 salted_nonce: salt_kernel(at1.pk_nonce, at1.index, b2(1)),
246 secret: q.depth0.node,
247 tag: q.depth0.tag,
248 valid: q.depth0.valid,
249 },
250 // Depth 2 takes depth 1's output, likewise.
251 depth2: ChainStepInput {
252 salted_nonce: salt_kernel(at2.pk_nonce, at2.index, b2(2)),
253 secret: q.depth1.node,
254 tag: q.depth1.tag,
255 valid: q.depth1.valid,
256 },
257 // Placeholders; every stage is overwritten just below.
258 carry: q.carry,
259 valid: q.valid,
260 };
261 // The new chain enters the carry. Nodes 1 and 2 hold the secret only
262 // as filler until their lanes produce them.
263 d.carry[0] = ChainCarry {
264 nodes: [input.secret, input.secret, input.secret],
265 pk_nonce: input.pk_nonce,
266 index: input.index,
267 };
268 // Shift every carry stage forward by one.
269 for stage in 1..SLICE_CHAIN_LATENCY {
270 d.carry[stage] = q.carry[stage - 1];
271 }
272 // Capture each lane's result into the carried node list as it emerges.
273 d.carry[CHAIN_STEP_LATENCY].nodes[1] = q.depth0.node;
274 d.carry[2 * CHAIN_STEP_LATENCY].nodes[2] = q.depth1.node;
275 // The valid bits shift in step with the carry.
276 d.valid[0] = input.valid;
277 for stage in 1..SLICE_CHAIN_LATENCY {
278 d.valid[stage] = q.valid[stage - 1];
279 }
280 // Output: carried positions 0..2 plus depth 2's result as position 3.
281 (
282 SliceChainOutput {
283 nodes: [at3.nodes[0], at3.nodes[1], at3.nodes[2], q.depth2.node],
284 tag: q.depth2.tag,
285 valid: q.valid[SLICE_CHAIN_LATENCY - 1] && !resetting,
286 ready: q.depth0.ready && q.depth1.ready && q.depth2.ready,
287 },
288 d,
289 )
290}