Skip to main content

Module chains

Module chains 

Source
Expand description

The verifier’s chain block: three recirculating chain lanes.

§The problem

To check a W-OTS+ signature, chain i must be hashed from its revealed node to the end of the chain: 3 − digit_i steps, where the digit comes from the message digest. Over a signature’s 68 chains that is 105 steps on average, but anything from 0 to 3 per chain, and only known once the digest is back. The signer’s chain engine (crate::slice_chain) cannot be reused as it is: it pushes every chain through all three steps in a fixed pipeline, which here would waste half of every lane.

§The idea: lanes that recirculate

Each lane is the signer’s chain lane (ChainLane, 20 cycles, one step per clock), closed into a ring. What leaves the lane either goes round again (it needs another step) or is finished (it is a chain end). A slot that comes back empty, or carries a finished chain, takes the next new chain. So every clock does a useful step whenever there is work:

             ┌───────────────────── recirculate (depth < 3) ─────────────────┐
             │                                                               │
  nodes ─▶ fetch ─▶ skid ─▶ ┌mux┐ ─▶ [ chain lane, 20 cycles ] ─▶ ─ R ─▶ ───┘
  (bank)     │      FIFO    └───┘          ▲ salt(depth)                │
             │                                                          │ depth = 3
             └── digit = 3: already an end ─▶ end queue ─┐              ▼
                                                         └──▶ end banks ─▶ row assembler

Chains whose digit is 3 need no step: they wait in their own small queue
for a cycle when the end port is free, so they never take a lane slot.
(Sharing one queue cost a lane slot per such chain, about six per record
and lane: 41.7 instead of about 35 cycles per record, measured in native
simulation.)

Chain c always uses lane c mod 3, so each lane owns its chains’ node and end storage and no two lanes ever write the same RAM.

§Storage

All RAMs are indexed by context (the slot of a record in flight, 0..64) and by k = c / 3, the chain’s number within its lane (0..23):

RAMPer laneAddressWritten byRead by
node banks2 (k even, odd)ctx·16 + k/2the loaderthis lane’s fetch
end banks5 (k mod 5)ctx·8 + k/5this lanethe row assembler
digits1ctxthe hasher (digest result)fetch, once per context
pk_nonce1 (+1 for the row assembler)ctxthe loaderfetch, once per context

Two node banks let the loader write up to four nodes per beat (four consecutive chains fall in four different banks, c mod 6). Five end banks per lane, fifteen in all (c mod 15), let the row assembler read any five consecutive chain ends at once: a 64-byte commitment block touches at most five chains.

§The row assembler

The endpoint commitment hashes pk_nonce ‖ end₀ ‖ … ‖ end₆₇ (1,240 bytes) as 20 BLAKE3 blocks. The hasher asks for block j of a context; two cycles later this block delivers it, built by a fixed byte map (ROW_SOURCE) from the fifteen end banks and the context’s pk_nonce. No block is ever stored: each is assembled when the BLAKE3 lane is about to use it.

§Contract

  • A context’s nodes and pk_nonce are written before its digits arrive.
  • Digits arrive at most once per context between retirements.
  • ChainsOutput::done[ctx] rises once all 68 chain ends of ctx are stored, and stays high until ChainsInput::retire names ctx.
  • A row request for ctx is made only while done[ctx] is high.

Structs§

ChainsInput
Inputs of the chain block.
ChainsOutput
Outputs of the chain block.
DigitsEvent
The digits of one record, from the hasher’s digest result.
LaneDigits
One lane’s share of a record’s digits: digit k belongs to chain lane + 3k.
NodeWrite
One node write into a lane’s node bank.
NonceWrite
A context’s pk_nonce, written once per record by the loader.
Retire
A context leaves the unit: its counters restart from zero.
RingInput
Inputs of one lane ring.
RingOutput
Outputs of one lane ring.
RowRequest
Row request from the hasher: commitment block row of context ctx.
VerifyChains
The three rings, the row assembler and its pk_nonce copy.
VerifyRing
One chain lane closed into a ring, with its node, end, digit and nonce RAMs.

Constants§

CONTEXTS
Records in flight per unit; a record’s context is its number mod 64.
END_BANKS
End banks per lane (k mod 5).
LANES
Chain lanes per unit. Chain c runs on lane c mod LANES.
LANE_CHAINS
Most chains one lane serves per record: lanes 0 and 1 get 23, lane 2 gets 22.
NODE_BANKS
Node banks per lane (k mod 2).
NONCE_SOURCE
Source index of the context’s pk_nonce (padded to 18 bytes).
ROWS
64-byte blocks of the endpoint commitment (16 + 68 × 18 = 1,240 bytes).
ROW_LATENCY
Cycles from a row request to its row on the output.
ROW_SLOT
For block j and end bank e, the k / 5 of the chain the block needs from that bank (0 if it needs none).
ROW_SOURCE
Where byte p of commitment block j comes from: (source, byte).
ROW_SOURCE_BANK
The source half of ROW_SOURCE (kernels index plain usize tables).
ROW_SOURCE_BYTE
The byte half of ROW_SOURCE.
ZERO_SOURCE
Source index of an all-zero node (bytes past the 1,240-byte message).

Functions§

ring_tag_kernel
Packs a ring tag: context (6 bits), chain (7), depth (2), k mod 5 (3), k / 5 (3).
verify_chains_kernel
The chain block’s logic: route writes to the rings, assemble rows.
verify_ring_kernel
One ring’s logic, once per clock.