Skip to main content

wots_rhdl/
sequencer.rs

1//! Historical single-context SHA-profile WOTS task sequencer.
2//!
3//! This controller intentionally permits only one outstanding compression
4//! request. It is the correctness baseline for dependency handling, tag
5//! validation, and output-credit ownership; it is not the throughput target.
6//!
7//! This module predates the selected lane-local production architecture. The
8//! active SHA signer uses twelve fine-grained
9//! [`crate::engine::SingleContextWorkEngine`] instances: four share one lane in
10//! each [`crate::cluster::LaneLocalCluster`], and [`crate::top::Sha256SignerTop`]
11//! replicates that complete cluster three times. Those contexts may overlap
12//! independent masks, segment PRFs, chains, and public-key blocks. They preserve
13//! this baseline's exact task counts, canonical tags, response validation, and
14//! output-credit ownership, but not its total serial order.
15//!
16//! Keep this sequencer as a small synthesis and control oracle. Do not compose
17//! nine copies and describe that result as the current performance top.
18
19use rhdl::prelude::*;
20use rhdl_fpga::core::dff::DFF;
21
22use crate::tag::{TagFields, decode_tag_kernel, encode_tag_kernel, tag_is_well_formed_kernel};
23
24/// Context count used by the superseded central-scheduler experiment.
25///
26/// The public name is retained for source compatibility with the historical
27/// tests. It is not the context count of the active production signer, which
28/// contains three clusters with four contexts each.
29pub const FUTURE_CONTEXTS: usize = 9;
30
31/// Start, compression-lane handshake, and output-credit inputs.
32#[allow(clippy::struct_excessive_bools)] // Each bool is a distinct hardware port.
33#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
34pub struct SequencerInput {
35    /// Request a new fused private-seed operation.
36    pub start_valid: bool,
37    /// Four-bit context identifier embedded in every compression tag.
38    pub context: b4,
39    /// Generation identifier embedded in every compression tag.
40    pub generation: b8,
41    /// Whether downstream has a result-buffer credit available to reserve.
42    pub output_credit_available: bool,
43    /// Compression request acceptance from the lane arbiter.
44    pub request_ready: bool,
45    /// Compression response is present.
46    pub response_valid: bool,
47    /// Opaque response tag returned unchanged by the compression lane.
48    pub response_tag: b32,
49}
50
51/// RHDL-visible exact retirement counters.
52#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
53pub struct HardwareTaskCounts {
54    /// Private-seed blocks retired.
55    pub seed: b11,
56    /// Public-seed blocks retired.
57    pub public_seed: b11,
58    /// Mask blocks retired.
59    pub mask: b11,
60    /// Segment PRF blocks retired.
61    pub segment_prf: b11,
62    /// Secret data blocks retired.
63    pub secret_data: b11,
64    /// Secret padding blocks retired.
65    pub secret_padding: b11,
66    /// Chain blocks retired.
67    pub chain: b11,
68    /// Public-key blocks retired.
69    pub public_key: b11,
70}
71
72/// Pack eight 11-bit counters into one scalar register value.
73///
74/// Keeping this state in a single `b88` avoids exceeding the synchronous derive
75/// macro's child-state tuple arity after digital structs are flattened.
76#[kernel]
77pub fn encode_hardware_counts_kernel(counts: HardwareTaskCounts) -> b88 {
78    let seed: b88 = counts.seed.resize();
79    let public_seed: b88 = counts.public_seed.resize();
80    let mask: b88 = counts.mask.resize();
81    let segment_prf: b88 = counts.segment_prf.resize();
82    let secret_data: b88 = counts.secret_data.resize();
83    let secret_padding: b88 = counts.secret_padding.resize();
84    let chain: b88 = counts.chain.resize();
85    let public_key: b88 = counts.public_key.resize();
86    seed | (public_seed << 11)
87        | (mask << 22)
88        | (segment_prf << 33)
89        | (secret_data << 44)
90        | (secret_padding << 55)
91        | (chain << 66)
92        | (public_key << 77)
93}
94
95/// Unpack the scalar counter register into its eight audit classes.
96#[kernel]
97pub fn decode_hardware_counts_kernel(counts: b88) -> HardwareTaskCounts {
98    HardwareTaskCounts {
99        seed: counts.resize(),
100        public_seed: (counts >> 11).resize(),
101        mask: (counts >> 22).resize(),
102        segment_prf: (counts >> 33).resize(),
103        secret_data: (counts >> 44).resize(),
104        secret_padding: (counts >> 55).resize(),
105        chain: (counts >> 66).resize(),
106        public_key: (counts >> 77).resize(),
107    }
108}
109
110/// Task-selector, handshake, error, completion, and audit outputs.
111#[allow(clippy::struct_excessive_bools)] // Each bool is a distinct hardware port.
112#[derive(Clone, Copy, Debug, Default, Digital, Eq, PartialEq)]
113pub struct SequencerOutput {
114    /// A start can be accepted only when idle and output credit is available.
115    pub start_ready: bool,
116    /// A context owns the controller.
117    pub active: bool,
118    /// A result-buffer credit is reserved for the active context.
119    pub output_credit_reserved: bool,
120    /// A compression request is waiting for lane acceptance.
121    pub request_valid: bool,
122    /// Opaque request tag. Stable while `request_valid && !request_ready`.
123    pub request_tag: b32,
124    /// Decoded selector fields for block/context-store routing.
125    pub request_fields: TagFields,
126    /// The supplied response tag exactly matches the outstanding request.
127    pub response_ready: bool,
128    /// One-cycle pulse after a valid malformed, stale, or unexpected response.
129    pub response_rejected: bool,
130    /// One-cycle pulse after all exact task counts retire.
131    pub done: bool,
132    /// One-cycle pulse returning the reserved result-buffer credit.
133    pub output_credit_release: bool,
134    /// Exact per-class retirement counts for the current/completed operation.
135    pub counts: HardwareTaskCounts,
136}
137
138/// One-context, one-outstanding-request fixed-work sequencer.
139#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
140#[rhdl(dq_no_prefix)]
141pub struct SingleContextSequencer {
142    active: DFF<bool>,
143    waiting: DFF<bool>,
144    credit_reserved: DFF<bool>,
145    cursor: DFF<b32>,
146    counts: DFF<b88>,
147    response_rejected: DFF<bool>,
148    done: DFF<bool>,
149    credit_release: DFF<bool>,
150}
151
152impl Default for SingleContextSequencer {
153    fn default() -> Self {
154        Self {
155            active: DFF::new(false),
156            waiting: DFF::new(false),
157            credit_reserved: DFF::new(false),
158            cursor: DFF::new(b32(0)),
159            counts: DFF::new(b88(0)),
160            response_rejected: DFF::new(false),
161            done: DFF::new(false),
162            credit_release: DFF::new(false),
163        }
164    }
165}
166
167impl SynchronousIO for SingleContextSequencer {
168    type I = SequencerInput;
169    type O = SequencerOutput;
170    type Kernel = single_context_sequencer_kernel;
171}
172
173/// State-transition kernel for [`SingleContextSequencer`].
174#[kernel]
175#[allow(clippy::used_underscore_binding)] // The RHDL macro consumes this port.
176pub fn single_context_sequencer_kernel(
177    _clock_reset: ClockReset,
178    input: SequencerInput,
179    q: Q,
180) -> (SequencerOutput, D) {
181    let mut d = D::dont_care();
182    d.active = q.active;
183    d.waiting = q.waiting;
184    d.credit_reserved = q.credit_reserved;
185    d.cursor = q.cursor;
186    d.counts = q.counts;
187    d.response_rejected = false;
188    d.done = false;
189    d.credit_release = false;
190
191    let request_fields = decode_tag_kernel(q.cursor);
192    let request_tag = q.cursor;
193    let current_counts = decode_hardware_counts_kernel(q.counts);
194    let mut next_cursor = request_fields;
195    let mut next_counts = current_counts;
196    let response_matches =
197        tag_is_well_formed_kernel(input.response_tag) && input.response_tag == request_tag;
198
199    if !q.active {
200        if input.start_valid && input.output_credit_available {
201            d.active = true;
202            d.waiting = false;
203            d.credit_reserved = true;
204            next_cursor = TagFields {
205                context: input.context,
206                kind: b3(0),
207                segment: b7(0),
208                chain_step: b4(0),
209                block_index: b6(0),
210                generation: input.generation,
211            };
212            next_counts = HardwareTaskCounts {
213                seed: b11(0),
214                public_seed: b11(0),
215                mask: b11(0),
216                segment_prf: b11(0),
217                secret_data: b11(0),
218                secret_padding: b11(0),
219                chain: b11(0),
220                public_key: b11(0),
221            };
222        }
223    } else if !q.waiting {
224        if input.request_ready {
225            d.waiting = true;
226        }
227    } else if input.response_valid {
228        if response_matches {
229            d.waiting = false;
230            match request_fields.kind {
231                Bits::<3>(0) => {
232                    next_counts.seed = current_counts.seed + b11(1);
233                    next_cursor.kind = b3(1);
234                }
235                Bits::<3>(1) => {
236                    next_counts.public_seed = current_counts.public_seed + b11(1);
237                    next_cursor.kind = b3(2);
238                    next_cursor.segment = b7(0);
239                }
240                Bits::<3>(2) => {
241                    next_counts.mask = current_counts.mask + b11(1);
242                    if request_fields.segment == b7(15) {
243                        next_cursor.kind = b3(3);
244                        next_cursor.segment = b7(0);
245                    } else {
246                        next_cursor.segment = request_fields.segment + b7(1);
247                    }
248                }
249                Bits::<3>(3) => {
250                    next_counts.segment_prf = current_counts.segment_prf + b11(1);
251                    next_cursor.kind = b3(4);
252                }
253                Bits::<3>(4) => {
254                    next_counts.secret_data = current_counts.secret_data + b11(1);
255                    next_cursor.kind = b3(5);
256                    next_cursor.block_index = b6(1);
257                }
258                Bits::<3>(5) => {
259                    next_counts.secret_padding = current_counts.secret_padding + b11(1);
260                    next_cursor.kind = b3(6);
261                    next_cursor.chain_step = b4(1);
262                    next_cursor.block_index = b6(0);
263                }
264                Bits::<3>(6) => {
265                    next_counts.chain = current_counts.chain + b11(1);
266                    if request_fields.chain_step == b4(15) {
267                        next_cursor.chain_step = b4(0);
268                        if request_fields.segment == b7(66) {
269                            next_cursor.kind = b3(7);
270                            next_cursor.segment = b7(0);
271                            next_cursor.block_index = b6(0);
272                        } else {
273                            next_cursor.kind = b3(3);
274                            next_cursor.segment = request_fields.segment + b7(1);
275                        }
276                    } else {
277                        next_cursor.chain_step = request_fields.chain_step + b4(1);
278                    }
279                }
280                _ => {
281                    next_counts.public_key = current_counts.public_key + b11(1);
282                    if request_fields.block_index == b6(33) {
283                        let exact_prefix = current_counts.seed == b11(1)
284                            && current_counts.public_seed == b11(1)
285                            && current_counts.mask == b11(16)
286                            && current_counts.segment_prf == b11(67)
287                            && current_counts.secret_data == b11(67)
288                            && current_counts.secret_padding == b11(67)
289                            && current_counts.chain == b11(1005)
290                            && current_counts.public_key == b11(33);
291                        if exact_prefix {
292                            d.active = false;
293                            d.credit_reserved = false;
294                            d.done = true;
295                            d.credit_release = true;
296                        } else {
297                            d.response_rejected = true;
298                            d.waiting = true;
299                            next_counts.public_key = current_counts.public_key;
300                        }
301                    } else {
302                        next_cursor.block_index = request_fields.block_index + b6(1);
303                    }
304                }
305            }
306        } else {
307            d.response_rejected = true;
308        }
309    }
310
311    let output = SequencerOutput {
312        start_ready: !q.active && input.output_credit_available,
313        active: q.active,
314        output_credit_reserved: q.credit_reserved,
315        request_valid: q.active && !q.waiting,
316        request_tag,
317        request_fields,
318        response_ready: q.active && q.waiting && response_matches,
319        response_rejected: q.response_rejected,
320        done: q.done,
321        output_credit_release: q.credit_release,
322        counts: current_counts,
323    };
324    d.cursor = encode_tag_kernel(next_cursor);
325    d.counts = encode_hardware_counts_kernel(next_counts);
326    (output, d)
327}
328
329const _: () = {
330    assert!(FUTURE_CONTEXTS <= 16);
331};