Skip to main content

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}