rhdl_dsig_scalable_core/verify/certs.rs
1//! The verifier's certificate cache, in front of the Ed25519 verify core.
2//!
3//! # The problem
4//!
5//! Every record carries its batch certificate: the 64-byte commitment and
6//! the Ed25519 signature over it. Verifying that signature costs about a
7//! thousand times more than the hash work, but all 128 records of a batch
8//! carry the *same* certificate. So the verifier checks each certificate
9//! once and remembers the answer, exactly as the reference
10//! [`VerifiedBatch`](crate::reference::VerifiedBatch) does in software: a
11//! record may reuse an answer only if its whole 128-byte certificate is
12//! identical to the one that was checked.
13//!
14//! # The idea
15//!
16//! 64 entries, picked by the low 6 bits of the batch id (commitment byte
17//! 24) mixed with the signer's key slot. So many because a certificate check takes 52,000–83,000 cycles
18//! (Ed25519 project, native simulation of the `1,1,1,1` core): a unit
19//! finishes a batch every ~4,500 cycles, so about 20 batches wait for their
20//! checks at once. Each holds a certificate (two 64-byte rows in block RAM) and a
21//! state:
22//!
23//! ```text
24//! miss: store, ask Ed25519
25//! FREE ───────────────────────────▶ PENDING ──ok──▶ VERIFIED
26//! ▲ │ │
27//! │ └──fail──▶ REJECTED
28//! └──── replaced by a different certificate (never while PENDING
29//! or while a waiting record still points at it)
30//! ```
31//!
32//! A lookup compares the record's commitment and signature with the entry's
33//! two rows. A hit on VERIFIED or REJECTED answers at once. A hit on PENDING,
34//! or a miss that stores the certificate and asks the Ed25519 core, answers
35//! "pending on entry `e`"; the record then waits at retirement until the
36//! entry settles. An entry that waiting records point at (its `users` count)
37//! is never replaced, so their answer cannot change under them.
38//!
39//! # Several signers
40//!
41//! The commitment does not name the signer's Ed25519 key, so one checked
42//! certificate must not answer for a record that claims a different signer.
43//! Each record arrives with its signer's key slot (0 when the verifier
44//! trusts one key, as the HBM kernel does). The slot is part of the entry
45//! choice (`batch id ^ slot << 2`), an entry remembers the slot it was
46//! checked for, a hit needs the same slot, and the Ed25519 request carries
47//! the slot so the cluster presents that slot's key.
48//!
49//! # Warm lookups
50//!
51//! A *warm* lookup brings a certificate in ahead of its records (the uBFT
52//! service's certificate announce, [`crate::ubft`]): it is looked up like a
53//! record's, and on a miss it is stored and checked, but it answers no
54//! record and holds no `users` count, so it never delays a verdict. When
55//! the entry it maps to is busy it is simply dropped; the first record of
56//! the batch then checks the certificate as before.
57//!
58//! # Timing
59//!
60//! The loader presents the commitment (beat 0) and the signature (beat 1).
61//! The answer comes a few cycles later, long before the record's 26 beats
62//! are in; [`CertsOutput::busy`] holds the next record's beat 0 back in the
63//! rare case a lookup must wait for an entry to free up.
64
65use crate::verify::chains::CONTEXTS;
66use rhdl::prelude::*;
67use rhdl_fpga::core::{
68 dff::DFF,
69 ram::synchronous::{In as RamIn, SyncBRAM, Write as RamWrite},
70};
71
72/// Certificate entries.
73pub const ENTRIES: usize = 64;
74
75/// Entry state: no certificate.
76pub const FREE: b2 = b2(0);
77/// Entry state: asked the Ed25519 core, no answer yet.
78pub const PENDING: b2 = b2(1);
79/// Entry state: the certificate verified.
80pub const VERIFIED: b2 = b2(2);
81/// Entry state: the certificate failed.
82pub const REJECTED: b2 = b2(3);
83
84/// A certificate check for the Ed25519 core.
85#[derive(Clone, Copy, Debug, PartialEq, Digital)]
86pub struct CertRequest {
87 /// A request is presented (held until taken).
88 pub valid: bool,
89 /// The entry that waits for the answer.
90 pub entry: b6,
91 /// The signer's key slot: the key the certificate must verify under.
92 pub slot: b4,
93 /// The 64-byte commitment, the signed message.
94 pub commitment: [b8; 64],
95 /// The Ed25519 signature `R ‖ S`.
96 pub signature: [b8; 64],
97}
98
99impl Default for CertRequest {
100 /// No request.
101 fn default() -> Self {
102 Self {
103 valid: false,
104 entry: b6(0),
105 slot: b4(0),
106 commitment: [b8(0); 64],
107 signature: [b8(0); 64],
108 }
109 }
110}
111
112/// The Ed25519 core's answer for an entry.
113#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
114pub struct CertResult {
115 /// An answer this cycle.
116 pub valid: bool,
117 /// The entry it belongs to.
118 pub entry: b6,
119 /// The certificate verified.
120 pub ok: bool,
121}
122
123/// What a record learns from its lookup.
124#[derive(Clone, Copy, Debug, PartialEq, Digital, Default)]
125pub struct CertAnswer {
126 /// The answer is presented this cycle.
127 pub valid: bool,
128 /// The record's context.
129 pub ctx: b6,
130 /// The answer is not known yet: wait for `entry` to settle.
131 pub pending: bool,
132 /// The entry (meaningful when `pending`).
133 pub entry: b6,
134 /// The certificate is valid (meaningful when not `pending`).
135 pub ok: bool,
136}
137
138/// Inputs of the cache.
139#[derive(Clone, Copy, Debug, PartialEq, Digital)]
140pub struct CertsInput {
141 /// Beat 0 of a record (its commitment) is presented.
142 pub commitment_valid: bool,
143 /// Beat 1 (its signature) is presented.
144 pub signature_valid: bool,
145 /// The beat.
146 pub beat: [b8; 64],
147 /// The record's context (with beat 0).
148 pub ctx: b6,
149 /// The record's signer key slot (with beat 0).
150 pub slot: b4,
151 /// The lookup is a warm one (with beat 0): no answer, no user.
152 pub warm: bool,
153 /// The Ed25519 core took the presented request this cycle.
154 pub taken: bool,
155 /// An answer from the Ed25519 core.
156 pub result: CertResult,
157 /// A record that waited on an entry retires (its `users` count drops).
158 pub release_valid: bool,
159 /// That entry.
160 pub release_entry: b6,
161}
162
163/// Outputs of the cache.
164#[derive(Clone, Copy, Debug, PartialEq, Digital)]
165pub struct CertsOutput {
166 /// Do not present a new commitment this cycle.
167 pub busy: bool,
168 /// The answer to a lookup.
169 pub answer: CertAnswer,
170 /// The request for the Ed25519 core.
171 pub request: CertRequest,
172 /// Each entry's state.
173 pub state: [b2; ENTRIES],
174 /// Requests issued so far.
175 pub checks: b32,
176 /// Answers that failed so far.
177 pub fails: b32,
178}
179
180/// Lookup phases.
181const IDLE: b3 = b3(0);
182/// Commitment stored; waiting for the signature.
183const HAVE_COMMITMENT: b3 = b3(1);
184/// Row 0 is here: compare it, ask for row 1.
185const COMPARE0: b3 = b3(2);
186/// Row 1 is here: decide (or wait for the entry, keeping row 1 addressed).
187const DECIDE: b3 = b3(3);
188/// Writing the signature row of a replaced entry.
189const WRITE_ROW1: b3 = b3(4);
190
191/// Resettable state.
192#[derive(Clone, Copy, Debug, PartialEq, Digital)]
193#[doc(hidden)]
194pub struct CertsControl {
195 /// Lookup phase.
196 phase: b3,
197 /// The looked-up entry.
198 entry: b6,
199 /// The record's context.
200 ctx: b6,
201 /// The record's signer key slot.
202 slot: b4,
203 /// The lookup is warm.
204 warm: bool,
205 /// Row 0 matched.
206 match0: bool,
207 /// Entry states.
208 state: [b2; ENTRIES],
209 /// Records waiting on each entry.
210 users: [b12; ENTRIES],
211 /// The key slot each entry was checked (or is being checked) for.
212 slots: [b4; ENTRIES],
213 /// A request is presented to the Ed25519 core.
214 request: bool,
215 /// Its entry.
216 request_entry: b6,
217 /// Its key slot.
218 request_slot: b4,
219 /// The answer register holds an answer.
220 answer: CertAnswer,
221 /// Requests issued.
222 checks: b32,
223 /// Failed answers.
224 fails: b32,
225}
226
227impl Default for CertsControl {
228 /// Every entry free, nothing in flight.
229 fn default() -> Self {
230 Self {
231 phase: IDLE,
232 entry: b6(0),
233 ctx: b6(0),
234 slot: b4(0),
235 warm: false,
236 match0: false,
237 state: [FREE; ENTRIES],
238 users: [b12(0); ENTRIES],
239 slots: [b4(0); ENTRIES],
240 request: false,
241 request_entry: b6(0),
242 request_slot: b4(0),
243 answer: CertAnswer::default(),
244 checks: b32(0),
245 fails: b32(0),
246 }
247 }
248}
249
250/// Resetless data.
251#[derive(Clone, Copy, Debug, PartialEq, Digital)]
252#[doc(hidden)]
253pub struct CertsData {
254 /// The record's commitment.
255 commitment: [b8; 64],
256 /// The record's signature.
257 signature: [b8; 64],
258 /// The presented request's commitment (kept while the next lookup runs).
259 request_commitment: [b8; 64],
260 /// The presented request's signature.
261 request_signature: [b8; 64],
262}
263
264/// The certificate cache.
265#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
266pub struct VerifyCerts {
267 /// Entry `e`'s commitment at row `2e`, signature at row `2e + 1`.
268 rows: SyncBRAM<[b8; 64], 7>,
269 /// Phases, states and counters.
270 control: DFF<CertsControl>,
271 /// The looked-up certificate.
272 data: DFF<CertsData>,
273}
274
275impl Default for VerifyCerts {
276 /// An empty cache.
277 fn default() -> Self {
278 Self {
279 rows: SyncBRAM::default(),
280 control: DFF::new(CertsControl::default()),
281 data: DFF::resetless(),
282 }
283 }
284}
285
286impl SynchronousIO for VerifyCerts {
287 type I = CertsInput;
288 type O = CertsOutput;
289 type Kernel = verify_certs_kernel;
290}
291
292/// Whether two 64-byte rows are equal (a 512-bit compare as a tree of bytes).
293#[allow(clippy::needless_range_loop)]
294#[kernel]
295pub fn rows_equal_kernel(a: [b8; 64], b: [b8; 64]) -> bool {
296 let mut same = [false; 64];
297 for i in 0..64 {
298 same[i] = a[i] == b[i];
299 }
300 let mut eights = [false; 8];
301 for g in 0..8 {
302 eights[g] = same[8 * g]
303 && same[8 * g + 1]
304 && same[8 * g + 2]
305 && same[8 * g + 3]
306 && same[8 * g + 4]
307 && same[8 * g + 5]
308 && same[8 * g + 6]
309 && same[8 * g + 7];
310 }
311 eights[0] && eights[1] && eights[2] && eights[3] && eights[4] && eights[5] && eights[6] && eights[7]
312}
313
314/// The cache's logic.
315#[allow(clippy::needless_range_loop, clippy::too_many_lines)]
316#[kernel]
317pub fn verify_certs_kernel(clock_reset: ClockReset, input: CertsInput, q: VerifyCertsQ) -> (CertsOutput, VerifyCertsD) {
318 // No reset gating here: every register this logic feeds resets itself,
319 // and keeping the reset out of the logic cones keeps it off the routed
320 // kernel's failing paths (verifier build v1).
321 let resetting = clock_reset.reset.any();
322 let _ = resetting;
323 let mut c = q.control;
324 let mut data = q.data;
325 let mut read_addr: b7 = q.control.entry.resize::<7>() << 1;
326 let mut write = RamWrite::<[b8; 64], 7> {
327 addr: q.control.entry.resize::<7>() << 1,
328 value: q.data.commitment,
329 enable: false,
330 };
331 c.answer.valid = false;
332 let mut state = q.control.state;
333 let mut users = q.control.users;
334 // Ed25519 answers settle their entry.
335 if input.result.valid {
336 for e in 0..ENTRIES {
337 if input.result.entry == b6(e as u128) {
338 state[e] = if input.result.ok { VERIFIED } else { REJECTED };
339 }
340 }
341 if !input.result.ok {
342 c.fails = q.control.fails + b32(1);
343 }
344 }
345 if input.taken {
346 c.request = false;
347 }
348 // Entry state and users of the looked-up entry.
349 let mut entry_state = FREE;
350 let mut entry_users = b12(0);
351 let mut entry_slot = b4(0);
352 for e in 0..ENTRIES {
353 if q.control.entry == b6(e as u128) {
354 entry_state = q.control.state[e];
355 entry_users = q.control.users[e];
356 entry_slot = q.control.slots[e];
357 }
358 }
359 let mut slots = q.control.slots;
360 let mut add_user = false;
361 if q.control.phase == IDLE {
362 if input.commitment_valid {
363 data.commitment = input.beat;
364 // Batch id bits 0..6, mixed with the key slot, pick the entry.
365 c.entry = (input.beat[24] ^ (input.slot.resize::<8>() << 2)).resize();
366 c.ctx = input.ctx;
367 c.slot = input.slot;
368 c.warm = input.warm;
369 c.phase = HAVE_COMMITMENT;
370 }
371 } else if q.control.phase == HAVE_COMMITMENT {
372 if input.signature_valid {
373 data.signature = input.beat;
374 read_addr = q.control.entry.resize::<7>() << 1;
375 c.phase = COMPARE0;
376 }
377 } else if q.control.phase == COMPARE0 {
378 // Row 0 is here; ask for row 1 and keep asking while deciding.
379 c.match0 = rows_equal_kernel(q.rows, q.data.commitment);
380 read_addr = (q.control.entry.resize::<7>() << 1) | b7(1);
381 c.phase = DECIDE;
382 } else if q.control.phase == DECIDE {
383 read_addr = (q.control.entry.resize::<7>() << 1) | b7(1);
384 let hit = q.control.match0
385 && rows_equal_kernel(q.rows, q.data.signature)
386 && entry_state != FREE
387 && entry_slot == q.control.slot;
388 let warm = q.control.warm;
389 if hit {
390 if !warm {
391 c.answer = CertAnswer {
392 valid: true,
393 ctx: q.control.ctx,
394 pending: entry_state == PENDING,
395 entry: q.control.entry,
396 ok: entry_state == VERIFIED,
397 };
398 add_user = entry_state == PENDING;
399 }
400 c.phase = IDLE;
401 } else if warm && (entry_state == PENDING || entry_users != b12(0) || q.control.request) {
402 // A warm lookup never waits: the entry is busy, drop it.
403 c.phase = IDLE;
404 } else if entry_state != PENDING && entry_users == b12(0) && !q.control.request {
405 // Replace the entry: commitment row now, signature row next.
406 write.enable = true;
407 c.phase = WRITE_ROW1;
408 for e in 0..ENTRIES {
409 if q.control.entry == b6(e as u128) {
410 state[e] = PENDING;
411 slots[e] = q.control.slot;
412 }
413 }
414 c.request = true;
415 c.request_entry = q.control.entry;
416 c.request_slot = q.control.slot;
417 data.request_commitment = q.data.commitment;
418 data.request_signature = q.data.signature;
419 c.checks = q.control.checks + b32(1);
420 if !warm {
421 c.answer = CertAnswer {
422 valid: true,
423 ctx: q.control.ctx,
424 pending: true,
425 entry: q.control.entry,
426 ok: false,
427 };
428 add_user = true;
429 }
430 }
431 // Otherwise wait here until the entry is free to replace.
432 } else {
433 // WRITE_ROW1
434 write = RamWrite::<[b8; 64], 7> {
435 addr: (q.control.entry.resize::<7>() << 1) | b7(1),
436 value: q.data.signature,
437 enable: true,
438 };
439 c.phase = IDLE;
440 }
441 // Users: one more for a pending answer, one fewer for a release.
442 for e in 0..ENTRIES {
443 let mut u = users[e];
444 if add_user && q.control.entry == b6(e as u128) {
445 u = u + b12(1);
446 }
447 if input.release_valid && input.release_entry == b6(e as u128) {
448 u = u - b12(1);
449 }
450 users[e] = u;
451 }
452 c.state = state;
453 c.users = users;
454 c.slots = slots;
455 let d = VerifyCertsD {
456 rows: RamIn::<[b8; 64], 7> { read_addr, write },
457 control: c,
458 data,
459 };
460 (
461 CertsOutput {
462 busy: q.control.phase != IDLE,
463 answer: CertAnswer {
464 valid: q.control.answer.valid,
465 ctx: q.control.answer.ctx,
466 pending: q.control.answer.pending,
467 entry: q.control.answer.entry,
468 ok: q.control.answer.ok,
469 },
470 request: CertRequest {
471 valid: q.control.request,
472 entry: q.control.request_entry,
473 slot: q.control.request_slot,
474 commitment: q.data.request_commitment,
475 signature: q.data.request_signature,
476 },
477 state: q.control.state,
478 checks: q.control.checks,
479 fails: q.control.fails,
480 },
481 d,
482 )
483}
484
485const _: () = assert!(CONTEXTS <= 4095, "users counts are 12 bits");