Skip to main content

Module slice

Module slice 

Source
Expand description

One complete slice: factory, subtree builder, signer and key cache.

§Why slices exist

Almost all of DSig’s work is hashing, and almost all of the hashing is making one-time keys (68 W-OTS+ chains per key). The way to sign faster is to make keys faster, and the way to do that within a LUT budget is to build one compact unit that makes and spends keys on its own, then copy it. That unit is a slice. Each slice makes one key per 68 clocks, so the engine’s signing rate grows with the number of slices (SLICES).

This is the unit the sustained engine repeats eight times. Everything a signature needs is made and spent inside it, except the batch certificate, which comes from the shared cert_unit.

                ┌──────────────────────── Slice ───────────────────────────┐
  config ──────▶│ SliceFactory ──cache_write──▶ 4 × UltraRAM cache banks    │
                │   │ key_done      ▲ Merkle/message slots      │ nodes     │
                │   ▼               │                           ▼           │
                │ SubtreeBuilder ───┘          SliceSigner ◀── proof port   │
                │   │ subtree root               │   ▲ certificate        │
                └───┼────────────────────────────┼───┼─────────────────────┘
                    ▼                            ▼   │
              cert_unit (shared)            frame beats ─▶ HBM writer
PartModuleRole in the slice
factorycrate::slice_factorymakes one key per 68 clocks; also hashes Merkle parents and messages for the others
bank0..bank3crate::cache_bankhold chain positions 0..3 of 512 keys until they are spent
subtreecrate::slice_merklebuilds this slice’s subtree of every batch, keeps the nodes for proofs
signercrate::slice_signerburns a key per message and writes a 1,642-byte signature frame

This file contains almost no logic. It is wiring: it connects the parts and computes one signal of its own, space (cache admission, below).

The same slice drawn as dataflow, with its clock budget and its routed LUT cost. The three chain lanes are what make it fast and what make it big:

One slice: one W-OTS+ key every 68 clocks Default Haraka-512 build. Every hash lane takes a new input every clock (II = 1); nothing waits. slice (× SLICES = 2, 4 or 8) BLAKE3 per key: 1 seed + 2 nonces + 20 XOF + 21 commitment = 44; + 1 Merkle + 1 message slot = 46 of every 68 clocks; 16 keys in flight (contexts) modulo timetableresidue = clock mod 68 BLAKE3 lane (factory)1 compression/clock, 28 clk XOF window1 secret (18 B)/clock Haraka-512depth 0 · 20 clk Haraka-512depth 1 · 20 clk Haraka-512depth 2 · 20 clk chain engine: 1 whole chain / clock key cache512 keys in UltraRAM subtree builderpk_hash leaves → root signerburn key, 68 digits, 1 node/clock frame1,642 B in 26 beats XOF: 68 secrets 68 chain ends → commitment → pk_hash subtree root certificate cert unit (shared): Merkle top + Ed25519 HBM writer Slice LUT (routed slices-2 link, S-boxes in LUTs): 127,502 3 Haraka chain lanes: 3 × 23,902 = 71,706 BLAKE3 16,422 rest 39,374 (cache, signer, subtree…) 68 chains × 3 steps = 204 chain steps per key = 3 lanes × 68 clocks: every lane busy every clock. 205 MHz ÷ 68 = 3.01M keys (and signatures) per second per slice; 2 / 4 / 8 slices → 6.0M / 12.1M / 24.1M sig/s ceiling. Bar to scale. Other chain lanes: BLAKE3 28 clk, 16,422 LUT; SHA-512 82 clk, ~43,200 LUT (estimate). Rest = SLICE_LUT − 4 lanes.

§The life of one key

  factory starts key n ───▶ 68 chains written to cache slot n mod 512
       (needs `space`)          │ key_done (pk_hash, nonces, batch, j)
                                ├──▶ signer: metadata RAM
                                └──▶ subtree: leaf j → … → subtree root ─▶ cert unit
                                                                 │
  cert unit ─▶ certificate for the batch ─▶ signer FIFO ◀────────┘ (much later)
  message arrives ─▶ signer BURNS key n (consumed += 1) ─▶ reads nodes ─▶ frame

A key is spent (“burned”) the moment the signer accepts a message for it, before any output is produced. If a frame is lost afterwards, the key is still gone. The project rule is that a one-time key is never reused after admission, timeout, reset or an uncertain launch; burning first is what makes that true here, because the counter only moves forward.

§Cache admission

The factory assigns cache slots in a ring of CACHE_SLOTS = 512 keys (32 batches). Each bank stores them densely, 68 rows per key (cache_address_kernel). The factory may start key n only while started − consumed < ADMIT_LIMIT = 512 − KEYS_PER_SLICE, so at least one whole batch of slack remains:

  • slot n mod 512 is free, because key n − 512 was burned long ago;
  • the subtree ring entry of batch b is free, because every key of the batch that last used it has been burned. Leaf j and its parents overwrite nodes that are siblings of other keys in the old batch, so freeing only key n − 512 would not be enough.
  key numbers in one slice, 8-slice build (512 slots, 16 keys per batch)

  … burned │ in flight: made, not burned │ may still start │ kept free │
  ─────────┼─────────────────────────────┼─────────────────┼───────────┼──▶ key n
       consumed                       started     consumed + 496  consumed + 512

  start allowed while started − consumed < ADMIT_LIMIT = 512 − 16 = 496

When the signer falls behind (HBM stalls, or certificates still in flight), the factory simply skips key starts: every compression of a skipped key becomes an idle slot.

Structs§

Slice
One slice of the sustained engine.
SliceInput
Slice inputs.
SliceOutput
Slice outputs.

Constants§

ADMIT_LIMIT
Keys that may be started but not yet burned: the cache less one batch.
SUBTREE_SHIFT
log2 KEYS_PER_SLICE as a shift amount: keys_started >> SUBTREE_SHIFT is the batch of the next key (RHDL shifts take Bits, not usize).

Functions§

bank_port_kernel
One cache bank’s port: write position p of a chain, read the signer’s address.
slice_kernel
Pure wiring between the slice’s parts.