rhdl_sha512_pipelined/lib.rs
1//! A fully unrolled, fully pipelined SHA-512 compression function in RHDL:
2//! **one 128-byte block per clock**, shared by DSig and Ed25519.
3//!
4//! # Building for Wolverine's U280
5//!
6//! This crate is a reusable RHDL compression pipeline, not a standalone XRT
7//! card kernel. `cargo build --locked -p rhdl_sha512_pipelined` builds its Rust
8//! library; it does not produce a `.xclbin`. Include the pipeline in an enclosing
9//! kernel and generate that kernel's RTL before packaging and linking with
10//! Vivado/Vitis 2023.2 for the installed U280 platform.
11//!
12//! For the existing card flows, follow the front-page instructions for
13//! [Ed25519](../rhdl_ed25519_scalable_core/index.html#building-for-wolverines-u280)
14//! or [DSig](../rhdl_dsig_scalable_core/index.html#building-for-wolverines-u280).
15//! Resource and timing acceptance belongs to the complete routed kernel.
16//!
17//! Run commands from the workspace root. Before compilation or RTL generation,
18//! record ownership in `PROJECT_COORDINATION.md` and
19//! `/home/sahasta/Development/FPGA_COORDINATION.md`, and use the applicable
20//! nonblocking `local-heavy.lock`; `crates/ed25519/tools/bounded.sh` wraps local
21//! Cargo commands with that lock and resource limits. Reserve Wolverine builds
22//! in both ledgers too, then use `crates/dsig/tools/with-u280-lock.sh` to hold
23//! `u280.lock`. Do not bypass a busy lock.
24//!
25//! ```text
26//! ShaIn ──▶ [input] ──▶ g0 ─▶ g1 ─▶ … ─▶ g7 ──▶ [final add] ──▶ ShaOut
27//! p₀ = h+K₀+W₀ (8 groups × 10 round stages = 80 rounds)
28//! ```
29//!
30//! Every SHA-512 round has its own pipeline stage, so a new block can enter
31//! every cycle and leaves [`LATENCY`] cycles later. There is no back-pressure:
32//! [`ShaCoreOut::ready`] is always high and the output must be taken when it
33//! appears (clients keep a credit counter; see the Ed25519 crate's
34//! `CompressStage` for the pattern).
35//!
36//! # Why this crate exists
37//!
38//! **Problem.** Ed25519 hashes with SHA-512: three blocks per signature and
39//! two per verification. Its first design gave each module a small private
40//! SHA ring that needs many cycles per block. Eleven such rings cost about
41//! 50k LUTs and still ran slowly.
42//!
43//! **Reasoning.** SHA-512 is 80 rounds of the same simple step. Unrolling
44//! every round into its own pipeline stage costs more LUTs in one place, but
45//! then a single unit finishes a block every clock. One fast unit, shared
46//! by every client, beats many slow private ones.
47//!
48//! **Decision.** One fully pipelined core, on its own *SHA line* whose
49//! allowance comes out of the chip budget. Lines that need less than a
50//! block per clock use [`folded`] cores instead (rings of 2–40 stages, 1/40
51//! to ½ block per clock), and the planner sizes the line to what the chip
52//! wants by default (a manual line is 65,000 LUTs and 20 DSPs by default; the 100 % budget of 485k LUTs and
53//! 4,520 DSPs is the engines' 420k / 4,500 plus this default). Ed25519 signing and verification share it through an arbiter.
54//! DSig's SHA-512 chain profile (`DSIG_CHAIN=sha512` in
55//! `rhdl_dsig_scalable_core`) instantiates its own copies inside its slices,
56//! because each slice needs three blocks per clock.
57//!
58//! **Status.** Bit-exact against `sha2` in simulation at full rate. The LUT
59//! cost (about 43,200 for one core) is an **estimate** until synthesized.
60//!
61//! # Making one round fit one clock
62//!
63//! A round is
64//!
65//! ```text
66//! T1 = h + Σ1(e) + Ch(e, f, g) + K_t + W_t T2 = Σ0(a) + Maj(a, b, c)
67//! a' = T1 + T2 e' = d + T1
68//! ```
69//!
70//! Five operands feed `T1`. Three of them, `h + K_t + W_t`, are known one
71//! round early: `h` of round `t` is `g` of round `t − 1`, `K_t` is a constant
72//! and `W_t` is already in the message window. So each stage also computes
73//! `p = g + K_{t+1} + W_{t+1}` for the *next* stage ([`Stage::p`]), and the
74//! critical path of a round is only `p + Σ1(e) + Ch` followed by one more add.
75//!
76//! The message schedule is a 16-word window that shifts one word per round;
77//! word `t + 16` is computed in the stage that consumes word `t`. Constant
78//! inputs (for example DSig's zero-padded chain blocks) and unused late
79//! schedule words are removed by synthesis.
80//!
81//! # Ports
82//!
83//! [`ShaIn`]: `valid`, a 16-bit `tag`, the block as 16 big-endian 64-bit
84//! words, and the chaining value: the standard IV when `use_iv`, else `chain`
85//! (so a multi-block message is resubmitted block by block). [`ShaOut`]: the
86//! new chaining value `chain + compress(chain, block)` and the tag, in order.
87//!
88//! # Validation
89//!
90//! `tests/compress.rs` compares every output with `sha2::compress512` on random
91//! blocks, chained two-block messages, and a block every cycle (full rate),
92//! and checks [`LATENCY`].
93//!
94//! # Where to go next
95//!
96//! | You want to… | Start here |
97//! |---|---|
98//! | understand one round | [`round_kernel`], then [`Stage`] |
99//! | see how rounds become a pipeline | [`RoundGroup`], [`Sha512Pipeline`] |
100//! | change the line's size in the planner | [`planner`] (feature `planner`) |
101//! | share the core between clients | the Ed25519 crate's `sha_client` (arbiter) |
102//! | hash DSig chain steps with it | `rhdl_dsig_scalable_core::chain_lane` |
103//! | build a line smaller than one block per clock | [`folded`]: rings of 2–40 stages, 1/40 to 1/2 block per clock |
104//! | reach above one block per clock | open problem: a fixed-length single-block variant, DSP adders |
105#![forbid(unsafe_code)]
106
107use rhdl::prelude::*;
108use rhdl_fpga::core::dff::DFF;
109
110pub mod folded;
111#[cfg(feature = "planner")]
112pub mod planner;
113
114/// Cycles from accepting a block to presenting its result: one input
115/// register, 80 round stages and one output register (the final addition).
116pub const LATENCY: usize = 82;
117
118/// SHA-512 initial chaining value.
119pub const IV: [b64; 8] = [
120 b64(0x6a09e667f3bcc908),
121 b64(0xbb67ae8584caa73b),
122 b64(0x3c6ef372fe94f82b),
123 b64(0xa54ff53a5f1d36f1),
124 b64(0x510e527fade682d1),
125 b64(0x9b05688c2b3e6c1f),
126 b64(0x1f83d9abfb41bd6b),
127 b64(0x5be0cd19137e2179),
128];
129
130/// Round constants `K_0..K_79`, plus a zero `K_80` so the last stage's
131/// look-ahead has something to read.
132pub const K: [b64; 81] = [
133 b64(0x428a2f98d728ae22), b64(0x7137449123ef65cd), b64(0xb5c0fbcfec4d3b2f), b64(0xe9b5dba58189dbbc),
134 b64(0x3956c25bf348b538), b64(0x59f111f1b605d019), b64(0x923f82a4af194f9b), b64(0xab1c5ed5da6d8118),
135 b64(0xd807aa98a3030242), b64(0x12835b0145706fbe), b64(0x243185be4ee4b28c), b64(0x550c7dc3d5ffb4e2),
136 b64(0x72be5d74f27b896f), b64(0x80deb1fe3b1696b1), b64(0x9bdc06a725c71235), b64(0xc19bf174cf692694),
137 b64(0xe49b69c19ef14ad2), b64(0xefbe4786384f25e3), b64(0x0fc19dc68b8cd5b5), b64(0x240ca1cc77ac9c65),
138 b64(0x2de92c6f592b0275), b64(0x4a7484aa6ea6e483), b64(0x5cb0a9dcbd41fbd4), b64(0x76f988da831153b5),
139 b64(0x983e5152ee66dfab), b64(0xa831c66d2db43210), b64(0xb00327c898fb213f), b64(0xbf597fc7beef0ee4),
140 b64(0xc6e00bf33da88fc2), b64(0xd5a79147930aa725), b64(0x06ca6351e003826f), b64(0x142929670a0e6e70),
141 b64(0x27b70a8546d22ffc), b64(0x2e1b21385c26c926), b64(0x4d2c6dfc5ac42aed), b64(0x53380d139d95b3df),
142 b64(0x650a73548baf63de), b64(0x766a0abb3c77b2a8), b64(0x81c2c92e47edaee6), b64(0x92722c851482353b),
143 b64(0xa2bfe8a14cf10364), b64(0xa81a664bbc423001), b64(0xc24b8b70d0f89791), b64(0xc76c51a30654be30),
144 b64(0xd192e819d6ef5218), b64(0xd69906245565a910), b64(0xf40e35855771202a), b64(0x106aa07032bbd1b8),
145 b64(0x19a4c116b8d2d0c8), b64(0x1e376c085141ab53), b64(0x2748774cdf8eeb99), b64(0x34b0bcb5e19b48a8),
146 b64(0x391c0cb3c5c95a63), b64(0x4ed8aa4ae3418acb), b64(0x5b9cca4f7763e373), b64(0x682e6ff3d6b2b8a3),
147 b64(0x748f82ee5defb2fc), b64(0x78a5636f43172f60), b64(0x84c87814a1f0ab72), b64(0x8cc702081a6439ec),
148 b64(0x90befffa23631e28), b64(0xa4506cebde82bde9), b64(0xbef9a3f7b2c67915), b64(0xc67178f2e372532b),
149 b64(0xca273eceea26619c), b64(0xd186b8c721c0c207), b64(0xeada7dd6cde0eb1e), b64(0xf57d4f7fee6ed178),
150 b64(0x06f067aa72176fba), b64(0x0a637dc5a2c898a6), b64(0x113f9804bef90dae), b64(0x1b710b35131c471b),
151 b64(0x28db77f523047d84), b64(0x32caab7b40c72493), b64(0x3c9ebe0a15c9bebc), b64(0x431d67c49c100d4c),
152 b64(0x4cc5d4becb3e42b6), b64(0x597f299cfc657e2a), b64(0x5fcb6fab3ad6faec), b64(0x6c44198c4a475817),
153 b64(0),
154];
155
156/// One block request.
157#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
158pub struct ShaIn {
159 /// A block is presented this cycle (always accepted).
160 pub valid: bool,
161 /// Returned unchanged with the result.
162 pub tag: b16,
163 /// The 128-byte block as 16 big-endian words.
164 pub block: [b64; 16],
165 /// Start from the standard [`IV`] instead of `chain`.
166 pub use_iv: bool,
167 /// Chaining value when `use_iv` is low.
168 pub chain: [b64; 8],
169}
170
171/// One finished block.
172#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
173pub struct ShaOut {
174 /// Present this cycle (must be taken; there is no back-pressure).
175 pub valid: bool,
176 /// The request's tag.
177 pub tag: b16,
178 /// New chaining value (the digest after the final block).
179 pub state: [b64; 8],
180}
181
182/// Core outputs.
183#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
184pub struct ShaCoreOut {
185 /// A request is accepted this cycle (always true for this core).
186 pub ready: bool,
187 /// The block that entered [`LATENCY`] cycles ago.
188 pub done: ShaOut,
189}
190
191/// The state carried between round stages.
192#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
193pub struct Stage {
194 /// A block occupies this stage.
195 pub valid: bool,
196 /// Its tag.
197 pub tag: b16,
198 /// The chaining value, kept for the final addition.
199 pub chain: [b64; 8],
200 /// Working variables `a..h`.
201 pub work: [b64; 8],
202 /// Message window: `w[0]` is this round's `W_t`.
203 pub w: [b64; 16],
204 /// `h + K_t + W_t` for this round, computed by the previous stage.
205 pub p: b64,
206}
207
208/// `Σ0(a) = rotr(a,28) ^ rotr(a,34) ^ rotr(a,39)`.
209#[kernel]
210pub fn big_sigma0(a: b64) -> b64 {
211 ((a >> 28) | (a << 36)) ^ ((a >> 34) | (a << 30)) ^ ((a >> 39) | (a << 25))
212}
213
214/// `Σ1(e) = rotr(e,14) ^ rotr(e,18) ^ rotr(e,41)`.
215#[kernel]
216pub fn big_sigma1(e: b64) -> b64 {
217 ((e >> 14) | (e << 50)) ^ ((e >> 18) | (e << 46)) ^ ((e >> 41) | (e << 23))
218}
219
220/// `σ0(x) = rotr(x,1) ^ rotr(x,8) ^ (x >> 7)`.
221#[kernel]
222pub fn small_sigma0(x: b64) -> b64 {
223 ((x >> 1) | (x << 63)) ^ ((x >> 8) | (x << 56)) ^ (x >> 7)
224}
225
226/// `σ1(x) = rotr(x,19) ^ rotr(x,61) ^ (x >> 6)`.
227#[kernel]
228pub fn small_sigma1(x: b64) -> b64 {
229 ((x >> 19) | (x << 45)) ^ ((x >> 61) | (x << 3)) ^ (x >> 6)
230}
231
232/// One SHA-512 round on `s`, plus the look-ahead `p` for the next round
233/// (whose constant is `k_next`).
234#[kernel]
235pub fn round_kernel(s: Stage, k_next: b64) -> Stage {
236 // The eight working variables, named as in FIPS 180-4. `h` is not
237 // needed here: it already sits inside `s.p = h + K_t + W_t`.
238 let a = s.work[0];
239 let b = s.work[1];
240 let c = s.work[2];
241 let d = s.work[3];
242 let e = s.work[4];
243 let f = s.work[5];
244 let g = s.work[6];
245 // "Choose": each bit of e picks f (1) or g (0).
246 let ch = (e & f) ^ (!e & g);
247 // "Majority": each bit is the majority of a, b and c.
248 let maj = (a & b) ^ (a & c) ^ (b & c);
249 // Only three operands remain on the critical path thanks to `p`.
250 let t1 = s.p + big_sigma1(e) + ch;
251 let t2 = big_sigma0(a) + maj;
252 // Message schedule: W_{t+16} = σ1(W_{t+14}) + W_{t+9} + σ0(W_{t+1}) + W_t.
253 // Late rounds compute words nobody reads; synthesis deletes them.
254 let next_word = small_sigma1(s.w[14]) + s.w[9] + small_sigma0(s.w[1]) + s.w[0];
255 // Shift the 16-word window by one and append the new word.
256 let w = [
257 s.w[1], s.w[2], s.w[3], s.w[4], s.w[5], s.w[6], s.w[7], s.w[8], s.w[9], s.w[10], s.w[11],
258 s.w[12], s.w[13], s.w[14], s.w[15], next_word,
259 ];
260 Stage {
261 valid: s.valid,
262 tag: s.tag,
263 chain: s.chain,
264 work: [t1 + t2, a, b, c, d + t1, e, f, g],
265 w,
266 // Next round: h is this round's g, and W is the window's new head.
267 p: g + k_next + w[0],
268 }
269}
270
271/// One registered round.
272#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
273pub struct RoundStage {
274 /// The pipeline register holding this round's result.
275 reg: DFF<Stage>,
276}
277
278impl Default for RoundStage {
279 fn default() -> Self {
280 Self {
281 reg: DFF::new(Stage::default()),
282 }
283 }
284}
285
286/// A round stage's input: the state and the next round's constant.
287#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
288pub struct RoundIn {
289 /// State entering the round.
290 pub stage: Stage,
291 /// `K_{t+1}`.
292 pub k_next: b64,
293}
294
295impl SynchronousIO for RoundStage {
296 type I = RoundIn;
297 type O = Stage;
298 type Kernel = round_stage_kernel;
299}
300
301/// Compute the round and register it.
302#[kernel]
303pub fn round_stage_kernel(_cr: ClockReset, input: RoundIn, q: RoundStageQ) -> (Stage, RoundStageD) {
304 (
305 q.reg,
306 RoundStageD {
307 reg: round_kernel(input.stage, input.k_next),
308 },
309 )
310}
311
312/// Ten consecutive rounds, `10·G .. 10·G + 10`.
313#[derive(Clone, Debug, Synchronous, SynchronousDQ, Default)]
314pub struct RoundGroup<const G: usize> {
315 // Rounds 10·G + 0 … 10·G + 9, in order. Groups of ten keep each RHDL
316 // struct small (RHDL handles about eleven synchronous children per
317 // struct comfortably), and eight groups make the 80 rounds.
318 /// Round `10·G`.
319 r0: RoundStage,
320 /// Round `10·G + 1`.
321 r1: RoundStage,
322 /// Round `10·G + 2`.
323 r2: RoundStage,
324 /// Round `10·G + 3`.
325 r3: RoundStage,
326 /// Round `10·G + 4`.
327 r4: RoundStage,
328 /// Round `10·G + 5`.
329 r5: RoundStage,
330 /// Round `10·G + 6`.
331 r6: RoundStage,
332 /// Round `10·G + 7`.
333 r7: RoundStage,
334 /// Round `10·G + 8`.
335 r8: RoundStage,
336 /// Round `10·G + 9`.
337 r9: RoundStage,
338}
339
340impl<const G: usize> SynchronousIO for RoundGroup<G> {
341 type I = Stage;
342 type O = Stage;
343 type Kernel = round_group_kernel<G>;
344}
345
346/// Wire the ten rounds in series, each with its look-ahead constant.
347#[kernel]
348pub fn round_group_kernel<const G: usize>(
349 _cr: ClockReset,
350 input: Stage,
351 q: RoundGroupQ<G>,
352) -> (Stage, RoundGroupD<G>) {
353 let d = RoundGroupD::<G> {
354 r0: RoundIn { stage: input, k_next: K[10 * G + 1] },
355 r1: RoundIn { stage: q.r0, k_next: K[10 * G + 2] },
356 r2: RoundIn { stage: q.r1, k_next: K[10 * G + 3] },
357 r3: RoundIn { stage: q.r2, k_next: K[10 * G + 4] },
358 r4: RoundIn { stage: q.r3, k_next: K[10 * G + 5] },
359 r5: RoundIn { stage: q.r4, k_next: K[10 * G + 6] },
360 r6: RoundIn { stage: q.r5, k_next: K[10 * G + 7] },
361 r7: RoundIn { stage: q.r6, k_next: K[10 * G + 8] },
362 r8: RoundIn { stage: q.r7, k_next: K[10 * G + 9] },
363 r9: RoundIn { stage: q.r8, k_next: K[10 * G + 10] },
364 };
365 (q.r9, d)
366}
367
368/// The whole core: input stage, 80 rounds, final addition.
369#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
370pub struct Sha512Pipeline {
371 /// Input register: the block, its chaining value and the first `p`.
372 input: DFF<Stage>,
373 /// Rounds 0 … 9.
374 g0: RoundGroup<0>,
375 /// Rounds 10 … 19.
376 g1: RoundGroup<1>,
377 /// Rounds 20 … 29.
378 g2: RoundGroup<2>,
379 /// Rounds 30 … 39.
380 g3: RoundGroup<3>,
381 /// Rounds 40 … 49.
382 g4: RoundGroup<4>,
383 /// Rounds 50 … 59.
384 g5: RoundGroup<5>,
385 /// Rounds 60 … 69.
386 g6: RoundGroup<6>,
387 /// Rounds 70 … 79.
388 g7: RoundGroup<7>,
389 /// Output register: the chaining value plus the working variables.
390 out: DFF<ShaOut>,
391}
392
393impl Default for Sha512Pipeline {
394 fn default() -> Self {
395 Self {
396 input: DFF::new(Stage::default()),
397 g0: RoundGroup::default(),
398 g1: RoundGroup::default(),
399 g2: RoundGroup::default(),
400 g3: RoundGroup::default(),
401 g4: RoundGroup::default(),
402 g5: RoundGroup::default(),
403 g6: RoundGroup::default(),
404 g7: RoundGroup::default(),
405 out: DFF::new(ShaOut::default()),
406 }
407 }
408}
409
410impl SynchronousIO for Sha512Pipeline {
411 type I = ShaIn;
412 type O = ShaCoreOut;
413 type Kernel = sha512_pipeline_kernel;
414}
415
416/// Load a block, run the groups in series, add the chaining value.
417#[kernel]
418pub fn sha512_pipeline_kernel(
419 cr: ClockReset,
420 input: ShaIn,
421 q: Sha512PipelineQ,
422) -> (ShaCoreOut, Sha512PipelineD) {
423 // During reset no block may enter or leave, so stale stages drain out
424 // as invalid.
425 let resetting = cr.reset.any();
426 // The chaining value: the standard IV for a message's first block,
427 // otherwise the previous block's result supplied by the client.
428 let chain = if input.use_iv { IV } else { input.chain };
429 // Round 0 starts with a..h = chain, and its look-ahead
430 // p = h + K_0 + W_0 is computed here, one stage early.
431 let entering = Stage {
432 valid: input.valid && !resetting,
433 tag: input.tag,
434 chain,
435 work: chain,
436 w: input.block,
437 p: chain[7] + K[0] + input.block[0],
438 };
439 // The block leaving round 79, and the feed-forward addition that turns
440 // the working variables into the new chaining value.
441 let last = q.g7;
442 let state = [
443 last.chain[0] + last.work[0],
444 last.chain[1] + last.work[1],
445 last.chain[2] + last.work[2],
446 last.chain[3] + last.work[3],
447 last.chain[4] + last.work[4],
448 last.chain[5] + last.work[5],
449 last.chain[6] + last.work[6],
450 last.chain[7] + last.work[7],
451 ];
452 let d = Sha512PipelineD {
453 input: entering,
454 g0: q.input,
455 g1: q.g0,
456 g2: q.g1,
457 g3: q.g2,
458 g4: q.g3,
459 g5: q.g4,
460 g6: q.g5,
461 g7: q.g6,
462 out: ShaOut {
463 valid: last.valid && !resetting,
464 tag: last.tag,
465 state,
466 },
467 };
468 (
469 ShaCoreOut {
470 ready: true,
471 done: q.out,
472 },
473 d,
474 )
475}