Skip to main content

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");