From 21848559ade9a4a9c496229bacf3f8821a353cb7 Mon Sep 17 00:00:00 2001 From: Hakim Filali Date: Mon, 29 Jun 2026 15:32:16 +0200 Subject: [PATCH 1/2] [sw,otbnsim] Adapt OTBNsim to the new mask accelerator RTL This commit changes the MAI part of the OTBNsim to align it with the current state of the RTL. Signed-off-by: Hakim Filali --- hw/ip/otbn/dv/otbnsim/sim/mai.py | 832 ++++++++++++++++++++------ hw/ip/otbn/dv/otbnsim/sim/mai_ispr.py | 48 +- hw/ip/otbn/dv/otbnsim/sim/sim.py | 29 +- hw/ip/otbn/dv/otbnsim/sim/state.py | 4 +- hw/ip/otbn/dv/otbnsim/sim/wsr.py | 4 + 5 files changed, 709 insertions(+), 208 deletions(-) diff --git a/hw/ip/otbn/dv/otbnsim/sim/mai.py b/hw/ip/otbn/dv/otbnsim/sim/mai.py index c3976054f7b3b..c8b1ac404c740 100644 --- a/hw/ip/otbn/dv/otbnsim/sim/mai.py +++ b/hw/ip/otbn/dv/otbnsim/sim/mai.py @@ -2,7 +2,8 @@ # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 -from typing import List, Optional, Tuple +from dataclasses import dataclass +from typing import Any, List, Optional, Tuple from .csr import CSRFile from .ispr import DumbISPR @@ -16,193 +17,578 @@ # meet certain constraints (e.g., being smaller than the modulus). CHECK_ACCELERATOR_CONSTRAINTS = False -# Latencies of the accelerators in cycles. -ACCELERATOR_LATENCIES = { - MaiOperation.A2B: 18, - MaiOperation.B2A: 18, - MaiOperation.SECADD: 18, -} - - -class MaskingAccelerator: - '''Models a masking accelerator which has a simple pipeline. - New operations can be pushed to the accelerator, and results can be popped from it. - Each step of the simulation advances the pipeline by one stage. - ''' - - def __init__(self, latency: int, mod_wsr: DumbISPR) -> None: - # Latency of the masking accelerator in cycles. - self.latency = latency - - # The MOD WSR is used to get the current modulus for operations. - self.mod_wsr = mod_wsr - - # The pipeline contains the two result shares and is modeled with a deque where None - # indicates an empty slot. - self.pipeline: List[Optional[Tuple[int, int]]] - self.pipeline = [None] * self.latency - - def push(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int) -> bool: - '''Try to push an operation to the masking accelerator pipeline. - - Returns True if the accelerator can accept it (free pipeline slot), False otherwise. - ''' - # This accelerator implementation features no backpressure, so we always accept new - # operations. Pop the leftmost pipeline slot and replace it with the new operation result. - # The result is computed immediately but will only be available after the full pipeline - # latency. - self.pipeline[0] = self._compute(in0_s0, in0_s1, in1_s0, in1_s1) - return True +_MASK32 = (1 << 32) - 1 + + +def _mod_smear(mod: int) -> int: + """Smear all set bits of mod rightward to fill below the MSB.""" + mask = mod & _MASK32 + for i in range(5): + mask |= mask >> (1 << i) + return mask & _MASK32 + + +def _urnd_fields(urnd_val: int) -> Tuple[int, int, int, int]: + """Unpack a 389-bit Bivium keystream word into its named fields. + + [321:0] rand: 322-bit HPC3 gadget randomness + [353:322] mask_0: 32-bit remask share 0 + [385:354] mask_1: 32-bit remask share 1 + [388:386] cnt: 3-bit starting counter offset + """ + rand = urnd_val & ((1 << 322) - 1) + mask_0 = (urnd_val >> 322) & _MASK32 + mask_1 = (urnd_val >> 354) & _MASK32 + cnt = (urnd_val >> 386) & 0x7 + return rand, mask_0, mask_1, cnt + + +def hpc3_vec(x0: int, x1: int, y0: int, y1: int, r: int, rp: int, + w0: int = 0, w1: int = 0) -> Tuple[int, int]: + """HPC3/HPC3o gadget operating on 32 independent bit-lanes simultaneously. + + EnW=0 (HPC3): z = x & y + EnW=1 (HPC3o): z = w ^ (x & y) + + All arguments are 32-bit packed integers. + r, rp: one fresh random bit per lane. + + Returns (z0, z1) satisfying z0^z1 == (x0^x1)&(y0^y1)^(w0^w1) per bit. + """ + y_masked_0 = y0 ^ r + y_masked_1 = y1 ^ r + inner_0 = (x0 & y_masked_0) ^ w0 ^ rp + inner_1 = (x1 & y_masked_1) ^ w1 ^ rp + return (inner_0 ^ (x0 & y_masked_1), + inner_1 ^ (x1 & y_masked_0)) + + +# Broadcast helpers indexed by (level-1), i.e. index 0 = level 1 = step 1. +# Used to broadcast the remote G/P values to all active positions in a group. +_BCAST_GS: Tuple[int, ...] = (0x55555555, 0x11111111, 0x01010101, 0x00010001, 0x00000001) +_BCAST_FILL: Tuple[int, ...] = (2, 12, 240, 0xFF00, 0xFFFF0000) + + +# Per-level masks for _advance_level, indexed by (level-1). +# At level lv, each 32-bit word is split into 32/period groups with a lower feedthrough +# half and an upper active half: +# active_mask upper-half bits of every group, written with new computed G/P +# inv_active lower-half bits, PG copies through unchanged +# p_mask active_mask minus group-0's active bits. Group-0 active +# PP is dropped because no other group reads group-0 PP as +# a remote. +# feedthrough_pp_mask lower-half bits of groups. Group-0 lower half is +# also zeroed so that rem_pp for group-0 stays 0, without +# this the initial non-zero pre_p values would persist via +# feedthrough and feed a non-zero rem_pp into the P gadget. +def _build_level_masks() -> Tuple[Tuple[int, int, int, int], ...]: + out = [] + for lv in range(1, 6): + step = 1 << (lv - 1) + period = 2 * step + active_mask = 0 + for k in range(32 // period): + active_mask |= ((1 << step) - 1) << (period * k + step) + inv_active = _MASK32 ^ active_mask + g0_active_mask = ((1 << step) - 1) << step + p_mask = active_mask ^ g0_active_mask + group0_all_mask = (1 << period) - 1 + feedthrough_pp_mask = inv_active & (_MASK32 ^ group0_all_mask) + out.append((active_mask, inv_active, p_mask, feedthrough_pp_mask)) + return tuple(out) + + +_LEVEL_MASKS: Tuple[Tuple[int, int, int, int], ...] = _build_level_masks() + + +# SIMD within register bit-splitting helpers (module-level for minimal call overhead). + +def _bs_extract(x: int, step: int) -> int: + """Extract bits at positions 0, step, 2*step, etc. from x and pack them into + bit positions 0, 1, 2, etc. of the result (bit i of result = bit i*step of x).""" + if step == 2: + x &= 0x5555555555555555 + x = (x | (x >> 1)) & 0x3333333333333333 + x = (x | (x >> 2)) & 0x0F0F0F0F0F0F0F0F + x = (x | (x >> 4)) & 0x00FF00FF00FF00FF + x = (x | (x >> 8)) & 0x0000FFFF0000FFFF + x = (x | (x >> 16)) & 0x00000000FFFFFFFF + return x + x &= 0x1111111111111111 + x = (x | (x >> 3)) & 0x0303030303030303 + x = (x | (x >> 6)) & 0x000F000F000F000F + x = (x | (x >> 12)) & 0x000000FF000000FF + x = (x | (x >> 24)) & 0x000000000000FFFF + return x + + +def _bs_scatter(x: int, gap: int) -> int: + """Spread bits of x into every (gap+1)-th position + (bit i of x = bit i*(gap+1) of result).""" + x = (x ^ (x << 16)) & 0x0000FFFF0000FFFF + x = (x ^ (x << 8)) & 0x00FF00FF00FF00FF + if gap == 8: + return x + x = (x ^ (x << 4)) & 0x0F0F0F0F0F0F0F0F + if gap == 4: + return x + x = (x ^ (x << 2)) & 0x3333333333333333 + if gap == 2: + return x + x = (x ^ (x << 1)) & 0x5555555555555555 + return x + + +# Per-level randomness extraction, one function per level. +# Everything is inlined to minimize function call overhead. + +def _ur0(ri: int) -> Tuple[int, int, int, int]: + """Level 0 (precompute) spread ri across 2 groups: + ri[63:0] & 0xFFFFFFFFFFFFFFFF -> (r, rp) at 0xFFFFFFFF.""" + n = ri & 0xFFFFFFFFFFFFFFFF + return _bs_extract(n, 2), _bs_extract(n >> 1, 2), 0, 0 + + +def _ur1(ri: int) -> Tuple[int, int, int, int]: + """Level 1 (step=1) spread ri across 4 groups: + ri[125:64] & 0x3FFFFFFFFFFFFFFF -> (r_G, rp_G, r_P, rp_P) at 0xAAAAAAAA.""" + n = (ri >> 64) & 0x3FFFFFFFFFFFFFFF + lower = n & 3 + upper = n >> 2 + r1 = _bs_extract(upper, 4) + r2 = _bs_extract(upper >> 1, 4) + r3 = _bs_extract(upper >> 2, 4) + r4 = _bs_extract(upper >> 3, 4) + v1 = (lower & 1) | (r1 << 1) + v2 = ((lower >> 1) & 1) | (r2 << 1) + return (_bs_scatter(v1, 1) << 1, _bs_scatter(v2, 1) << 1, + _bs_scatter(r3, 1) << 3, _bs_scatter(r4, 1) << 3) + + +def _ur2(ri: int) -> Tuple[int, int, int, int]: + """Level 2 (step=2) spread ri across 4 groups: + ri[185:126] & 0xFFFFFFFFFFFFFFF -> (r_G, rp_G, r_P, rp_P) at 0xCCCCCCCC.""" + n = (ri >> 126) & 0xFFFFFFFFFFFFFFF + lower = n & 0xF + upper = n >> 4 + p1 = _bs_extract(lower, 2) + p2 = _bs_extract(lower >> 1, 2) + r1 = _bs_extract(upper, 4) + r2 = _bs_extract(upper >> 1, 4) + r3 = _bs_extract(upper >> 2, 4) + r4 = _bs_extract(upper >> 3, 4) + v1 = p1 | (r1 << 2) + v2 = p2 | (r2 << 2) + return (_bs_scatter(v1, 2) << 2, _bs_scatter(v2, 2) << 2, + _bs_scatter(r3, 2) << 6, _bs_scatter(r4, 2) << 6) + + +def _ur3(ri: int) -> Tuple[int, int, int, int]: + """Level 3 (step=4) spread ri across 4 groups: + ri[241:186] & 0xFFFFFFFFFFFFFF -> (r_G, rp_G, r_P, rp_P) at 0xF0F0F0F0.""" + n = (ri >> 186) & 0xFFFFFFFFFFFFFF + lower = n & 0xFF + upper = n >> 8 + p1 = _bs_extract(lower, 2) + p2 = _bs_extract(lower >> 1, 2) + r1 = _bs_extract(upper, 4) + r2 = _bs_extract(upper >> 1, 4) + r3 = _bs_extract(upper >> 2, 4) + r4 = _bs_extract(upper >> 3, 4) + v1 = p1 | (r1 << 4) + v2 = p2 | (r2 << 4) + return (_bs_scatter(v1, 4) << 4, _bs_scatter(v2, 4) << 4, + _bs_scatter(r3, 4) << 12, _bs_scatter(r4, 4) << 12) + + +def _ur4(ri: int) -> Tuple[int, int, int, int]: + """Level 4 (step=8) spread ri across 4 groups: + ri[289:242] & 0xFFFFFFFFFFFF -> (r_G, rp_G, r_P, rp_P) at 0xFF00FF00.""" + n = (ri >> 242) & 0xFFFFFFFFFFFF + lower = n & 0xFFFF + upper = n >> 16 + p1 = _bs_extract(lower, 2) + p2 = _bs_extract(lower >> 1, 2) + r1 = _bs_extract(upper, 4) + r2 = _bs_extract(upper >> 1, 4) + r3 = _bs_extract(upper >> 2, 4) + r4 = _bs_extract(upper >> 3, 4) + v1 = p1 | (r1 << 8) + v2 = p2 | (r2 << 8) + return (_bs_scatter(v1, 8) << 8, _bs_scatter(v2, 8) << 8, + _bs_scatter(r3, 8) << 24, _bs_scatter(r4, 8) << 24) + + +def _ur5(ri: int) -> Tuple[int, int, int, int]: + """Level 5 (step=16) spread ri across 2 groups: + ri[321:290] & 0xFFFFFFFF -> (r_G, rp_G) at 0xFFFF0000.""" + n = (ri >> 290) & 0xFFFFFFFF + return _bs_extract(n, 2) << 16, _bs_extract(n >> 1, 2) << 16, 0, 0 + + +_UNPACK_RAND_FNS: Tuple[Any, ...] = (_ur0, _ur1, _ur2, _ur3, _ur4, _ur5) + + +@dataclass +class _SecAddStage: + # pg / pp / pre_p: 32-bit packed shares (s0, s1). + pg: Tuple[int, int] + pp: Tuple[int, int] + # pre_p carries the initial XOR propagate (inp1 ^ inp2) through all stages unchanged. + pre_p: Tuple[int, int] + # How many prefix-tree levels have been applied (0 = pre-compute only). + level: int + # rand from the cycle when this stage was last advanced (or created). + # Used by the NEXT advance: level-L gadgets consume the rand that was + # current when level L-1 ran (one cycle earlier). + rand: int = 0 + + +class SecureAdder: + """Cycle-accurate model of the secure adder. + + Masked Sklansky parallel-prefix adder, Width=32, Stages=5, Latency=6 cycles. + Inputs are first-order Boolean sharings (share0, share1) of 32-bit values. + Output is a (Width+1)-bit sharing that includes the carry-out in bit [32]. + rand (322 bits) must be fresh every cycle the pipeline is active. + + Calling convention each clock cycle: + result = adder.peek() # read output (must come first) + adder.step(rand, stall) # shift pipeline + adder.push(inp1, inp2, rand) # inject new input (same rand) + """ + + WIDTH = 32 + STAGES = 5 # log2(32) + LATENCY = STAGES + 1 + RAND_WIDTH = 2 * (STAGES * WIDTH + 1) # 322 + + def __init__(self) -> None: + self._stages: List[Optional[_SecAddStage]] = [None] * self.LATENCY + self._num_in_flight: int = 0 + + def push(self, inp1: Tuple[int, int], inp2: Tuple[int, int], + rand: int) -> None: + """Apply level-0 pre-compute and place result in pipeline stage 0. + + Must be called after step() within the same clock cycle. + rand bits [63:0] are consumed (2 bits per HPC3 gadget, 32 gadgets). + """ + self._stages[0] = self._precompute(inp1, inp2, rand) + self._num_in_flight += 1 + + def step(self, rand: int, stall: bool = False) -> None: + """Advance the pipeline by one stage unless stalled. + + Iterates from the tail so each entry is read before being overwritten. + Each in-flight entry advances one prefix-tree level using the rand + stored in it (from the previous cycle). After advancing, the stored + rand is updated to the current rand for the next level's advance. + Clears stage 0 so that push() can inject a new element this cycle. + """ + if stall: + return + if self._num_in_flight == 0: + return # nothing to advance + tail_was_set = self._stages[self.LATENCY - 1] is not None + for si in range(self.LATENCY - 1, 0, -1): + src = self._stages[si - 1] + if src is not None: + new_stage = self._advance_level(src, src.rand) + new_stage.rand = rand + self._stages[si] = new_stage + else: + self._stages[si] = None + self._stages[0] = None + if tail_was_set: + self._num_in_flight -= 1 def peek(self) -> Optional[Tuple[int, int]]: - '''Read the current output of the masking accelerator pipeline.''' - return self.pipeline[-1] + """Return output shares (33-bit each) if a result is ready, else None.""" + stage = self._stages[-1] + return self._final_sum(stage) if stage is not None else None + + def _precompute(self, inp1: Tuple[int, int], inp2: Tuple[int, int], + rand: int) -> _SecAddStage: + """Level-0: pg = inp1 AND inp2 via 32 parallel HPC3 gadgets, pp = inp1 XOR inp2.""" + s0_a, s1_a = inp1 + s0_b, s1_b = inp2 + r, rp, _, _ = _ur0(rand) + pg_s0, pg_s1 = hpc3_vec(s0_a, s1_a, s0_b, s1_b, r, rp) + pre_p_s0 = (s0_a ^ s0_b) & _MASK32 + pre_p_s1 = (s1_a ^ s1_b) & _MASK32 + pre_p = (pre_p_s0, pre_p_s1) + return _SecAddStage( + pg=(pg_s0 & _MASK32, pg_s1 & _MASK32), + pp=pre_p, + pre_p=pre_p, + level=0, + rand=rand, + ) + + def _advance_level(self, stage: _SecAddStage, rand: int) -> _SecAddStage: + """Apply the next Sklansky prefix-tree level using vectorized HPC3 gadgets. + + All 32 bit-lanes are processed simultaneously. Active bits (upper half of + each 2*step-bit group) receive computed G/P values, feedthrough bits + (lower half) are copied. Group-0 pp is always 0. + """ + level = stage.level + 1 + lv_idx = level - 1 + step = 1 << lv_idx + + pg_s0, pg_s1 = stage.pg + pp_s0, pp_s1 = stage.pp + + # Broadcast each group's remote (position step-1 within the group) to all + # active positions in that group (step..2*step-1). Uses precomputed + # group_sel (_BCAST_GS) and fill multiplier (_BCAST_FILL). + gs = _BCAST_GS[lv_idx] + fill = _BCAST_FILL[lv_idx] + shift = step - 1 + rem_pg_s0 = ((pg_s0 >> shift) & gs) * fill & _MASK32 + rem_pg_s1 = ((pg_s1 >> shift) & gs) * fill & _MASK32 + rem_pp_s0 = ((pp_s0 >> shift) & gs) * fill & _MASK32 + rem_pp_s1 = ((pp_s1 >> shift) & gs) * fill & _MASK32 + + r_G, rp_G, r_P, rp_P = _UNPACK_RAND_FNS[level](rand) + + active_mask, inv_active, p_mask, feedthrough_pp_mask = _LEVEL_MASKS[lv_idx] + + # G gadgets (HPC3o): new_pg[bi] = pg[bi] ^ (pp[bi] & pg[remote]) + g_s0, g_s1 = hpc3_vec(rem_pg_s0, rem_pg_s1, pp_s0, pp_s1, r_G, rp_G, pg_s0, pg_s1) + + # P gadgets (HPC3): new_pp[bi] = pp[remote] & pp[bi] (non-group-0 active only) + p_s0, p_s1 = hpc3_vec(rem_pp_s0, rem_pp_s1, pp_s0, pp_s1, r_P, rp_P) + + # Merge values into the next level values: + # active -> values that were just computed, + # feedthrough -> copy from previous level, + # group-0 pp bits -> 0. + new_pg_s0 = (pg_s0 & inv_active) | (g_s0 & active_mask) + new_pg_s1 = (pg_s1 & inv_active) | (g_s1 & active_mask) + new_pp_s0 = (pp_s0 & feedthrough_pp_mask) | (p_s0 & p_mask) + new_pp_s1 = (pp_s1 & feedthrough_pp_mask) | (p_s1 & p_mask) + + return _SecAddStage( + pg=(new_pg_s0 & _MASK32, new_pg_s1 & _MASK32), + pp=(new_pp_s0 & _MASK32, new_pp_s1 & _MASK32), + pre_p=stage.pre_p, + level=level, + rand=0, + ) + + def _final_sum(self, stage: _SecAddStage) -> Tuple[int, int]: + """Combinational sum: result[bi] = pre_p[bi] ^ pg[bi-1], carry-out at bit 32.""" + pg_s0, pg_s1 = stage.pg + pre_p_s0, pre_p_s1 = stage.pre_p + # (pg << 1) places pg[bi-1] at position bi, bit 0 gets 0 (no carry-in). + res_s0 = (pre_p_s0 ^ ((pg_s0 << 1) & _MASK32)) | ((pg_s0 >> 31) << 32) + res_s1 = (pre_p_s1 ^ ((pg_s1 << 1) & _MASK32)) | ((pg_s1 >> 31) << 32) + return res_s0, res_s1 + + +class _MaskingAccelerator: + """Pipeline-accurate model of the masking accelerator using SecureAdder. + + Implements all four modes selected by `mode`: + SecAdd: single pass, no modular reduction, latency 7. + SecAddMod: two-pass modular adder, latency 15 / 22. + A2B: arithmetic-to-boolean conversion, latency 15 / 22. + B2A: boolean-to-arithmetic conversion, latency 16 / 23. + + Calling convention (from MaskingAcceleratorInterface each cycle): + peek(): read oldest result + step(rand, mask_0, mask_1): advance pipeline + push(s00,s01,s10,s11, + rand, mask_0, mask_1): register one pass-1 input + """ + + VEC_SIZE = 8 # number of 32-bit lanes per batch + + def __init__(self, mod_wsr: DumbISPR, mode: MaiOperation) -> None: + self._mod_wsr = mod_wsr + self._mode = mode + self._adder = SecureAdder() + # Completed (s0_32, s1_32) results ready for writeback. Not reset between + # batches so that writeback can drain while the next batch is already starting. + self._output_queue: List[Tuple[int, int]] = [] + self._reset_batch() + + def _reset_batch(self) -> None: + # Input register: (inp1, inp2) waiting to be pushed to the adder. + self._inp_reg: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None + # Pass-1 result FIFO: 33-bit share pairs buffered for pass-2. + self._pass1_buf: List[Tuple[int, int]] = [] + # B2A only: mask_mod values pushed alongside each input element. + self._mask_fifo: List[int] = [] + # How many pass-1 inputs have been pushed from the register into the adder. + self._pass1_adder_count: int = 0 + # How many pass-2 inputs have been pushed to the adder. + self._pass2_fed: int = 0 + # True once all pass-1 inputs are done and pass-2 is self-feeding. + self._in_pass2: bool = False + # Running total of adder outputs collected this batch. + self._adder_outputs: int = 0 + + def push(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int, + mask_0: int, mask_1: int) -> None: + """Encode one pass-1 element and store it in the input register.""" + mod = self._modulus() + inp1, inp2 = self._encode(in0_s0, in0_s1, in1_s0, in1_s1, + mask_0, mask_1, mod) + self._inp_reg = (inp1, inp2) + if self._mode == MaiOperation.B2A: + self._mask_fifo.append(mask_0 & _mod_smear(mod)) + + def step(self, rand: int, stall: bool = False) -> None: + """Advance the pipeline one cycle.""" + if stall: + return + + # 1. Collect adder output before advancing. + raw = self._adder.peek() + if raw is not None: + self._adder_outputs += 1 + if self._enable_mod(): + if self._adder_outputs <= self.VEC_SIZE: + self._pass1_buf.append(raw) # pass-1 -> buffer + else: + self._emit_result(raw) # pass-2 -> output + else: + s0, s1 = raw + self._output_queue.append((s0 & _MASK32, s1 & _MASK32)) + + # Once the last expected output is collected, reset input-tracking so the + # next batch starts clean. _output_queue is left intact, writeback may + # still be draining it. + total_outputs = 2 * self.VEC_SIZE if self._enable_mod() else self.VEC_SIZE + if self._adder_outputs == total_outputs: + assert not self._mask_fifo, \ + "B2A mask_fifo not empty at batch end, push/emit count mismatch" + self._reset_batch() + + # 2. Advance adder pipeline. + self._adder.step(rand) + + # 3. Push input register -> adder (mutually exclusive with pass-2 feed). + pushed_p1 = False + if self._inp_reg is not None: + inp1, inp2 = self._inp_reg + self._adder.push(inp1, inp2, rand) + self._inp_reg = None + self._pass1_adder_count += 1 + pushed_p1 = True + + # 4. Enter pass-2 one cycle after the last pass-1 push. + if (self._enable_mod() + and self._pass1_adder_count == self.VEC_SIZE + and not self._in_pass2 + and not pushed_p1): + self._in_pass2 = True + + # 5. Self-feed pass-2 inputs (one per cycle, not on same cycle as a p1 push). + if (self._in_pass2 + and not pushed_p1 + and self._pass2_fed < self.VEC_SIZE + and self._pass1_buf): + p1 = self._pass1_buf.pop(0) + inp1_p2, inp2_p2 = self._pass2_inputs(p1) + self._adder.push(inp1_p2, inp2_p2, rand) + self._pass2_fed += 1 - def step(self) -> None: - '''Advance the pipeline by one stage if possible.''' - # This accelerator implementation features no backpressure, so we always advance the - # pipeline. We insert an unused pipeline slot which is replaced in case a new item is - # pushed. The last element is dropped. - none: List[Optional[Tuple[int, int]]] = [None] - self.pipeline = none + self.pipeline[:self.latency - 2] + def peek(self) -> Optional[Tuple[int, int]]: + """Return the oldest completed (s0, s1) pair, or None.""" + if self._output_queue: + return self._output_queue.pop(0) + return None def is_busy(self) -> bool: - '''Return True if the accelerator is busy (has pending operations), False otherwise.''' - # The accelerator is busy if there is at least one non-None item in the pipeline. - return any(slot is not None for slot in self.pipeline) + if self._inp_reg is not None: + return True + if self._adder._num_in_flight > 0: + return True + return bool(self._pass1_buf or self._output_queue) def _modulus(self) -> int: - '''Return the bits 31:0 of the MOD WSR, for use as a modulus.''' - return self.mod_wsr.read_unsigned() & ((1 << 32) - 1) - - def _compute(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int) -> Tuple[int, int]: - '''Compute the result of the masking operation.''' - raise NotImplementedError - - -class A2BAccelerator(MaskingAccelerator): - def __init__(self, mod_wsr: DumbISPR): - super().__init__(ACCELERATOR_LATENCIES[MaiOperation.A2B], mod_wsr) - - def _compute(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int) -> Tuple[int, int]: - # The current placeholder implementation removes the arithmetic mask and adds a new boolean - # mask. We use a fixed mask until the exact design is known. - # - # Input: (x - s mod q, s), (x - s) + s mod q = x, 0 <= s < q - # Output: (x XOR r, r), (x XOR r) XOR r = x, 0 <= x, r < 2^k - - # in1_s0 and in1_s1 are not used by the A2B accelerator - - s = in0_s1 - # We take a fixed mask which satisfies the constraints until the exact design is known. - r = self._modulus() // 2 - secret = (in0_s0 + s) % self._modulus() - masked_secret = (secret ^ r) - - # Optionally, we crash if the constraints are not met. - if CHECK_ACCELERATOR_CONSTRAINTS: - assert self._modulus() < 2**32 - assert 0 <= s < self._modulus() - assert 0 <= r < 2**32 - assert 0 <= secret < self._modulus() - - # Limit results to 32 bits - masked_secret &= ((1 << 32) - 1) - r &= ((1 << 32) - 1) - return (masked_secret, r) - - -class B2AAccelerator(MaskingAccelerator): - def __init__(self, mod_wsr: DumbISPR): - super().__init__(ACCELERATOR_LATENCIES[MaiOperation.B2A], mod_wsr) - - def _compute(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int) -> Tuple[int, int]: - # The current placeholder implementation removes the boolean mask and adds a new arithmetic - # mask. We use a fixed mask until the exact design is known. - # - # Input: (x XOR r, r), 0 <= x, r < 2^k - # Output: (x - s mod q, s), (x - s) + s mod q = x, 0 <= s < q - - # in1_s0 and in1_s1 are not used by the B2A accelerator - - # We take a fixed mask which satisfies the constraints until the exact design is known. - s = self._modulus() // 2 - r = in0_s1 - - secret = in0_s0 ^ r - masked_secret = (secret - s) % self._modulus() - - # Optionally, we crash if the constraints are not met. - if CHECK_ACCELERATOR_CONSTRAINTS: - assert self._modulus() < 2**32 - assert 0 <= in0_s0 < self._modulus() - assert 0 <= r < 2**32 - assert 0 <= s < self._modulus() - - # Limit results to 32 bits - masked_secret &= ((1 << 32) - 1) - s &= ((1 << 32) - 1) - return (masked_secret, s) - - -class SecAddModkAccelerator(MaskingAccelerator): - def __init__(self, mod_wsr: DumbISPR): - super().__init__(ACCELERATOR_LATENCIES[MaiOperation.SECADD], mod_wsr) - - def _compute(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int) -> Tuple[int, int]: - # The current placeholder implementation removes the boolean masks, adds the secrets - # modulo 2**32, and adds a new boolean mask. We use a fixed mask until the exact design is - # known. - # - # Input: (x xor r1, r1), (y xor s1, s1), 0 <= x, y, s, r < q < 2^k - # Output: ((x + y mod q) XOR t, t) - r1 = in0_s1 - s1 = in1_s1 - # We take a fixed mask until the exact design is known. - t = self._modulus() // 2 - - x = in0_s0 ^ r1 - y = in1_s0 ^ s1 - sum = (x + y) % 2**32 - masked_sum = sum ^ t - - if CHECK_ACCELERATOR_CONSTRAINTS: - assert self._modulus() < 2**32 - assert 0 <= x < self._modulus() - assert 0 <= y < self._modulus() - assert 0 <= r1 < self._modulus() - assert 0 <= s1 < self._modulus() - - # Limit results to 32 bits - masked_sum &= ((1 << 32) - 1) - t &= ((1 << 32) - 1) - return (masked_sum, t) + return self._mod_wsr.read_unsigned() & _MASK32 + + def _enable_mod(self) -> bool: + return self._mode != MaiOperation.SECADD + + def _encode(self, in0_s0: int, in0_s1: int, in1_s0: int, in1_s1: int, + mask_0: int, mask_1: int, + mod: int) -> Tuple[Tuple[int, int], Tuple[int, int]]: + """Compute (inp1_share, inp2_share) for the adder per mode.""" + mod_neg = (-mod) & _MASK32 + if self._mode == MaiOperation.A2B: + # inp1 = (a0 ^ r0, r0) + # inp2 = ((a1 + mod_neg) ^ r1, r1) + inp1 = (in0_s0 ^ mask_0, mask_0) + inp2 = (((in0_s1 + mod_neg) & _MASK32) ^ mask_1, mask_1) + elif self._mode == MaiOperation.B2A: + # inp1 = in0_i (Boolean sharing of a) + # inp2 = ((-mask_mod) ^ r1, r1) + mask_mod = mask_0 & _mod_smear(mod) + inp1 = (in0_s0, in0_s1) + inp2 = (((-mask_mod) & _MASK32) ^ mask_1, mask_1) + else: + # SecAdd / SecAddMod: pass through directly. + inp1 = (in0_s0, in0_s1) + inp2 = (in1_s0, in1_s1) + return inp1, inp2 + + def _pass2_inputs( + self, p1: Tuple[int, int] + ) -> Tuple[Tuple[int, int], Tuple[int, int]]: + """Compute pass-2 adder inputs from a 33-bit pass-1 result. + + carry0=1 means share 0 overflowed: no correction needed. + carry1=1 means share 1 overflowed: add modulus to share 1 to correct. + """ + s0, s1 = p1 + mod = self._modulus() + carry0 = (s0 >> 32) & 1 + carry1 = (s1 >> 32) & 1 + inp1 = (s0 & _MASK32, s1 & _MASK32) + inp2 = (0 if carry0 else mod, mod if carry1 else 0) + return inp1, inp2 + + def _emit_result(self, raw: Tuple[int, int]) -> None: + """Finalise a pass-2 (or direct) output and enqueue it.""" + s0, s1 = raw + if self._mode == MaiOperation.B2A: + # result[0] = mask_mod (popped from FIFO) + # result[1] = XOR of the two masked shares, narrowed by mod_smear + mask_mod = self._mask_fifo.pop(0) + mod = self._modulus() + diff = (s0 & _MASK32) ^ (s1 & _MASK32) + self._output_queue.append((mask_mod, diff & _mod_smear(mod))) + else: + self._output_queue.append((s0 & _MASK32, s1 & _MASK32)) class MaskingAcceleratorInterface: def __init__(self, csrs: CSRFile, wsrs: WSRFile) -> None: + # _cnt: element-index offset for dispatch. Loaded from URND.cnt during + # the secure wipe and after each 8-element dispatch. Initialised to 0; + # the first real value comes from on_sec_wipe_zero_step() before EXEC. + self._cnt = 0 + # _wb_cnt: element-index offset for writeback. Matches _cnt at operation + # start and is updated when writeback completes, ensuring dispatch and + # writeback for the same operation use the same counter value. + self._wb_cnt = 0 self.on_start(csrs, wsrs) - def _accelerator(self) -> MaskingAccelerator: - '''Return the currently selected masking accelerator based on the operation field.''' - return self._all_accelerators[self.csrs.MAI_CTRL.current_operation()] - def on_start(self, csrs: CSRFile, wsrs: WSRFile) -> None: '''Reset the MAI for the start of an OTBN execution''' self.csrs = csrs self.wsrs = wsrs - # All available accelerators are instantiated here in a dictionary. - # The currently active accelerator is selected based on the operation field in MAI_CTRL. - # Changing the operation while an operation is ongoing is not allowed (see - # is_valid_ctrl_change). Thus, the step() method can simply read the operation field each - # cycle to get the current accelerator. - # TODO: Decide whether the accelerators are reset or not - self._all_accelerators = { - MaiOperation.A2B: A2BAccelerator(self.wsrs.MOD), - MaiOperation.B2A: B2AAccelerator(self.wsrs.MOD), - MaiOperation.SECADD: SecAddModkAccelerator(self.wsrs.MOD), - } + # Single accelerator object shared handles all modes. Its _mode is updated + # to the current MAI_CTRL operation when a new operation starts (start bit + # fires). + self._accel = _MaskingAccelerator(self.wsrs.MOD, MaiOperation.A2B) # Dispatch related variables # The dispatch logic is responsible for pushing inputs into the accelerator. @@ -214,6 +600,25 @@ def on_start(self, csrs: CSRFile, wsrs: WSRFile) -> None: # output WSRs. self._writeback_idx = 0 + # NOTE: _cnt and _wb_cnt are intentionally not reset here. They are + # captured during the preceding secure wipe via on_sec_wipe_zero_step() + # and must survive the on_start() call so that the first MAI operation + # uses the correct element ordering. + + # Deferred busy clear: set True when the last output is written, cleared + # in the following step() to model the one-cycle register-update latency. + self._pending_busy_clear = False + + def on_sec_wipe_zero_step(self) -> None: + '''Called during the secure-wipe zero step to latch the initial counter. + + The URND Bivium has been stepped up to this cycle in _step_wiping(). The + keystream for this cycle is already in wsrs.URND._next_value. + ''' + _, _, _, cnt = _urnd_fields(self.wsrs.URND.pending_value()) + self._cnt = cnt + self._wb_cnt = cnt + def step(self) -> None: '''Advance the MAI simulation by one cycle. @@ -223,32 +628,53 @@ def step(self) -> None: # Setting values "immediately" simplifies the status flag handling because the abort case # must not be considered. + rand, mask_0, mask_1, _ = _urnd_fields(self.wsrs.URND._value) + + # Apply deferred busy clear from the previous cycle. + if self._pending_busy_clear: + self._pending_busy_clear = False + self.csrs.MAI_STATUS.update_busy_bit(False) + + # Idle fast-path: if nothing is active and no new start is pending, + # skip all pipeline/writeback work. + if (not self.is_dispatching + and not self.csrs.MAI_STATUS.is_busy() + and not self.csrs.MAI_CTRL.is_start_bit_set()): + return + # Writeback logic: # Get the newest result and write it into the output WSRs. This is done before # advancing the pipeline to model the fact that the result is available at # the start of the cycle. - results = self._accelerator().peek() + results = self._accel.peek() if results is not None: + res_s0, res_s1 = results + wb_idx = (self._wb_cnt + self._writeback_idx) % _MaskingAccelerator.VEC_SIZE # Write to the output WSRs - self.wsrs.MAI_RES_S0.set_32bit_unsigned(results[0], self._writeback_idx) - self.wsrs.MAI_RES_S1.set_32bit_unsigned(results[1], self._writeback_idx) + self.wsrs.MAI_RES_S0.set_32bit_unsigned(res_s0, wb_idx) + self.wsrs.MAI_RES_S1.set_32bit_unsigned(res_s1, wb_idx) self._writeback_idx += 1 - # Detect if we finished writing back - if self._writeback_idx >= 8: + # Detect if we finished writing back. Defer the busy clear by one cycle + # to model the one-cycle register-update latency. Update _wb_cnt from + # _cnt so the next operation's writeback uses the cnt latched at the + # end of this operation's dispatch. + if self._writeback_idx >= _MaskingAccelerator.VEC_SIZE: self._writeback_idx = 0 - # Reset the busy bit in the cycle where the last result is set. - self.csrs.MAI_STATUS.update_busy_bit(False) + self._wb_cnt = self._cnt + self._pending_busy_clear = True # Advance the accelerator pipeline - self._accelerator().step() + self._accel.step(rand) # Start a new operation if start bit was set in last cycle if self.csrs.MAI_CTRL.is_start_bit_set(): # The start bit may only be set if the MAI is not busy. If this assertion fails, the # check when writing to the MAI_CTRL CSR is wrong. assert not self.csrs.MAI_STATUS.is_busy() - # Begin pushing inputs in the dispatch logic + self._accel._mode = self.csrs.MAI_CTRL.current_operation() + # _cnt was already captured in the wipe phase via on_sec_wipe_zero_step(). + # Begin pushing inputs in the dispatch logic. self.is_dispatching = True # Immediately set the busy bit so the current instruction is not allowed to start the # next execution. @@ -260,18 +686,32 @@ def step(self) -> None: self.csrs.MAI_CTRL.update_start_bit(False) if self.is_dispatching: - self._accelerator().push(self.wsrs.MAI_IN0_S0.read_32bit_unsigned(self._dispatch_idx), - self.wsrs.MAI_IN0_S1.read_32bit_unsigned(self._dispatch_idx), - self.wsrs.MAI_IN1_S0.read_32bit_unsigned(self._dispatch_idx), - self.wsrs.MAI_IN1_S1.read_32bit_unsigned(self._dispatch_idx)) - self._dispatch_idx += 1 + # B2A rejection sampling. + # Stall this cycle without advancing _dispatch_idx so the next cycle's URND + # provides a fresh mask_mod candidate. + b2a_stall = False + if self.csrs.MAI_CTRL.current_operation() == MaiOperation.B2A: + mod = self.wsrs.MOD.read_unsigned() & _MASK32 + if mod > 0 and (mask_0 & _mod_smear(mod)) >= mod: + b2a_stall = True + if not b2a_stall: + idx = (self._cnt + self._dispatch_idx) % _MaskingAccelerator.VEC_SIZE + in0_s0 = self.wsrs.MAI_IN0_S0.read_32bit_unsigned(idx) + in0_s1 = self.wsrs.MAI_IN0_S1.read_32bit_unsigned(idx) + in1_s0 = self.wsrs.MAI_IN1_S0.read_32bit_unsigned(idx) + in1_s1 = self.wsrs.MAI_IN1_S1.read_32bit_unsigned(idx) + self._accel.push(in0_s0, in0_s1, in1_s0, in1_s1, mask_0, mask_1) + self._dispatch_idx += 1 # Detect if we have finished dispatching - if self._dispatch_idx >= 8: + if self._dispatch_idx >= _MaskingAccelerator.VEC_SIZE: self._dispatch_idx = 0 self.is_dispatching = False - # Immediately set the input-ready bit as the input WSRs can be overwritten in this - # cycle. + # Latch the random dispatch index for the next execution. + _, _, _, new_cnt = _urnd_fields(self.wsrs.URND._value) + self._cnt = new_cnt + # Immediately set the input-ready bit as the input WSRs can be overwritten in + # this cycle. self.csrs.MAI_STATUS.update_input_ready_bit(True) def is_busy(self) -> bool: @@ -290,16 +730,20 @@ def ready_to_start(self) -> bool: def is_valid_ctrl_change(self, value: int) -> bool: '''Return whether writing value to the MAI_CTRL CSR is currently allowed.''' - # Starting is only allowed if MAI is ready. - if self.csrs.MAI_CTRL.would_set_start_bit(value) and not self.ready_to_start(): + # Bits [31:6] are reserved; any non-zero bits there are a software error. + if self.csrs.MAI_CTRL.has_reserved_bits(value): return False - # We only allow setting the operation to valid options. - if not self.csrs.MAI_CTRL.is_valid_operation(value): + # Changing the operation while the MAI is busy is a software error. + if self.is_busy() and self.csrs.MAI_CTRL.would_change_raw_op(value): return False - # Changing the operation is only allowed if MAI is not busy / no operation is ongoing. - if self.csrs.MAI_CTRL.would_change_op(value) and self.is_busy(): - return False + # When start fires, the MAI must not be busy and the next operation value must be + # a valid choice. + if self.csrs.MAI_CTRL.would_set_start_bit(value): + if not self.ready_to_start(): + return False + if not self.csrs.MAI_CTRL.is_raw_op_valid(): + return False return True diff --git a/hw/ip/otbn/dv/otbnsim/sim/mai_ispr.py b/hw/ip/otbn/dv/otbnsim/sim/mai_ispr.py index 6caf2eaa2db8f..af831e746279c 100644 --- a/hw/ip/otbn/dv/otbnsim/sim/mai_ispr.py +++ b/hw/ip/otbn/dv/otbnsim/sim/mai_ispr.py @@ -28,19 +28,20 @@ def on_start(self) -> None: super().on_start() # On start, the default operation is set. self._operation = MaiOperation.A2B + self._raw_op: int = int(MaiOperation.A2B) self._start_bit = False self._value = self._get_value() - def _construct_value(self, start_bit: bool, operation: MaiOperation) -> int: - '''Construct a register value based on a operation and start bit combination.''' - raw = (((operation & self.OPERATION_MASK) << self.OPERATION_OFFSET) | - (start_bit << self.START_BIT_OFFSET)) + def _construct_value(self, start_bit: bool, raw_op: int) -> int: + '''Construct a register value from raw op bits and a start bit.''' + raw = (((raw_op & self.OPERATION_MASK) << self.OPERATION_OFFSET) | + (int(start_bit) << self.START_BIT_OFFSET)) assert 0 <= raw < (1 << self.width) return raw def _get_value(self) -> int: - '''Construct the register value based on the current operation and start bit.''' - return self._construct_value(self._start_bit, self._operation) + '''Construct the register value based on the current raw op bits and start bit.''' + return self._construct_value(self._start_bit, self._raw_op) def _extract_start_bit(self, value: int) -> bool: '''Extract the start bit from a register value.''' @@ -52,6 +53,10 @@ def _extract_operation(self, value: int) -> MaiOperation: # If the conversion failed, the check when updating the operation did fail already. return MaiOperation((value >> self.OPERATION_OFFSET) & self.OPERATION_MASK) + def _extract_raw_op(self, value: int) -> int: + '''Extract the raw (possibly invalid) operation bits from a register value.''' + return (value >> self.OPERATION_OFFSET) & self.OPERATION_MASK + def _extract_fields(self, value: int) -> tuple[bool, MaiOperation]: '''Extract the fields from a register value.''' return self._extract_start_bit(value), self._extract_operation(value) @@ -72,11 +77,31 @@ def _update_fields(self, start_bit: Optional[bool] = None, def commit(self) -> None: if self._next_value is not None: - self._start_bit, self._operation = self._extract_fields(self._next_value) + self._start_bit = self._extract_start_bit(self._next_value) + self._raw_op = self._extract_raw_op(self._next_value) + try: + self._operation = MaiOperation(self._raw_op) + except ValueError: + pass # keep _operation as the last valid MaiOperation self._value = self._next_value self._next_value = None self._pending_write = False + def has_reserved_bits(self, value: int) -> bool: + '''Return True if value has any bits set outside the defined [5:0] field. + + Mirrors RTL ispr_mai_sw_err.rsvd_csr_write: any write with bits [31:6] non-zero. + ''' + valid_mask = (self.OPERATION_MASK << self.OPERATION_OFFSET) | self.START_BIT_MASK + return bool(value & ~valid_mask & 0xFFFFFFFF) + + def is_raw_op_valid(self) -> bool: + '''Return True if the currently registered op field is a valid MaiOperation. + + Mirrors RTL ma_mask_op_q validity check used by invalid_op detection. + ''' + return self.is_valid_operation(self._raw_op << self.OPERATION_OFFSET) + def is_start_bit_set(self) -> bool: '''Get the start bit from the CSR.''' return self._start_bit @@ -106,11 +131,12 @@ def is_valid_operation(self, value: int) -> bool: except ValueError: return False - def would_change_op(self, value: int) -> bool: - '''Return whether writing value to the CSR would change the operation field. - The value to be checked must specify a valid operation option otherwise we crash. + def would_change_raw_op(self, value: int) -> bool: + '''Return whether writing value would change the op field (compares raw bits). + + Safe to call with any value, including those with invalid op encodings. ''' - return self._extract_operation(value) != self.current_operation() + return self._extract_raw_op(value) != self._raw_op class MaiStatusCSR(DumbISPR): diff --git a/hw/ip/otbn/dv/otbnsim/sim/sim.py b/hw/ip/otbn/dv/otbnsim/sim/sim.py index 975551b504d2f..e868d66328fc6 100644 --- a/hw/ip/otbn/dv/otbnsim/sim/sim.py +++ b/hw/ip/otbn/dv/otbnsim/sim/sim.py @@ -7,7 +7,7 @@ from .constants import ErrBits, LcTx, Status, read_lc_tx_t from .decode import EmptyInsn from .isa import OTBNInsn -from .state import OTBNState, FsmState +from .state import OTBNState, FsmState, WIPE_CYCLES from .stats import ExecutionStats from .trace import Trace @@ -433,6 +433,10 @@ def _step_wiping(self, verbose: bool) -> StepRes: was_wiping = self.state.wipe_cycles > 0 if was_wiping: self.state.wipe_cycles -= 1 + # Step the URND Bivium every wipe cycle, matching the RTL where prim_trivium + # runs continuously. This keeps the Bivium in sync with the RTL so that the + # MAI's cnt bits captured in on_sec_wipe_zero_step() match in_cnt_load_val_q. + self.state.wsrs.URND.step() is_good = not self.state.lock_after_wipe locking = self.state.rma_req == LcTx.ON or not is_good @@ -469,6 +473,29 @@ def _step_wiping(self, verbose: bool) -> StepRes: else: self._delayed_insn_cnt_zero(1) + if self.state.wipe_cycles == WIPE_CYCLES - 97: + # Corresponds to the RTL cycle where sec_wipe_mai_i (= sec_wipe_zero_o) + # fires, which is when the FSM enters OtbnStartStopSecureWipeAllZero after + # completing the three register-overwriting phases: + # + # OtbnStartStopSecureWipeWdrUrnd: 33 cycles (32 WDRs + 1 ACC timing) + # OtbnStartStopSecureWipeAccModBaseUrnd: 32 cycles (ACC, MOD, base regs) + # OtbnStartStopSecureWipeExtIsprsUrnd: 32 cycles (MAI ISPRs + reserved) + # = 97 URND-advancing cycles total + # + # The URND value seen by the MAI at that moment is not the raw prim_trivium + # output but the registered copy one cycle behind it (otbn_rnd.sv: + # urnd_data_q <= key_o, registered each posedge). So in_cnt_load_val_q + # captures the Bivium output from step 97, not step 98. + # + # In the ISS, _WIPE_CYCLES = 100 and wipe_cycles decrements once per step, so + # after 97 steps wipe_cycles = 3. At that point _next_value holds the step-97 + # Bivium output, matching what the RTL latches into in_cnt_load_val_q. + final_wipe_round_cnt = (self.state.wipe_rounds_done == + (self.state.wipe_rounds_to_do - 1)) + if final_wipe_round_cnt: + self.state.mai.on_sec_wipe_zero_step() + if self.state.wipe_cycles == 1: # This is the penultimate clock cycle of a wipe round. We want to # model the behaviour that we expect "at the end of a wipe round". diff --git a/hw/ip/otbn/dv/otbnsim/sim/state.py b/hw/ip/otbn/dv/otbnsim/sim/state.py index 405d518b429c0..7a85c5748e880 100644 --- a/hw/ip/otbn/dv/otbnsim/sim/state.py +++ b/hw/ip/otbn/dv/otbnsim/sim/state.py @@ -23,7 +23,7 @@ # The number of cycles spent per round of a secure wipe. This takes constant # time in the RTL, mirrored here. The constant here needs to be incremented # by one compared to the constant found in RTL (`otbn_core_model.sv`) -_WIPE_CYCLES = 99 + 1 +WIPE_CYCLES = 99 + 1 class FsmState(IntEnum): @@ -484,7 +484,7 @@ def set_fsm_state(self, new_state: FsmState) -> None: # the wiping operation itself will take. wiping_next = new_state == FsmState.WIPING if wiping_next: - self.wipe_cycles = _WIPE_CYCLES + self.wipe_cycles = WIPE_CYCLES self._next_fsm_state = new_state def set_flags(self, fg: int, flags: FlagReg) -> None: diff --git a/hw/ip/otbn/dv/otbnsim/sim/wsr.py b/hw/ip/otbn/dv/otbnsim/sim/wsr.py index 0782b65680329..241a5a2863fac 100644 --- a/hw/ip/otbn/dv/otbnsim/sim/wsr.py +++ b/hw/ip/otbn/dv/otbnsim/sim/wsr.py @@ -148,6 +148,10 @@ def set_seed(self, value: int) -> None: # generate a keystream. self.running = True + def pending_value(self) -> int: + '''Return the Bivium output scheduled by step(), before commit() latches it.''' + return self._next_value + def step(self) -> None: # Schedule an state update and readout the keystream. self._trivium.update() From f10a675cd1bfc77e4210d609bdafabfa134e338a Mon Sep 17 00:00:00 2001 From: Hakim Filali Date: Mon, 29 Jun 2026 16:03:35 +0200 Subject: [PATCH 2/2] [sw,otbn] Re-enable secAdd for OTBN smoke test Signed-off-by: Hakim Filali --- hw/ip/otbn/dv/smoke/smoke_expected.txt | 6 +++--- hw/ip/otbn/dv/smoke/smoke_test.s | 19 +++++++++++++------ sw/device/tests/otbn_isa_test.c | 10 +++++----- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/hw/ip/otbn/dv/smoke/smoke_expected.txt b/hw/ip/otbn/dv/smoke/smoke_expected.txt index b8a038419b3b0..d2d9f71a9c5d9 100644 --- a/hw/ip/otbn/dv/smoke/smoke_expected.txt +++ b/hw/ip/otbn/dv/smoke/smoke_expected.txt @@ -35,7 +35,7 @@ x26 | 0x00000016 x27 | 0x0000001a x28 | 0x00400000 x29 | 0x00018000 -x30 | 0x53bcb7d3 +x30 | 0x9ccf1600 x31 | 0x00000804 Final Bignum Register Values: @@ -71,5 +71,5 @@ w26 | 0x78fccc06_2228e9d6_89c9b54f_887cf1ee_efbafabd_f9bfd9ee_baeebbbb_dbff9bfa w27 | 0x28a88802_00088990_8888a00a_88189108_828aa820_09981808_8822aa2a_11109898 w28 | 0xd25666ac_bbb1704f_23631fe5_11e568d7_6d30528f_f027c1f7_32cc1191_caef0343 w29 | 0x4f0d4b81_9f24f0c1_64341d3c_26628bdb_5763bcdf_63388709_e0654fef_eb0953c2 -w30 | 0x2167f87d_e9ee7ac7_ffa3d88b_ab123192_aee49292_4efa2ec9_b55098e0_68ba2fa1 -w31 | 0x37adadae_f9dbff5e_73880075_5466a52c_67a8c221_6978ad1b_25769434_0f09b7c8 +w30 | 0x0fa66f64_739babfd_463a6559_44ad0257_98ef0ef8_84628bf7_edbbd1d6_8b78539b +w31 | 0x00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000 diff --git a/hw/ip/otbn/dv/smoke/smoke_test.s b/hw/ip/otbn/dv/smoke/smoke_test.s index 7376ee131469d..170101f4187f5 100644 --- a/hw/ip/otbn/dv/smoke/smoke_test.s +++ b/hw/ip/otbn/dv/smoke/smoke_test.s @@ -372,17 +372,24 @@ bn.wsrr w13, MAI_IN0_S1 bn.wsrr w14, MAI_IN1_S0 bn.wsrr w15, MAI_IN1_S1 -# TODO: uncomment the code below once the OTBNsim model of MAI is aligned with the RTL implementation. # Execute SecAdd on MAI -# li x30, 0x2f -# csrrw x0, MAI_CTRL, x30 +li x30, 0x2f +csrrw x0, MAI_CTRL, x30 # Poll for completion of MAI execution -# jal x1, mai_poll_busy +jal x1, mai_poll_busy # Read back results from MAI -# bn.wsrr w30, MAI_RES_S0 -# bn.wsrr w31, MAI_RES_S1 +bn.wsrr w30, MAI_RES_S0 +bn.wsrr w31, MAI_RES_S1 +# SecAdd result: in0[k] = w12[k] ^ w13[k], in1[k] = w14[k] ^ w15[k], +# w30[k] = in0[k] + in1[k] mod 2^32 for each 32-bit coefficient k. +# in0 = 0x8d09995b_664e0f38_de9ee010_e8d217a0_98ef0d70_8f12b880_edbbce4e_b598fc3d +# in1 = 0x829cd609_0d4d9cc5_679b8549_5bdaeab7_00000188_f54fd377_00000388_d5df575e +# w30 = in0 + in1 mod 2^32 per lane = 0x0fa66f64_739babfd_463a6559_44ad0257_98ef0ef8_84628bf7_edbbd1d6_8b78539b +bn.xor w30, w30, w31 +# w31 = 0x00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000 +bn.xor w31, w31, w31 # Read from URND in the OTBN Verilator smoke test and 0x0 when running on the FPGA or chip-level test .ifnotdef deterministic diff --git a/sw/device/tests/otbn_isa_test.c b/sw/device/tests/otbn_isa_test.c index 69baa61bd7f8e..3381b78dc1cca 100644 --- a/sw/device/tests/otbn_isa_test.c +++ b/sw/device/tests/otbn_isa_test.c @@ -31,7 +31,7 @@ static const otbn_addr_t kWdrState = OTBN_ADDR_T_INIT(smoke_test, wdr_state); enum { kNumExpectedGprs = 30, kNumExpectedWdrs = 32, - kExpectedInstrCount = 299, + kExpectedInstrCount = 322, }; // The expected values of the GPRs and WDRs are taken from @@ -104,10 +104,10 @@ static const uint32_t kExpectedWdrs[kNumExpectedWdrs][8] = { 0x23631fe5, 0xbbb1704f, 0xd25666ac}, [29] = {0xeb0953c2, 0xe0654fef, 0x63388709, 0x5763bcdf, 0x26628bdb, 0x64341d3c, 0x9f24f0c1, 0x4f0d4b81}, - [30] = {0x68ba2fa1, 0xb55098e0, 0x4efa2ec9, 0xaee49292, 0xab123192, - 0xffa3d88b, 0xe9ee7ac7, 0x2167f87d}, - [31] = {0x0f09b7c8, 0x25769434, 0x6978ad1b, 0x67a8c221, 0x5466a52c, - 0x73880075, 0xf9dbff5e, 0x37adadae}, + [30] = {0x8b78539b, 0xedbbd1d6, 0x84628bf7, 0x98ef0ef8, 0x44ad0257, + 0x463a6559, 0x739babfd, 0x0fa66f64}, + [31] = {0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000}, }; bool test_main(void) {