Skip to main content

xark_bits/
lib.rs

1//! `xark-bits`: word-level bit gadgets written entirely in the `Field` subset.
2//! Single-element primitives (booleanity, `to_bits`/`from_bits`, and the boolean
3//! AND/OR/XOR/NOT) now live as methods on [`xark::Field`]; this crate keeps
4//! only the WORD-level layers (`[Field; N]` arrays), built on those methods.
5//!
6//! Build with `-Zalways-encode-mir` so the compiler can read this crate's MIR
7//! across the crate boundary (the workspace `.cargo/config.toml` sets this).
8
9#![no_std]
10// Circuit-lowered gadget code: the xark compiler rejects compound assignment on
11// `Field` (`+=`/`-=`/`*=`), so `x = x + y` is required — not a clippy oversight.
12#![allow(clippy::assign_op_pattern)]
13
14use xark::{Field, require_eq};
15
16// 32-bit word layer (little-endian bit index: bits[i] has weight 2^i), the
17// building block for SHA-256-style hashes. Bitwise ops are bit-by-bit on
18// `[Field; 32]`; rotations/shifts are pure re-wiring (zero gates); `add32` is
19// modular addition via carry decomposition.
20
21pub fn and32(a: [Field; 32], b: [Field; 32]) -> [Field; 32] {
22    let mut out = [Field::from(0u8); 32];
23    let mut i = 0usize;
24    while i < 32 {
25        out[i] = a[i].and(b[i]);
26        i += 1;
27    }
28    out
29}
30
31pub fn xor32(a: [Field; 32], b: [Field; 32]) -> [Field; 32] {
32    let mut out = [Field::from(0u8); 32];
33    let mut i = 0usize;
34    while i < 32 {
35        out[i] = a[i].xor(b[i]);
36        i += 1;
37    }
38    out
39}
40
41pub fn or32(a: [Field; 32], b: [Field; 32]) -> [Field; 32] {
42    let mut out = [Field::from(0u8); 32];
43    let mut i = 0usize;
44    while i < 32 {
45        out[i] = a[i].or(b[i]);
46        i += 1;
47    }
48    out
49}
50
51pub fn not32(a: [Field; 32]) -> [Field; 32] {
52    let mut out = [Field::from(0u8); 32];
53    let mut i = 0usize;
54    while i < 32 {
55        out[i] = a[i].not();
56        i += 1;
57    }
58    out
59}
60
61/// Rotate-right by `n` bits (pure re-wiring, zero gates): `out[i] = a[(i+n) % 32]`.
62pub fn rotr32(a: [Field; 32], n: usize) -> [Field; 32] {
63    let mut out = [Field::from(0u8); 32];
64    let mut i = 0usize;
65    while i < 32 {
66        out[i] = a[(i + n) % 32];
67        i += 1;
68    }
69    out
70}
71
72/// Shift-right by `n` bits, zero-filling (pure re-wiring, zero gates):
73/// `out[i] = a[i+n]` for `i+n < 32`, else `0`.
74pub fn shr32(a: [Field; 32], n: usize) -> [Field; 32] {
75    let mut out = [Field::from(0u8); 32];
76    let mut i = 0usize;
77    while i < 32 {
78        if i + n < 32 {
79            out[i] = a[i + n];
80        }
81        i += 1;
82    }
83    out
84}
85
86/// Modular addition mod 2^32.
87///
88/// Adds the two words as field elements (a sum `< 2^33`), decomposes the sum
89/// into 33 bits (32 result bits + 1 carry), and returns the low 32 bits. Cost:
90/// 33 booleanity gates + 1 recomposition equality.
91pub fn add32(a: [Field; 32], b: [Field; 32]) -> [Field; 32] {
92    let sum = Field::from_bits::<32>(a) + Field::from_bits::<32>(b);
93
94    // Decompose the 33-bit sum.
95    let mut bits = [Field::from(0u8); 33];
96    let mut i = 0usize;
97    while i < 33 {
98        bits[i] = Field::hint_bit(sum, i);
99        i += 1;
100    }
101    let mut i = 0usize;
102    while i < 33 {
103        bits[i].require_bool();
104        i += 1;
105    }
106    let mut acc = Field::from(0u8);
107    let mut pow = Field::from(1u8);
108    let mut i = 0usize;
109    while i < 33 {
110        acc = acc + bits[i] * pow;
111        pow = pow + pow;
112        i += 1;
113    }
114    require_eq(acc, sum);
115
116    // Return the low 32 bits (the carry, bits[32], is discarded).
117    let mut out = [Field::from(0u8); 32];
118    let mut i = 0usize;
119    while i < 32 {
120        out[i] = bits[i];
121        i += 1;
122    }
123    out
124}
125
126/// `(a + b + c) mod 2^32` — a three-input modular add done as a SINGLE 34-bit
127/// range-check instead of two chained [`add32`]s.
128///
129/// The three words sum to `< 3·2^32 < 2^34`, so decomposing the field sum into
130/// 34 bits (32 result + 2 carry) and returning the low 32 pins `(a+b+c) mod
131/// 2^32` in one gadget. Cost: 34 booleanity gates + 1 recomposition, vs the
132/// chain's `2×(33+1) = 68`. Soundness: `formal/Formal/Blake.lean`
133/// `add3Mod32_bit_sound` (with `add3Mod32_eq_nested` bridging it to the
134/// `addMod32 (addMod32 a b) c` the BLAKE `G` spec uses).
135pub fn add3(a: [Field; 32], b: [Field; 32], c: [Field; 32]) -> [Field; 32] {
136    let sum = Field::from_bits::<32>(a) + Field::from_bits::<32>(b) + Field::from_bits::<32>(c);
137
138    // Decompose the 34-bit sum.
139    let mut bits = [Field::from(0u8); 34];
140    let mut i = 0usize;
141    while i < 34 {
142        bits[i] = Field::hint_bit(sum, i);
143        i += 1;
144    }
145    let mut i = 0usize;
146    while i < 34 {
147        bits[i].require_bool();
148        i += 1;
149    }
150    let mut acc = Field::from(0u8);
151    let mut pow = Field::from(1u8);
152    let mut i = 0usize;
153    while i < 34 {
154        acc = acc + bits[i] * pow;
155        pow = pow + pow;
156        i += 1;
157    }
158    require_eq(acc, sum);
159
160    // Return the low 32 bits (the 2 carry bits, bits[32..34], are discarded).
161    let mut out = [Field::from(0u8); 32];
162    let mut i = 0usize;
163    while i < 32 {
164        out[i] = bits[i];
165        i += 1;
166    }
167    out
168}
169
170// 64-bit word layer (Keccak-f[1600] and other 64-bit lanes). Same little-endian
171// conventions as the 32-bit layer. Keccak uses only XOR/AND/NOT/rotations.
172
173/// Decompose `x` into its 64 little-endian bits (boolean-constrained + pinned).
174pub fn to_bits64(x: Field) -> [Field; 64] {
175    let mut bits = [Field::from(0u8); 64];
176    let mut i = 0usize;
177    while i < 64 {
178        bits[i] = Field::hint_bit(x, i);
179        i += 1;
180    }
181    let mut i = 0usize;
182    while i < 64 {
183        bits[i].require_bool();
184        i += 1;
185    }
186    let mut acc = Field::from(0u8);
187    let mut pow = Field::from(1u8);
188    let mut i = 0usize;
189    while i < 64 {
190        acc = acc + bits[i] * pow;
191        pow = pow + pow;
192        i += 1;
193    }
194    require_eq(acc, x);
195    bits
196}
197
198/// Recompose 64 little-endian bits into a field element (no gates).
199pub fn from_bits64(bits: [Field; 64]) -> Field {
200    let mut acc = Field::from(0u8);
201    let mut pow = Field::from(1u8);
202    let mut i = 0usize;
203    while i < 64 {
204        acc = acc + bits[i] * pow;
205        pow = pow + pow;
206        i += 1;
207    }
208    acc
209}
210
211pub fn and64(a: [Field; 64], b: [Field; 64]) -> [Field; 64] {
212    let mut out = [Field::from(0u8); 64];
213    let mut i = 0usize;
214    while i < 64 {
215        out[i] = a[i].and(b[i]);
216        i += 1;
217    }
218    out
219}
220
221pub fn xor64(a: [Field; 64], b: [Field; 64]) -> [Field; 64] {
222    let mut out = [Field::from(0u8); 64];
223    let mut i = 0usize;
224    while i < 64 {
225        out[i] = a[i].xor(b[i]);
226        i += 1;
227    }
228    out
229}
230
231pub fn not64(a: [Field; 64]) -> [Field; 64] {
232    let mut out = [Field::from(0u8); 64];
233    let mut i = 0usize;
234    while i < 64 {
235        out[i] = a[i].not();
236        i += 1;
237    }
238    out
239}
240
241/// Rotate-left by `n` bits (pure re-wiring, zero gates): in little-endian,
242/// `out[i] = a[(i + 64 - (n % 64)) % 64]`. This is Keccak's rho rotation.
243pub fn rotl64(a: [Field; 64], n: usize) -> [Field; 64] {
244    let m = n % 64;
245    let mut out = [Field::from(0u8); 64];
246    let mut i = 0usize;
247    while i < 64 {
248        out[i] = a[(i + 64 - m) % 64];
249        i += 1;
250    }
251    out
252}
253
254/// Rotate-right by `n` bits (pure re-wiring): `out[i] = a[(i + n) % 64]`.
255pub fn rotr64(a: [Field; 64], n: usize) -> [Field; 64] {
256    let mut out = [Field::from(0u8); 64];
257    let mut i = 0usize;
258    while i < 64 {
259        out[i] = a[(i + n) % 64];
260        i += 1;
261    }
262    out
263}
264
265// Word-array helpers shared by the SHA-256-family hashes (SHA-256/BLAKE2s/BLAKE3).
266
267/// Read the `t`-th 32-bit word out of an `N`-word array (a constant-indexed
268/// copy of `arr[t]`).
269pub fn read_n<const N: usize>(arr: [[Field; 32]; N], t: usize) -> [Field; 32] {
270    let mut out = [Field::from(0u8); 32];
271    let mut j = 0usize;
272    while j < 32usize {
273        out[j] = arr[t][j];
274        j += 1;
275    }
276    out
277}
278
279/// The SHA-256 initialization vector (8 words), reused as the BLAKE2s/BLAKE3
280/// chaining IV.
281pub fn sha256_iv() -> [Field; 8] {
282    [
283        Field::from(1779033703u32), // 0x6A09E667
284        Field::from(3144134277u32), // 0xBB67AE85
285        Field::from(1013904242u32), // 0x3C6EF372
286        Field::from(2773480762u32), // 0xA54FF53A
287        Field::from(1359893119u32), // 0x510E527F
288        Field::from(2600822924u32), // 0x9B05688C
289        Field::from(528734635u32),  // 0x1F83D9AB
290        Field::from(1541459225u32), // 0x5BE0CD19
291    ]
292}