|
6 | 6 | //! |
7 | 7 | //! where F = F_com stacked over F_eval, and ‖·‖₂ is the column l_2 norm |
8 | 8 | //! over the concatenated coefficient vector of each column of W. |
9 | | -//! |
10 | | -//! Reference: SALSAA paper §3–4. Python: `06_salsaa/relations.py`. |
11 | 9 |
|
12 | 10 | use crate::mat::Mat; |
13 | 11 | use crate::ring::Rq; |
@@ -67,7 +65,7 @@ impl<const Q: u64, const D: usize> LinInstance<Q, D> { |
67 | 65 | self.h.ncols() |
68 | 66 | } |
69 | 67 |
|
70 | | - /// n_top — cols of H = rows of F. |
| 68 | + /// n_top — \bar F, number of rows of the commitment matrix. |
71 | 69 | pub fn n_top(&self) -> usize { |
72 | 70 | self.f_com.nrows() |
73 | 71 | } |
@@ -113,38 +111,37 @@ impl<const Q: u64, const D: usize> LinWitness<Q, D> { |
113 | 111 | } |
114 | 112 | } |
115 | 113 |
|
116 | | -/// A LinInstance plus a verified LinWitness. Construction must enforce: |
117 | | -/// 1. Dimension consistency (H, F, W, Y all line up) |
118 | | -/// 2. Algebraic relation: H · (F · W) = Y |
119 | | -/// 3. l_2 norm bound: max_i ‖w_i‖₂ ≤ β |
120 | | -/// |
121 | | -/// `new` returns `Self` here for symmetry with Python. You can refactor |
122 | | -/// to `Result<Self, RelError>` (Ch 9 practice) if you want explicit |
123 | | -/// error variants instead of panic. |
124 | | -#[derive(Debug, Clone)] |
125 | | -pub struct LinRelation<const Q: u64, const D: usize> { |
126 | | - pub instance: LinInstance<Q, D>, |
127 | | - pub witness: LinWitness<Q, D>, |
128 | | -} |
129 | | - |
| 114 | +/// l2 norm squared of a column is the sum of the norm squared of all ring elements |
130 | 115 | fn col_l2_norm_squared<const Q: u64, const D: usize>(col: &[Rq<Q, D>]) -> u64 { |
131 | 116 | col.iter().map(|r| r.l2_norm_squared()).sum() |
132 | 117 | } |
133 | 118 |
|
134 | | -fn max_col_l2_norm_squared<const Q: u64, const D: usize>(w: &Mat<Rq<Q, D>>) -> u64 { |
| 119 | +/// l2 norm of a matrix is the max norm among its columns. |
| 120 | +/// this function returns the max l2 norm **squared** |
| 121 | +fn mat_l2_norm_squared<const Q: u64, const D: usize>(w: &Mat<Rq<Q, D>>) -> u64 { |
135 | 122 | (0..w.ncols()) |
136 | 123 | .map(|j| col_l2_norm_squared(&w.col(j))) |
137 | 124 | .max() |
138 | 125 | .unwrap_or(0) |
139 | 126 | } |
140 | 127 |
|
| 128 | +/// A LinInstance plus a verified LinWitness. Construction must enforce: |
| 129 | +/// 1. Dimension consistency (H, F, W, Y all line up) |
| 130 | +/// 2. Algebraic relation: H · (F · W) = Y |
| 131 | +/// 3. l_2 norm bound: max_i ‖w_i‖₂ ≤ β |
| 132 | +#[derive(Debug, Clone)] |
| 133 | +pub struct LinRelation<const Q: u64, const D: usize> { |
| 134 | + pub instance: LinInstance<Q, D>, |
| 135 | + pub witness: LinWitness<Q, D>, |
| 136 | +} |
| 137 | + |
141 | 138 | impl<const Q: u64, const D: usize> LinRelation<Q, D> { |
142 | 139 | pub fn new(instance: LinInstance<Q, D>, witness: LinWitness<Q, D>) -> Self { |
143 | 140 | // 1. l_2 norm of W must be <= \beta |
144 | | - let l2_norm_squared_w = max_col_l2_norm_squared(&witness.w); |
| 141 | + let l2_norm_squared_w = mat_l2_norm_squared(&witness.w); |
145 | 142 | let l2_norm_bound_squared = instance.beta * instance.beta; |
146 | 143 | assert!( |
147 | | - l2_norm_squared_w < l2_norm_bound_squared, |
| 144 | + l2_norm_squared_w <= l2_norm_bound_squared, |
148 | 145 | "exceeded norm bound: actual norm squared={}, norm bound={}", |
149 | 146 | l2_norm_squared_w, |
150 | 147 | l2_norm_bound_squared, |
@@ -217,12 +214,6 @@ mod tests { |
217 | 214 | Mat::new(v) |
218 | 215 | } |
219 | 216 |
|
220 | | - // NOTE: Initial Σ^lin has F_eval = 0 × m. Current `Mat::new` requires |
221 | | - // at least one row, so 0-row matrices can't be constructed yet. All |
222 | | - // tests below use F_eval with ≥ 1 row to dodge that limitation. When |
223 | | - // you decide how to handle the empty case (Option, Mat::empty, etc.), |
224 | | - // add a test for the initial state. |
225 | | - |
226 | 217 | // ─── LinWitness ─── |
227 | 218 |
|
228 | 219 | #[test] |
@@ -346,8 +337,73 @@ mod tests { |
346 | 337 | let _ = LinRelation::new(inst, wit); |
347 | 338 | } |
348 | 339 |
|
349 | | - // NOTE: norm-bound violation test (||w_i||_2 > β) is deferred — it |
350 | | - // needs `Zq::centered()` (mapping v ∈ [0, q) → signed [-q/2, q/2]) |
351 | | - // which isn't on Zq yet. Once that exists, add a `should_panic` test |
352 | | - // putting a column of large-coefficient entries into W with β = 1. |
| 340 | + // ─── norm-bound checks (the ‖·‖₂ branch of LinRelation::new) ─── |
| 341 | + |
| 342 | + #[test] |
| 343 | + fn test_relation_norm_at_boundary_constructs() { |
| 344 | + // ‖w‖² == β². Bound is non-strict (≤) so this MUST pass. |
| 345 | + // W = [[c(1)]]: coeffs [1, 0, 0, 0]. centered norm² = 1. β = 1 → β² = 1. |
| 346 | + // F = F_com = [[1]] (no eval block), so F · W = [[1]] = Y. |
| 347 | + let h = Mat::<R>::identity(1); |
| 348 | + let f_com = mat(&[[1]]); |
| 349 | + let f_eval = Mat::<R>::zero(0, 1); |
| 350 | + let w = mat(&[[1]]); |
| 351 | + let y = mat(&[[1]]); |
| 352 | + |
| 353 | + let inst = LinInstance::new(h, f_com, f_eval, y, 1); |
| 354 | + let wit = LinWitness::new(w); |
| 355 | + let _ = LinRelation::new(inst, wit); // must not panic |
| 356 | + } |
| 357 | + |
| 358 | + #[test] |
| 359 | + #[should_panic(expected = "exceeded norm bound")] |
| 360 | + fn test_relation_norm_violation_panics() { |
| 361 | + // ‖w‖² > β². W = [[c(5)]]: centered coeffs [5, 0, 0, 0], norm² = 25. |
| 362 | + // β = 1 → β² = 1. 25 > 1 ⇒ must panic. |
| 363 | + // Y = F · W = c(5) so the algebraic relation alone would still hold. |
| 364 | + let h = Mat::<R>::identity(1); |
| 365 | + let f_com = mat(&[[1]]); |
| 366 | + let f_eval = Mat::<R>::zero(0, 1); |
| 367 | + let w = mat(&[[5]]); |
| 368 | + let y = mat(&[[5]]); |
| 369 | + |
| 370 | + let inst = LinInstance::new(h, f_com, f_eval, y, 1); |
| 371 | + let wit = LinWitness::new(w); |
| 372 | + let _ = LinRelation::new(inst, wit); |
| 373 | + } |
| 374 | + |
| 375 | + // ─── initial state: empty evaluation block ─── |
| 376 | + |
| 377 | + #[test] |
| 378 | + fn test_instance_initial_state_empty_f_eval() { |
| 379 | + // F_eval = 0 × m means "no evaluation rows yet". This is the |
| 380 | + // initial Σ^lin shape before any with_extra_eval call. F should |
| 381 | + // equal F_com (stacking 0 rows on top changes nothing). |
| 382 | + let h = Mat::<R>::identity(2); |
| 383 | + let f_com = mat(&[[1, 2], [3, 4]]); |
| 384 | + let f_eval = Mat::<R>::zero(0, 2); // explicit 0 × 2 — ncols carried |
| 385 | + let y = Mat::<R>::zero(2, 1); |
| 386 | + let inst = LinInstance::new(h, f_com.clone(), f_eval, y, 10); |
| 387 | + |
| 388 | + assert_eq!(inst.n_hat(), 2); |
| 389 | + assert_eq!(inst.n(), 2); // F_com.nrows + F_eval.nrows = 2 + 0 |
| 390 | + assert_eq!(inst.n_top(), 2); // F_com.nrows |
| 391 | + assert_eq!(inst.m(), 2); |
| 392 | + assert_eq!(inst.f(), f_com, "F == F_com when F_eval is empty"); |
| 393 | + } |
| 394 | + |
| 395 | + #[test] |
| 396 | + fn test_relation_with_empty_f_eval_constructs() { |
| 397 | + // End-to-end: a satisfied Σ^lin with the initial empty F_eval. |
| 398 | + let h = Mat::<R>::identity(2); |
| 399 | + let f_com = mat(&[[1, 2], [3, 4]]); |
| 400 | + let f_eval = Mat::<R>::zero(0, 2); |
| 401 | + let w = mat(&[[1], [1]]); |
| 402 | + // F · W = [[1+2], [3+4]] = [[3], [7]] (constant-poly entries) |
| 403 | + let y = mat(&[[3], [7]]); |
| 404 | + |
| 405 | + let inst = LinInstance::new(h, f_com, f_eval, y, 10); |
| 406 | + let wit = LinWitness::new(w); |
| 407 | + let _ = LinRelation::new(inst, wit); // must not panic |
| 408 | + } |
353 | 409 | } |
0 commit comments