Sketch it out in Python first
Before dealing with hardware cycles and signals, let's write down exactly what values we need to generate after accepting a piece of corpus data. This snippet handles one single scalar value, but remember: the real hardware is doing this same math across 16 parallel lanes and multiple residue limbs.
def encrypted_product(q0, q1, d0, d1, modulus):
e0 = (q0 * d0) % modulus
stash = (q1 * d0) % modulus # phase 0 state
e1 = (q0 * d1 + stash) % modulus
e2 = (q1 * d1) % modulus
return e0, e1, e2
assert encrypted_product(2, 3, 5, 7, 97) == (10, 29, 21)The source code comments are a contract
// Two protocols share one datapath:
// ct × pt: out_a[i] = c0[i]*pt[i], out_b[i] = c1[i]*pt[i]
// ct × ct: e0 = q0*d0, e1 = q0*d1 + q1*d0, e2 = q1*d1
// phase 0 stashes q1*d0; phase 1 completes e1 and e2.
// No evaluation keys; the client decrypts with (1, s, s^2).Here's how it plays out: The query components (q₀ and q₁) are loaded and live permanently in on-chip RAM. Meanwhile, the corpus words come streaming in from memory. If we're scoring a plaintext corpus, the hardware grabs one row of the corpus and multiplies it by both query components at once. But if we're scoring an encrypted corpus, the hardware receives d₀ and d₁ on alternating clock phases, reusing the exact same multipliers.
Time-sharing to build the cross term
The concept of a phase is crucial state information. It tells the hardware which piece of the corpus data just arrived. If the pipeline stalls, the hardware has to freeze the arithmetic data AND the phase bit together. If you mess that up, you might accidentally combine d₁ from one transaction with the stashed q₁d₀ from a completely different transaction.
When one part stalls, the whole pipeline freezes
wire en = ~(m_valid & ~m_ready);
assign s_ready = en;If we have valid output ready to go, but the next guy in line says they aren't ready (m_ready is low), our enable wire (en) drops to zero. That single wire freezes the query reads, the registers, the phase chains, and the multipliers. Everything locks up in perfect alignment. In hardware, keeping data synchronized is vastly more important than keeping a specific multiplier busy.
Worked example
A traffic jam between phases
Let's say the hardware accepts d₀ for row r, computes e₀, and stashes the cross term. Suddenly, the pipeline stalls before d₁ can transfer. The phase stays locked at 1, the row address stays r, and the stash holds onto q₁d₀. When the traffic clears, the hardware accepts d₁ and finishes computing e₁ and e₂. A stall just stretches time; it never scrambles the data pairings.
Test the timeline interactively
This notebook doesn't do any cryptography. Instead, it lets you play with the control logic: can you build a pipeline that survives totally random stalls without dropping, duplicating, or scrambling items?
Change downstream stalls and latency while watching offers, transfers, accepted order, and completion order.
Runs entirely in this browser. Python executes in Pyodide WebAssembly with no remote kernel. The construction code stays visible while reactive dependents recompute whenever you change an input.
Check your understanding
Why do we stash q₁d₀ during phase 0 when scoring an encrypted corpus?
Section summary
- Query data stays parked on-chip, while corpus data streams by.
- 16 lanes chunk through wide coefficient groups simultaneously.
- Encrypted mode cleverly time-shares the multipliers using phases.
- A global enable signal ensures the pipeline stays perfectly aligned during stalls.
Change downstream stalls and latency while watching offers, transfers, accepted order, and completion order.
Runs entirely in this browser. Python executes in Pyodide WebAssembly with no remote kernel. The construction code stays visible while reactive dependents recompute whenever you change an input.
Repository layer · second pass
How does the score engine schedule a simple algebraic product?
At each transformed coefficient and residue limb, ciphertext-ciphertext mode needs four modular products grouped into e₀,e₁,e₂. A resource-constrained engine may time-share lanes across phases: compute c₀d₀ and c₁d₀, stash the cross partial, then compute c₀d₁ and c₁d₁ and finish e₁.
The arithmetic formula is simple; correctness risk lies in phase, tag, valid, and backpressure alignment. State must freeze on stalls and partial products must stay paired with the same transaction, coefficient, limb, and modulus.
Reasoning chain
- 1
Write component equations.
- 2
Assign products to lanes and phases.
- 3
List state carried between phases.
- 4
Gate every advance on handshake.
- 5
Align tags with result latency.
- 6
Scoreboard components independently.
Worked trace
Two-phase cross term
- Phase 0 computes c₀d₀ and c₁d₀.
- Store c₁d₀ with transaction metadata.
- Phase 1 computes c₀d₁ and c₁d₁.
- Output e₁=c₁d₀+c₀d₁.
Result. A stall between phases must preserve the stored partial exactly.
Executable lens · Python
Make the hidden state visible
def ct_ct(c0,c1,d0,d1):
phase0=(c0*d0,c1*d0)
phase1=(c0*d1,c1*d1)
return phase0[0], phase0[1]+phase1[0], phase1[1]Retype this example, predict each intermediate value, and then change one input that touches a boundary.
Misconception clinic
Tempting mistakes
- Pairing a saved partial with the next transaction.
- Advancing phase while downstream blocks output.
Retrieval and transfer
Close the book first
- Draw the phase/state table.
- Add modulus reduction points.
- Write assertions tying phase and transaction ID.