|
| 1 | +#import "/book.typ": book-page, aside, rj |
| 2 | +#import "/src.typ": load_config, load_chip |
| 3 | +#import "/chip.typ": ( |
| 4 | + render_chip_variable_table, |
| 5 | + total_nr_variables, |
| 6 | + total_nr_instantiated_columns, |
| 7 | + render_constraint_table, |
| 8 | + render_chip_assumptions, |
| 9 | + render_chip_padding_table, |
| 10 | +) |
| 11 | + |
| 12 | +#let config = load_config() |
| 13 | + |
| 14 | +#show: book-page("sha256.typ") |
| 15 | + |
| 16 | +#let sha256chip = load_chip("src/sha256.toml", config) |
| 17 | +#let sha256msgschedchip = load_chip("src/sha256msgsched.toml", config) |
| 18 | +#let sha256roundchip = load_chip("src/sha256round.toml", config) |
| 19 | +#let rotxorchip = load_chip("src/rotxor.toml", config) |
| 20 | +#let sha256 = raw(sha256chip.name) |
| 21 | +#let sha256msgsched = raw(sha256msgschedchip.name) |
| 22 | +#let sha256round = raw(sha256roundchip.name) |
| 23 | +#let rotxor = raw(rotxorchip.name) |
| 24 | + |
| 25 | +The following chips constitute an accelerator for the SHA256 compression function; |
| 26 | +other aspects of SHA256 hashing (such as repeated compression invocation, |
| 27 | +input padding and state initialization) fall outside the scope of this accelerator. |
| 28 | + |
| 29 | +The base #sha256 chip provides the `ECALL` interface, interacts with memory and then delegates to the #sha256msgsched and #sha256round chips |
| 30 | +to perform the message schedule and the compression rounds, respectively. |
| 31 | +The `SHA256_M` interaction signature is used to represent the output of the message schedule. |
| 32 | +The `SHA256_K` interaction signature is used to represent the `k` constants. |
| 33 | +It could either be instantiated with a (short) precomputed table, or through hardcoded LogUp contributions in this chip. |
| 34 | +For this exposition, we choose the former option, and present a table further below. |
| 35 | +Additionally, we introduce a #rotxor chip to perform the common action of computing the XOR of three rotations (or shifts) of a word. |
| 36 | + |
| 37 | +Most of the structure and variable naming follows the pseudocode of the wikipedia page#footnote(link("https://web.archive.org/web/20260320010021/https://en.wikipedia.org/wiki/SHA-2#Pseudocode")). |
| 38 | + |
| 39 | += #sha256 chip |
| 40 | + |
| 41 | +== Columns |
| 42 | +#let nr_variables = total_nr_variables(sha256chip) |
| 43 | +#let nr_columns = total_nr_instantiated_columns(sha256chip, config) |
| 44 | + |
| 45 | +The #sha256 chip leverages #nr_variables variables, spanning #nr_columns columns: |
| 46 | +#render_chip_variable_table(sha256chip, config) |
| 47 | + |
| 48 | +== Constraints |
| 49 | + |
| 50 | +The first responsibility of the chip is to read the current state and message chunk from memory, |
| 51 | +passed as arguments through pointers. |
| 52 | +Since the memory ranges could overlap, we read the chunk first (in @sha256:c:read_chunk, at timestamp `timestamp`), before reading and writing the state (in @sha256:c:read_state, at timestamp `timestamp + 1`). |
| 53 | +The addresses containing the state and the current chunk are passed in as arguments `A0 = x10` and `A1 = x11`, respectively. |
| 54 | +Note that following the SHA256 spec, this state and the chunks are read and written as big-endian. |
| 55 | +#render_constraint_table(sha256chip, config, groups: "memory") |
| 56 | + |
| 57 | +Then we prepare the message schedule, by emitting the input chunk with multiplicities |
| 58 | +corresponding to the number of times it will be read during a compression evaluation. |
| 59 | +The #sha256msgsched chip itself is implicitly invoked by itself and #sha256round, setting the `amount` |
| 60 | +column appropriately for the number of times the `w` value is required. |
| 61 | +#render_constraint_table(sha256chip, config, groups: "sched") |
| 62 | + |
| 63 | +And finally, we provide the boundaries for the #sha256round chip and the |
| 64 | +final addition of the compression to the old state. |
| 65 | +Observe that we embed the addition into the upper 32 bits of a double word, |
| 66 | +in order to satisfy and use the `ADD` chip. |
| 67 | +#render_constraint_table(sha256chip, config, groups: "compress") |
| 68 | + |
| 69 | +In this VM, we assign syscall number -1 to the #sha256 accelerator. |
| 70 | +The chip therefore contributes the following interaction to the lookup-argument: |
| 71 | +#render_constraint_table(sha256chip, config, groups: "lookup") |
| 72 | + |
| 73 | +== Padding |
| 74 | + |
| 75 | +#render_chip_padding_table(sha256chip, config) |
| 76 | + |
| 77 | += #sha256msgsched chip |
| 78 | + |
| 79 | +== Columns |
| 80 | + |
| 81 | +#let nr_variables = total_nr_variables(sha256msgschedchip) |
| 82 | +#let nr_columns = total_nr_instantiated_columns(sha256msgschedchip, config) |
| 83 | + |
| 84 | +The #sha256msgsched chip leverages #nr_variables variables, spanning #nr_columns columns: |
| 85 | +#render_chip_variable_table(sha256msgschedchip, config) |
| 86 | + |
| 87 | +== Assumptions |
| 88 | + |
| 89 | +#render_chip_assumptions(sha256msgschedchip, config) |
| 90 | + |
| 91 | +== Constraints |
| 92 | + |
| 93 | +First, we gather the dependencies from earlier in the message schedule. |
| 94 | + |
| 95 | +#render_constraint_table(sha256msgschedchip, config, groups: "lookback") |
| 96 | + |
| 97 | +Then, we calculate the result. |
| 98 | +It suffices to check that the carry of adding four range-checked words |
| 99 | +into a range-checked word is not too big, following the logic from @add. |
| 100 | +In this case, using the `IS_BYTE` constraint allows us to add multiple words together |
| 101 | +at the same time, without needing to store and range-check intermediate results. |
| 102 | +#render_constraint_table(sha256msgschedchip, config, groups: "calc") |
| 103 | + |
| 104 | +Finally, we contribute to the LogUp. |
| 105 | +#render_constraint_table(sha256msgschedchip, config, groups: "output") |
| 106 | + |
| 107 | += #sha256round chip |
| 108 | + |
| 109 | +== Columns |
| 110 | + |
| 111 | +#let nr_variables = total_nr_variables(sha256roundchip) |
| 112 | +#let nr_columns = total_nr_instantiated_columns(sha256roundchip, config) |
| 113 | + |
| 114 | +The #sha256round chip leverages #nr_variables variables, spanning #nr_columns columns: |
| 115 | +#render_chip_variable_table(sha256roundchip, config) |
| 116 | + |
| 117 | +== Assumptions |
| 118 | + |
| 119 | +#render_chip_assumptions(sha256roundchip, config) |
| 120 | + |
| 121 | +== Constraints |
| 122 | + |
| 123 | +First, we compute the necessary intermediate values. |
| 124 | +#let bitand = math.class("binary", math.amp) |
| 125 | +To compute `maj`, observe that $ (a bitand b) xor (a bitand c) xor (b bitand c) = (a bitand b) xor (c bitand (a xor b)), $ |
| 126 | +by distribution. |
| 127 | +Additionally, since for this form, $(a bitand b)$ and $(a xor b)$ are disjoint, so are $(a bitand b)$ and $(c bitand (a xor b))$, |
| 128 | +and hence we can replace that top-level XOR with a field addition to compute $(a bitand b) + (c bitand (a xor b))$, |
| 129 | +needing fewer intermediate columns. |
| 130 | +Similarly, `ch` can be written as $(e bitand f) + ((2^32 - 1 - e) bitand g)$. |
| 131 | +#render_constraint_table(sha256roundchip, config, groups: "value") |
| 132 | + |
| 133 | +Then we constrain the addition for the new state, constraining additions with the same `IS_BYTE` trick as before. |
| 134 | +#render_constraint_table(sha256roundchip, config, groups: "addition") |
| 135 | + |
| 136 | +Finally, we chain the rounds together through the interactions. |
| 137 | +#render_constraint_table(sha256roundchip, config, groups: "output") |
| 138 | + |
| 139 | +== Padding |
| 140 | + |
| 141 | +#render_chip_padding_table(sha256roundchip, config) |
| 142 | + |
| 143 | += #rotxor chip |
| 144 | + |
| 145 | + |
| 146 | +This chip takes as input `a`, `r0`, `r1`, `r2` (4-bit values) and a bit `last_rot` to compute |
| 147 | +$ |
| 148 | + cases( |
| 149 | + (a >>> (16 + r_0)) xor (a >>> (16 + r_0 - r_1)) xor (a >>> r_2) quad "if" #`last_rot`, |
| 150 | + (a >>> (16 + r_0)) xor (a >>> (16 + r_0 - r_1)) xor (a >> r_2) quad "if" #`!last_rot` |
| 151 | + ), |
| 152 | +$ |
| 153 | +where we let $>>>$ denote right rotation and $>>$ logical shift right. |
| 154 | +We choose this representation so that all shift amounts required fit into 4 bits, |
| 155 | +making the usage of `HWSL` more straightforward and avoid extra columns to represent more bits. |
| 156 | + |
| 157 | +== Columns |
| 158 | + |
| 159 | +#let nr_variables = total_nr_variables(rotxorchip) |
| 160 | +#let nr_columns = total_nr_instantiated_columns(rotxorchip, config) |
| 161 | +The #rotxor chip leverages #nr_variables variables, spanning #nr_columns columns: |
| 162 | + |
| 163 | +#render_chip_variable_table(rotxorchip, config) |
| 164 | + |
| 165 | +== Assumptions |
| 166 | + |
| 167 | +Range checking for all elements is inherited from the bitwise lookups. |
| 168 | +We can safely assume that no `r_i` will be zero, and avoid extra work due to right rotation needing `16 - shift` as arguments to the `HWSL` interactions. |
| 169 | +#render_chip_assumptions(rotxorchip, config) |
| 170 | + |
| 171 | +== Constraints |
| 172 | + |
| 173 | +We first compute all rotations (or shifts) of `a`. |
| 174 | +`a1` is computed as a left rotation of `a0`, in order to not need |
| 175 | +additional columns to represent the full right-rotation amounts. |
| 176 | +#render_constraint_table(rotxorchip, config, groups: "shift") |
| 177 | + |
| 178 | +Then the bitwise XOR of the results. |
| 179 | +#render_constraint_table(rotxorchip, config, groups: "xor") |
| 180 | + |
| 181 | +And finally contribute to the lookup argument. |
| 182 | +#render_constraint_table(rotxorchip, config, groups: "output") |
| 183 | + |
| 184 | +== Padding |
| 185 | + |
| 186 | +#render_chip_padding_table(rotxorchip, config) |
| 187 | + |
| 188 | += Constant lookup |
| 189 | + |
| 190 | +#let sha256_kchip = load_chip("src/sha256consts.toml", config) |
| 191 | +#let sha256_k = raw(sha256_kchip.name) |
| 192 | + |
| 193 | +As mentioned, we provide the round constants through a short precomputed lookup table: #sha256_k. |
| 194 | + |
| 195 | +#render_chip_variable_table(sha256_kchip, config) |
| 196 | +#render_constraint_table(sha256_kchip, config) |
| 197 | + |
| 198 | += Notes/optimizations |
| 199 | +- This could instead be designed following the #link("https://github.com/riscv/riscv-crypto")[RISC-V Crypto Scalar extension `Zknh`], |
| 200 | + for wider compatibility, but this design is likely to be more efficient. |
| 201 | + It is still possible, if desired, to expose #rotxor (or a selection of parameter instantiations thereof) |
| 202 | + as implementation for these primitives. |
| 203 | +- The message schedule could be exposed as its own ECALL instead, but the direct integration leads to better efficiency. |
| 204 | +- Some of these chips could be made narrower, at the cost of introducing some extra lookups and extra tables to compute and store intermediate results. |
0 commit comments