Skip to main content

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}