diff --git a/.gitignore b/.gitignore index 139d9604..12045e54 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ /revision_logs /*.log /*.status +/tmp_*.sh /bench \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 17df1c3c..b25f316f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstyle" version = "1.0.13" @@ -411,6 +420,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -653,6 +671,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustix" version = "1.1.3" @@ -911,10 +946,14 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index 9f8710a5..13ee0e10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ digest = "0.10" bitvec = "1" keccak-asm = { version = "0.1.4" } tempfile = { version = "3.19.1" } -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } [dev-dependencies] sequential-test = "0.2.4" diff --git a/build.rs b/build.rs index 626e4997..7ecb9314 100644 --- a/build.rs +++ b/build.rs @@ -62,7 +62,7 @@ fn main() { .flag("-std=c++17") .flag("-Xcompiler") .flag("-fPIC") - .flag(&format!("-arch=sm_{cuda_arch}")); + .flag(format!("-arch=sm_{cuda_arch}")); if !debug_build { build.flag("-lineinfo"); } diff --git a/cuda/src/matrix/MatrixDecompose.cu b/cuda/src/matrix/MatrixDecompose.cu index c9d4180a..778d06eb 100644 --- a/cuda/src/matrix/MatrixDecompose.cu +++ b/cuda/src/matrix/MatrixDecompose.cu @@ -29,6 +29,55 @@ namespace constexpr size_t kDecomposeMaxGridZ = 65535; } +__device__ __forceinline__ int64_t centered_lift_u64(uint64_t residue, uint64_t modulus) +{ + __int128 value = static_cast<__int128>(residue); + if (static_cast(residue) * 2U > static_cast(modulus)) + { + value -= static_cast<__int128>(modulus); + } + return static_cast(value); +} + +__device__ __forceinline__ int64_t balanced_digit_step(int64_t value, int64_t base, int64_t *next) +{ + int64_t quotient = value / base; + int64_t remainder = value % base; + if (remainder < 0) + { + remainder += base; + quotient -= 1; + } + const int64_t half = base / 2; + if (remainder < half) + { + *next = quotient; + return remainder; + } + if (remainder > half) + { + *next = quotient + 1; + return remainder - base; + } + if ((quotient & 1LL) == 0) + { + *next = quotient; + return half; + } + *next = quotient + 1; + return half - base; +} + +__device__ __forceinline__ uint64_t signed_digit_to_residue(int64_t digit, uint64_t modulus) +{ + if (digit >= 0) + { + return static_cast(digit) % modulus; + } + const uint64_t magnitude = static_cast(-digit) % modulus; + return magnitude == 0 ? 0 : modulus - magnitude; +} + __global__ void matrix_decompose_all_slots_kernel( const uint8_t *src_base, uint8_t *const *dst_bases, @@ -45,8 +94,10 @@ __global__ void matrix_decompose_all_slots_kernel( size_t out_cols, size_t log_base_q, uint32_t src_bits, + uint64_t src_modulus, uint32_t base_bits, uint32_t digits_per_tower, + bool balanced, size_t src_digit_offset_base, size_t poly_offset, size_t slot_offset) @@ -92,18 +143,39 @@ __global__ void matrix_decompose_all_slots_kernel( const uint64_t residue = matrix_load_limb_u64(src_base, poly_idx, coeff_idx, src_stride_bytes, src_coeff_bytes); - const uint32_t shift = digit_idx * base_bits; - uint64_t mask = 0; - if (shift < src_bits) + uint64_t digit = 0; + if (balanced) { - const uint32_t remaining = src_bits - shift; - const uint32_t digit_bits = min(base_bits, remaining); - mask = digit_bits >= 64 ? ~uint64_t{0} : ((uint64_t{1} << digit_bits) - 1); + int64_t value = centered_lift_u64(residue, src_modulus); + int64_t signed_digit = 0; + const int64_t base = int64_t{1} << base_bits; + for (uint32_t idx = 0; idx <= digit_idx; ++idx) + { + int64_t next = 0; + const int64_t current_digit = balanced_digit_step(value, base, &next); + if (idx == digit_idx) + { + signed_digit = current_digit; + } + value = next; + } + digit = signed_digit_to_residue(signed_digit, out_modulus); } - uint64_t digit = shift >= 64 ? 0 : ((residue >> shift) & mask); - if (out_modulus != 0 && digit >= out_modulus) + else { - digit %= out_modulus; + const uint32_t shift = digit_idx * base_bits; + uint64_t mask = 0; + if (shift < src_bits) + { + const uint32_t remaining = src_bits - shift; + const uint32_t digit_bits = min(base_bits, remaining); + mask = digit_bits >= 64 ? ~uint64_t{0} : ((uint64_t{1} << digit_bits) - 1); + } + digit = shift >= 64 ? 0 : ((residue >> shift) & mask); + if (out_modulus != 0 && digit >= out_modulus) + { + digit %= out_modulus; + } } const size_t row = poly_idx / src_cols; @@ -128,8 +200,10 @@ int launch_decompose_all_slots_kernel( size_t out_cols, size_t log_base_q, uint32_t src_bits, + uint64_t src_modulus, uint32_t base_bits, uint32_t digits_per_tower, + bool balanced, size_t src_digit_offset_base, cudaStream_t stream) { @@ -194,8 +268,10 @@ int launch_decompose_all_slots_kernel( out_cols, log_base_q, src_bits, + src_modulus, base_bits, digits_per_tower, + balanced, src_digit_offset_base, poly_offset, slot_offset); @@ -1284,8 +1360,10 @@ static int gpu_matrix_decompose_base_impl( out->cols, out_log_base_q, src_bits, + src->ctx->moduli[src_idx], base_bits, digits_per_tower, + !small, src_digit_offset_base, dispatch_stream); if (status != 0) diff --git a/references/implement_abe.pdf b/references/implement_abe.pdf new file mode 100644 index 00000000..ad3f87c6 Binary files /dev/null and b/references/implement_abe.pdf differ diff --git a/src/bench_estimator/mod.rs b/src/bench_estimator/mod.rs index 20c3a634..6fc6bb9d 100644 --- a/src/bench_estimator/mod.rs +++ b/src/bench_estimator/mod.rs @@ -913,7 +913,7 @@ mod tests { &self, _src_slots: &[(u32, Option)], ) -> CircuitBenchEstimate { - bench(6.0, 6.5, 17) + bench(6.0, 6.6, 17) } fn estimate_slot_reduce( diff --git a/src/circuit/poly_circuit/eval.rs b/src/circuit/poly_circuit/eval.rs index bd5073d1..965e94f0 100644 --- a/src/circuit/poly_circuit/eval.rs +++ b/src/circuit/poly_circuit/eval.rs @@ -13,6 +13,33 @@ impl PolyCircuit

{ slot_transfer_evaluator: Option<&dyn SlotTransferEvaluator>, parallel_gates: Option, ) -> Vec + where + E: Evaluable

, + E::Compact: Send + Sync, + PE: PltEvaluator, + PE: Sync, + { + let one_compact = one.to_compact(); + let input_compacts = inputs.into_iter().map(E::to_compact).collect(); + self.eval_from_compacts( + params, + one_compact, + input_compacts, + plt_evaluator, + slot_transfer_evaluator, + parallel_gates, + ) + } + + pub fn eval_from_compacts( + &self, + params: &E::Params, + one_compact: E::Compact, + input_compacts: Vec, + plt_evaluator: Option<&PE>, + slot_transfer_evaluator: Option<&dyn SlotTransferEvaluator>, + parallel_gates: Option, + ) -> Vec where E: Evaluable

, E::Compact: Send + Sync, @@ -21,10 +48,9 @@ impl PolyCircuit

{ { let (call_id_base, gate_id_base) = self.eval_gate_id_bases(); let parallel_gates = crate::env::resolve_circuit_parallel_gates(parallel_gates); - let one_compact = Arc::new(one.to_compact()); + let one_compact = Arc::new(one_compact); let one = Arc::new(E::from_compact(params, one_compact.as_ref())); - let input_compacts = - inputs.into_iter().map(|input| Arc::new(input.to_compact())).collect::>(); + let input_compacts = input_compacts.into_iter().map(Arc::new).collect::>(); let scoped_gate_ids = self.build_scoped_gate_id_map(&call_id_base, &gate_id_base); let call_prefix = ScopedGateKey::from(0u32); let outputs = std::thread::scope(|scope| { @@ -902,32 +928,11 @@ impl PolyCircuit

{ .iter() .copied() .for_each(|gate_id| eval_gate(gate_id, eval_params, eval_one)); - } else if !regular_chunks.is_empty() { - let mut loaded_curr = Some(load_chunk(regular_chunks[0])); - let mut computed_prev: Option>> = None; - for chunk_idx in 0..regular_chunks.len() { - let to_store = computed_prev.take(); - let to_compute = - loaded_curr.take().expect("loaded chunk missing in pipeline"); - let next_chunk = regular_chunks.get(chunk_idx + 1).copied(); - let ((), (computed_curr, loaded_next)) = rayon::join( - || { - if let Some(computed_chunk) = to_store { - store_chunk(computed_chunk); - } - }, - || { - rayon::join( - || compute_chunk(to_compute), - || next_chunk.map(load_chunk), - ) - }, - ); - computed_prev = Some(computed_curr); - loaded_curr = loaded_next; - } - if let Some(last_chunk) = computed_prev.take() { - store_chunk(last_chunk); + } else { + for chunk in regular_chunks { + let loaded = load_chunk(chunk); + let computed = compute_chunk(loaded); + store_chunk(computed); } } } diff --git a/src/func_enc/aky24/error_simulation.rs b/src/func_enc/aky24/error_simulation.rs index 52204475..717083a4 100644 --- a/src/func_enc/aky24/error_simulation.rs +++ b/src/func_enc/aky24/error_simulation.rs @@ -20,7 +20,7 @@ use crate::{ poly::{PolyParams, dcrt::poly::DCRTPoly}, simulator::{ SimulatorContext, - error_norm::{ErrorNorm, compute_preimage_norm}, + error_norm::{ErrorNorm, compute_preimage_sigma}, poly_matrix_norm::PolyMatrixNorm, poly_norm::PolyNorm, }, @@ -41,9 +41,9 @@ pub struct Aky24CrtDepthSearchCandidate { pub inputs: Aky24DecErrorSimulationInputs, /// Error-norm representation of the circuit constant one. pub one: ErrorNorm, - /// Error bound for the Ring-GSW decryption-key input. + /// Raw sigma-mode error norm for the Ring-GSW decryption-key input. pub decryption_key: ErrorNorm, - /// Decoder error bounds in the layout expected by the noise-refresh simulator. + /// Raw sigma-mode decoder errors in the layout expected by the noise-refresh simulator. pub decoders: Vec, } @@ -77,16 +77,16 @@ pub struct Aky24DecErrorSimulation { /// The vector contains at most two entries. Entry `0` is the first refresh from the caller's /// initial PRF-seed error norms. Entry `1`, when present, is the second refresh, which also /// represents later rounds because the refreshed PRF-seed error input is then the same CBD - /// rounded-error bound in every subsequent round. + /// rounded-error sigma-mode norm in every subsequent round. pub prf_seed_bit_refreshes: Vec, - /// Final conservative error bound before AKY24 bit decoding. + /// Final raw sigma-mode error before AKY24 bit decoding. /// - /// This bound adds the function-evaluation output error, the final PRG mask decode/recompose + /// This value adds the function-evaluation output error, the final PRG mask decode/recompose /// error for the selected `prf_mask_output_coeff_bits`, and the `c_b0 * output_preimage` /// functional-key decoder term. The PRF mask value range itself is not included here; callers - /// that search for a valid mask width compare this bound and the candidate mask range against - /// the `q / 4` margin. - pub error_bound: PolyMatrixNorm, + /// that search for a valid mask width convert this value to a maximum coefficient bound exactly + /// once before comparing it and the candidate mask range against the `q / 4` margin. + pub error: PolyMatrixNorm, } /// Inputs needed by the top-level AKY24 decryption error simulation. @@ -96,30 +96,30 @@ pub struct Aky24DecErrorSimulation { /// seed refresh path, final mask PRG/decrypt path, and final decoder arithmetic. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Aky24DecErrorSimulationInputs { - /// Error bound of the ciphertext `c_b0` term used by AKY24 decoder preimages. + /// Raw sigma-mode error norm of the ciphertext `c_b0` term used by AKY24 decoder preimages. pub c_b0_error_norm: PolyMatrixNorm, - /// Error bounds for the inputs to `build_func_circuit`, in circuit input order. + /// Raw sigma-mode errors for the inputs to `build_func_circuit`, in circuit input order. /// /// For the current AKY24 decryption path this means the functional decryption-key encoding /// followed by the message input encodings. pub func_input_error_norms: Vec, - /// Error bounds for the current encrypted private PRF-seed bits before public PRF traversal. + /// Raw sigma-mode errors for the current encrypted private PRF-seed bits before public PRF traversal. /// /// The top-level simulator assumes this vector is uniform across all PRF seed bits and asserts /// that invariant before relying on PRG output symmetry. pub prf_seed_error_norms: Vec, /// AKY24 function whose circuit should be evaluated in the error simulator. pub func: Aky24Func, - /// Norm bound for the functional-key output preimage used in the final `c_b0 * preimage` + /// Raw sigma-mode norm for the functional-key output preimage used in the final `c_b0 * preimage` /// decoder term. - pub output_preimage_norm: PolyMatrixNorm, + pub output_preimage_sigma: PolyMatrixNorm, } impl Aky24DecErrorSimulationInputs { /// Builds standard AKY24 decryption error-simulation inputs from AKY24 parameters. /// /// This constructor derives the ciphertext `c_b0` error, function input encoding errors, - /// initial PRF-seed encoding errors, and final functional-key preimage norm from the parameter + /// initial PRF-seed encoding errors, and final functional-key preimage sigma from the parameter /// dimensions and Gaussian widths. The public PRF seed is intentionally not included because /// the simulator uses the all-zero public-seed branch without changing the error size. pub fn new_from_params(params: &Aky24Params, func: Aky24Func) -> Self { @@ -157,11 +157,11 @@ impl Aky24DecErrorSimulationInputs { }, ); let func_input_count = build_func_circuit(params, &func).num_input(); - let output_preimage_norm = PolyMatrixNorm::new( + let output_preimage_sigma = PolyMatrixNorm::fresh_preimage( ctx.clone(), ctx.m_b, func.output_size(), - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None), + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None), None, ); Self { @@ -169,7 +169,7 @@ impl Aky24DecErrorSimulationInputs { func_input_error_norms: vec![encoding_error_norm.clone(); func_input_count], prf_seed_error_norms: vec![encoding_error_norm; params.prf_seed_bits()], func, - output_preimage_norm, + output_preimage_sigma, } } } @@ -265,7 +265,7 @@ where slot_transfer_evaluator, ); debug!( - function_error = %function_error.poly_norm.norm, + function_error = %function_error.poly_norm.sigma, "AKY24 function error simulation finished" ); let first_seed_error = inputs @@ -319,8 +319,8 @@ where .iter() .max_by(|lhs, rhs| { lhs.poly_norm - .norm - .partial_cmp(&rhs.poly_norm.norm) + .sigma + .partial_cmp(&rhs.poly_norm.sigma) .expect("noise-refresh rounded-error norms must be comparable") }) .expect("noise-refresh simulation must produce at least one rounded error") @@ -371,13 +371,14 @@ where let final_mask_encoding_error = mask_outputs.remove(0).matrix_norm; let final_mask_error = final_mask_encoding_error.clone() * PolyMatrixNorm::gadget_decomposed(final_mask_encoding_error.clone_ctx(), 1); - let decoder_error = inputs.c_b0_error_norm * &inputs.output_preimage_norm; - let error_bound = function_error + final_mask_error + decoder_error; + let decoder_error = inputs.c_b0_error_norm * &inputs.output_preimage_sigma; + let error = function_error + final_mask_error + decoder_error; info!( - error_bound = %error_bound.poly_norm.norm, + error_sigma = %error.poly_norm.sigma, + error_bound = %error.maximum_coefficient_bound(), "finished AKY24 decryption error simulation" ); - Some(Aky24DecErrorSimulation { prf_seed_bit_refreshes, error_bound }) + Some(Aky24DecErrorSimulation { prf_seed_bit_refreshes, error }) } /// Searches for the largest PRF mask coefficient bit-width that satisfies the AKY24 decode margin. @@ -436,7 +437,7 @@ where |simulation| { simulation .as_ref() - .map(|simulation| simulation.error_bound.poly_norm.norm.clone()) + .map(|simulation| simulation.error.maximum_coefficient_bound()) .unwrap_or_else(|| biguint_to_decimal(q.as_ref())) }, ) @@ -447,7 +448,8 @@ where let simulation = result.simulation?; debug!( candidate = result.mask_bits, - error_bound = %simulation.error_bound.poly_norm.norm, + error_sigma = %simulation.error.poly_norm.sigma, + error_bound = %simulation.error.maximum_coefficient_bound(), "AKY24 PRF mask bit candidate selected" ); Some(Aky24PrfMaskOutputCoeffBitsSearchResult { @@ -560,7 +562,7 @@ fn aky24_security_margins_hold( }; if !validate_error_bound_security_margin( &final_threshold, - &simulation.error_bound.poly_norm.norm, + &simulation.error.maximum_coefficient_bound(), security_bit, ) { return false; @@ -573,9 +575,9 @@ fn aky24_security_margins_hold( let worst_error = refresh .pre_round_outputs .iter() - .map(|error| error.poly_norm.norm.clone()) + .map(|error| error.maximum_coefficient_bound()) .max_by(|lhs, rhs| { - lhs.partial_cmp(rhs).expect("noise-refresh pre-rounding norms must be comparable") + lhs.partial_cmp(rhs).expect("noise-refresh pre-rounding bounds must be comparable") }) .expect("noise-refresh simulation must produce at least one pre-round output"); validate_error_bound_security_margin(&threshold, &worst_error, security_bit) diff --git a/src/gadgets/fhe/ckks.rs b/src/gadgets/fhe/ckks.rs index 1f0d5e8b..b3b662e0 100644 --- a/src/gadgets/fhe/ckks.rs +++ b/src/gadgets/fhe/ckks.rs @@ -13,7 +13,9 @@ use crate::{ dcrt::{params::DCRTPolyParams, poly::DCRTPoly}, }, sampler::{DistType, PolyUniformSampler, uniform::DCRTPolyUniformSampler}, + simulator::poly_norm::maximum_coefficient_bound_from_sigma, }; +use bigdecimal::{BigDecimal, FromPrimitive, ToPrimitive}; use num_bigint::BigUint; use std::sync::Arc; @@ -173,7 +175,10 @@ impl CKKSContext

{ } fn initial_ciphertext_error_bound(&self) -> BigUint { - BigUint::from((6.5 * self.error_sigma).ceil() as u64) + let sigma = BigDecimal::from_f64(self.error_sigma) + .expect("CKKS error sigma must be finite"); + let bound = maximum_coefficient_bound_from_sigma(&sigma); + BigUint::from(bound.ceil().to_u64().expect("CKKS error bound must fit in u64")) } fn q_window_modulus(&self, level_offset: usize, active_levels: usize) -> BigUint { diff --git a/src/gadgets/fhe/ring_gsw.rs b/src/gadgets/fhe/ring_gsw.rs index 59a2439c..bf56de72 100644 --- a/src/gadgets/fhe/ring_gsw.rs +++ b/src/gadgets/fhe/ring_gsw.rs @@ -2045,7 +2045,7 @@ mod tests { assert_eq!(input.randomizer_norm, ctx.fresh_randomizer_norm()); assert_eq!(input.randomizer_norm.nrow, ctx.width()); assert_eq!(input.randomizer_norm.ncol, ctx.width()); - assert_eq!(input.randomizer_norm.poly_norm.norm, BigDecimal::from(1u64)); + assert_eq!(input.randomizer_norm.poly_norm.sigma, BigDecimal::from(1u64)); assert_eq!(input.max_plaintext, BigUint::from(1u64)); } @@ -2059,12 +2059,12 @@ mod tests { assert_eq!(estimated.nrow, 1); assert_eq!(estimated.ncol, 1); assert_eq!(estimated.ctx(), ctx.randomizer_norm_ctx.as_ref()); - assert!(estimated.poly_norm.norm > BigDecimal::from(0u64)); + assert!(estimated.poly_norm.sigma > BigDecimal::from(0u64)); let zero_sigma = input.estimate_decryption_error_norm(0.0); assert_eq!(zero_sigma.nrow, 1); assert_eq!(zero_sigma.ncol, 1); - assert_eq!(zero_sigma.poly_norm.norm, BigDecimal::from(0u64)); + assert_eq!(zero_sigma.poly_norm.sigma, BigDecimal::from(0u64)); } #[test] diff --git a/src/gadgets/fhe/ring_gsw_gpu_tests.rs b/src/gadgets/fhe/ring_gsw_gpu_tests.rs index ac5979cf..51ae6541 100644 --- a/src/gadgets/fhe/ring_gsw_gpu_tests.rs +++ b/src/gadgets/fhe/ring_gsw_gpu_tests.rs @@ -127,13 +127,13 @@ fn test_ring_gsw_add_circuit_decrypts_to_expected_integer_sum_with_noisy_public_ let rhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); let sum = lhs.add(&rhs, &mut circuit); let estimated_error = sum.estimate_decryption_error_norm(error_sigma); - let estimated_error_norm = &estimated_error.poly_norm.norm; + let estimated_error_bound = estimated_error.maximum_coefficient_bound(); let q_modulus = active_q_modulus(ctx.nested_rns.as_ref()); let threshold = q_over_two_p_threshold(&q_modulus, plaintext_modulus); assert!( - estimated_error_norm < &q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), - "estimated add decryption error {} must stay below q/(2p) {}", - estimated_error_norm, + estimated_error_bound < q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), + "estimated add decryption error bound {} must stay below q/(2p) {}", + estimated_error_bound, threshold ); let wire_secret_key = circuit.input(1).at(0).as_single_wire(); @@ -229,13 +229,13 @@ fn test_ring_gsw_sub_circuit_decrypts_to_expected_integer_difference_with_noisy_ let rhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); let difference = lhs.sub(&rhs, &mut circuit); let estimated_error = difference.estimate_decryption_error_norm(error_sigma); - let estimated_error_norm = &estimated_error.poly_norm.norm; + let estimated_error_bound = estimated_error.maximum_coefficient_bound(); let q_modulus = active_q_modulus(ctx.nested_rns.as_ref()); let threshold = q_over_two_p_threshold(&q_modulus, plaintext_modulus); assert!( - estimated_error_norm < &q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), - "estimated sub decryption error {} must stay below q/(2p) {}", - estimated_error_norm, + estimated_error_bound < q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), + "estimated sub decryption error bound {} must stay below q/(2p) {}", + estimated_error_bound, threshold ); let wire_secret_key = circuit.input(1).at(0).as_single_wire(); @@ -332,13 +332,13 @@ fn test_ring_gsw_mul_circuit_decrypts_to_expected_integer_product_with_noisy_pub let rhs = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); let product = lhs.mul(&rhs, &mut circuit); let estimated_error = product.estimate_decryption_error_norm(error_sigma); - let estimated_error_norm = &estimated_error.poly_norm.norm; + let estimated_error_bound = estimated_error.maximum_coefficient_bound(); let q_modulus = active_q_modulus(ctx.nested_rns.as_ref()); let threshold = q_over_two_p_threshold(&q_modulus, plaintext_modulus); assert!( - estimated_error_norm < &q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), - "estimated mul decryption error {} must stay below q/(2p) {}", - estimated_error_norm, + estimated_error_bound < q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), + "estimated mul decryption error bound {} must stay below q/(2p) {}", + estimated_error_bound, threshold ); let wire_secret_key = circuit.input(1).at(0).as_single_wire(); @@ -436,13 +436,13 @@ fn test_ring_gsw_chained_mul_circuit_decrypts_to_expected_integer_product_with_n let rhs2 = RingGswCiphertext::input(ctx.clone(), None, &mut circuit); let chained_product = lhs.mul(&rhs1, &mut circuit).mul(&rhs2, &mut circuit); let estimated_error = chained_product.estimate_decryption_error_norm(error_sigma); - let estimated_error_norm = &estimated_error.poly_norm.norm; + let estimated_error_bound = estimated_error.maximum_coefficient_bound(); let q_modulus = active_q_modulus(ctx.nested_rns.as_ref()); let threshold = q_over_two_p_threshold(&q_modulus, plaintext_modulus); assert!( - estimated_error_norm < &q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), - "estimated chained-mul decryption error {} must stay below q/(2p) {}", - estimated_error_norm, + estimated_error_bound < q_over_two_p_threshold_bigdecimal(&q_modulus, plaintext_modulus), + "estimated chained-mul decryption error bound {} must stay below q/(2p) {}", + estimated_error_bound, threshold ); let wire_secret_key = circuit.input(1).at(0).as_single_wire(); diff --git a/src/input_injector/diamond_gpu.rs b/src/input_injector/diamond_gpu.rs index 4d9a2786..ba6cc6ce 100644 --- a/src/input_injector/diamond_gpu.rs +++ b/src/input_injector/diamond_gpu.rs @@ -16,9 +16,9 @@ where { device_id: i32, params: ::Params, - source_b_by_state: Vec, - source_trapdoor_by_state: Vec, - target_b_by_state: Vec, + source_b: M, + source_trapdoor: T, + target_b: M, } struct LoadedDiamondPreprocessChunk @@ -141,46 +141,6 @@ where if device_ids.is_empty() { vec![0] } else { device_ids } } - fn prepare_gpu_k_stage_shared( - &self, - source_checkpoint_bytes: &[(Arc<[u8]>, Arc<[u8]>)], - target_b_bytes: &[Arc<[u8]>], - ) -> Vec> { - self.effective_gpu_device_ids() - .into_par_iter() - .map(|device_id| { - let local_params = self.params.params_for_device(device_id); - let source_b_by_state = source_checkpoint_bytes - .iter() - .map(|(b_bytes, _)| M::from_compact_bytes(&local_params, b_bytes.as_ref())) - .collect::>(); - let source_trapdoor_by_state = source_checkpoint_bytes - .iter() - .map(|(_, trapdoor_bytes)| { - TS::trapdoor_from_bytes(&local_params, trapdoor_bytes.as_ref()) - .unwrap_or_else(|| { - panic!( - "DiamondInjector failed to decode K-stage trapdoor checkpoint on device {}", - device_id - ) - }) - }) - .collect::>(); - let target_b_by_state = target_b_bytes - .iter() - .map(|bytes| M::from_compact_bytes(&local_params, bytes.as_ref())) - .collect::>(); - GpuDiamondKStageShared { - device_id, - params: local_params.clone(), - source_b_by_state, - source_trapdoor_by_state, - target_b_by_state, - } - }) - .collect() - } - fn prepare_gpu_eval_shared( &self, family_specs: &HashMap, @@ -246,8 +206,6 @@ where dir_path: &Path, level: usize, tasks: Vec, - source_checkpoint_bytes: &[(Arc<[u8]>, Arc<[u8]>)], - target_b_bytes: &[Arc<[u8]>], secret_mask_bytes: &HashMap>, ) -> usize { if tasks.is_empty() { @@ -256,146 +214,218 @@ where let stage_started = Instant::now(); let trap_sampler = TS::new(&self.params, self.trapdoor_sigma); - let shared_by_device = - self.prepare_gpu_k_stage_shared(source_checkpoint_bytes, target_b_bytes); - let device_count = shared_by_device.len().max(1); - let task_batches = round_robin_batches(&tasks, device_count); - let wave_count = task_batches.iter().map(Vec::len).max().unwrap_or(0); + let device_ids = self.effective_gpu_device_ids(); + let device_count = device_ids.len().max(1); + let total_tasks = tasks.len(); + let mut tasks_by_state = Vec::>::new(); + for task in tasks { + let DiamondPreprocessChunkTask::K { state_idx, .. } = task; + if tasks_by_state.len() <= state_idx { + tasks_by_state.resize_with(state_idx + 1, Vec::new); + } + tasks_by_state[state_idx].push(task); + } info!( level, device_count, - total_tasks = tasks.len(), - progress = preprocess_progress_label(0, tasks.len()), + total_tasks, + progress = preprocess_progress_label(0, total_tasks), "diamond injector gpu preprocess: K stage starting" ); - let load_wave = |wave_idx: usize| { - let loaded_wave = task_batches - .par_iter() - .zip(shared_by_device.par_iter()) - .enumerate() - .filter_map(|(device_slot, (batch, shared))| { - let task = batch.get(wave_idx).copied()?; - let load_started = Instant::now(); - let DiamondPreprocessChunkTask::K { - level: task_level, - digit_value, - state_idx, - chunk_idx, - } = task; - debug_assert_eq!(task_level, level); - let secret_mask = M::from_compact_bytes( - &shared.params, - secret_mask_bytes - .get(&digit_value) - .unwrap_or_else(|| { - panic!( - "missing K-stage secret checkpoint for level {}, digit {}", - level, digit_value - ) - }) - .as_ref(), - ); - let target = self.build_k_target_chunk_with_params( - &shared.params, - level, - digit_value, - state_idx, - &secret_mask, - &shared.target_b_by_state[state_idx], - chunk_idx, - ); - drop(secret_mask); - debug!( - device_id = shared.device_id, - level, - digit_value, - state_idx, - chunk_idx, - load_s = maybe_elapsed_s(load_started), - "diamond injector gpu preprocess: loaded K-stage chunk" - ); - Some(LoadedDiamondPreprocessChunk { - device_slot, - task, - target, - load_s: maybe_elapsed_s(load_started), - }) - }) - .collect::>(); - (!loaded_wave.is_empty()).then_some(loaded_wave) - }; + let mut completed_tasks = 0usize; + for (state_idx, state_tasks) in tasks_by_state.into_iter().enumerate() { + if state_tasks.is_empty() { + continue; + } - let compute_wave = |loaded_wave: Vec>| { - loaded_wave - .into_par_iter() - .map(|loaded| { - let shared = &shared_by_device[loaded.device_slot]; - let compute_started = Instant::now(); - let DiamondPreprocessChunkTask::K { level, state_idx, .. } = loaded.task; - let source_state_idx = self.transition_source_state_idx(level, state_idx); - let output = trap_sampler.preimage( - &shared.params, - &shared.source_trapdoor_by_state[source_state_idx], - &shared.source_b_by_state[source_state_idx], - &loaded.target, - ); - ComputedDiamondPreprocessChunk { - task: loaded.task, - output, - load_s: loaded.load_s, - compute_s: maybe_elapsed_s(compute_started), + let source_state_idx = self.transition_source_state_idx(level, state_idx); + let (source_b_raw_bytes, source_trapdoor_raw_bytes) = + self.load_or_sample_b_checkpoint_bytes(dir_path, level - 1, source_state_idx); + let source_b_bytes = Arc::<[u8]>::from(source_b_raw_bytes); + let source_trapdoor_bytes = Arc::<[u8]>::from(source_trapdoor_raw_bytes); + let (target_b_raw_bytes, _) = + self.load_or_sample_b_checkpoint_bytes(dir_path, level, state_idx); + let target_b_state_bytes = Arc::<[u8]>::from(target_b_raw_bytes); + let state_started = Instant::now(); + let shared_by_device = device_ids + .par_iter() + .map(|device_id| { + let device_id = *device_id; + let local_params = self.params.params_for_device(device_id); + let source_b = M::from_compact_bytes(&local_params, source_b_bytes.as_ref()); + let source_trapdoor = TS::trapdoor_from_bytes( + &local_params, + source_trapdoor_bytes.as_ref(), + ) + .unwrap_or_else(|| { + panic!( + "DiamondInjector failed to decode K-stage trapdoor checkpoint on device {}", + device_id + ) + }); + let target_b = + M::from_compact_bytes(&local_params, target_b_state_bytes.as_ref()); + GpuDiamondKStageShared { + device_id, + params: local_params.clone(), + source_b, + source_trapdoor, + target_b, } }) - .collect::>() - }; + .collect::>(); + let task_batches = round_robin_batches(&state_tasks, device_count); + let wave_count = task_batches.iter().map(Vec::len).max().unwrap_or(0); - let mut completed_tasks = 0usize; - let mut next_wave_idx = 1usize; - let mut current_wave = if wave_count == 0 { None } else { load_wave(0) }; - let mut previous_outputs = None; - while let Some(loaded_wave) = current_wave.take() { - let should_load_next = next_wave_idx < wave_count; - let previous_outputs_to_store = previous_outputs.take(); - let wave_started = Instant::now(); - let ((computed_outputs, next_loaded_wave), stored_now) = rayon::join( - || { - rayon::join( - || compute_wave(loaded_wave), - || if should_load_next { load_wave(next_wave_idx) } else { None }, - ) - }, - || { - previous_outputs_to_store - .map(|outputs| self.store_preprocess_wave(dir_path, outputs)) - .unwrap_or(0) - }, - ); - completed_tasks += stored_now; info!( level, - wave = next_wave_idx, + state_idx, + source_state_idx, + state_tasks = state_tasks.len(), wave_count, + progress = preprocess_progress_label(completed_tasks, total_tasks), + "diamond injector gpu preprocess: K stage state starting" + ); + + let load_wave = |wave_idx: usize| { + let loaded_wave = task_batches + .par_iter() + .zip(shared_by_device.par_iter()) + .enumerate() + .filter_map(|(device_slot, (batch, shared))| { + let task = batch.get(wave_idx).copied()?; + let load_started = Instant::now(); + let DiamondPreprocessChunkTask::K { + level: task_level, + digit_value, + state_idx: task_state_idx, + chunk_idx, + } = task; + debug_assert_eq!(task_level, level); + debug_assert_eq!(task_state_idx, state_idx); + let secret_mask = M::from_compact_bytes( + &shared.params, + secret_mask_bytes + .get(&digit_value) + .unwrap_or_else(|| { + panic!( + "missing K-stage secret checkpoint for level {}, digit {}", + level, digit_value + ) + }) + .as_ref(), + ); + let target = self.build_k_target_chunk_with_params( + &shared.params, + level, + digit_value, + task_state_idx, + &secret_mask, + &shared.target_b, + chunk_idx, + ); + drop(secret_mask); + debug!( + device_id = shared.device_id, + level, + digit_value, + state_idx = task_state_idx, + chunk_idx, + load_s = maybe_elapsed_s(load_started), + "diamond injector gpu preprocess: loaded K-stage chunk" + ); + Some(LoadedDiamondPreprocessChunk { + device_slot, + task, + target, + load_s: maybe_elapsed_s(load_started), + }) + }) + .collect::>(); + (!loaded_wave.is_empty()).then_some(loaded_wave) + }; + + let compute_wave = |loaded_wave: Vec>| { + loaded_wave + .into_par_iter() + .map(|loaded| { + let shared = &shared_by_device[loaded.device_slot]; + let compute_started = Instant::now(); + let output = trap_sampler.preimage( + &shared.params, + &shared.source_trapdoor, + &shared.source_b, + &loaded.target, + ); + ComputedDiamondPreprocessChunk { + task: loaded.task, + output, + load_s: loaded.load_s, + compute_s: maybe_elapsed_s(compute_started), + } + }) + .collect::>() + }; + + let mut next_wave_idx = 1usize; + let mut current_wave = if wave_count == 0 { None } else { load_wave(0) }; + let mut previous_outputs = None; + while let Some(loaded_wave) = current_wave.take() { + let should_load_next = next_wave_idx < wave_count; + let previous_outputs_to_store = previous_outputs.take(); + let wave_started = Instant::now(); + let ((computed_outputs, next_loaded_wave), stored_now) = rayon::join( + || { + rayon::join( + || compute_wave(loaded_wave), + || if should_load_next { load_wave(next_wave_idx) } else { None }, + ) + }, + || { + previous_outputs_to_store + .map(|outputs| self.store_preprocess_wave(dir_path, outputs)) + .unwrap_or(0) + }, + ); + completed_tasks += stored_now; + info!( + level, + state_idx, + wave = next_wave_idx, + wave_count, + completed_tasks, + total_tasks, + progress = preprocess_progress_label(completed_tasks, total_tasks), + elapsed_s = maybe_elapsed_s(wave_started), + "diamond injector gpu preprocess: K stage wave completed" + ); + previous_outputs = Some(computed_outputs); + current_wave = next_loaded_wave; + next_wave_idx += 1; + } + if let Some(outputs) = previous_outputs { + completed_tasks += self.store_preprocess_wave(dir_path, outputs); + } + + info!( + level, + state_idx, + source_state_idx, completed_tasks, - total_tasks = tasks.len(), - progress = preprocess_progress_label(completed_tasks, tasks.len()), - elapsed_s = maybe_elapsed_s(wave_started), - "diamond injector gpu preprocess: K stage wave completed" + total_tasks, + progress = preprocess_progress_label(completed_tasks, total_tasks), + elapsed_s = maybe_elapsed_s(state_started), + "diamond injector gpu preprocess: K stage state finished" ); - previous_outputs = Some(computed_outputs); - current_wave = next_loaded_wave; - next_wave_idx += 1; - } - if let Some(outputs) = previous_outputs { - completed_tasks += self.store_preprocess_wave(dir_path, outputs); } info!( level, completed_tasks, - total_tasks = tasks.len(), - progress = preprocess_progress_label(completed_tasks, tasks.len()), + total_tasks, + progress = preprocess_progress_label(completed_tasks, total_tasks), elapsed_s = maybe_elapsed_s(stage_started), "diamond injector gpu preprocess: K stage finished" ); @@ -589,9 +619,8 @@ where let mut total_tasks = 0usize; let mut completed_tasks = 0usize; - // Process K_{i,b,j} level by level so each stage only keeps B_{i-1}, - // B_i, the trapdoor of B_{i-1}, and the current level's secret masks - // resident at once. + // Process K_{i,b,j} level by level, while the GPU K-stage streams each + // source/target state pair so resident checkpoint data stays bounded. for level in 1..=self.input_count { let secret_mask_bytes = (0..self.base) .map(|digit_value| { @@ -621,20 +650,6 @@ where } } total_tasks += stage_tasks.len(); - let source_checkpoint_bytes = (0..self.state_count_at_level(level - 1)) - .map(|source_idx| { - let (b_bytes, t_bytes) = - self.load_or_sample_b_checkpoint_bytes(dir_path, level - 1, source_idx); - (Arc::<[u8]>::from(b_bytes), Arc::<[u8]>::from(t_bytes)) - }) - .collect::>(); - let target_b_bytes = (0..self.state_count_at_level(level)) - .map(|state_idx| { - let (b_bytes, _) = - self.load_or_sample_b_checkpoint_bytes(dir_path, level, state_idx); - Arc::<[u8]>::from(b_bytes) - }) - .collect::>(); if stage_tasks.is_empty() { info!( level, @@ -642,14 +657,8 @@ where "diamond injector gpu preprocess: K stage already checkpointed" ); } else { - completed_tasks += self.preprocess_k_stage_gpu( - dir_path, - level, - stage_tasks, - &source_checkpoint_bytes, - &target_b_bytes, - &secret_mask_bytes, - ); + completed_tasks += + self.preprocess_k_stage_gpu(dir_path, level, stage_tasks, &secret_mask_bytes); } } @@ -818,7 +827,7 @@ mod tests { ); secret_matrix = secret_matrix * secret_mask; } - let base_public_matrix = preprocess_out.final_pub_matrices[0].clone(); + let base_public_matrix = preprocess_out.final_public_matrix(&injector.params, 0); let base_selector = GpuDCRTPolyMatrix::from_poly_vec_row( ¶ms, vec![secret_matrix.entry(0, 0), k.clone()], @@ -830,7 +839,8 @@ mod tests { let state_idx = injector.bit_state_idx(digit_idx, bit_idx); let bit_value = ((digits[digit_idx] as usize) >> bit_idx) & 1; let bit_plaintext = TestPoly::from_usize_to_constant(¶ms, bit_value); - let bit_public_matrix = preprocess_out.final_pub_matrices[state_idx].clone(); + let bit_public_matrix = + preprocess_out.final_public_matrix(&injector.params, state_idx); let bit_selector = GpuDCRTPolyMatrix::from_poly_vec_row( ¶ms, vec![secret_matrix.entry(0, 0), secret_matrix.entry(0, 0) * &bit_plaintext], diff --git a/src/input_injector/mod.rs b/src/input_injector/mod.rs index 6ba72e22..b86a8b03 100644 --- a/src/input_injector/mod.rs +++ b/src/input_injector/mod.rs @@ -79,8 +79,9 @@ pub struct DiamondInjectorPreprocessOut where M: PolyMatrix, { - pub final_trapdoors: Vec, - pub final_pub_matrices: Vec, + final_trapdoor_bytes: Vec>, + final_pub_matrix_bytes: Vec>, + _marker: PhantomData<(M, T)>, } impl DiamondInjectorPreprocessOut @@ -89,21 +90,31 @@ where { pub fn final_state_count(&self) -> usize { debug_assert_eq!( - self.final_trapdoors.len(), - self.final_pub_matrices.len(), + self.final_trapdoor_bytes.len(), + self.final_pub_matrix_bytes.len(), "DiamondInjector final checkpoint vector length mismatch" ); - self.final_pub_matrices.len() + self.final_pub_matrix_bytes.len() } - pub fn final_checkpoint(&self, state_idx: usize) -> (&T, &M) { + pub fn final_trapdoor_bytes(&self, state_idx: usize) -> &[u8] { assert!( state_idx < self.final_state_count(), "DiamondInjector final state index out of range: {} >= {}", state_idx, self.final_state_count() ); - (&self.final_trapdoors[state_idx], &self.final_pub_matrices[state_idx]) + &self.final_trapdoor_bytes[state_idx] + } + + pub fn final_public_matrix(&self, params: &::Params, state_idx: usize) -> M { + assert!( + state_idx < self.final_state_count(), + "DiamondInjector final state index out of range: {} >= {}", + state_idx, + self.final_state_count() + ); + M::from_compact_bytes(params, &self.final_pub_matrix_bytes[state_idx]) } } @@ -684,21 +695,21 @@ where #[cfg(feature = "gpu")] { self.preprocess_gpu(dir_path, k); - let mut final_trapdoors = + let mut final_trapdoor_bytes = Vec::with_capacity(self.state_count_at_level(self.input_count)); - let mut final_pub_matrices = + let mut final_pub_matrix_bytes = Vec::with_capacity(self.state_count_at_level(self.input_count)); for state_idx in 0..self.state_count_at_level(self.input_count) { - let (final_pub_matrix_bytes, final_trapdoor_bytes) = + let (public_matrix_bytes, trapdoor_bytes) = self.load_or_sample_b_checkpoint_bytes(dir_path, self.input_count, state_idx); - final_trapdoors.push( - TS::trapdoor_from_bytes(&self.params, &final_trapdoor_bytes) - .expect("DiamondInjector final trapdoor checkpoint must decode"), - ); - final_pub_matrices - .push(M::from_compact_bytes(&self.params, &final_pub_matrix_bytes)); + final_trapdoor_bytes.push(trapdoor_bytes); + final_pub_matrix_bytes.push(public_matrix_bytes); } - return DiamondInjectorPreprocessOut { final_trapdoors, final_pub_matrices }; + return DiamondInjectorPreprocessOut { + final_trapdoor_bytes, + final_pub_matrix_bytes, + _marker: PhantomData, + }; } #[cfg(not(feature = "gpu"))] @@ -780,12 +791,19 @@ where } } DiamondInjectorPreprocessOut { - final_trapdoors: trapdoors + final_trapdoor_bytes: trapdoors .pop() - .expect("DiamondInjector must keep final trapdoor checkpoints"), - final_pub_matrices: b_checkpoints + .expect("DiamondInjector must keep final trapdoor checkpoints") + .into_iter() + .map(|trapdoor| TS::trapdoor_to_bytes(&trapdoor)) + .collect(), + final_pub_matrix_bytes: b_checkpoints .pop() - .expect("DiamondInjector must keep final public matrix checkpoints"), + .expect("DiamondInjector must keep final public matrix checkpoints") + .into_iter() + .map(|matrix| matrix.to_compact_bytes()) + .collect(), + _marker: PhantomData, } } } @@ -852,7 +870,7 @@ mod tests { uniform::DCRTPolyUniformSampler, }, simulator::{ - SimulatorContext, error_norm::compute_preimage_norm, poly_matrix_norm::PolyMatrixNorm, + SimulatorContext, error_norm::compute_preimage_sigma, poly_matrix_norm::PolyMatrixNorm, }, utils::bigdecimal_bits_ceil, }; @@ -870,6 +888,21 @@ mod tests { DCRTPolyTrapdoorSampler, >; + fn assert_poly_matrix_bound_eq(actual: &PolyMatrixNorm, expected: &PolyMatrixNorm) { + assert_eq!(actual.nrow, expected.nrow); + assert_eq!(actual.ncol, expected.ncol); + assert_eq!(actual.ncol_sqrt, expected.ncol_sqrt); + assert_eq!(actual.poly_norm, expected.poly_norm); + assert_eq!(actual.zero_rows, expected.zero_rows); + } + + fn assert_poly_matrix_bounds_eq(actual: &[PolyMatrixNorm], expected: &[PolyMatrixNorm]) { + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected.iter()) { + assert_poly_matrix_bound_eq(actual, expected); + } + } + #[sequential_test::sequential] #[test] fn test_diamond_injector_online_eval_returns_exact_bgg_relations() { @@ -902,7 +935,7 @@ mod tests { assert_eq!(secret_mask.size(), (DIAMOND_SECRET_SIZE, DIAMOND_SECRET_SIZE)); secret_matrix = secret_matrix * secret_mask; } - let base_public_matrix = preprocess_out.final_pub_matrices[0].clone(); + let base_public_matrix = preprocess_out.final_public_matrix(&injector.params, 0); let base_selector = DCRTPolyMatrix::from_poly_vec_row(¶ms, vec![secret_matrix.entry(0, 0), k.clone()]); assert_eq!(states[0], base_selector * base_public_matrix); @@ -912,7 +945,8 @@ mod tests { let state_idx = injector.bit_state_idx(digit_idx, bit_idx); let bit_value = ((digits[digit_idx] as usize) >> bit_idx) & 1; let bit_plaintext = TestPoly::from_usize_to_constant(¶ms, bit_value); - let bit_public_matrix = preprocess_out.final_pub_matrices[state_idx].clone(); + let bit_public_matrix = + preprocess_out.final_public_matrix(&injector.params, state_idx); let bit_selector = DCRTPolyMatrix::from_poly_vec_row( ¶ms, vec![secret_matrix.entry(0, 0), secret_matrix.entry(0, 0) * &bit_plaintext], @@ -952,22 +986,27 @@ mod tests { state_cols, BigDecimal::from_f64(injector.error_sigma).expect("error_sigma must be finite"), ); - let expected_preimage_norm = compute_preimage_norm( + let expected_preimage_sigma = compute_preimage_sigma( &ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(injector.state_row_size() / DIAMOND_SECRET_SIZE), None, ); - let expected_transition = PolyMatrixNorm::new( + let expected_transition = PolyMatrixNorm::fresh_preimage( ctx.clone(), state_cols, state_cols, - expected_preimage_norm.clone(), + expected_preimage_sigma.clone(), + None, + ); + let expected_output = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + state_cols, + gadget_cols, + expected_preimage_sigma, None, ); - let expected_output = - PolyMatrixNorm::new(ctx.clone(), state_cols, gadget_cols, expected_preimage_norm, None); let expected_regular_selector = PolyMatrixNorm::new( ctx.clone(), injector.state_row_size(), @@ -1040,9 +1079,9 @@ mod tests { expected_state_errors = next_state_errors; } - assert_eq!(simulated.state_errors, expected_state_errors); - assert_eq!(simulated.secret_state_factors, expected_secret_factors); - assert_eq!(simulated.output_preimage, expected_output); + assert_poly_matrix_bounds_eq(&simulated.state_errors, &expected_state_errors); + assert_poly_matrix_bounds_eq(&simulated.secret_state_factors, &expected_secret_factors); + assert_poly_matrix_bound_eq(&simulated.output_preimage, &expected_output); } #[test] @@ -1073,7 +1112,7 @@ mod tests { .collect::>(); let max_error = projected_errors .iter() - .map(|norm| norm.poly_norm.norm.clone()) + .map(|norm| norm.maximum_coefficient_bound()) .max() .expect("state error list must be non-empty"); diff --git a/src/input_injector/simulation.rs b/src/input_injector/simulation.rs index b35d7de9..ec56a425 100644 --- a/src/input_injector/simulation.rs +++ b/src/input_injector/simulation.rs @@ -4,7 +4,7 @@ use crate::{ poly::PolyParams, sampler::{PolyHashSampler, PolyTrapdoorSampler, PolyUniformSampler}, simulator::{ - SimulatorContext, error_norm::compute_preimage_norm, poly_matrix_norm::PolyMatrixNorm, + SimulatorContext, error_norm::compute_preimage_sigma, poly_matrix_norm::PolyMatrixNorm, }, }; use bigdecimal::BigDecimal; @@ -16,18 +16,18 @@ use tracing::debug; #[derive(Debug, Clone, PartialEq, Eq)] /// Error-growth summary for Diamond state insertion. /// -/// The simulator exposes the final Diamond state bounds and the generic -/// one-step final projection preimage bound. Callers that own output -/// projections can combine these values for their concrete one, k, input, or -/// decoder outputs. +/// The simulator exposes envelope-mode error norms for the final Diamond +/// states and the generic one-step final projection preimage norm. Callers +/// that own output projections combine these values internally and read the +/// public maximum coefficient bound directly from the norm. pub struct DiamondInputErrorSimulation { /// Final propagated error for each Diamond state branch. pub state_errors: Vec, /// Final secret-selector norm for each Diamond state branch. /// /// These factors model the product of the secret transition matrices that - /// multiply the sampled Gaussian target errors. Callers that need a bound - /// on the final online secret, such as noise-refresh rounding analysis, + /// multiply the sampled Gaussian target errors. Callers that need a norm + /// for the final online secret, such as noise-refresh rounding analysis, /// should use the factor for the branch they decode from. pub secret_state_factors: Vec, /// Generic final projection preimage norm from the final state basis to a @@ -63,17 +63,27 @@ where .expect("DiamondInjector error_sigma must be finite"); let initial_state_error = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, state_cols, initial_sigma); - let preimage_norm = compute_preimage_norm( + let preimage_sigma = compute_preimage_sigma( &ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(self.state_row_size() / DIAMOND_SECRET_SIZE), None, ); - let transition_preimage = - PolyMatrixNorm::new(ctx.clone(), state_cols, state_cols, preimage_norm.clone(), None); - let output_preimage = - PolyMatrixNorm::new(ctx.clone(), state_cols, gadget_cols, preimage_norm, None); + let transition_preimage = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + state_cols, + state_cols, + preimage_sigma.clone(), + None, + ); + let output_preimage = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + state_cols, + gadget_cols, + preimage_sigma, + None, + ); let transition_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), self.state_row_size(), @@ -160,7 +170,7 @@ where ?secret_state_factors, ?state_errors, ?output_preimage, - "diamond input-insertion simulator final state bounds", + "diamond input-insertion simulator final state sigmas", ); DiamondInputErrorSimulation { state_errors, secret_state_factors, output_preimage } diff --git a/src/io/aky24_io/simulation.rs b/src/io/aky24_io/simulation.rs index e1b69554..ac9a8f7b 100644 --- a/src/io/aky24_io/simulation.rs +++ b/src/io/aky24_io/simulation.rs @@ -23,7 +23,7 @@ use crate::{ }, simulator::{ SimulatorContext, - error_norm::{ErrorNorm, compute_preimage_norm}, + error_norm::{ErrorNorm, compute_preimage_sigma}, poly_matrix_norm::PolyMatrixNorm, poly_norm::PolyNorm, }, @@ -42,7 +42,7 @@ const REPRESENTATIVE_GOLDREICH_SEED_BITS: usize = 5; pub struct Aky24IOErrorSimulation { /// Fresh encoding error that replaces DiamondIO input-injection state error. pub initial_fresh_error: ErrorNorm, - /// Final decoder term derived from fresh `c_b0` error and output preimage norm. + /// Final decoder term derived from fresh `c_b0` error and output preimage sigma. pub final_decoder_error: PolyMatrixNorm, /// Representative noise-refresh simulation per PRF seed-refresh round. pub prf_refreshes: Vec, @@ -589,14 +589,14 @@ where let decryption_key = initial_fresh_error.clone(); let refresh_decoder_error = initial_fresh_error.clone(); let c_b0_error = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, ctx.m_b, sigma); - let output_preimage_norm = PolyMatrixNorm::new( + let output_preimage_sigma = PolyMatrixNorm::fresh_preimage( ctx.clone(), ctx.m_b, 1, - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None), + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None), None, ); - let final_decoder_projection_error = c_b0_error * &output_preimage_norm; + let final_decoder_projection_error = c_b0_error * &output_preimage_sigma; let final_decoder_error = ErrorNorm::new(PolyNorm::one(ctx.clone()), final_decoder_projection_error); let seed_wire_error = sim_utils::initial_seed_wire_error( @@ -616,9 +616,9 @@ where ); info!( initial_fresh_error_bits = - bigdecimal_bits_ceil(&initial_fresh_error.matrix_norm.poly_norm.norm), + bigdecimal_bits_ceil(&initial_fresh_error.matrix_norm.maximum_coefficient_bound()), final_decoder_error_bits = - bigdecimal_bits_ceil(&final_decoder_error.matrix_norm.poly_norm.norm), + bigdecimal_bits_ceil(&final_decoder_error.matrix_norm.maximum_coefficient_bound()), "AKY24IO fresh initial error simulation base finished" ); @@ -1135,7 +1135,7 @@ where plt_evaluator, slot_transfer_evaluator, ); - let error = &simulation.noisy_plaintext_error.poly_norm.norm; + let error = &simulation.noisy_plaintext_error.maximum_coefficient_bound(); let max_valid_bits = sim_utils::final_mask_coeff_bits_for_error_margin( &prefix.cpu_params, error, @@ -1237,8 +1237,9 @@ where projection_residual_error.clone() + &prefix.final_decoder_error.matrix_norm; info!( projection_residual_bits = - bigdecimal_bits_ceil(&projection_residual_error.poly_norm.norm), - noisy_plaintext_bits = bigdecimal_bits_ceil(&noisy_plaintext_error.poly_norm.norm), + bigdecimal_bits_ceil(&projection_residual_error.maximum_coefficient_bound()), + noisy_plaintext_bits = + bigdecimal_bits_ceil(&noisy_plaintext_error.maximum_coefficient_bound()), "AKY24IO final output error simulation finished" ); @@ -1372,7 +1373,7 @@ fn aky24_io_security_margins_hold( }; if !validate_error_bound_security_margin( &final_threshold, - &simulation.noisy_plaintext_error.poly_norm.norm, + &simulation.noisy_plaintext_error.maximum_coefficient_bound(), security_bit, ) { return false; @@ -1539,8 +1540,12 @@ mod tests { assert_eq!(base.final_decoder_error.matrix_norm.ncol, 1); assert_eq!( base.initial_fresh_error.matrix_norm.poly_norm.norm, + BigDecimal::from_f64(13.0).unwrap() + ); + assert_eq!( + base.initial_fresh_error.matrix_norm.maximum_coefficient_bound(), BigDecimal::from_f64(13.0).unwrap(), - "fresh initial ErrorNorm must come directly from 6.5 * error_sigma" + "fresh initial ErrorNorm public bound stores the high-probability envelope directly" ); } diff --git a/src/io/diamond_io.rs b/src/io/diamond_io.rs index 05961f0f..20ec5ef0 100644 --- a/src/io/diamond_io.rs +++ b/src/io/diamond_io.rs @@ -406,11 +406,14 @@ where let lookup_top = enc_lookup_base_matrix.clone(); let lookup_bottom = M::zero(params, DIAMOND_SECRET_SIZE, lookup_top.col_size()); let lookup_target = lookup_top.concat_rows(&[&lookup_bottom]); - let (lookup_trapdoor, lookup_public_matrix) = preprocess_out.final_checkpoint(0); + let lookup_trapdoor = + TS::trapdoor_from_bytes(params, preprocess_out.final_trapdoor_bytes(0)) + .expect("DiamondInjector final trapdoor checkpoint must decode"); + let lookup_public_matrix = preprocess_out.final_public_matrix(params, 0); let lookup_base_preimage = TS::new(params, self.injector.trapdoor_sigma).preimage( params, - lookup_trapdoor, - lookup_public_matrix, + &lookup_trapdoor, + &lookup_public_matrix, &lookup_target, ); Self::write_io_matrix(dir_path, Self::enc_lookup_base_preimage_id(), &lookup_base_preimage); @@ -498,11 +501,14 @@ where DIAMOND_SECRET_SIZE, DirectoryDecoderArtifacts::new(dir_path, "diamond_io"), |_, target| { - let (trapdoor, public_matrix) = preprocess_out.final_checkpoint(0); + let trapdoor = + TS::trapdoor_from_bytes(params, preprocess_out.final_trapdoor_bytes(0)) + .expect("DiamondInjector final trapdoor checkpoint must decode"); + let public_matrix = preprocess_out.final_public_matrix(params, 0); TS::new(params, self.injector.trapdoor_sigma).preimage( params, - trapdoor, - public_matrix, + &trapdoor, + &public_matrix, target, ) }, diff --git a/src/io/diamond_io/simulation.rs b/src/io/diamond_io/simulation.rs index a9f789e5..488a1272 100644 --- a/src/io/diamond_io/simulation.rs +++ b/src/io/diamond_io/simulation.rs @@ -668,8 +668,9 @@ where ); info!( max_candidate, - final_mask_prg_bits = - bigdecimal_bits_ceil(&cached_final_mask_prg_output.matrix_norm.poly_norm.norm), + final_mask_prg_bits = bigdecimal_bits_ceil( + &cached_final_mask_prg_output.matrix_norm.maximum_coefficient_bound() + ), "DiamondIO PRF mask bit search cached final-mask representative PRG" ); let cached_final_mask_base_error = sim_utils::simulate_final_mask_base_error( @@ -684,8 +685,9 @@ where "DiamondIO", ); info!( - final_mask_base_bits = - bigdecimal_bits_ceil(&cached_final_mask_base_error.matrix_norm.poly_norm.norm), + final_mask_base_bits = bigdecimal_bits_ceil( + &cached_final_mask_base_error.matrix_norm.maximum_coefficient_bound() + ), "DiamondIO PRF mask bit search cached final-mask representative decrypt" ); let mut low = 1usize; @@ -703,7 +705,7 @@ where plt_evaluator, slot_transfer_evaluator, ); - let error = &simulation.noisy_plaintext_error.poly_norm.norm; + let error = &simulation.noisy_plaintext_error.maximum_coefficient_bound(); let max_valid_bits = sim_utils::final_mask_coeff_bits_for_error_margin( &prefix.cpu_params, error, @@ -833,7 +835,7 @@ where info!( state_count = input_injection.state_errors.len(), projected_state_error_bits = - bigdecimal_bits_ceil(&projected_state_error.poly_norm.norm), + bigdecimal_bits_ceil(&projected_state_error.maximum_coefficient_bound()), "DiamondIO input-injection error simulation finished" ); debug!(state_errors = ?input_injection.state_errors, "DiamondIO simulated state errors"); @@ -1054,14 +1056,13 @@ where round_idx, v_bits = evaluation.round.noise_refresh.v_bits, refreshed_seed_error_bits = bigdecimal_bits_ceil( - &evaluation.refreshed_seed.matrix_norm.poly_norm.norm + &evaluation.refreshed_seed.matrix_norm.maximum_coefficient_bound() ), selected_ciphertext_decryption_bits = bigdecimal_bits_ceil( &evaluation .round .representative_selected_prg_ciphertext_decryption_error - .poly_norm - .norm + .maximum_coefficient_bound() ), "DiamondIO reusing fixed-v steady-state PRF refresh error simulation" ); @@ -1105,14 +1106,16 @@ where self.ring_gsw_public_key_error_sigma.unwrap_or(0.0), ); info!( - function_secret_bits = - bigdecimal_bits_ceil(&function_secret_error.matrix_norm.poly_norm.norm), + function_secret_bits = bigdecimal_bits_ceil( + &function_secret_error.matrix_norm.maximum_coefficient_bound() + ), function_public_bottom_plaintext_bits = - bigdecimal_bits_ceil(&function_public_bottom_error.plaintext_norm.norm), - function_public_bottom_encoding_bits = - bigdecimal_bits_ceil(&function_public_bottom_error.matrix_norm.poly_norm.norm), + bigdecimal_bits_ceil(&function_public_bottom_error.plaintext_norm.sigma), + function_public_bottom_encoding_bits = bigdecimal_bits_ceil( + &function_public_bottom_error.matrix_norm.maximum_coefficient_bound() + ), accumulated_public_bottom_decryption_bits = - bigdecimal_bits_ceil(&public_bottom_decryption_error.poly_norm.norm), + bigdecimal_bits_ceil(&public_bottom_decryption_error.maximum_coefficient_bound()), "DiamondIO representative GoldreichPRF output error simulation finished" ); Some(DiamondIOMaskIndependentErrorPrefix { @@ -1186,14 +1189,17 @@ where candidate, valid, selected_prg_bits = bigdecimal_bits_ceil( - &evaluation.round.representative_selected_prg_output.matrix_norm.poly_norm.norm + &evaluation + .round + .representative_selected_prg_output + .matrix_norm + .maximum_coefficient_bound() ), selected_ciphertext_decryption_bits = bigdecimal_bits_ceil( &evaluation .round .representative_selected_prg_ciphertext_decryption_error - .poly_norm - .norm + .maximum_coefficient_bound() ), "DiamondIO fixed-v noise-refresh security candidate evaluated" ); @@ -1261,12 +1267,11 @@ where round_idx, v_bits = core.refresh.v_bits, refreshed_seed_error_bits = - bigdecimal_bits_ceil(&core.refreshed_seed.matrix_norm.poly_norm.norm), + bigdecimal_bits_ceil(&core.refreshed_seed.matrix_norm.maximum_coefficient_bound()), selected_ciphertext_decryption_bits = bigdecimal_bits_ceil( &material_state .representative_selected_prg_ciphertext_decryption_error - .poly_norm - .norm + .maximum_coefficient_bound() ), noise_refresh_material_branch_count_online = 1usize, "DiamondIO PRF refresh error simulation finished" @@ -1333,7 +1338,8 @@ where info!( round_idx, selected_ciphertext_decryption_bits = bigdecimal_bits_ceil( - &representative_selected_prg_ciphertext_decryption_error.poly_norm.norm + &representative_selected_prg_ciphertext_decryption_error + .maximum_coefficient_bound() ), "DiamondIO fixed-v PRF refresh representative ciphertext-state simulation finished" ); @@ -1347,9 +1353,10 @@ where ); info!( round_idx, - selected_prg_bits = bigdecimal_bits_ceil(&selected.matrix_norm.poly_norm.norm), + selected_prg_bits = + bigdecimal_bits_ceil(&selected.matrix_norm.maximum_coefficient_bound()), material_wire_bits = - bigdecimal_bits_ceil(&material_wire_error.matrix_norm.poly_norm.norm), + bigdecimal_bits_ceil(&material_wire_error.matrix_norm.maximum_coefficient_bound()), "DiamondIO fixed-v PRF refresh material-input simulation finished" ); let state = DiamondIOPrfRefreshMaterialState { @@ -1381,13 +1388,13 @@ where .unwrap_or_else(|| "None".to_string()), seed_error_matrix_norms: seed_errors .iter() - .map(|error| error.matrix_norm.poly_norm.norm.to_string()) + .map(|error| error.matrix_norm.poly_norm.sigma.to_string()) .collect(), seed_ciphertext_randomizer_rows: seed_ciphertext_randomizer_norm.nrow, seed_ciphertext_randomizer_cols: seed_ciphertext_randomizer_norm.ncol, seed_ciphertext_randomizer_norm: seed_ciphertext_randomizer_norm .poly_norm - .norm + .sigma .to_string(), } } @@ -1461,8 +1468,9 @@ where }); info!( prf_mask_output_coeff_bits, - final_mask_prg_bits = - bigdecimal_bits_ceil(&final_mask_prg_output.matrix_norm.poly_norm.norm), + final_mask_prg_bits = bigdecimal_bits_ceil( + &final_mask_prg_output.matrix_norm.maximum_coefficient_bound() + ), "DiamondIO final mask representative PRG error simulation finished" ); let final_mask_base_error = cached_final_mask_base_error.unwrap_or_else(|| { @@ -1507,15 +1515,17 @@ where let input_injection_projection_error = diamond_io_input_injection_projection_error(&prefix.input_injection); let input_injection_error_bits = - bigdecimal_bits_ceil(&input_injection_projection_error.poly_norm.norm); - let noisy_plaintext_bits = bigdecimal_bits_ceil(&noisy_plaintext_error.poly_norm.norm); + bigdecimal_bits_ceil(&input_injection_projection_error.maximum_coefficient_bound()); + let noisy_plaintext_bits = + bigdecimal_bits_ceil(&noisy_plaintext_error.maximum_coefficient_bound()); info!( projected_evaluated_bits = - bigdecimal_bits_ceil(&projected_evaluated_error.poly_norm.norm), - public_bottom_decryption_bits = - bigdecimal_bits_ceil(&prefix.public_bottom_decryption_error.poly_norm.norm), + bigdecimal_bits_ceil(&projected_evaluated_error.maximum_coefficient_bound()), + public_bottom_decryption_bits = bigdecimal_bits_ceil( + &prefix.public_bottom_decryption_error.maximum_coefficient_bound() + ), projection_residual_bits = - bigdecimal_bits_ceil(&projection_residual_error.poly_norm.norm), + bigdecimal_bits_ceil(&projection_residual_error.maximum_coefficient_bound()), noisy_plaintext_bits, input_injection_error_bits, input_injection_error_percent_upper_bound = @@ -1588,7 +1598,8 @@ where round_idx, conceptual_output_bits, range_start, - output_error_bits = bigdecimal_bits_ceil(&cached.matrix_norm.poly_norm.norm), + output_error_bits = + bigdecimal_bits_ceil(&cached.matrix_norm.maximum_coefficient_bound()), "DiamondIO representative Goldreich PRG ErrorNorm cache hit" ); return cached; @@ -1640,7 +1651,8 @@ where range_start, simulated_active_levels = 1usize, extrapolated_active_levels = full_active_levels, - output_error_bits = bigdecimal_bits_ceil(&output.matrix_norm.poly_norm.norm), + output_error_bits = + bigdecimal_bits_ceil(&output.matrix_norm.maximum_coefficient_bound()), "DiamondIO representative Goldreich PRG error simulated" ); output_cache @@ -1662,11 +1674,11 @@ where seed_bits: self.seed_bits, ring_gsw_level_offset: self.ring_gsw_level_offset, full_active_levels, - one_plaintext_norm: one.plaintext_norm.norm.to_string(), - one_matrix_norm: one.matrix_norm.poly_norm.norm.to_string(), + one_plaintext_norm: one.plaintext_norm.sigma.to_string(), + one_matrix_norm: one.matrix_norm.maximum_coefficient_bound().to_string(), seed_error_matrix_norms: seed_errors .iter() - .map(|error| error.matrix_norm.poly_norm.norm.to_string()) + .map(|error| error.matrix_norm.poly_norm.sigma.to_string()) .collect(), } } @@ -1748,12 +1760,13 @@ fn diamond_io_input_injection_scalar_projection_error( .state_errors .first() .expect("DiamondIO input injection must produce a base final state error"); - let scalar_output_preimage = PolyMatrixNorm::new( - input_injection.output_preimage.clone_ctx(), + let scalar_output_preimage = PolyMatrixNorm::from_parts( input_injection.output_preimage.nrow, 1, - input_injection.output_preimage.poly_norm.norm.clone(), + input_injection.output_preimage.poly_norm.clone(), None, + input_injection.output_preimage.deps.clone(), + input_injection.output_preimage.clt_ready, ); state_error.clone() * &scalar_output_preimage } @@ -1791,7 +1804,7 @@ fn diamond_io_security_margins_hold( }; if !validate_error_bound_security_margin( &final_threshold, - &simulation.noisy_plaintext_error.poly_norm.norm, + &simulation.noisy_plaintext_error.maximum_coefficient_bound(), security_bit, ) { return false; @@ -2085,11 +2098,11 @@ mod tests { ); assert!( - second_seed_randomizer.poly_norm.norm > first_seed_randomizer.poly_norm.norm, + second_seed_randomizer.poly_norm.sigma > first_seed_randomizer.poly_norm.sigma, "the refreshed next_seed Ring-GSW ciphertext must carry accumulated randomizer noise" ); assert!( - second_decryption_error.poly_norm.norm > first_decryption_error.poly_norm.norm, + second_decryption_error.poly_norm.sigma > first_decryption_error.poly_norm.sigma, "PRF ciphertext-side decryption error must grow from one seed round to the next" ); } @@ -2146,9 +2159,9 @@ mod tests { &residual, ); assert_eq!(refresh.pre_round_outputs[0].ncol, ctx.m_g); - assert_eq!(refresh.pre_round_outputs[0].poly_norm.norm, BigDecimal::from(12u32)); + assert_eq!(refresh.pre_round_outputs[0].poly_norm.sigma, BigDecimal::from(12u32)); assert_eq!( - refresh.rounded_errors[0].poly_norm.norm, + refresh.rounded_errors[0].poly_norm.sigma, BigDecimal::from(11u32), "ciphertext decryption residual is a scalar pre-rounding perturbation, not refreshed seed BGG error" ); @@ -2277,16 +2290,16 @@ mod tests { prefix.prf_refreshes[2] .representative_selected_prg_ciphertext_decryption_error .poly_norm - .norm > + .sigma > prefix.prf_refreshes[1] .representative_selected_prg_ciphertext_decryption_error .poly_norm - .norm, + .sigma, "ciphertext-side FHE noise must keep accumulating across PRF seed rounds" ); assert!( - prefix.prf_refreshes[2].noise_refresh.pre_round_outputs[0].poly_norm.norm > - prefix.prf_refreshes[1].noise_refresh.pre_round_outputs[0].poly_norm.norm, + prefix.prf_refreshes[2].noise_refresh.pre_round_outputs[0].poly_norm.sigma > + prefix.prf_refreshes[1].noise_refresh.pre_round_outputs[0].poly_norm.sigma, "noise-refresh pre-round error must grow when the underlying seed ciphertext decryption residual grows" ); } diff --git a/src/io/diamond_io/utils.rs b/src/io/diamond_io/utils.rs index 6302a9ad..15603e67 100644 --- a/src/io/diamond_io/utils.rs +++ b/src/io/diamond_io/utils.rs @@ -616,11 +616,14 @@ where bottom = bottom - &(gadget * plaintext); } let target = top.concat_rows(&[&bottom]); - let (trapdoor, public_matrix) = preprocess_out.final_checkpoint(state_idx); + let trapdoor = + TS::trapdoor_from_bytes(params, preprocess_out.final_trapdoor_bytes(state_idx)) + .expect("DiamondInjector final trapdoor checkpoint must decode"); + let public_matrix = preprocess_out.final_public_matrix(params, state_idx); TS::new(params, self.injector.trapdoor_sigma).preimage( params, - trapdoor, - public_matrix, + &trapdoor, + &public_matrix, &target, ) } diff --git a/src/io/utils/simulation.rs b/src/io/utils/simulation.rs index bab5e677..12b098df 100644 --- a/src/io/utils/simulation.rs +++ b/src/io/utils/simulation.rs @@ -2,7 +2,7 @@ use std::{sync::Arc, thread, time::Duration}; use bigdecimal::BigDecimal; use num_bigint::{BigInt, BigUint}; -use num_traits::FromPrimitive; +use num_traits::{FromPrimitive, Zero}; use crate::{ circuit::{Evaluable, PolyCircuit}, @@ -32,6 +32,7 @@ use crate::{ }, simulator::{ SimulatorContext, + dependency_set::DependencySet, error_norm::ErrorNorm, lattice_estimator::{Distribution, run_lattice_estimator_cli_with_timeout}, poly_matrix_norm::PolyMatrixNorm, @@ -550,9 +551,9 @@ pub(crate) fn noise_refresh_worst_pre_rounding_error_bound( refresh .pre_round_outputs .iter() - .map(|error| error.poly_norm.norm.clone()) + .map(|error| error.maximum_coefficient_bound()) .max_by(|lhs, rhs| { - lhs.partial_cmp(rhs).expect("noise-refresh pre-rounding norms must be comparable") + lhs.partial_cmp(rhs).expect("noise-refresh pre-rounding bounds must be comparable") }) .expect("noise-refresh simulation must produce at least one pre-round output") } @@ -682,7 +683,7 @@ pub(crate) fn branch_rebase_decoder_error(error: ErrorNorm, target_col_size: usi error.matrix_norm.clone_ctx(), error.matrix_norm.nrow, target_col_size, - error.matrix_norm.poly_norm.norm, + error.matrix_norm.poly_norm.sigma, error.matrix_norm.zero_rows, ), ) @@ -734,7 +735,7 @@ pub(crate) fn ciphertext_decryption_error_from_randomizer( randomizer_norm.clone_ctx(), bottom_half_randomizer.ncol, 1, - decomposed_randomizer_norm.poly_norm.norm.clone(), + decomposed_randomizer_norm.poly_norm.sigma.clone(), None, ); let public_key_error = PolyMatrixNorm::sample_gauss( @@ -748,7 +749,7 @@ pub(crate) fn ciphertext_decryption_error_from_randomizer( output_ctx, 1, 1, - raw.poly_norm.norm * BigDecimal::from(full_active_levels as u64), + raw.poly_norm.sigma * BigDecimal::from(full_active_levels as u64), None, ) } @@ -1071,8 +1072,8 @@ pub(crate) fn refreshed_seed_from_noise_refresh( .iter() .max_by(|lhs, rhs| { lhs.poly_norm - .norm - .partial_cmp(&rhs.poly_norm.norm) + .sigma + .partial_cmp(&rhs.poly_norm.sigma) .unwrap_or_else(|| panic!("{protocol_name} rounded errors must be comparable")) }) .expect("noise refresh must produce rounded errors") @@ -1159,10 +1160,16 @@ pub(crate) fn expand_logical_errors( } pub(crate) fn scale_error_norm(input: ErrorNorm, scale: &BigDecimal) -> ErrorNorm { - ErrorNorm { - plaintext_norm: input.plaintext_norm * scale, - matrix_norm: input.matrix_norm * scale, - } + let scale_is_zero = scale.is_zero(); + let pubkey_deps = + if scale_is_zero { DependencySet::empty() } else { input.pubkey_deps.clone() }; + let is_pubkey_random = if scale_is_zero { false } else { input.is_pubkey_random }; + ErrorNorm::from_parts( + input.plaintext_norm * scale, + input.matrix_norm * scale, + pubkey_deps, + is_pubkey_random, + ) } pub(crate) fn assert_same_matrix_shape(lhs: &PolyMatrixNorm, rhs: &PolyMatrixNorm, message: &str) { diff --git a/src/matrix/dcrt_poly.rs b/src/matrix/dcrt_poly.rs index 0d915107..90cfe7cd 100644 --- a/src/matrix/dcrt_poly.rs +++ b/src/matrix/dcrt_poly.rs @@ -9,7 +9,8 @@ use crate::{ utils::block_size, }; use itertools::Itertools; -use num_traits::ToPrimitive; +use num_bigint::BigUint; +use num_traits::{ToPrimitive, Zero}; use openfhe::ffi::{DCRTPolyGadgetVector, MatrixGen, SetMatrixElement}; use rayon::prelude::*; use std::{io::Read, ops::Range, path::Path}; @@ -175,7 +176,7 @@ impl PolyMatrix for DCRTPolyMatrix { .map(|i| { parallel_iter!(0..ncol) .map(|j| { - self.dcrt_decompose_poly(&entries[i][j], base_bits) + self.dcrt_small_decompose_poly_unsigned(&entries[i][j], base_bits) .into_iter() .take(k) .collect::>() @@ -450,10 +451,120 @@ impl DCRTPolyMatrix { DCRTPolyMatrix::from_cpp_matrix_ptr(params, &CppMatrix::new(g_vec_cpp)) } + fn mod_inverse_u64(value: u64, modulus: u64) -> u64 { + let mut t = 0i128; + let mut new_t = 1i128; + let mut r = modulus as i128; + let mut new_r = value as i128; + while new_r != 0 { + let quotient = r / new_r; + let old_t = t; + t = new_t; + new_t = old_t - quotient * new_t; + let old_r = r; + r = new_r; + new_r = old_r - quotient * new_r; + } + assert_eq!(r, 1, "CRT modulus component must be invertible"); + if t < 0 { + t += modulus as i128; + } + t as u64 + } + + fn crt_reconstruct_residues(moduli: &[u64], modulus: &BigUint, residues: &[u64]) -> BigUint { + debug_assert_eq!(moduli.len(), residues.len()); + let mut acc = BigUint::zero(); + for (&tower_modulus, &residue) in moduli.iter().zip(residues) { + if residue == 0 { + continue; + } + let tower_modulus_big = BigUint::from(tower_modulus); + let partial_modulus = modulus / &tower_modulus_big; + let partial_modulus_mod_tower = (&partial_modulus % tower_modulus) + .to_u64() + .expect("partial CRT modulus residue must fit in u64"); + let inv = Self::mod_inverse_u64(partial_modulus_mod_tower, tower_modulus); + acc += BigUint::from(residue) * &partial_modulus * BigUint::from(inv); + } + acc % modulus + } + + fn centered_lift_residue(residue: u64, modulus: u64) -> i128 { + let residue = residue as i128; + let modulus = modulus as i128; + if residue * 2 > modulus { residue - modulus } else { residue } + } + + fn balanced_digit_step(value: i128, base: i128) -> (i128, i128) { + let floor_quotient = value.div_euclid(base); + let remainder = value.rem_euclid(base); + let half = base / 2; + if remainder < half { + (remainder, floor_quotient) + } else if remainder > half { + (remainder - base, floor_quotient + 1) + } else if floor_quotient % 2 == 0 { + (half, floor_quotient) + } else { + (half - base, floor_quotient + 1) + } + } + + fn signed_digit_to_residue(digit: i128, modulus: u64) -> u64 { + let modulus_i = modulus as i128; + let residue = digit.rem_euclid(modulus_i); + residue as u64 + } + fn dcrt_decompose_poly(&self, poly: &DCRTPoly, base_bits: u32) -> Vec { - // Use OpenFHE's native decomposition for consistency with its gadget vector, - // but clamp each digit to its valid bit-width, especially for the most-significant - // digit when base_bits does not divide crt_bits. + let (moduli, _, crt_depth) = self.params.to_crt(); + debug_assert_eq!(moduli.len(), crt_depth); + let digits_per_tower = self.params.crt_bits().div_ceil(base_bits as usize); + let log_base_q = digits_per_tower * crt_depth; + let base = 1i128.checked_shl(base_bits).expect("base_bits must fit in i128 shift"); + let modulus = self.params.modulus(); + let coeffs = poly.coeffs(); + let per_digit_coeffs = parallel_iter!(0..log_base_q) + .map(|digit_idx| { + let tower_idx = digit_idx / digits_per_tower; + let local_digit_idx = digit_idx % digits_per_tower; + coeffs + .iter() + .map(|coeff| { + let residue = (coeff.value() % moduli[tower_idx]) + .to_u64() + .expect("CRT tower residue must fit in u64"); + let mut value = Self::centered_lift_residue(residue, moduli[tower_idx]); + let mut digit = 0i128; + for idx in 0..=local_digit_idx { + let (current_digit, next) = Self::balanced_digit_step(value, base); + if idx == local_digit_idx { + digit = current_digit; + } + value = next; + } + if local_digit_idx + 1 == digits_per_tower { + debug_assert_eq!(value, 0, "balanced decomposition carry must vanish"); + } + let residues = moduli + .iter() + .map(|&tower_modulus| { + Self::signed_digit_to_residue(digit, tower_modulus) + }) + .collect::>(); + Self::crt_reconstruct_residues(&moduli, &modulus, &residues).to_u64_digits() + }) + .collect::>() + }) + .collect::>(); + per_digit_coeffs + .iter() + .map(|coeff_values| DCRTPoly::poly_gen_from_vec(&self.params, coeff_values)) + .collect() + } + + fn dcrt_small_decompose_poly_unsigned(&self, poly: &DCRTPoly, base_bits: u32) -> Vec { let decomposed = poly.get_poly().Decompose(base_bits); let cpp_decomposed = CppMatrix::new(decomposed); let decompose_last_mask = match self.params.decompose_last_mask() { @@ -464,30 +575,21 @@ impl DCRTPolyMatrix { .collect(); } }; + let decompose_last_mask = BigUint::from(decompose_last_mask); let digits_per_tower = self.params.crt_bits().div_ceil(base_bits as usize); let last_digit_in_tower = digits_per_tower - 1; parallel_iter!(0..cpp_decomposed.ncol()) .map(|idx| { let digit_poly = cpp_decomposed.entry(0, idx); - // Determine effective bit-width for this digit within its CRT tower if idx % digits_per_tower != last_digit_in_tower { return digit_poly; } - - // Clamp the coefficients of this digit to the mask (u64 assumed) - let masked_values: Vec> = digit_poly + let coeffs = digit_poly .coeffs() - .into_par_iter() - .map(|c| { - let value = c - .value() - .to_u64() - .expect("coefficient must fit in u64 for masked decomposition"); - vec![value & decompose_last_mask] - }) - .collect(); - - DCRTPoly::poly_gen_from_vec(&self.params, &masked_values) + .into_iter() + .map(|elem| (elem.value() & &decompose_last_mask).to_u64_digits()) + .collect::>(); + DCRTPoly::poly_gen_from_vec(&self.params, &coeffs) }) .collect() } @@ -600,6 +702,62 @@ mod tests { assert_eq!(matrix, expected_matrix); } + fn first_coeff_tower_residue( + params: &DCRTPolyParams, + poly: &DCRTPoly, + tower_idx: usize, + ) -> u64 { + let (moduli, _, _) = params.to_crt(); + (poly.coeffs()[0].value() % moduli[tower_idx]) + .to_u64() + .expect("tower residue must fit in u64") + } + + #[test] + fn test_matrix_decompose_balanced_odd_for_centered_inputs() { + let params = DCRTPolyParams::new(4, 2, 17, 3); + let modulus = params.modulus(); + let x = DCRTPolyMatrix::from_poly_vec( + ¶ms, + vec![vec![DCRTPoly::from_usize_to_constant(¶ms, 12)]], + ); + let minus_x = DCRTPolyMatrix::from_poly_vec( + ¶ms, + vec![vec![DCRTPoly::from_biguint_to_constant( + ¶ms, + modulus.as_ref() - BigUint::from(12u32), + )]], + ); + + assert_eq!(minus_x.decompose(), -x.decompose()); + } + + #[test] + fn test_matrix_decompose_balanced_round_to_even_tie_digits() { + let params = DCRTPolyParams::new(4, 2, 17, 3); + let (moduli, _, _) = params.to_crt(); + let digits_per_tower = params.crt_bits().div_ceil(params.base_bits() as usize); + let x = DCRTPolyMatrix::from_poly_vec( + ¶ms, + vec![vec![DCRTPoly::from_usize_to_constant(¶ms, 12)]], + ); + let decomposed = x.decompose(); + + // 12 = (-4) + 8 * 2 because the first quotient is odd at the B/2 tie. + let first_digit = decomposed.entry(0, 0); + let second_digit = decomposed.entry(1, 0); + assert_eq!(first_coeff_tower_residue(¶ms, &first_digit, 0), moduli[0] - 4); + assert_eq!(first_coeff_tower_residue(¶ms, &first_digit, 1), moduli[1] - 4); + assert_eq!(first_coeff_tower_residue(¶ms, &second_digit, 0), 2); + assert_eq!(first_coeff_tower_residue(¶ms, &second_digit, 1), 2); + + // The same constant is represented independently in each CRT tower. + let second_tower_first_digit = decomposed.entry(digits_per_tower, 0); + let second_tower_second_digit = decomposed.entry(digits_per_tower + 1, 0); + assert_eq!(first_coeff_tower_residue(¶ms, &second_tower_first_digit, 1), moduli[1] - 4); + assert_eq!(first_coeff_tower_residue(¶ms, &second_tower_second_digit, 1), 2); + } + #[test] fn test_matrix_decompose_chunk_matches_full_decompose() { let params = DCRTPolyParams::new(4, 2, 17, 3); diff --git a/src/matrix/gpu_dcrt_poly.rs b/src/matrix/gpu_dcrt_poly.rs index b4ff5b25..ec414843 100644 --- a/src/matrix/gpu_dcrt_poly.rs +++ b/src/matrix/gpu_dcrt_poly.rs @@ -938,6 +938,10 @@ impl PolyMatrix for GpuDCRTPolyMatrix { GpuDCRTPolyMatrix::add_in_place(self, rhs); } + fn sub_in_place(&mut self, rhs: &Self) { + GpuDCRTPolyMatrix::sub_in_place(self, rhs); + } + fn copy_block_from( &mut self, src: &Self, @@ -2191,6 +2195,89 @@ mod tests { assert_eq!(rebuilt, full); } + fn first_cpu_coeff_tower_residue( + params: &DCRTPolyParams, + poly: &DCRTPoly, + tower_idx: usize, + ) -> u64 { + let (moduli, _, _) = params.to_crt(); + (poly.coeffs()[0].value() % moduli[tower_idx]) + .to_u64() + .expect("tower residue must fit in u64") + } + + #[test] + #[sequential] + fn test_gpu_matrix_decompose_matches_cpu_balanced_digits() { + gpu_device_sync(); + let params = DCRTPolyParams::new(128, 2, 17, 3); + let gpu_params = gpu_params_from_cpu(¶ms); + let gpu_matrix = GpuDCRTPolyMatrix::from_poly_vec( + &gpu_params, + vec![vec![GpuDCRTPoly::from_usize_to_constant(&gpu_params, 12)]], + ); + let cpu_matrix = gpu_matrix.to_cpu_matrix(); + + assert_eq!(gpu_matrix.decompose().to_cpu_matrix(), cpu_matrix.decompose()); + } + + #[test] + #[sequential] + fn test_gpu_matrix_decompose_balanced_odd_for_centered_inputs() { + gpu_device_sync(); + let params = DCRTPolyParams::new(128, 2, 17, 3); + let gpu_params = gpu_params_from_cpu(¶ms); + let modulus = gpu_params.modulus(); + let x = GpuDCRTPolyMatrix::from_poly_vec( + &gpu_params, + vec![vec![GpuDCRTPoly::from_usize_to_constant(&gpu_params, 12)]], + ); + let minus_x = GpuDCRTPolyMatrix::from_poly_vec( + &gpu_params, + vec![vec![GpuDCRTPoly::from_biguint_to_constant( + &gpu_params, + modulus.as_ref() - BigUint::from(12u32), + )]], + ); + + assert_eq!(minus_x.decompose(), -x.decompose()); + } + + #[test] + #[sequential] + fn test_gpu_matrix_decompose_balanced_round_to_even_tie_digits() { + gpu_device_sync(); + let params = DCRTPolyParams::new(128, 2, 17, 3); + let gpu_params = gpu_params_from_cpu(¶ms); + let (moduli, _, _) = params.to_crt(); + let digits_per_tower = params.crt_bits().div_ceil(params.base_bits() as usize); + let x = GpuDCRTPolyMatrix::from_poly_vec( + &gpu_params, + vec![vec![GpuDCRTPoly::from_usize_to_constant(&gpu_params, 12)]], + ); + let decomposed_cpu = x.decompose().to_cpu_matrix(); + + let first_digit = decomposed_cpu.entry(0, 0); + let second_digit = decomposed_cpu.entry(1, 0); + assert_eq!(first_cpu_coeff_tower_residue(¶ms, &first_digit, 0), moduli[0] - 4); + assert_eq!(first_cpu_coeff_tower_residue(¶ms, &first_digit, 1), moduli[1] - 4); + assert_eq!(first_cpu_coeff_tower_residue(¶ms, &second_digit, 0), 2); + assert_eq!(first_cpu_coeff_tower_residue(¶ms, &second_digit, 1), 2); + + let second_tower_first_digit = decomposed_cpu.entry(digits_per_tower, 0); + let second_tower_second_digit = decomposed_cpu.entry(digits_per_tower + 1, 0); + assert_eq!( + first_cpu_coeff_tower_residue(¶ms, &second_tower_first_digit, 0), + moduli[0] - 4 + ); + assert_eq!( + first_cpu_coeff_tower_residue(¶ms, &second_tower_first_digit, 1), + moduli[1] - 4 + ); + assert_eq!(first_cpu_coeff_tower_residue(¶ms, &second_tower_second_digit, 0), 2); + assert_eq!(first_cpu_coeff_tower_residue(¶ms, &second_tower_second_digit, 1), 2); + } + #[test] #[sequential] fn test_gpu_matrix_small_decompose_chunk_matches_full_small_decompose() { diff --git a/src/matrix/mod.rs b/src/matrix/mod.rs index 5efc5c29..e05ff138 100644 --- a/src/matrix/mod.rs +++ b/src/matrix/mod.rs @@ -66,6 +66,10 @@ pub trait PolyMatrix: *self = self.clone() + rhs; } + fn sub_in_place(&mut self, rhs: &Self) { + *self = self.clone() - rhs; + } + fn copy_block_from( &mut self, src: &Self, diff --git a/src/noise_refresh/simulation.rs b/src/noise_refresh/simulation.rs index 00145dab..c955127c 100644 --- a/src/noise_refresh/simulation.rs +++ b/src/noise_refresh/simulation.rs @@ -18,11 +18,15 @@ use crate::{ naive_vec::build_noise_refresh_material_circuit, }, poly::{PolyParams, dcrt::poly::DCRTPoly}, - simulator::{SimulatorContext, error_norm::ErrorNorm, poly_matrix_norm::PolyMatrixNorm}, + simulator::{ + SimulatorContext, dependency_set::DependencySet, error_norm::ErrorNorm, + poly_matrix_norm::PolyMatrixNorm, + }, slot_transfer::SlotTransferEvaluator, }; use bigdecimal::BigDecimal; use num_bigint::BigUint; +use num_traits::Zero; use rayon::prelude::*; use std::sync::Arc; use tracing::{debug, info}; @@ -47,7 +51,7 @@ pub fn max_safe_noise_refresh_v_bits( let q_max = q_moduli.iter().copied().max().expect("CRT modulus list must be nonempty"); let full_q: Arc = params.modulus().into(); let available = decode_threshold(full_q.as_ref(), &DecodeThreshold::new(q_max)) - - &pre_rounding_error.poly_norm.norm; + pre_rounding_error.maximum_coefficient_bound(); max_centered_mask_bits_for_available_range(&available, params.modulus_bits()) } @@ -322,7 +326,7 @@ where candidate, valid, ?max_valid_bits, - worst_pre_rounding_norm = %worst_pre_rounding.poly_norm.norm, + worst_pre_rounding_norm = %worst_pre_rounding.poly_norm.sigma, "noise-refresh candidate evaluated" ); if valid { @@ -469,7 +473,7 @@ where let collapsed_column = collapsed_column + &material_decryption_error; refresh_nrow = Some(collapsed_column.nrow); refresh_ncol += collapsed_column.ncol; - refresh_entry_norm += collapsed_column.poly_norm.norm; + refresh_entry_norm += collapsed_column.poly_norm.sigma; } let refresh_term = PolyMatrixNorm::new( ctx.clone(), @@ -689,7 +693,7 @@ where for _ in 0..log_base_q { refresh_nrow = Some(collapsed_column.nrow); refresh_ncol += collapsed_column.ncol; - refresh_norm += collapsed_column.poly_norm.norm.clone(); + refresh_norm += collapsed_column.poly_norm.sigma.clone(); } let refresh_term = PolyMatrixNorm::new( ctx.clone(), @@ -719,8 +723,8 @@ fn worst_pre_rounding_error(simulation: &NoiseRefreshErrorSimulation) -> &PolyMa .iter() .max_by(|lhs, rhs| { lhs.poly_norm - .norm - .partial_cmp(&rhs.poly_norm.norm) + .sigma + .partial_cmp(&rhs.poly_norm.sigma) .expect("noise-refresh pre-rounding norms must be comparable") }) .expect("simulation must produce at least one pre-round output") @@ -758,10 +762,16 @@ fn extrapolate_matrix_norms_to_active_levels(norms: &mut [PolyMatrixNorm], activ } fn scale_error_norm(input: ErrorNorm, scale: &BigDecimal) -> ErrorNorm { - ErrorNorm { - plaintext_norm: input.plaintext_norm * scale, - matrix_norm: input.matrix_norm * scale, - } + let scale_is_zero = scale.is_zero(); + let pubkey_deps = + if scale_is_zero { DependencySet::empty() } else { input.pubkey_deps.clone() }; + let is_pubkey_random = if scale_is_zero { false } else { input.is_pubkey_random }; + ErrorNorm::from_parts( + input.plaintext_norm * scale, + input.matrix_norm * scale, + pubkey_deps, + is_pubkey_random, + ) } struct SymmetricNoiseRefreshPrgErrorPrefix @@ -951,8 +961,8 @@ where .max_by(|lhs, rhs| { lhs.matrix_norm .poly_norm - .norm - .partial_cmp(&rhs.matrix_norm.poly_norm.norm) + .sigma + .partial_cmp(&rhs.matrix_norm.poly_norm.sigma) .expect("representative material norms must be comparable") }) .expect("representative material circuit must produce output wires") @@ -1060,14 +1070,14 @@ where output_ctx.clone(), error_norm.nrow, error_norm.ncol, - error_norm.poly_norm.norm, + error_norm.poly_norm.sigma, error_norm.zero_rows, ), PolyMatrixNorm::new( output_ctx, mask_norm.nrow, mask_norm.ncol, - mask_norm.poly_norm.norm, + mask_norm.poly_norm.sigma, mask_norm.zero_rows, ), ) @@ -1186,8 +1196,8 @@ where .material_error_decryption_error .ncol .max(prg_prefix.material_mask_decryption_error.ncol), - prg_prefix.material_error_decryption_error.poly_norm.norm.clone() * coefficient_scale + - prg_prefix.material_mask_decryption_error.poly_norm.norm.clone() * mask_scale, + prg_prefix.material_error_decryption_error.poly_norm.sigma.clone() * coefficient_scale + + prg_prefix.material_mask_decryption_error.poly_norm.sigma.clone() * mask_scale, None, ) } @@ -1227,7 +1237,7 @@ where output_ctx, error_norm.nrow, error_norm.ncol, - error_norm.poly_norm.norm, + error_norm.poly_norm.sigma, error_norm.zero_rows, ); // One representative CBD ciphertext bounds both CBD error ciphertexts and uniform mask @@ -1250,14 +1260,12 @@ mod tests { }; #[test] - fn test_max_safe_noise_refresh_v_bits_accepts_exact_power_of_two_available_range() { + fn test_max_safe_noise_refresh_v_bits_reserves_maximum_coefficient_bound() { let params = DCRTPolyParams::new(2, 2, 20, 5); let (q_moduli, _crt_bits, _crt_depth) = params.to_crt(); let q_max = q_moduli.iter().copied().max().expect("CRT moduli must be nonempty"); let full_q: Arc = params.modulus().into(); - let exact_available = BigDecimal::from(128u32); - let pre_rounding_error = - decode_threshold(full_q.as_ref(), &DecodeThreshold::new(q_max)) - exact_available; + let threshold = decode_threshold(full_q.as_ref(), &DecodeThreshold::new(q_max)); let ctx = Arc::new(SimulatorContext::new( BigDecimal::from(1u32), BigDecimal::from(2u32), @@ -1265,9 +1273,12 @@ mod tests { 1, 1, )); - let pre_rounding = PolyMatrixNorm::new(ctx, 1, 1, pre_rounding_error, None); + let pre_rounding = PolyMatrixNorm::new(ctx, 1, 1, BigDecimal::from(2u32), None); + let expected_available = threshold - pre_rounding.maximum_coefficient_bound(); + let expected = + max_centered_mask_bits_for_available_range(&expected_available, params.modulus_bits()); - assert_eq!(max_safe_noise_refresh_v_bits(¶ms, &pre_rounding), Some(8)); + assert_eq!(max_safe_noise_refresh_v_bits(¶ms, &pre_rounding), expected); } #[test] diff --git a/src/sampler/trapdoor/gpu.rs b/src/sampler/trapdoor/gpu.rs index 13032131..870e82c8 100644 --- a/src/sampler/trapdoor/gpu.rs +++ b/src/sampler/trapdoor/gpu.rs @@ -273,29 +273,54 @@ impl PolyTrapdoorSampler for GpuDCRTPolyTrapdoorSampler { ); let perturb_start = Instant::now(); - let perturbed_syndrome = { - let p1_rows = p1.row_size(); - let p2_rows = p2.row_size(); - debug_assert_eq!( - public_matrix.col_size(), - p1_rows + p2_rows, - "public matrix columns must match perturbation rows", - ); - let public_left = public_matrix.slice(0, d, 0, p1_rows); + let p1_rows = p1.row_size(); + let p2_rows = p2.row_size(); + debug_assert_eq!( + public_matrix.col_size(), + p1_rows + p2_rows, + "public matrix columns must match perturbation rows", + ); + let mut perturbed_syndrome = target.clone(); + { let public_right = public_matrix.slice(0, d, p1_rows, p1_rows + p2_rows); - let p_hat_image = (&public_left * &p1) + (&public_right * &p2); - let p_hat_image = if p_hat_image.col_size() == target_cols { - p_hat_image + let right_image = &public_right * &p2; + let right_image = if right_image.col_size() == target_cols { + right_image } else { - p_hat_image.slice_columns(0, target_cols) + right_image.slice_columns(0, target_cols) }; - target - &p_hat_image - }; + perturbed_syndrome.sub_in_place(&right_image); + } + { + let public_left = public_matrix.slice(0, d, 0, p1_rows); + let left_image = &public_left * &p1; + let left_image = if left_image.col_size() == target_cols { + left_image + } else { + left_image.slice_columns(0, target_cols) + }; + perturbed_syndrome.sub_in_place(&left_image); + } tracing::debug!( elapsed_ms = perturb_start.elapsed().as_secs_f64() * 1_000.0, "gpu preimage: computed perturbed_syndrome" ); + let assemble_start = Instant::now(); + let p1_level = p1.level(); + let p1_is_ntt = p1.is_ntt(); + let mut out = GpuDCRTPolyMatrix::new_empty_with_state( + params, + p1_rows + p2_rows, + target_cols, + p1_level, + p1_is_ntt, + ); + out.copy_block_from(&p1, 0, 0, 0, 0, p1_rows, target_cols); + out.copy_block_from(&p2, p1_rows, 0, 0, 0, p2_rows, target_cols); + drop(p1); + drop(p2); + let gauss_start = Instant::now(); let z_hat_mat = perturbed_syndrome.gauss_samp_gq_arb_base(self.c, self.sigma, random_gpu_rng_seed()); @@ -310,19 +335,6 @@ impl PolyTrapdoorSampler for GpuDCRTPolyTrapdoorSampler { elapsed_ms = r_mul_start.elapsed().as_secs_f64() * 1_000.0, "gpu preimage: computed r * z_hat" ); - let assemble_start = Instant::now(); - let mut out = GpuDCRTPolyMatrix::new_empty_with_state( - params, - p1.row_size() + p2.row_size(), - target_cols, - p1.level(), - p1.is_ntt(), - ); - out.copy_block_from(&p1, 0, 0, 0, 0, p1.row_size(), target_cols); - out.copy_block_from(&p2, p1.row_size(), 0, 0, 0, p2.row_size(), target_cols); - drop(p1); - drop(p2); - out.add_block_from(&r_z_hat, 0, 0, 0, 0, r_z_hat.row_size(), target_cols); tracing::debug!( elapsed_ms = assemble_start.elapsed().as_secs_f64() * 1_000.0, @@ -487,7 +499,9 @@ mod tests { params::DCRTPolyParams, }, }, - simulator::error_norm::compute_preimage_norm, + simulator::{ + error_norm::compute_preimage_sigma, poly_norm::maximum_coefficient_bound_from_sigma, + }, }; use bigdecimal::{BigDecimal, FromPrimitive}; use num_bigint::{BigInt, BigUint}; @@ -705,8 +719,8 @@ mod tests { .expect("ring dimension sqrt should exist"); let base = BigDecimal::from_biguint(BigUint::from(1u32) << params.base_bits(), 0); let m_g = (size * params.modulus_digits()) as u64; - let preimage_norm_bound = - compute_preimage_norm(&ring_dim_sqrt, m_g, &base, None, bound_sigma); + let preimage_sigma = compute_preimage_sigma(&ring_dim_sqrt, m_g, &base, None, bound_sigma); + let preimage_bound = maximum_coefficient_bound_from_sigma(&preimage_sigma); let modulus = params.modulus(); for sample_idx in 0..4usize { @@ -723,14 +737,14 @@ mod tests { let centered_abs = if value < neg { value } else { neg }; let centered_bd = BigDecimal::from(BigInt::from(centered_abs.clone())); assert!( - centered_bd < preimage_norm_bound, - "preimage coeff exceeds compute_preimage_norm bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", + centered_bd < preimage_bound, + "preimage coeff exceeds preimage maximum coefficient bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", sample_idx, i, j, k, centered_abs, - preimage_norm_bound + preimage_bound ); } } @@ -740,20 +754,20 @@ mod tests { #[test] #[sequential] - fn test_gpu_preimage_coefficients_below_compute_preimage_norm() { + fn test_gpu_preimage_coefficients_below_compute_preimage_sigma() { assert_gpu_preimage_reconstructs_target_and_respects_norm_bound(SIGMA, None); } #[test] #[sequential] - fn test_gpu_preimage_coefficients_below_compute_preimage_norm_non_default_sigma() { + fn test_gpu_preimage_coefficients_below_compute_preimage_sigma_non_default_sigma() { let sigma = SIGMA * 1.25; assert_gpu_preimage_reconstructs_target_and_respects_norm_bound(sigma, Some(sigma)); } #[test] #[sequential] - fn test_gpu_p_hat_coefficients_below_compute_preimage_norm() { + fn test_gpu_p_hat_coefficients_below_compute_preimage_sigma() { gpu_device_sync(); let size = 2usize; let cpu_params = DCRTPolyParams::new(1 << 10, 5, 51, 17); @@ -767,7 +781,8 @@ mod tests { .expect("ring dimension sqrt should exist"); let base = BigDecimal::from_biguint(BigUint::from(1u32) << params.base_bits(), 0); let m_g = (size * params.modulus_digits()) as u64; - let preimage_norm_bound = compute_preimage_norm(&ring_dim_sqrt, m_g, &base, None, None); + let preimage_sigma = compute_preimage_sigma(&ring_dim_sqrt, m_g, &base, None, None); + let preimage_bound = maximum_coefficient_bound_from_sigma(&preimage_sigma); let modulus = params.modulus(); let n = params.ring_dimension() as usize; let k = params.modulus_digits(); @@ -795,14 +810,14 @@ mod tests { let centered_abs = if value < neg { value } else { neg }; let centered_bd = BigDecimal::from(BigInt::from(centered_abs.clone())); assert!( - centered_bd < preimage_norm_bound, - "p_hat coeff exceeds compute_preimage_norm bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", + centered_bd < preimage_bound, + "p_hat coeff exceeds preimage maximum coefficient bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", sample_idx, i, j, coeff_idx, centered_abs, - preimage_norm_bound + preimage_bound ); } } @@ -831,7 +846,8 @@ mod tests { .expect("ring dimension sqrt should exist"); let base = BigDecimal::from_biguint(BigUint::from(1u32) << base_params.base_bits(), 0); let m_g = (size * base_params.modulus_digits()) as u64; - let preimage_norm_bound = compute_preimage_norm(&ring_dim_sqrt, m_g, &base, None, None); + let preimage_sigma = compute_preimage_sigma(&ring_dim_sqrt, m_g, &base, None, None); + let preimage_bound = maximum_coefficient_bound_from_sigma(&preimage_sigma); let modulus = base_params.modulus(); struct DeviceCase { @@ -893,15 +909,15 @@ mod tests { let centered_abs = if value < neg { value } else { neg }; let centered_bd = BigDecimal::from(BigInt::from(centered_abs.clone())); assert!( - centered_bd < preimage_norm_bound, - "restored preimage coeff exceeds compute_preimage_norm bound (src_device={}, dst_device={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={})", + centered_bd < preimage_bound, + "restored preimage coeff exceeds preimage maximum coefficient bound (src_device={}, dst_device={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={})", case.src_device, case.dst_device, i, j, coeff_idx, centered_abs, - preimage_norm_bound + preimage_bound ); } } diff --git a/src/sampler/trapdoor/sampler.rs b/src/sampler/trapdoor/sampler.rs index 01a06ff9..56269c6f 100644 --- a/src/sampler/trapdoor/sampler.rs +++ b/src/sampler/trapdoor/sampler.rs @@ -282,7 +282,9 @@ mod test { element::PolyElem, poly::PolyParams, sampler::{PolyUniformSampler, uniform::DCRTPolyUniformSampler}, - simulator::error_norm::compute_preimage_norm, + simulator::{ + error_norm::compute_preimage_sigma, poly_norm::maximum_coefficient_bound_from_sigma, + }, }; use bigdecimal::{BigDecimal, FromPrimitive}; use num_bigint::{BigInt, BigUint}; @@ -639,8 +641,8 @@ mod test { .expect("ring dimension sqrt should exist"); let base = BigDecimal::from_biguint(BigUint::from(1u32) << params.base_bits(), 0); let m_g = (size * params.modulus_digits()) as u64; - let preimage_norm_bound = - compute_preimage_norm(&ring_dim_sqrt, m_g, &base, None, bound_sigma); + let preimage_sigma = compute_preimage_sigma(&ring_dim_sqrt, m_g, &base, None, bound_sigma); + let preimage_bound = maximum_coefficient_bound_from_sigma(&preimage_sigma); let modulus = params.modulus(); for sample_idx in 0..4usize { @@ -657,14 +659,14 @@ mod test { let centered_abs = if value < neg { value } else { neg }; let centered_bd = BigDecimal::from(BigInt::from(centered_abs.clone())); assert!( - centered_bd < preimage_norm_bound, - "preimage coeff exceeds compute_preimage_norm bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", + centered_bd < preimage_bound, + "preimage coeff exceeds preimage maximum coefficient bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", sample_idx, i, j, coeff_idx, centered_abs, - preimage_norm_bound + preimage_bound ); } } @@ -674,20 +676,20 @@ mod test { #[test] #[sequential_test::sequential] - fn test_preimage_coefficients_below_compute_preimage_norm() { + fn test_preimage_coefficients_below_compute_preimage_sigma() { assert_preimage_reconstructs_target_and_respects_norm_bound(SIGMA, None); } #[test] #[sequential_test::sequential] - fn test_preimage_coefficients_below_compute_preimage_norm_non_default_sigma() { + fn test_preimage_coefficients_below_compute_preimage_sigma_non_default_sigma() { let sigma = SIGMA * 1.25; assert_preimage_reconstructs_target_and_respects_norm_bound(sigma, Some(sigma)); } #[test] #[sequential_test::sequential] - fn test_p_hat_coefficients_below_compute_preimage_norm() { + fn test_p_hat_coefficients_below_compute_preimage_sigma() { let size = 2usize; let params = DCRTPolyParams::new(1 << 10, 5, 51, 17); let trapdoor_sampler = DCRTPolyTrapdoorSampler::new(¶ms, SIGMA); @@ -699,7 +701,8 @@ mod test { .expect("ring dimension sqrt should exist"); let base = BigDecimal::from_biguint(BigUint::from(1u32) << params.base_bits(), 0); let m_g = (size * params.modulus_digits()) as u64; - let preimage_norm_bound = compute_preimage_norm(&ring_dim_sqrt, m_g, &base, None, None); + let preimage_sigma = compute_preimage_sigma(&ring_dim_sqrt, m_g, &base, None, None); + let preimage_bound = maximum_coefficient_bound_from_sigma(&preimage_sigma); let modulus = params.modulus(); let n = params.ring_dimension() as usize; let k = params.modulus_digits(); @@ -742,14 +745,14 @@ mod test { let centered_abs = if value < neg { value } else { neg }; let centered_bd = BigDecimal::from(BigInt::from(centered_abs.clone())); assert!( - centered_bd < preimage_norm_bound, - "p_hat coeff exceeds compute_preimage_norm bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", + centered_bd < preimage_bound, + "p_hat coeff exceeds preimage maximum coefficient bound at sample={}, row={}, col={}, coeff_idx={}, centered_abs={}, bound={}", sample_idx, i, j, coeff_idx, centered_abs, - preimage_norm_bound + preimage_bound ); } } diff --git a/src/simulator/dependency_set.rs b/src/simulator/dependency_set.rs new file mode 100644 index 00000000..cf924593 --- /dev/null +++ b/src/simulator/dependency_set.rs @@ -0,0 +1,75 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SourceId(pub u64); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum DependencySet { + Known(Vec), + Unknown, +} + +impl DependencySet { + pub fn empty() -> Self { + DependencySet::Known(Vec::new()) + } + + pub fn singleton(source_id: SourceId) -> Self { + DependencySet::Known(vec![source_id]) + } + + pub fn is_disjoint(&self, other: &Self) -> bool { + let (DependencySet::Known(lhs), DependencySet::Known(rhs)) = (self, other) else { + return false; + }; + let mut i = 0usize; + let mut j = 0usize; + while i < lhs.len() && j < rhs.len() { + match lhs[i].cmp(&rhs[j]) { + std::cmp::Ordering::Less => i += 1, + std::cmp::Ordering::Greater => j += 1, + std::cmp::Ordering::Equal => return false, + } + } + true + } + + pub fn union(&self, other: &Self) -> Self { + let (DependencySet::Known(lhs), DependencySet::Known(rhs)) = (self, other) else { + return DependencySet::Unknown; + }; + let mut out = Vec::with_capacity(lhs.len() + rhs.len()); + let mut i = 0usize; + let mut j = 0usize; + while i < lhs.len() || j < rhs.len() { + let next = match (lhs.get(i), rhs.get(j)) { + (Some(l), Some(r)) => match l.cmp(r) { + std::cmp::Ordering::Less => { + i += 1; + *l + } + std::cmp::Ordering::Greater => { + j += 1; + *r + } + std::cmp::Ordering::Equal => { + i += 1; + j += 1; + *l + } + }, + (Some(l), None) => { + i += 1; + *l + } + (None, Some(r)) => { + j += 1; + *r + } + (None, None) => break, + }; + if out.last().copied() != Some(next) { + out.push(next); + } + } + DependencySet::Known(out) + } +} diff --git a/src/simulator/error_norm.rs b/src/simulator/error_norm.rs index a45243a2..6e43b300 100644 --- a/src/simulator/error_norm.rs +++ b/src/simulator/error_norm.rs @@ -2,33 +2,107 @@ use crate::{ circuit::Evaluable, impl_binop_with_refs, poly::dcrt::poly::DCRTPoly, - simulator::{SimulatorContext, poly_matrix_norm::PolyMatrixNorm, poly_norm::PolyNorm}, + simulator::{ + SimulatorContext, dependency_set::DependencySet, poly_matrix_norm::PolyMatrixNorm, + poly_norm::PolyNorm, + }, }; use bigdecimal::BigDecimal; use num_bigint::BigUint; +use num_traits::Zero; use std::{ ops::{Add, Mul, Sub}, sync::Arc, }; +use tracing::debug; pub use super::eval_error::{ AffinePltEvaluator, AffineSlotTransferEvaluator, NormBggPolyEncodingSTEvaluator, NormNaiveBggEncodingVecSTEvaluator, NormPltCommitEvaluator, NormPltGGH15Evaluator, - NormPltLWEEvaluator, compute_preimage_norm, + NormPltLWEEvaluator, compute_preimage_norm, compute_preimage_sigma, }; // Note: h_norm and plaintext_norm computed here can be larger than the modulus . // In such a case, the error after circuit evaluation could be too large. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct ErrorNorm { pub plaintext_norm: PolyNorm, pub matrix_norm: PolyMatrixNorm, + pub pubkey_deps: DependencySet, + pub is_pubkey_random: bool, } +impl PartialEq for ErrorNorm { + fn eq(&self, other: &Self) -> bool { + self.plaintext_norm == other.plaintext_norm && + self.matrix_norm == other.matrix_norm && + self.pubkey_deps == other.pubkey_deps && + self.is_pubkey_random == other.is_pubkey_random + } +} + +impl Eq for ErrorNorm {} + impl ErrorNorm { pub fn new(plaintext_norm: PolyNorm, matrix_norm: PolyMatrixNorm) -> Self { + Self::deterministic_pubkey(plaintext_norm, matrix_norm) + } + + pub fn fresh_input(plaintext_norm: PolyNorm, matrix_norm: PolyMatrixNorm) -> Self { + let ctx = plaintext_norm.ctx.clone(); + Self::from_parts( + plaintext_norm, + matrix_norm, + DependencySet::singleton(ctx.fresh_source_id()), + true, + ) + } + + pub fn from_parts( + plaintext_norm: PolyNorm, + matrix_norm: PolyMatrixNorm, + pubkey_deps: DependencySet, + is_pubkey_random: bool, + ) -> Self { debug_assert_eq!(plaintext_norm.ctx, matrix_norm.clone_ctx()); - Self { plaintext_norm: plaintext_norm.into_constant(), matrix_norm } + Self { + plaintext_norm: plaintext_norm.into_constant_poly(), + matrix_norm, + pubkey_deps, + is_pubkey_random, + } + } + + pub fn deterministic_pubkey(plaintext_norm: PolyNorm, matrix_norm: PolyMatrixNorm) -> Self { + Self::from_parts(plaintext_norm, matrix_norm, DependencySet::empty(), false) + } + + pub fn fresh_lut_output(plaintext_norm: PolyNorm, matrix_norm: PolyMatrixNorm) -> Self { + let ctx = plaintext_norm.ctx.clone(); + Self::from_parts( + plaintext_norm, + matrix_norm, + DependencySet::singleton(ctx.fresh_source_id()), + true, + ) + } + + pub fn rhs_pubkey_gadget_norm(&self, gadget_poly: PolyNorm) -> PolyMatrixNorm { + let ctx = gadget_poly.ctx.clone(); + debug!( + rule = "RHS_PUBKEY_GADGET", + rhs_is_pubkey_random = self.is_pubkey_random, + rhs_pubkey_deps = ?self.pubkey_deps, + "simulator BGG RHS public-key gadget norm" + ); + PolyMatrixNorm::from_parts( + ctx.m_g, + ctx.m_g, + gadget_poly, + None, + self.pubkey_deps.clone(), + self.is_pubkey_random, + ) } #[inline] @@ -44,30 +118,37 @@ impl ErrorNorm { impl_binop_with_refs!(ErrorNorm => Add::add(self, rhs: &ErrorNorm) -> ErrorNorm { debug_assert_eq!(self.ctx(), rhs.ctx()); - ErrorNorm { - plaintext_norm: &self.plaintext_norm + &rhs.plaintext_norm, - matrix_norm: &self.matrix_norm + &rhs.matrix_norm, - } + ErrorNorm::from_parts( + &self.plaintext_norm + &rhs.plaintext_norm, + &self.matrix_norm + &rhs.matrix_norm, + self.pubkey_deps.union(&rhs.pubkey_deps), + false, + ) }); // Note: norm of the subtraction result is bounded by a sum of the norms of the input matrices, // i.e., |A-B| < |A| + |B| impl_binop_with_refs!(ErrorNorm => Sub::sub(self, rhs: &ErrorNorm) -> ErrorNorm { debug_assert_eq!(self.ctx(), rhs.ctx()); - ErrorNorm { - plaintext_norm: &self.plaintext_norm + &rhs.plaintext_norm, - matrix_norm: &self.matrix_norm + &rhs.matrix_norm, - } + ErrorNorm::from_parts( + &self.plaintext_norm + &rhs.plaintext_norm, + &self.matrix_norm + &rhs.matrix_norm, + self.pubkey_deps.union(&rhs.pubkey_deps), + false, + ) }); impl_binop_with_refs!(ErrorNorm => Mul::mul(self, rhs: &ErrorNorm) -> ErrorNorm { debug_assert_eq!(self.ctx(), rhs.ctx()); - ErrorNorm { - plaintext_norm: &self.plaintext_norm * &rhs.plaintext_norm, - matrix_norm: &self.matrix_norm * - PolyMatrixNorm::gadget_decomposed(self.clone_ctx(), self.ctx().m_g) + - rhs.matrix_norm.clone() * &self.plaintext_norm, - } + let rhs_gadget = rhs.rhs_pubkey_gadget_norm( + PolyMatrixNorm::gadget_decomposed(self.clone_ctx(), self.ctx().m_g).poly_norm, + ); + ErrorNorm::from_parts( + &self.plaintext_norm * &rhs.plaintext_norm, + &self.matrix_norm * &rhs_gadget + rhs.matrix_norm.clone() * &self.plaintext_norm, + self.pubkey_deps.union(&rhs.pubkey_deps), + false, + ) }); impl Evaluable for ErrorNorm { @@ -85,21 +166,35 @@ impl Evaluable for ErrorNorm { fn small_scalar_mul(&self, _: &Self::Params, scalar: &[u32]) -> Self { let scalar_max = BigDecimal::from(*scalar.iter().max().unwrap()); - let scalar_poly = PolyNorm::constant(self.clone_ctx(), scalar_max); - ErrorNorm { - matrix_norm: self.matrix_norm.clone() * &scalar_poly, - plaintext_norm: self.plaintext_norm.clone() * &scalar_poly, - } + let scalar_poly = PolyNorm::constant(self.clone_ctx(), scalar_max.clone()); + let (pubkey_deps, is_pubkey_random) = if scalar_max.is_zero() { + (DependencySet::empty(), false) + } else { + (self.pubkey_deps.clone(), self.is_pubkey_random) + }; + ErrorNorm::from_parts( + self.plaintext_norm.clone() * &scalar_poly, + self.matrix_norm.clone() * &scalar_poly, + pubkey_deps, + is_pubkey_random, + ) } fn large_scalar_mul(&self, _: &Self::Params, scalar: &[BigUint]) -> Self { let scalar_max = scalar.iter().max().unwrap().clone(); let scalar_bd = BigDecimal::from(num_bigint::BigInt::from(scalar_max)); - let scalar_poly = PolyNorm::constant(self.clone_ctx(), scalar_bd); - ErrorNorm { - matrix_norm: self.matrix_norm.clone() * + let scalar_poly = PolyNorm::constant(self.clone_ctx(), scalar_bd.clone()); + let (pubkey_deps, is_pubkey_random) = if scalar_bd.is_zero() { + (DependencySet::empty(), false) + } else { + (self.pubkey_deps.clone(), false) + }; + ErrorNorm::from_parts( + self.plaintext_norm.clone() * &scalar_poly, + self.matrix_norm.clone() * PolyMatrixNorm::gadget_decomposed(self.clone_ctx(), self.ctx().m_g), - plaintext_norm: self.plaintext_norm.clone() * &scalar_poly, - } + pubkey_deps, + is_pubkey_random, + ) } } diff --git a/src/simulator/eval_error/engine.rs b/src/simulator/eval_error/engine.rs index 48a57d89..7210d0bb 100644 --- a/src/simulator/eval_error/engine.rs +++ b/src/simulator/eval_error/engine.rs @@ -11,8 +11,8 @@ impl PolyCircuit { /// /// The key optimization is that nested sub-circuits are not re-simulated from scratch for each /// call. Instead, `ErrorNormSubCircuitSummary` captures each sub-circuit once as an affine map - /// from input bounds to output bounds, and this function substitutes actual caller inputs into - /// that cached summary. + /// from input sigma profiles to output sigma profiles, and this function substitutes actual + /// caller inputs into that cached summary. pub(super) fn eval_max_error_norm( &self, one_error: ErrorNorm, @@ -314,7 +314,7 @@ impl PolyCircuit { suffix, input_idx, ); - self.clone_error_norm_matrix_norm_for_gate( + self.clone_error_norm_value_for_gate( input_id, &wires, &input_gate_positions, @@ -497,7 +497,7 @@ impl PolyCircuit { input_ids.as_ref(), input_idx, ); - self.clone_error_norm_matrix_norm_for_gate( + self.clone_error_norm_value_for_gate( input_id, &wires, &input_gate_positions, diff --git a/src/simulator/eval_error/evaluators.rs b/src/simulator/eval_error/evaluators.rs index c29ee1fd..7bcd7241 100644 --- a/src/simulator/eval_error/evaluators.rs +++ b/src/simulator/eval_error/evaluators.rs @@ -1,4 +1,5 @@ use super::*; +use crate::simulator::poly_norm::high_probability_envelope_from_sigma; pub struct NormBggPolyEncodingSTEvaluator { pub const_term: PolyMatrixNorm, @@ -20,17 +21,17 @@ impl NormBggPolyEncodingSTEvaluator { BigDecimal::from_f64(e_b0_sigma).expect("e_b0_sigma must be finite"), ); - let b0_preimage_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(1), None); + let b0_preimage_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(1), None); debug!( "{}", format!( - "BGG poly-encoding slot-transfer preimage norm bits {}", - bigdecimal_bits_ceil(&b0_preimage_norm) + "BGG poly-encoding slot-transfer preimage sigma bits {}", + bigdecimal_bits_ceil(&b0_preimage_sigma) ) ); - let matrix_norm_bits = |m: &PolyMatrixNorm| bigdecimal_bits_ceil(&m.poly_norm.norm); + let matrix_norm_bits = |m: &PolyMatrixNorm| bigdecimal_bits_ceil(&m.poly_norm.sigma); let log_matrix_norm_bits = |name: &str, m: &PolyMatrixNorm| { debug!( "NormBggPolyEncodingSTEvaluator::new {} matrix norm bits {}", @@ -49,16 +50,19 @@ impl NormBggPolyEncodingSTEvaluator { log_matrix_norm_bits("s_vec", &s_vec); // `c_b0 * gate_preimage` with `B0 * gate_preimage = target + error`. - let gate_preimage = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, b0_preimage_norm.clone(), None); + let gate_preimage = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_g, + b0_preimage_sigma.clone(), + None, + ); log_matrix_norm_bits("gate_preimage", &gate_preimage); - let gaussian_bound = gaussian_tail_bound_factor(); - let gate_target_error = PolyMatrixNorm::new( + let gate_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_g, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); log_matrix_norm_bits("gate_target_error", &gate_target_error); let gate_target_error_term = s_vec.clone() * &gate_target_error; @@ -69,29 +73,37 @@ impl NormBggPolyEncodingSTEvaluator { log_matrix_norm_bits("const_term", &const_term); // `((c_b0 * slot_preimage_b0) * slot_preimage_b1) * plaintext`. - let slot_preimage_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, 2 * ctx.m_b, b0_preimage_norm.clone(), None); + let slot_preimage_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + 2 * ctx.m_b, + b0_preimage_sigma.clone(), + None, + ); log_matrix_norm_bits("slot_preimage_b0", &slot_preimage_b0); - let b1_preimage_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(2), None); + let b1_preimage_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(2), None); // `preimage_b1` targets the `B1` basis, whose trapdoor size is `2 * secret_size`. - let slot_preimage_b1 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b * 2, ctx.m_g, b1_preimage_norm.clone(), None); + let slot_preimage_b1 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b * 2, + ctx.m_g, + b1_preimage_sigma.clone(), + None, + ); log_matrix_norm_bits("slot_preimage_b1", &slot_preimage_b1); - let slot_preimage_b0_target_error = PolyMatrixNorm::new( + let slot_preimage_b0_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_b * 2, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); log_matrix_norm_bits("slot_preimage_b0_target_error", &slot_preimage_b0_target_error); - let slot_preimage_b1_target_error = PolyMatrixNorm::new( + let slot_preimage_b1_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size * 2, ctx.m_g, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); log_matrix_norm_bits("slot_preimage_b1_target_error", &slot_preimage_b1_target_error); let slot_secret_and_identity = PolyMatrixNorm::new( @@ -149,7 +161,7 @@ impl SlotTransferEvaluator for NormBggPolyEncodingSTEvaluator { (input.matrix_norm.clone() * &self.input_vector_multiplier) * &scalar_bd; let transfer_plaintext_term = self.transfer_plaintext_multiplier.clone() * &plaintext_norm; let matrix_norm = &self.const_term + &input_vector_term + &transfer_plaintext_term; - ErrorNorm { matrix_norm, plaintext_norm } + ErrorNorm::from_parts(plaintext_norm, matrix_norm, input.pubkey_deps.clone(), false) } } @@ -171,7 +183,11 @@ impl AffineSlotTransferEvaluator for NormBggPolyEncodingSTEvaluator { .add_expr(&AffineErrorNormExpr::constant( &self.const_term + &(self.transfer_plaintext_multiplier.clone() * &plaintext_norm), )); - ErrorNormSummaryExpr { plaintext_norm, matrix_expr } + ErrorNormSummaryExpr::deterministic_pubkey( + plaintext_norm, + matrix_expr, + input.pubkey_summary.scale(&scalar_bd), + ) } } @@ -236,25 +252,26 @@ pub struct NormPltLWEEvaluator { impl NormPltLWEEvaluator { pub fn new(ctx: Arc, e_b_sigma: &BigDecimal) -> Self { - let k_high_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); - let k_high_norm_bits = bigdecimal_bits_ceil(&k_high_norm); + let k_high_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); + let k_high_norm_bits = + bigdecimal_bits_ceil(&high_probability_envelope_from_sigma(&k_high_sigma)); let k_low = PolyMatrixNorm::gadget_decomposed(ctx.clone(), ctx.m_g); - debug!("{}", format!("preimage norm bits {}", k_high_norm_bits)); + debug!("{}", format!("preimage sigma bits {}", k_high_norm_bits)); debug!( "LWE PLT k_low decomposition norm bits {}", - bigdecimal_bits_ceil(&k_low.poly_norm.norm) + bigdecimal_bits_ceil(&k_low.poly_norm.sigma) ); - let e_b_init = PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_b, e_b_sigma * 6, None); - let e_b_times_k_high = - &e_b_init * &PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, k_high_norm, None); + let e_b_init = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, ctx.m_b, e_b_sigma.clone()); + let e_b_times_k_high = &e_b_init * + &PolyMatrixNorm::fresh_preimage(ctx.clone(), ctx.m_b, ctx.m_g, k_high_sigma, None); debug!( "LWE PLT const term norm bits {}", - bigdecimal_bits_ceil(&e_b_times_k_high.poly_norm.norm) + bigdecimal_bits_ceil(&e_b_times_k_high.poly_norm.sigma) ); debug!( "LWE PLT e_input multiplier norm bits {}", - bigdecimal_bits_ceil(&k_low.poly_norm.norm) + bigdecimal_bits_ceil(&k_low.poly_norm.sigma) ); Self { e_b_times_k_high, k_low } } @@ -274,7 +291,7 @@ impl PltEvaluator for NormPltLWEEvaluator { let plaintext_bd = BigDecimal::from(num_bigint::BigInt::from(plt.max_output_row().1.value().clone())); let plaintext_norm = PolyNorm::constant(input.clone_ctx(), plaintext_bd); - ErrorNorm { matrix_norm, plaintext_norm } + ErrorNorm::fresh_lut_output(plaintext_norm, matrix_norm) } } @@ -293,7 +310,7 @@ impl AffinePltEvaluator for NormPltLWEEvaluator { .matrix_expr .transform_matrix(&self.k_low) .add_expr(&AffineErrorNormExpr::constant(self.e_b_times_k_high.clone())); - ErrorNormSummaryExpr { plaintext_norm, matrix_expr } + ErrorNormSummaryExpr::fresh_lut_output(plaintext_norm, matrix_expr) } } #[derive(Debug, Clone)] @@ -314,14 +331,12 @@ impl NormPltGGH15Evaluator { .ok() .map(|raw| matches!(raw.as_str(), "1" | "true" | "TRUE" | "yes" | "YES")) .unwrap_or(false); - let matrix_norm_bits = |m: &PolyMatrixNorm| bigdecimal_bits_ceil(&m.poly_norm.norm); - let gaussian_bound = gaussian_tail_bound_factor(); - - let preimage_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); - debug!("{}", format!("preimage norm bits {}", bigdecimal_bits_ceil(&preimage_norm))); - let e_b_init = - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_b, e_b_sigma * &gaussian_bound, None); + let matrix_norm_bits = |m: &PolyMatrixNorm| bigdecimal_bits_ceil(&m.poly_norm.sigma); + + let preimage_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); + debug!("{}", format!("preimage sigma bits {}", bigdecimal_bits_ceil(&preimage_sigma))); + let e_b_init = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, ctx.m_b, e_b_sigma.clone()); let s_vec = PolyMatrixNorm::new( ctx.clone(), 1, @@ -331,15 +346,19 @@ impl NormPltGGH15Evaluator { ); // Corresponds to `preimage_gate1` sampled in `sample_gate_preimages_batch` stage1 // from target `S_g * B1 + error` (B1 now has size d, so this is m_b x m_b). - let preimage_gate1_from_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_b, preimage_norm.clone(), None); + let preimage_gate1_from_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_b, + preimage_sigma.clone(), + None, + ); // Corresponds to stage1 Gaussian `error` in target `S_g * B1 + error`. - let stage1_target_error = PolyMatrixNorm::new( + let stage1_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_b, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); let gate1_from_eb = e_b_init.clone() * &preimage_gate1_from_b0; let gate1_from_s = s_vec.clone() * &stage1_target_error; @@ -354,52 +373,68 @@ impl NormPltGGH15Evaluator { // Corresponds to `v_idx` in `public_lookup`. let v_idx = PolyMatrixNorm::gadget_decomposed(ctx.clone(), ctx.m_g); // Corresponds to `preimage_gate2_identity` (B0 preimage for identity/out term). - let preimage_gate2_identity_from_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, preimage_norm.clone(), None); + let preimage_gate2_identity_from_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_g, + preimage_sigma.clone(), + None, + ); // Corresponds to `preimage_gate2_gy` (B0 preimage for gy term). - let preimage_gate2_gy_from_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, preimage_norm.clone(), None); + let preimage_gate2_gy_from_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_g, + preimage_sigma.clone(), + None, + ); // Corresponds to `preimage_gate2_v` (B0 preimage for v_idx term). - let preimage_gate2_v_from_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, preimage_norm.clone(), None); + let preimage_gate2_v_from_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_g, + preimage_sigma.clone(), + None, + ); // Corresponds to `preimage_gate2_vx` after the stage-5 refactor. - let preimage_gate2_vx_from_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, preimage_norm.clone(), None); + let preimage_gate2_vx_from_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_g, + preimage_sigma.clone(), + None, + ); // Corresponds to Gaussian `error` added in stage2 target // `S_g * w_block_identity + out_matrix + error`. - let stage2_identity_target_error = PolyMatrixNorm::new( + let stage2_identity_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_g, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); // Corresponds to Gaussian `error` added in stage3 target // `S_g * w_block_gy - gadget + error`. - let stage3_gy_target_error = PolyMatrixNorm::new( + let stage3_gy_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_g, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); // Corresponds to Gaussian `error` added in stage4 target // `S_g * w_block_v - (input_matrix * u_g_decomposed) + error`. - let stage4_v_target_error = PolyMatrixNorm::new( + let stage4_v_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_g, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); // Corresponds to Gaussian `error` added in stage5 target // `S_g * w_block_vx + u_g_matrix + error`. - let stage5_vx_target_error = PolyMatrixNorm::new( + let stage5_vx_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, ctx.m_g, - e_mat_sigma * &gaussian_bound, - None, + e_mat_sigma.clone(), ); let gate2_identity_from_eb = e_b_init.clone() * &preimage_gate2_identity_from_b0; @@ -434,8 +469,13 @@ impl NormPltGGH15Evaluator { // from a target that includes identity + gy + v + vx components, and // `public_lookup` subtracts `preimage_gate1 * preimage_lut` without additional // multipliers. - let preimage_lut_total = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, preimage_norm.clone(), None); + let preimage_lut_total = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + ctx.m_g, + preimage_sigma.clone(), + None, + ); // Corresponds to subtraction term // `c_b0 * (preimage_gate1 * preimage_lut)` in `public_lookup`. let const_term_lut_subtraction_total = gate1_error_total.clone() * preimage_lut_total; @@ -447,14 +487,14 @@ impl NormPltGGH15Evaluator { "{}", format!( "GGH15 PLT const term norm bits {}", - bigdecimal_bits_ceil(&const_term.poly_norm.norm) + bigdecimal_bits_ceil(&const_term.poly_norm.sigma) ) ); debug!( "{}", format!( "GGH15 PLT input-plaintext multiplier norm bits {}", - bigdecimal_bits_ceil(&input_plaintext_multiplier.poly_norm.norm) + bigdecimal_bits_ceil(&input_plaintext_multiplier.poly_norm.sigma) ) ); @@ -492,7 +532,7 @@ impl NormPltGGH15Evaluator { "{}", format!( "GGH15 PLT e_input multiplier norm bits {}", - bigdecimal_bits_ceil(&e_input_multiplier.poly_norm.norm) + bigdecimal_bits_ceil(&e_input_multiplier.poly_norm.sigma) ) ); @@ -516,7 +556,7 @@ impl PltEvaluator for NormPltGGH15Evaluator { let plaintext_term = self.input_plaintext_multiplier.clone() * &input.plaintext_norm; let matrix_norm = &self.const_term + &plaintext_term + &input.matrix_norm * &self.e_input_multiplier; - ErrorNorm { matrix_norm, plaintext_norm } + ErrorNorm::fresh_lut_output(plaintext_norm, matrix_norm) } } @@ -536,7 +576,7 @@ impl AffinePltEvaluator for NormPltGGH15Evaluator { .matrix_expr .transform_matrix(&self.e_input_multiplier) .add_expr(&AffineErrorNormExpr::constant(&self.const_term + &plaintext_term)); - ErrorNormSummaryExpr { plaintext_norm, matrix_expr } + ErrorNormSummaryExpr::fresh_lut_output(plaintext_norm, matrix_expr) } } @@ -558,13 +598,13 @@ impl NormPltCommitEvaluator { "NormPltCommitEvaluator padded_len={} lut_vector_len={}", padded_len, lut_vector_len ); - let preimage_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); - let t_bottom = PolyMatrixNorm::new( + let preimage_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); + let t_bottom = PolyMatrixNorm::fresh_preimage( ctx.clone(), ctx.m_b, tree_base * ctx.m_b * ctx.m_g * ctx.m_g, - preimage_norm.clone(), + preimage_sigma.clone(), None, ); let j_mat = PolyMatrixNorm::new( @@ -577,11 +617,11 @@ impl NormPltCommitEvaluator { let verifier_base = t_bottom * &j_mat; let verifier_norm = verifier_base * PolyMatrixNorm::gadget_decomposed_with_secret_size(ctx.clone(), ctx.m_b, ctx.m_b); - let t_top = PolyMatrixNorm::new( + let t_top = PolyMatrixNorm::fresh_preimage( ctx.clone(), tree_base * ctx.m_b * ctx.m_b * ctx.m_g, tree_base * ctx.m_b * ctx.m_g * ctx.m_g, - preimage_norm.clone(), + preimage_sigma.clone(), None, ); let t_top_j_mat = &t_top * &j_mat; @@ -618,13 +658,16 @@ impl NormPltCommitEvaluator { lhs + opening_base_last }; - let gaussian_bound = gaussian_tail_bound_factor(); - let init_error = - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_b, error_sigma * &gaussian_bound, None); - let preimage = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, verifier_norm.nrow, preimage_norm, None); + let init_error = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, ctx.m_b, error_sigma.clone()); + let preimage = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + verifier_norm.nrow, + preimage_sigma, + None, + ); let lut_term = &init_error * preimage * verifier_norm + init_error * opening_norm; - debug!("lut_term norm bits {}", bigdecimal_bits_ceil(&lut_term.poly_norm.norm)); + debug!("lut_term norm bits {}", bigdecimal_bits_ceil(&lut_term.poly_norm.sigma)); Self { lut_term } } } @@ -647,9 +690,9 @@ impl PltEvaluator for NormPltCommitEvaluator { let m_g = ctx.m_g; let matrix_norm = &self.lut_term + &input.matrix_norm * PolyMatrixNorm::gadget_decomposed(ctx, m_b); - // info!("matrix_norm norm bits {}", bigdecimal_bits_ceil(&matrix_norm.poly_norm.norm)); + // info!("matrix_norm norm bits {}", bigdecimal_bits_ceil(&matrix_norm.poly_norm.sigma)); let (matrix_norm, _) = matrix_norm.split_cols(m_g); - ErrorNorm { matrix_norm, plaintext_norm } + ErrorNorm::fresh_lut_output(plaintext_norm, matrix_norm) } } @@ -672,11 +715,11 @@ impl AffinePltEvaluator for NormPltCommitEvaluator { .transform_matrix(&PolyMatrixNorm::gadget_decomposed(ctx, m_b)) .add_expr(&AffineErrorNormExpr::constant(self.lut_term.clone())) .split_cols_left(m_g); - ErrorNormSummaryExpr { plaintext_norm, matrix_expr } + ErrorNormSummaryExpr::fresh_lut_output(plaintext_norm, matrix_expr) } } -pub fn compute_preimage_norm( +pub fn compute_preimage_sigma( ring_dim_sqrt: &BigDecimal, m_g: u64, base: &BigDecimal, @@ -692,9 +735,24 @@ pub fn compute_preimage_norm( let term = BigDecimal::from(b_nrow as u64).sqrt().unwrap() * ring_dim_sqrt.clone() * m_g_sqrt + two_sqrt * ring_dim_sqrt + c_1; - let preimage_norm = - c_0 * BigDecimal::from_f32(6.5).unwrap() * sigma.clone() * ((base + 1) * sigma) * term; - // let preimage_norm_bits = bigdecimal_bits_ceil(&preimage_norm); - // info!("{}", format!("preimage norm bits {}", preimage_norm_bits)); - preimage_norm + let preimage_sigma = c_0 * sigma.clone() * ((base + 1) * sigma) * term; + // let preimage_sigma_bits = bigdecimal_bits_ceil(&preimage_sigma); + // info!("{}", format!("preimage sigma bits {}", preimage_sigma_bits)); + preimage_sigma +} + +pub fn compute_preimage_norm( + ring_dim_sqrt: &BigDecimal, + m_g: u64, + base: &BigDecimal, + b_nrow: Option, + sigma: Option, +) -> BigDecimal { + high_probability_envelope_from_sigma(&compute_preimage_sigma( + ring_dim_sqrt, + m_g, + base, + b_nrow, + sigma, + )) } diff --git a/src/simulator/eval_error/gates.rs b/src/simulator/eval_error/gates.rs index 75e60c56..527b67c5 100644 --- a/src/simulator/eval_error/gates.rs +++ b/src/simulator/eval_error/gates.rs @@ -22,9 +22,15 @@ impl PolyCircuit { PolyNorm::one(ctx.clone()), PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, e_init_norm.clone(), None), ); - let input_error = ErrorNorm::new( + let input_error = ErrorNorm::fresh_input( PolyNorm::constant(ctx.clone(), input_norm_bound.clone()), - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, e_init_norm.clone(), None), + PolyMatrixNorm::fresh_random_with_norm( + ctx.clone(), + 1, + ctx.m_g, + e_init_norm.clone(), + None, + ), ); info!("e_init_norm bits {}", bigdecimal_bits_ceil(e_init_norm)); info!("input_norm_bound bits {}", bigdecimal_bits_ceil(&input_norm_bound)); @@ -227,33 +233,6 @@ impl PolyCircuit { panic!("error-norm value missing for gate {gate_id}") } - /// Clone only the matrix-norm component for one gate. - /// - /// Summary construction often needs just the matrix half of an `ErrorNorm`, so this helper - /// keeps that access pattern explicit and avoids rebuilding the full value object in - /// callers. - pub(super) fn clone_error_norm_matrix_norm_for_gate( - &self, - gate_id: GateId, - wires: &ErrorNormWireStore, - input_gate_positions: &ErrorNormInputGatePositions, - input_values: &[Option>], - ) -> PolyMatrixNorm { - if let Some(value) = wires.get(gate_id) { - return value.matrix_norm.clone(); - } - if let Some(&input_idx) = input_gate_positions.get(&gate_id) { - return input_values[input_idx] - .as_ref() - .unwrap_or_else(|| { - panic!("error-norm matrix norm missing for input gate {gate_id}") - }) - .matrix_norm - .clone(); - } - panic!("error-norm matrix norm missing for gate {gate_id}") - } - /// Clone only the plaintext-norm component for one concrete gate value. pub(super) fn clone_error_norm_plaintext_norm_for_value_gate( &self, diff --git a/src/simulator/eval_error/mod.rs b/src/simulator/eval_error/mod.rs index 70f52316..a01ae45b 100644 --- a/src/simulator/eval_error/mod.rs +++ b/src/simulator/eval_error/mod.rs @@ -1,5 +1,6 @@ use super::{ - SimulatorContext, error_norm::ErrorNorm, poly_matrix_norm::PolyMatrixNorm, poly_norm::PolyNorm, + SimulatorContext, dependency_set::DependencySet, error_norm::ErrorNorm, + poly_matrix_norm::PolyMatrixNorm, poly_norm::PolyNorm, }; use crate::{ circuit::{ @@ -16,7 +17,7 @@ use crate::{ use bigdecimal::BigDecimal; use dashmap::DashMap; use num_bigint::BigUint; -use num_traits::{FromPrimitive, One}; +use num_traits::{FromPrimitive, One, Zero}; use rayon::{ iter::{ IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, @@ -54,12 +55,12 @@ type ErrorNormSubCircuitSummaryCache = /// Hash-friendly identity for one `PolyNorm`. /// /// The summary cache needs a stable key, but `PolyNorm` itself contains shared context and big -/// decimal state. The cache only cares about the simulator context identity and the textual bound, -/// so this key strips the runtime object down to those two stable components. +/// decimal state. The cache only cares about the simulator context identity, raw sigma, and +/// constant-polynomial flag, so this key strips the runtime object down to those stable components. struct ErrorNormPolyNormKey { ctx_id: usize, - norm: String, - is_constant: bool, + sigma: String, + is_constant_poly: bool, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -127,16 +128,16 @@ struct ErrorNormSummaryRegistry { fn error_norm_poly_norm_key(norm: &PolyNorm) -> ErrorNormPolyNormKey { ErrorNormPolyNormKey { ctx_id: Arc::as_ptr(&norm.ctx) as usize, - norm: norm.norm.to_string(), - is_constant: norm.is_constant, + sigma: norm.sigma.to_string(), + is_constant_poly: norm.is_constant_poly, } } /// Build the cache key for one sub-circuit summary request. /// /// This intentionally captures only the circuit identity plus the caller-observed plaintext -/// profile. If two call sites expose the same input bounds, they can share the exact same affine -/// summary. +/// profile. If two call sites expose the same input sigma profiles, they can share the exact same +/// affine summary. fn error_norm_sub_circuit_summary_cache_key( circuit_key: usize, sub_circuit_id: usize, @@ -205,11 +206,12 @@ fn validate_input_plaintext_norms_against_ranges( for (offset, actual_norm) in actual_input_plaintext_norms[range.start..range.end].iter().enumerate() { + let actual_bound = actual_norm.max_coefficients_bound(); assert!( - actual_norm.norm <= max_norm_bd, + actual_bound <= max_norm_bd, "{context}: input plaintext norm at index {} exceeds declared max (actual={}, max={})", range.start + offset, - actual_norm.norm, + actual_bound, max_norm_bd ); normalized.push(PolyNorm::constant(Arc::clone(norm_ctx), max_norm_bd.clone())); @@ -259,15 +261,7 @@ fn normalize_sub_circuit_input_plaintext_norms( /// summed sub-circuit calls: similar profiles are likely to share cache entries and have similar /// substitution costs. fn error_norm_summary_profile_max_bits(input_plaintext_norms: &[PolyNorm]) -> u64 { - input_plaintext_norms.iter().map(|norm| bigdecimal_bits_ceil(&norm.norm)).max().unwrap_or(0) -} - -/// Fixed Gaussian tail multiplier used when turning simulator noise parameters into hard bounds. -/// -/// This session does not change the bound itself; the helper exists only so all extracted files use -/// the exact same constant and the intent remains visible near the error simulator. -fn gaussian_tail_bound_factor() -> BigDecimal { - BigDecimal::from_f32(6.5).unwrap() + input_plaintext_norms.iter().map(|norm| bigdecimal_bits_ceil(&norm.sigma)).max().unwrap_or(0) } /// Resolve the `input_idx`-th input of a direct sub-circuit call that uses a shared-prefix input @@ -969,15 +963,238 @@ impl AffineErrorNormExpr { } } +#[derive(Debug, Clone, PartialEq, Eq)] +struct ErrorNormPubkeySummary { + deps: DependencySet, + input_indices: Vec, + is_pubkey_random: bool, +} + +impl ErrorNormPubkeySummary { + fn empty() -> Self { + Self { deps: DependencySet::empty(), input_indices: Vec::new(), is_pubkey_random: false } + } + + fn from_error_norm(value: &ErrorNorm) -> Self { + Self { + deps: value.pubkey_deps.clone(), + input_indices: Vec::new(), + is_pubkey_random: value.is_pubkey_random, + } + } + + fn input(input_idx: usize) -> Self { + Self { + deps: DependencySet::empty(), + input_indices: vec![input_idx], + is_pubkey_random: true, + } + } + + fn fresh_random(ctx: &SimulatorContext) -> Self { + Self { + deps: DependencySet::singleton(ctx.fresh_source_id()), + input_indices: Vec::new(), + is_pubkey_random: true, + } + } + + fn union_input_indices(lhs: &[usize], rhs: &[usize]) -> Vec { + let mut out = Vec::with_capacity(lhs.len() + rhs.len()); + let mut i = 0usize; + let mut j = 0usize; + while i < lhs.len() || j < rhs.len() { + let next = match (lhs.get(i), rhs.get(j)) { + (Some(l), Some(r)) => match l.cmp(r) { + std::cmp::Ordering::Less => { + i += 1; + *l + } + std::cmp::Ordering::Greater => { + j += 1; + *r + } + std::cmp::Ordering::Equal => { + i += 1; + j += 1; + *l + } + }, + (Some(l), None) => { + i += 1; + *l + } + (None, Some(r)) => { + j += 1; + *r + } + (None, None) => break, + }; + if out.last().copied() != Some(next) { + out.push(next); + } + } + out + } + + fn deterministic_union(&self, rhs: &Self) -> Self { + Self { + deps: self.deps.union(&rhs.deps), + input_indices: Self::union_input_indices(&self.input_indices, &rhs.input_indices), + is_pubkey_random: false, + } + } + + fn scale(&self, scalar: &BigDecimal) -> Self { + if scalar.is_zero() { + return Self::empty(); + } + Self { + deps: self.deps.clone(), + input_indices: self.input_indices.clone(), + is_pubkey_random: false, + } + } + + fn small_scalar_mul(&self, scalar: &BigDecimal) -> Self { + if scalar.is_zero() { + return Self::empty(); + } + Self { + deps: self.deps.clone(), + input_indices: self.input_indices.clone(), + is_pubkey_random: self.is_pubkey_random, + } + } + + fn remap_input_indices(&self, input_sources: &[usize]) -> Self { + let mut remapped = self + .input_indices + .iter() + .map(|input_idx| { + *input_sources.get(*input_idx).unwrap_or_else(|| { + panic!( + "error-norm summary input index {input_idx} out of range during metadata remap" + ) + }) + }) + .collect::>(); + remapped.sort_unstable(); + remapped.dedup(); + Self { + deps: self.deps.clone(), + input_indices: remapped, + is_pubkey_random: self.is_pubkey_random, + } + } + + fn substitute_exprs(&self, actual_inputs: &[Arc]) -> Self { + if self.is_pubkey_random && + self.deps == DependencySet::empty() && + self.input_indices.len() == 1 + { + return actual_inputs + .get(self.input_indices[0]) + .unwrap_or_else(|| { + panic!( + "error-norm summary input index {} out of range during metadata substitution", + self.input_indices[0] + ) + }) + .pubkey_summary + .clone(); + } + let mut out = Self { + deps: self.deps.clone(), + input_indices: Vec::new(), + is_pubkey_random: self.is_pubkey_random && self.input_indices.is_empty(), + }; + for input_idx in &self.input_indices { + let input = actual_inputs.get(*input_idx).unwrap_or_else(|| { + panic!( + "error-norm summary input index {input_idx} out of range during metadata substitution" + ) + }); + out = out.deterministic_union(&input.pubkey_summary); + } + out + } + + fn substitute_inputs_with_cached( + &self, + actual_input_at: &F, + cache: &mut HashMap, + ) -> Self + where + F: Fn(usize) -> Arc + Sync, + { + if self.is_pubkey_random && + self.deps == DependencySet::empty() && + self.input_indices.len() == 1 + { + return cache + .entry(self.input_indices[0]) + .or_insert_with(|| actual_input_at(self.input_indices[0]).pubkey_summary.clone()) + .clone(); + } + let mut out = Self { + deps: self.deps.clone(), + input_indices: Vec::new(), + is_pubkey_random: self.is_pubkey_random && self.input_indices.is_empty(), + }; + for input_idx in &self.input_indices { + let input_summary = cache + .entry(*input_idx) + .or_insert_with(|| actual_input_at(*input_idx).pubkey_summary.clone()) + .clone(); + out = out.deterministic_union(&input_summary); + } + out + } + + fn rhs_pubkey_gadget_norm(&self, gadget_poly: PolyNorm) -> PolyMatrixNorm { + let ctx = gadget_poly.ctx.clone(); + let deps = + if self.input_indices.is_empty() { self.deps.clone() } else { DependencySet::Unknown }; + PolyMatrixNorm::from_parts(ctx.m_g, ctx.m_g, gadget_poly, None, deps, self.is_pubkey_random) + } + + fn evaluate_with_cached( + &self, + input_error_norm_at: &F, + cache: &mut HashMap, + ) -> (DependencySet, bool) + where + F: Fn(usize) -> ErrorNorm + Sync, + { + if self.is_pubkey_random && + self.deps == DependencySet::empty() && + self.input_indices.len() == 1 + { + let input = cache + .entry(self.input_indices[0]) + .or_insert_with(|| input_error_norm_at(self.input_indices[0])); + return (input.pubkey_deps.clone(), input.is_pubkey_random); + } + let mut deps = self.deps.clone(); + for input_idx in &self.input_indices { + let input = cache.entry(*input_idx).or_insert_with(|| input_error_norm_at(*input_idx)); + deps = deps.union(&input.pubkey_deps); + } + (deps, self.is_pubkey_random && self.input_indices.is_empty()) + } +} + #[derive(Debug, Clone)] /// Reusable symbolic error bound for one gate or one sub-circuit output. /// -/// The plaintext component is already concrete for the profiled input bounds. The matrix component -/// stays affine in the caller inputs, which allows `ErrorNormSubCircuitSummary` to substitute or -/// remap many outputs without rerunning the full simulator. +/// The plaintext component is already concrete for the profiled input sigma profile. The matrix +/// component stays affine in the caller inputs, which allows `ErrorNormSubCircuitSummary` to +/// substitute or remap many outputs without rerunning the full simulator. pub struct ErrorNormSummaryExpr { plaintext_norm: PolyNorm, matrix_expr: AffineErrorNormExpr, + pubkey_summary: ErrorNormPubkeySummary, } impl ErrorNormSummaryExpr { @@ -991,24 +1208,44 @@ impl ErrorNormSummaryExpr { fn constant(value: ErrorNorm) -> Self { Self { + pubkey_summary: ErrorNormPubkeySummary::from_error_norm(&value), plaintext_norm: value.plaintext_norm, matrix_expr: AffineErrorNormExpr::constant(value.matrix_norm), } } fn input_with_plaintext_norm(input_idx: usize, plaintext_norm: PolyNorm) -> Self { - Self { plaintext_norm, matrix_expr: AffineErrorNormExpr::input(input_idx) } + Self { + plaintext_norm, + matrix_expr: AffineErrorNormExpr::input(input_idx), + pubkey_summary: ErrorNormPubkeySummary::input(input_idx), + } + } + + fn fresh_lut_output(plaintext_norm: PolyNorm, matrix_expr: AffineErrorNormExpr) -> Self { + let pubkey_summary = ErrorNormPubkeySummary::fresh_random(&plaintext_norm.ctx); + Self { plaintext_norm, matrix_expr, pubkey_summary } + } + + fn deterministic_pubkey( + plaintext_norm: PolyNorm, + matrix_expr: AffineErrorNormExpr, + pubkey_summary: ErrorNormPubkeySummary, + ) -> Self { + Self { plaintext_norm, matrix_expr, pubkey_summary } } fn add_bound(&self, rhs: &Self) -> Self { Self { plaintext_norm: &self.plaintext_norm + &rhs.plaintext_norm, matrix_expr: self.matrix_expr.add_expr(&rhs.matrix_expr), + pubkey_summary: self.pubkey_summary.deterministic_union(&rhs.pubkey_summary), } } fn add_assign_bound(&mut self, rhs: Self) { self.plaintext_norm = &self.plaintext_norm + &rhs.plaintext_norm; + self.pubkey_summary = self.pubkey_summary.deterministic_union(&rhs.pubkey_summary); self.matrix_expr.add_assign_expr(rhs.matrix_expr); } @@ -1020,19 +1257,25 @@ impl ErrorNormSummaryExpr { Self { plaintext_norm: self.plaintext_norm.clone() * &scalar_poly, matrix_expr: self.matrix_expr.transform_scalar(scalar), + pubkey_summary: self.pubkey_summary.scale(scalar), } } fn mul_bound(&self, rhs: &Self) -> Self { + let rhs_gadget = rhs.pubkey_summary.rhs_pubkey_gadget_norm( + PolyMatrixNorm::gadget_decomposed( + self.plaintext_norm.ctx.clone(), + self.plaintext_norm.ctx.m_g, + ) + .poly_norm, + ); Self { plaintext_norm: &self.plaintext_norm * &rhs.plaintext_norm, matrix_expr: self .matrix_expr - .transform_matrix(&PolyMatrixNorm::gadget_decomposed( - self.plaintext_norm.ctx.clone(), - self.plaintext_norm.ctx.m_g, - )) + .transform_matrix(&rhs_gadget) .add_expr(&rhs.matrix_expr.transform_poly(&self.plaintext_norm)), + pubkey_summary: self.pubkey_summary.deterministic_union(&rhs.pubkey_summary), } } @@ -1042,19 +1285,21 @@ impl ErrorNormSummaryExpr { Self { plaintext_norm: self.plaintext_norm.clone() * &scalar_poly, matrix_expr: self.matrix_expr.transform_scalar(&scalar_max), + pubkey_summary: self.pubkey_summary.small_scalar_mul(&scalar_max), } } fn large_scalar_mul_bound(&self, scalar: &[BigUint]) -> Self { let scalar_max = scalar.iter().max().unwrap().clone(); let scalar_bd = BigDecimal::from(num_bigint::BigInt::from(scalar_max)); - let scalar_poly = PolyNorm::constant(self.plaintext_norm.ctx.clone(), scalar_bd); + let scalar_poly = PolyNorm::constant(self.plaintext_norm.ctx.clone(), scalar_bd.clone()); Self { plaintext_norm: self.plaintext_norm.clone() * &scalar_poly, matrix_expr: self.matrix_expr.transform_matrix(&PolyMatrixNorm::gadget_decomposed( self.plaintext_norm.ctx.clone(), self.plaintext_norm.ctx.m_g, )), + pubkey_summary: self.pubkey_summary.scale(&scalar_bd), } } @@ -1066,9 +1311,13 @@ impl ErrorNormSummaryExpr { where F: Fn(usize) -> Arc + Sync, { + let mut pubkey_cache = HashMap::::new(); Self { plaintext_norm: self.plaintext_norm.clone(), matrix_expr: self.matrix_expr.substitute_inputs_with_cached(actual_input_at, cache), + pubkey_summary: self + .pubkey_summary + .substitute_inputs_with_cached(actual_input_at, &mut pubkey_cache), } } @@ -1080,6 +1329,7 @@ impl ErrorNormSummaryExpr { Self { plaintext_norm: self.plaintext_norm.clone(), matrix_expr: self.matrix_expr.remap_input_indices(input_sources), + pubkey_summary: self.pubkey_summary.remap_input_indices(input_sources), } } } @@ -1203,6 +1453,7 @@ impl ErrorNormSubCircuitSummary { fn compose_output_range_shared( &self, output_range: std::ops::Range, + actual_inputs: &[Arc], input_sources: &[AffineErrorNormExpr], ) -> Vec> { let outputs = &self.outputs[output_range]; @@ -1261,6 +1512,7 @@ impl ErrorNormSubCircuitSummary { Arc::new(ErrorNormSummaryExpr { plaintext_norm: expr.plaintext_norm.clone(), matrix_expr, + pubkey_summary: expr.pubkey_summary.substitute_exprs(actual_inputs), }) }; if outputs.len() < ERROR_NORM_EXPR_PAR_BATCH_SIZE { @@ -1288,7 +1540,11 @@ impl ErrorNormSubCircuitSummary { return self.remap_output_range_shared(output_range, input_sources.as_ref()); } if let Some(input_sources) = Self::small_affine_input_sources(actual_inputs) { - return self.compose_output_range_shared(output_range, input_sources.as_ref()); + return self.compose_output_range_shared( + output_range, + actual_inputs, + input_sources.as_ref(), + ); } self.substitute_output_range_with_input_fn(output_range, &|input_idx| { actual_inputs @@ -1320,7 +1576,11 @@ impl ErrorNormSubCircuitSummary { } if let Some(input_sources) = Self::small_affine_input_sources(actual_inputs) { return self - .compose_output_range_shared(output_idx..output_idx + 1, input_sources.as_ref()) + .compose_output_range_shared( + output_idx..output_idx + 1, + actual_inputs, + input_sources.as_ref(), + ) .into_iter() .next() .unwrap_or_else(|| { @@ -1390,20 +1650,30 @@ impl ErrorNormSubCircuitSummary { fn evaluate_output_range_with_shared_cache( &self, output_range: std::ops::Range, - input_matrix_norm_at: &F, + input_error_norm_at: &F, ) -> Vec where - F: Fn(usize) -> PolyMatrixNorm + Sync, + F: Fn(usize) -> ErrorNorm + Sync, { let outputs = &self.outputs[output_range]; if outputs.len() < ERROR_NORM_EXPR_PAR_BATCH_SIZE { outputs .par_iter() .map(|expr| { - let mut cache = HashMap::::new(); - ErrorNorm::new( + let mut matrix_cache = HashMap::::new(); + let matrix_norm = expr.matrix_expr.evaluate_with_cached( + &|input_idx| input_error_norm_at(input_idx).matrix_norm, + &mut matrix_cache, + ); + let mut pubkey_cache = HashMap::::new(); + let (pubkey_deps, is_pubkey_random) = expr + .pubkey_summary + .evaluate_with_cached(input_error_norm_at, &mut pubkey_cache); + ErrorNorm::from_parts( expr.plaintext_norm.clone(), - expr.matrix_expr.evaluate_with_cached(input_matrix_norm_at, &mut cache), + matrix_norm, + pubkey_deps, + is_pubkey_random, ) }) .collect() @@ -1411,14 +1681,23 @@ impl ErrorNormSubCircuitSummary { outputs .par_chunks(ERROR_NORM_EXPR_PAR_BATCH_SIZE) .map(|expr_chunk| { - let mut cache = HashMap::::new(); + let mut matrix_cache = HashMap::::new(); + let mut pubkey_cache = HashMap::::new(); expr_chunk .iter() .map(|expr| { - ErrorNorm::new( + let matrix_norm = expr.matrix_expr.evaluate_with_cached( + &|input_idx| input_error_norm_at(input_idx).matrix_norm, + &mut matrix_cache, + ); + let (pubkey_deps, is_pubkey_random) = expr + .pubkey_summary + .evaluate_with_cached(input_error_norm_at, &mut pubkey_cache); + ErrorNorm::from_parts( expr.plaintext_norm.clone(), - expr.matrix_expr - .evaluate_with_cached(input_matrix_norm_at, &mut cache), + matrix_norm, + pubkey_deps, + is_pubkey_random, ) }) .collect::>() @@ -1470,9 +1749,10 @@ struct PolyNormRun { #[derive(Debug, Clone, PartialEq, Eq)] /// Run-length encoded plaintext profile for summary cache keys. /// -/// Large shared-prefix call groups often repeat the same plaintext bounds many times. Compressing -/// those runs reduces both cache-key allocation cost and temporary cloning during summary -/// preparation, while `materialize` reconstructs the exact flat list when the simulator needs it. +/// Large shared-prefix call groups often repeat the same plaintext sigma profiles many times. +/// Compressing those runs reduces both cache-key allocation cost and temporary cloning during +/// summary preparation, while `materialize` reconstructs the exact flat list when the simulator +/// needs it. struct CompressedPolyNormRuns { total_len: usize, runs: Arc<[PolyNormRun]>, @@ -1521,7 +1801,7 @@ impl CompressedPolyNormRuns { /// /// The `SharedPrefix` variant mirrors the circuit representation used by direct sub-circuit calls: /// one reusable prefix input-set plus a call-local suffix. Keeping that distinction here lets the -/// summary preparation path reuse compressed prefix bounds across many calls. +/// summary preparation path reuse compressed prefix sigma profiles across many calls. enum ErrorNormInputPlaintextProfile { Flat(CompressedPolyNormRuns), SharedPrefix { prefix_norms: CompressedPolyNormRuns, suffix_norms: CompressedPolyNormRuns }, @@ -1572,7 +1852,7 @@ mod summary; pub use evaluators::{ NormBggPolyEncodingSTEvaluator, NormNaiveBggEncodingVecSTEvaluator, NormPltCommitEvaluator, - NormPltGGH15Evaluator, NormPltLWEEvaluator, compute_preimage_norm, + NormPltGGH15Evaluator, NormPltLWEEvaluator, compute_preimage_norm, compute_preimage_sigma, }; #[cfg(test)] diff --git a/src/simulator/eval_error/tests.rs b/src/simulator/eval_error/tests.rs index dad829d4..abd4fcae 100644 --- a/src/simulator/eval_error/tests.rs +++ b/src/simulator/eval_error/tests.rs @@ -6,11 +6,12 @@ use crate::{ Poly, dcrt::{params::DCRTPolyParams, poly::DCRTPoly}, }, - simulator::SimulatorContext, + simulator::{SimulatorContext, dependency_set::DependencySet}, slot_transfer::SlotTransferEvaluator, utils::bigdecimal_bits_ceil, }; use bigdecimal::BigDecimal; +use num_bigint::BigUint; fn make_ctx() -> Arc { // secpar_sqrt=50, ring_dim_sqrt=1024, base=32, log_base_q=(128/32)*7 = 28 @@ -23,6 +24,46 @@ fn make_ctx() -> Arc { )) } +fn assert_matrix_bound_eq(actual: &PolyMatrixNorm, expected: &PolyMatrixNorm) { + assert_eq!(actual.nrow, expected.nrow); + assert_eq!(actual.ncol, expected.ncol); + assert_eq!(actual.ncol_sqrt, expected.ncol_sqrt); + assert_eq!(actual.poly_norm, expected.poly_norm); + assert_eq!(actual.zero_rows, expected.zero_rows); +} + +fn assert_error_bound_eq(actual: &ErrorNorm, expected: &ErrorNorm) { + assert_eq!(actual.plaintext_norm, expected.plaintext_norm); + assert_matrix_bound_eq(&actual.matrix_norm, &expected.matrix_norm); +} + +fn assert_error_bounds_eq(actual: &[ErrorNorm], expected: &[ErrorNorm]) { + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected.iter()) { + assert_error_bound_eq(actual, expected); + } +} + +fn assert_error_bound_covers(actual: &ErrorNorm, expected: &ErrorNorm) { + assert_eq!(actual.plaintext_norm, expected.plaintext_norm); + assert_eq!(actual.matrix_norm.nrow, expected.matrix_norm.nrow); + assert_eq!(actual.matrix_norm.ncol, expected.matrix_norm.ncol); + assert_eq!(actual.matrix_norm.ncol_sqrt, expected.matrix_norm.ncol_sqrt); + assert!( + actual.matrix_norm.poly_norm.norm >= expected.matrix_norm.poly_norm.norm, + "actual matrix norm {} does not cover expected {}", + actual.matrix_norm.poly_norm.norm, + expected.matrix_norm.poly_norm.norm + ); +} + +fn assert_error_bounds_cover(actual: &[ErrorNorm], expected: &[ErrorNorm]) { + assert_eq!(actual.len(), expected.len()); + for (actual, expected) in actual.iter().zip(expected.iter()) { + assert_error_bound_covers(actual, expected); + } +} + fn simulate_max_error_norm_via_generic_eval_reference( circuit: &PolyCircuit, ctx: Arc, @@ -35,9 +76,15 @@ fn simulate_max_error_norm_via_generic_eval_reference( PolyNorm::one(ctx.clone()), PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, e_init_norm.clone(), None), ); - let input_error = ErrorNorm::new( - PolyNorm::new(ctx.clone(), input_norm_bound), - PolyMatrixNorm::new(ctx, 1, one_error.ctx().m_g, e_init_norm.clone(), None), + let input_error = ErrorNorm::fresh_input( + PolyNorm::constant(ctx.clone(), input_norm_bound), + PolyMatrixNorm::fresh_random_with_norm( + ctx, + 1, + one_error.ctx().m_g, + e_init_norm.clone(), + None, + ), ); circuit.eval(&(), one_error, vec![input_error; input_size], plt_evaluator, None, None) } @@ -46,14 +93,24 @@ const E_B_SIGMA: f64 = 4.0; const E_INIT_NORM: u32 = 26; #[test] -fn test_compute_preimage_norm_uses_optional_sigma() { +fn test_poly_norm_sample_gauss_records_envelope_norm() { + let ctx = make_ctx(); + let sigma = BigDecimal::from(4u64); + let sampled = PolyNorm::sample_gauss(ctx, sigma.clone()); + + assert_eq!(sampled.norm, BigDecimal::from(26u64)); + assert_eq!(sampled.maximum_coefficient_bound(), BigDecimal::from(26u64)); +} + +#[test] +fn test_compute_preimage_sigma_uses_optional_sigma() { let ctx = make_ctx(); let default_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, None); let explicit_default_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, Some(4.578)); + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, Some(4.578)); let larger_sigma_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, Some(6.0)); + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, None, Some(6.0)); assert_eq!(default_norm, explicit_default_norm); assert!(larger_sigma_norm > default_norm); @@ -66,17 +123,50 @@ fn test_constant_poly_norm_mul_skips_ring_dim_sqrt() { let rhs = PolyNorm::constant(ctx.clone(), BigDecimal::from(5u64)); let product = &lhs * &rhs; - assert_eq!(product.norm, BigDecimal::from(15u64)); - assert!(product.is_constant); + assert_eq!(product.sigma, BigDecimal::from(15u64)); + assert!(product.is_constant_poly); let general = PolyNorm::new(ctx.clone(), BigDecimal::from(5u64)); let mixed = &lhs * &general; - assert_eq!(mixed.norm, BigDecimal::from(15u64)); - assert!(!mixed.is_constant); + assert_eq!(mixed.sigma, BigDecimal::from(15u64)); + assert!(!mixed.is_constant_poly); let general_product = &general * &general; - assert_eq!(general_product.norm, BigDecimal::from(25u64) * &ctx.ring_dim_sqrt); - assert!(!general_product.is_constant); + assert_eq!(general_product.sigma, BigDecimal::from(25u64) * &ctx.ring_dim_sqrt); + assert!(!general_product.is_constant_poly); +} + +#[test] +fn test_sub_circuit_plaintext_range_accepts_constant_at_declared_max() { + let ctx = make_ctx(); + let ranges = vec![SubCircuitInputMaxPlaintextNormRange::new(0, 1, BigUint::from(7u64))]; + let actual = vec![PolyNorm::constant(ctx.clone(), BigDecimal::from(7u64))]; + + let normalized = validate_input_plaintext_norms_against_ranges( + &ranges, + &actual, + &ctx, + "constant plaintext range test", + ); + + assert_eq!(normalized.len(), 1); + assert_eq!(normalized[0].sigma, BigDecimal::from(7u64)); + assert!(normalized[0].is_constant_poly); +} + +#[test] +#[should_panic(expected = "exceeds declared max")] +fn test_sub_circuit_plaintext_range_rejects_nonconstant_public_bound_above_declared_max() { + let ctx = make_ctx(); + let ranges = vec![SubCircuitInputMaxPlaintextNormRange::new(0, 1, BigUint::from(12u64))]; + let actual = vec![PolyNorm::new(ctx.clone(), BigDecimal::from(13u64))]; + + validate_input_plaintext_norms_against_ranges( + &ranges, + &actual, + &ctx, + "nonconstant plaintext range test", + ); } #[test] @@ -98,12 +188,18 @@ fn test_wire_norm_addition() { ); assert_eq!(out.len(), 1); // Build expected from input wires and add them - let in_wire = ErrorNorm::new( - PolyNorm::new(ctx.clone(), input_bound), - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(E_INIT_NORM), None), + let in_wire = ErrorNorm::fresh_input( + PolyNorm::constant(ctx.clone(), input_bound), + PolyMatrixNorm::fresh_random_with_norm( + ctx.clone(), + 1, + ctx.m_g, + BigDecimal::from(E_INIT_NORM), + None, + ), ); let expected = &in_wire + &in_wire; - assert_eq!(out[0], expected); + assert_error_bound_eq(&out[0], &expected); } #[test] @@ -123,12 +219,18 @@ fn test_wire_norm_subtraction() { None, ); assert_eq!(out.len(), 1); - let in_wire = ErrorNorm::new( - PolyNorm::new(ctx.clone(), input_bound), - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(E_INIT_NORM), None), + let in_wire = ErrorNorm::fresh_input( + PolyNorm::constant(ctx.clone(), input_bound), + PolyMatrixNorm::fresh_random_with_norm( + ctx.clone(), + 1, + ctx.m_g, + BigDecimal::from(E_INIT_NORM), + None, + ), ); let expected = &in_wire - &in_wire; // subtraction bound equals addition bound - assert_eq!(out[0], expected); + assert_error_bound_eq(&out[0], &expected); } #[test] @@ -151,12 +253,18 @@ fn test_wire_norm_multiplication() { assert_eq!(out.len(), 1); // Build expected = in_wire * in_wire - let in_wire = ErrorNorm::new( - PolyNorm::new(ctx.clone(), input_bound), - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(E_INIT_NORM), None), + let in_wire = ErrorNorm::fresh_input( + PolyNorm::constant(ctx.clone(), input_bound), + PolyMatrixNorm::fresh_random_with_norm( + ctx.clone(), + 1, + ctx.m_g, + BigDecimal::from(E_INIT_NORM), + None, + ), ); let expected = &in_wire * &in_wire; - assert_eq!(out[0], expected); + assert_error_bound_eq(&out[0], &expected); } #[test] @@ -184,7 +292,7 @@ fn test_wire_norm_simulator_multiplication_matches_generic_eval() { &e_init_norm, None::<&NormPltLWEEvaluator>, ); - assert_eq!(out, generic); + assert_error_bounds_eq(&out, &generic); } #[test] @@ -238,10 +346,10 @@ fn test_wire_norm_simulator_mul_binary_tree_plaintext_one_matches_generic_eval() ); assert_eq!(simulated.len(), 1); - assert_eq!(simulated, generic); + assert_error_bounds_eq(&simulated, &generic); println!( "mul_binary_tree_output_error_bits={}", - bigdecimal_bits_ceil(&simulated[0].matrix_norm.poly_norm.norm) + bigdecimal_bits_ceil(&simulated[0].matrix_norm.poly_norm.sigma) ); } @@ -269,37 +377,49 @@ fn test_wire_norm_slot_transfer_matches_bgg_poly_encoding_bound() { let out = evaluator.slot_transfer(&(), &input, &src_slots, GateId(0)); - let b0_preimage_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(1), None); + let b0_preimage_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(1), None); let s_vec = PolyMatrixNorm::new(ctx.clone(), 1, ctx.secret_size, BigDecimal::one(), None); - let gate_preimage = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, ctx.m_g, b0_preimage_norm.clone(), None); - let gate_target_error = PolyMatrixNorm::new( + let gate_preimage = PolyMatrixNorm::fresh_preimage( ctx.clone(), - ctx.secret_size, + ctx.m_b, ctx.m_g, - BigDecimal::from_f64(E_B_SIGMA * 6.5).unwrap(), + b0_preimage_sigma.clone(), None, ); - let slot_preimage_b0 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b, 2 * ctx.m_b, b0_preimage_norm.clone(), None); - let b1_preimage_norm = - compute_preimage_norm(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(2), None); - let slot_preimage_b1 = - PolyMatrixNorm::new(ctx.clone(), ctx.m_b * 2, ctx.m_g, b1_preimage_norm.clone(), None); - let slot_preimage_b0_target_error = PolyMatrixNorm::new( + let gate_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size, + ctx.m_g, + BigDecimal::from_f64(E_B_SIGMA).unwrap(), + ); + let slot_preimage_b0 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), + ctx.m_b, + 2 * ctx.m_b, + b0_preimage_sigma.clone(), + None, + ); + let b1_preimage_sigma = + compute_preimage_sigma(&ctx.ring_dim_sqrt, ctx.m_g as u64, &ctx.base, Some(2), None); + let slot_preimage_b1 = PolyMatrixNorm::fresh_preimage( + ctx.clone(), ctx.m_b * 2, - BigDecimal::from_f64(E_B_SIGMA * 6.5).unwrap(), + ctx.m_g, + b1_preimage_sigma.clone(), None, ); - let slot_preimage_b1_target_error = PolyMatrixNorm::new( + let slot_preimage_b0_target_error = PolyMatrixNorm::sample_gauss( + ctx.clone(), + ctx.secret_size, + ctx.m_b * 2, + BigDecimal::from_f64(E_B_SIGMA).unwrap(), + ); + let slot_preimage_b1_target_error = PolyMatrixNorm::sample_gauss( ctx.clone(), ctx.secret_size * 2, ctx.m_g, - BigDecimal::from_f64(E_B_SIGMA * 6.5).unwrap(), - None, + BigDecimal::from_f64(E_B_SIGMA).unwrap(), ); let slot_secret_and_identity = PolyMatrixNorm::new( ctx.clone(), @@ -320,7 +440,9 @@ fn test_wire_norm_slot_transfer_matches_bgg_poly_encoding_bound() { (input.matrix_norm.clone() * &input_vector_multiplier) * &scalar_bd + transfer_plaintext_multiplier * &plaintext_norm; - assert_eq!(out, ErrorNorm { plaintext_norm, matrix_norm }); + let expected = + ErrorNorm::from_parts(plaintext_norm, matrix_norm, input.pubkey_deps.clone(), false); + assert_error_bound_eq(&out, &expected); } #[test] @@ -346,7 +468,7 @@ fn test_wire_norm_slot_transfer_bound_is_independent_of_slot_count() { GateId(1), ); - assert_eq!(out_single, out_many); + assert_error_bound_eq(&out_single, &out_many); } #[test] @@ -361,7 +483,7 @@ fn test_wire_norm_naive_bgg_encoding_vec_slot_transfer_is_free() { let out = evaluator.slot_transfer(&(), &input, &src_slots, GateId(0)); - assert_eq!(out, input.small_scalar_mul(&(), &[3])); + assert_error_bound_eq(&out, &input.small_scalar_mul(&(), &[3])); } #[test] @@ -383,12 +505,13 @@ fn test_wire_norm_naive_bgg_encoding_vec_slot_transfer_affine_path_is_free() { None::<&NormPltLWEEvaluator>, Some(&evaluator), ); - let input = ErrorNorm::new( - PolyNorm::new(ctx.clone(), input_bound), - PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, e_init_norm, None), + let input = ErrorNorm::fresh_input( + PolyNorm::constant(ctx.clone(), input_bound), + PolyMatrixNorm::fresh_random_with_norm(ctx.clone(), 1, ctx.m_g, e_init_norm, None), ); + let expected = vec![input.small_scalar_mul(&(), &[4])]; - assert_eq!(out, vec![input.small_scalar_mul(&(), &[4])]); + assert_error_bounds_eq(&out, &expected); } #[test] @@ -441,7 +564,7 @@ fn test_wire_norm_lwe_plt_bounds() { ); assert_eq!(out.len(), 1); // Bound must be max output coeff across LUT entries (7) - assert_eq!(out[0].plaintext_norm.norm, BigDecimal::from(7u64)); + assert_eq!(out[0].plaintext_norm.sigma, BigDecimal::from(7u64)); } #[test] @@ -498,7 +621,7 @@ fn test_wire_norm_ggh15_plt_bounds() { ); assert_eq!(out.len(), 1); // Bound must be max output coeff across LUT entries (7) - assert_eq!(out[0].plaintext_norm.norm, BigDecimal::from(7u64)); + assert_eq!(out[0].plaintext_norm.sigma, BigDecimal::from(7u64)); } #[test] @@ -552,7 +675,7 @@ fn test_wire_norm_simulator_ggh15_plt_uses_lut_plaintext_bound() { None, ); assert_eq!(out.len(), 1); - assert_eq!(out[0].plaintext_norm.norm, BigDecimal::from(7u64)); + assert_eq!(out[0].plaintext_norm.sigma, BigDecimal::from(7u64)); } #[test] @@ -592,8 +715,8 @@ fn test_wire_norm_simulator_sub_circuit_matches_generic_eval() { let mut circuit = PolyCircuit::::new(); let inputs = circuit.input(2).to_vec(); let sub_circuit_id = circuit.register_sub_circuit(sub_circuit); - let left = circuit.call_sub_circuit(sub_circuit_id, &[inputs[0]]); - let right = circuit.call_sub_circuit(sub_circuit_id, &[inputs[1]]); + let left = circuit.call_sub_circuit(sub_circuit_id, [inputs[0]]); + let right = circuit.call_sub_circuit(sub_circuit_id, [inputs[1]]); let summed = circuit.add_gate(left[0], right[0]); let plt_id = circuit.register_public_lookup(plt); let out = circuit.public_lookup_gate(summed, plt_id); @@ -626,7 +749,7 @@ fn test_wire_norm_simulator_sub_circuit_matches_generic_eval() { Some(&plt_evaluator), ); - assert_eq!(simulated, generic); + assert_error_bounds_cover(&simulated, &generic); } #[test] @@ -641,8 +764,8 @@ fn test_wire_norm_simulator_sub_circuit_recomputes_for_new_plaintext_profile() { let inputs = circuit.input(1).to_vec(); let doubled = circuit.add_gate(inputs[0], inputs[0]); let sub_circuit_id = circuit.register_sub_circuit(sub_circuit); - let left = circuit.call_sub_circuit(sub_circuit_id, &[inputs[0]]); - let right = circuit.call_sub_circuit(sub_circuit_id, &[doubled]); + let left = circuit.call_sub_circuit(sub_circuit_id, [inputs[0]]); + let right = circuit.call_sub_circuit(sub_circuit_id, [doubled]); let out = circuit.add_gate(left[0], right[0]); circuit.output(vec![out]); @@ -667,7 +790,7 @@ fn test_wire_norm_simulator_sub_circuit_recomputes_for_new_plaintext_profile() { None::<&NormPltLWEEvaluator>, ); - assert_eq!(simulated, generic); + assert_error_bounds_cover(&simulated, &generic); } #[test] @@ -681,15 +804,15 @@ fn test_wire_norm_simulator_nested_sub_circuit_matches_generic_eval() { let outer_inputs = outer_sub_circuit.input(2).to_vec(); let inner_sub_circuit_id = outer_sub_circuit.register_sub_circuit(inner_sub_circuit); let inner_from_second = - outer_sub_circuit.call_sub_circuit(inner_sub_circuit_id, &[outer_inputs[1]]); + outer_sub_circuit.call_sub_circuit(inner_sub_circuit_id, [outer_inputs[1]]); let outer_out = outer_sub_circuit.add_gate(outer_inputs[0], inner_from_second[0]); outer_sub_circuit.output(vec![outer_out]); let mut circuit = PolyCircuit::::new(); let inputs = circuit.input(2).to_vec(); let outer_sub_circuit_id = circuit.register_sub_circuit(outer_sub_circuit); - let left = circuit.call_sub_circuit(outer_sub_circuit_id, &[inputs[0], inputs[1]]); - let right = circuit.call_sub_circuit(outer_sub_circuit_id, &[inputs[1], inputs[0]]); + let left = circuit.call_sub_circuit(outer_sub_circuit_id, [inputs[0], inputs[1]]); + let right = circuit.call_sub_circuit(outer_sub_circuit_id, [inputs[1], inputs[0]]); let out = circuit.add_gate(left[0], right[0]); circuit.output(vec![out]); @@ -714,7 +837,7 @@ fn test_wire_norm_simulator_nested_sub_circuit_matches_generic_eval() { None::<&NormPltLWEEvaluator>, ); - assert_eq!(simulated, generic); + assert_error_bounds_eq(&simulated, &generic); } #[test] @@ -772,5 +895,106 @@ fn test_wire_norm_commit_plt_bounds() { ); assert_eq!(out.len(), 1); // Bound must be max output coeff across LUT entries (7) - assert_eq!(out[0].plaintext_norm.norm, BigDecimal::from(7u64)); + assert_eq!(out[0].plaintext_norm.sigma, BigDecimal::from(7u64)); +} + +#[test] +fn test_error_norm_rhs_pubkey_gadget_uses_rhs_metadata() { + let ctx = make_ctx(); + let lhs_error = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, ctx.m_g, BigDecimal::from(2u64)); + let lhs = ErrorNorm::from_parts( + PolyNorm::one(ctx.clone()), + lhs_error, + DependencySet::singleton(ctx.fresh_source_id()), + false, + ); + let rhs_pubkey_deps = DependencySet::singleton(ctx.fresh_source_id()); + let rhs = ErrorNorm::from_parts( + PolyNorm::one(ctx.clone()), + PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(3u64), None), + rhs_pubkey_deps.clone(), + true, + ); + let rhs_gadget = rhs + .rhs_pubkey_gadget_norm(PolyMatrixNorm::gadget_decomposed(ctx.clone(), ctx.m_g).poly_norm); + + assert_eq!(rhs_gadget.deps, rhs_pubkey_deps); + assert!(rhs_gadget.clt_ready); + + let product = lhs.matrix_norm * &rhs_gadget; + assert!(product.clt_ready); +} + +#[test] +fn test_error_norm_rhs_pubkey_gadget_overlap_forces_worst_case() { + let ctx = make_ctx(); + let shared = DependencySet::singleton(ctx.fresh_source_id()); + let lhs_matrix = PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(2u64), None) + .with_deps(shared.clone(), true); + let rhs = ErrorNorm::from_parts( + PolyNorm::one(ctx.clone()), + PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(3u64), None), + shared, + true, + ); + let rhs_gadget = rhs + .rhs_pubkey_gadget_norm(PolyMatrixNorm::gadget_decomposed(ctx.clone(), ctx.m_g).poly_norm); + let product = lhs_matrix * &rhs_gadget; + + assert!(rhs_gadget.clt_ready); + assert!(!product.clt_ready); +} + +#[test] +fn test_error_norm_lut_and_ordinary_gate_pubkey_randomness_flags() { + let ctx = make_ctx(); + let input = ErrorNorm::fresh_input( + PolyNorm::one(ctx.clone()), + PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(2u64), None), + ); + assert!(input.is_pubkey_random); + + let lut = ErrorNorm::fresh_lut_output( + PolyNorm::one(ctx.clone()), + PolyMatrixNorm::new(ctx.clone(), 1, ctx.m_g, BigDecimal::from(3u64), None), + ); + assert!(lut.is_pubkey_random); + assert!(input.pubkey_deps.is_disjoint(&lut.pubkey_deps)); + + let small_scaled_input = input.small_scalar_mul(&(), &[3]); + assert!(small_scaled_input.is_pubkey_random); + assert_eq!(small_scaled_input.pubkey_deps, input.pubkey_deps); + + let zero_scaled_input = input.small_scalar_mul(&(), &[0]); + assert!(!zero_scaled_input.is_pubkey_random); + assert_eq!(zero_scaled_input.pubkey_deps, DependencySet::empty()); + + let ordinary = input + &lut; + assert!(!ordinary.is_pubkey_random); +} + +#[test] +fn test_error_norm_sub_circuit_summary_preserves_forwarded_input_pubkey_metadata() { + let mut sub_circuit = PolyCircuit::::new(); + let sub_inputs = sub_circuit.input(1).to_vec(); + sub_circuit.output(vec![sub_inputs[0]]); + + let mut circuit = PolyCircuit::::new(); + let inputs = circuit.input(1).to_vec(); + let sub_circuit_id = circuit.register_sub_circuit(sub_circuit); + let out = circuit.call_sub_circuit(sub_circuit_id, [inputs[0]]); + circuit.output(vec![out[0]]); + + let ctx = make_ctx(); + let out = circuit.simulate_max_error_norm( + ctx, + BigDecimal::from(3u64), + 1, + &BigDecimal::from(5u64), + None::<&NormPltLWEEvaluator>, + None, + ); + + assert!(out[0].is_pubkey_random); + assert!(out[0].pubkey_deps != DependencySet::empty()); } diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index ebe08d2d..fe6e900c 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -1,12 +1,14 @@ use bigdecimal::BigDecimal; +use std::sync::atomic::{AtomicU64, Ordering}; +pub mod dependency_set; pub mod error_norm; pub mod eval_error; pub mod lattice_estimator; pub mod poly_matrix_norm; pub mod poly_norm; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct SimulatorContext { // pub secpar_sqrt: BigDecimal, pub ring_dim_sqrt: BigDecimal, @@ -32,4 +34,23 @@ impl SimulatorContext { Self { ring_dim_sqrt, base, secret_size, log_base_q, log_base_q_small, m_g, m_b } } + + pub fn fresh_source_id(&self) -> dependency_set::SourceId { + static SOURCE_COUNTER: AtomicU64 = AtomicU64::new(0); + dependency_set::SourceId(SOURCE_COUNTER.fetch_add(1, Ordering::Relaxed)) + } +} + +impl PartialEq for SimulatorContext { + fn eq(&self, other: &Self) -> bool { + self.ring_dim_sqrt == other.ring_dim_sqrt && + self.base == other.base && + self.secret_size == other.secret_size && + self.log_base_q == other.log_base_q && + self.m_g == other.m_g && + self.m_b == other.m_b && + self.log_base_q_small == other.log_base_q_small + } } + +impl Eq for SimulatorContext {} diff --git a/src/simulator/poly_matrix_norm.rs b/src/simulator/poly_matrix_norm.rs index 3fd2efbe..4a032a2d 100644 --- a/src/simulator/poly_matrix_norm.rs +++ b/src/simulator/poly_matrix_norm.rs @@ -1,20 +1,41 @@ -use super::poly_norm::PolyNorm; +use super::{ + dependency_set::DependencySet, + poly_norm::{PolyNorm, high_probability_envelope_from_sigma}, +}; use crate::{impl_binop_with_refs, simulator::SimulatorContext}; use bigdecimal::BigDecimal; +use num_traits::Zero; use std::{ ops::{Add, AddAssign, Mul, MulAssign}, sync::Arc, }; +use tracing::debug; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct PolyMatrixNorm { pub nrow: usize, pub ncol: usize, pub ncol_sqrt: BigDecimal, pub poly_norm: PolyNorm, pub zero_rows: Option, + pub deps: DependencySet, + pub clt_ready: bool, +} + +impl PartialEq for PolyMatrixNorm { + fn eq(&self, other: &Self) -> bool { + self.nrow == other.nrow && + self.ncol == other.ncol && + self.ncol_sqrt == other.ncol_sqrt && + self.poly_norm == other.poly_norm && + self.zero_rows == other.zero_rows && + self.deps == other.deps && + self.clt_ready == other.clt_ready + } } +impl Eq for PolyMatrixNorm {} + impl PolyMatrixNorm { pub fn new( ctx: Arc, @@ -23,39 +44,92 @@ impl PolyMatrixNorm { norm: BigDecimal, zero_rows: Option, ) -> Self { - PolyMatrixNorm { + Self::from_parts( nrow, ncol, - ncol_sqrt: BigDecimal::from(ncol as u64).sqrt().expect("sqrt(ncol) to failed"), - poly_norm: PolyNorm::new(ctx, norm), + PolyNorm::new(ctx, norm), zero_rows, - } + DependencySet::empty(), + false, + ) } - pub fn sample_gauss( + pub fn fresh_preimage( ctx: Arc, nrow: usize, ncol: usize, - sigma: BigDecimal, + preimage_sigma: BigDecimal, + zero_rows: Option, + ) -> Self { + let deps = DependencySet::singleton(ctx.fresh_source_id()); + Self::from_parts( + nrow, + ncol, + PolyNorm::new(ctx, high_probability_envelope_from_sigma(&preimage_sigma)), + zero_rows, + deps, + true, + ) + } + + pub fn fresh_random_with_norm( + ctx: Arc, + nrow: usize, + ncol: usize, + norm: BigDecimal, + zero_rows: Option, + ) -> Self { + let deps = DependencySet::singleton(ctx.fresh_source_id()); + Self::from_parts(nrow, ncol, PolyNorm::new(ctx, norm), zero_rows, deps, true) + } + + pub fn from_parts( + nrow: usize, + ncol: usize, + poly_norm: PolyNorm, + zero_rows: Option, + deps: DependencySet, + clt_ready: bool, ) -> Self { PolyMatrixNorm { nrow, ncol, ncol_sqrt: BigDecimal::from(ncol as u64).sqrt().expect("sqrt(ncol) to failed"), - poly_norm: PolyNorm::sample_gauss(ctx, sigma), - zero_rows: None, + poly_norm, + zero_rows, + deps, + clt_ready, } } + pub fn sample_gauss( + ctx: Arc, + nrow: usize, + ncol: usize, + sigma: BigDecimal, + ) -> Self { + let deps = DependencySet::singleton(ctx.fresh_source_id()); + Self::from_parts(nrow, ncol, PolyNorm::sample_gauss(ctx, sigma), None, deps, true) + } + + fn balanced_gadget_digit_norm(ctx: &SimulatorContext) -> BigDecimal { + let sigma = ((&ctx.base * &ctx.base + BigDecimal::from(2u64)) / BigDecimal::from(12u64)) + .sqrt() + .expect("sqrt balanced gadget digit variance failed"); + high_probability_envelope_from_sigma(&sigma) + } + // this only support d = 1 pub fn gadget_decomposed(ctx: Arc, ncol: usize) -> Self { - PolyMatrixNorm { - nrow: ctx.m_g, + let digit_norm = Self::balanced_gadget_digit_norm(&ctx); + Self::from_parts( + ctx.m_g, ncol, - ncol_sqrt: BigDecimal::from(ncol as u64).sqrt().expect("sqrt(ncol) to failed"), - poly_norm: PolyNorm::new(ctx.clone(), ctx.base.clone() - BigDecimal::from(1u64)), - zero_rows: None, - } + PolyNorm::new(ctx.clone(), digit_norm), + None, + DependencySet::empty(), + false, + ) } pub fn gadget_decomposed_with_secret_size( @@ -63,13 +137,34 @@ impl PolyMatrixNorm { secret_size: usize, ncol: usize, ) -> Self { - PolyMatrixNorm { - nrow: secret_size * ctx.log_base_q, + let digit_norm = Self::balanced_gadget_digit_norm(&ctx); + Self::from_parts( + secret_size * ctx.log_base_q, ncol, - ncol_sqrt: BigDecimal::from(ncol as u64).sqrt().expect("sqrt(ncol) to failed"), - poly_norm: PolyNorm::new(ctx.clone(), ctx.base.clone() - BigDecimal::from(1u64)), - zero_rows: None, - } + PolyNorm::new(ctx.clone(), digit_norm), + None, + DependencySet::empty(), + false, + ) + } + + pub fn with_deps(mut self, deps: DependencySet, clt_ready: bool) -> Self { + self.deps = deps; + self.clt_ready = clt_ready; + self + } + + pub fn rhs_pubkey_gadget( + ctx: Arc, + ncol: usize, + deps: DependencySet, + clt_ready: bool, + ) -> Self { + Self::gadget_decomposed(ctx, ncol).with_deps(deps, clt_ready) + } + + pub fn maximum_coefficient_bound(&self) -> BigDecimal { + self.poly_norm.norm.clone() } #[inline] @@ -112,6 +207,8 @@ impl_binop_with_refs!(PolyMatrixNorm => Add::add(self, rhs: &PolyMatrixNorm) -> ncol_sqrt: self.ncol_sqrt.clone(), poly_norm: &self.poly_norm + &rhs.poly_norm, zero_rows: None, + deps: self.deps.union(&rhs.deps), + clt_ready: false, } }); @@ -120,6 +217,8 @@ impl AddAssign for PolyMatrixNorm { assert!(self.nrow == rhs.nrow && self.ncol == rhs.ncol, "matrix dims must match"); self.poly_norm += rhs.poly_norm; self.zero_rows = None; + self.deps = self.deps.union(&rhs.deps); + self.clt_ready = false; // nrow, ncol, ncol_sqrt unchanged } } @@ -127,27 +226,52 @@ impl AddAssign for PolyMatrixNorm { impl_binop_with_refs!(PolyMatrixNorm => Mul::mul(self, rhs: &PolyMatrixNorm) -> PolyMatrixNorm { assert!(self.poly_norm.ctx == rhs.poly_norm.ctx, "ctx must match"); assert!(self.ncol == rhs.nrow, "inner dims must match for multiplication"); - let scale = if let Some(z) = rhs.zero_rows { - BigDecimal::from(self.ncol as u64 - z as u64).sqrt().expect("sqrt(ncol) to failed") + let effective_inner_dim = if let Some(z) = rhs.zero_rows { self.ncol - z } else { self.ncol }; + let deps_disjoint = self.deps.is_disjoint(&rhs.deps); + let use_clt = deps_disjoint && (self.clt_ready || rhs.clt_ready); + let out_clt_ready = deps_disjoint && self.clt_ready && rhs.clt_ready; + let inner = BigDecimal::from(effective_inner_dim as u64); + let contraction = if self.poly_norm.is_const_poly || rhs.poly_norm.is_const_poly { + inner } else { - self.ncol_sqrt.clone() + inner * &self.ctx().ring_dim_sqrt * &self.ctx().ring_dim_sqrt }; - let pn = (&self.poly_norm * &rhs.poly_norm) * scale; - PolyMatrixNorm { nrow: self.nrow, ncol: rhs.ncol, ncol_sqrt: rhs.ncol_sqrt.clone(), poly_norm: pn, zero_rows: None } + let scale = if use_clt { contraction.sqrt().expect("sqrt(K) failed") } else { contraction.clone() }; + let norm = scale * &self.poly_norm.norm * &rhs.poly_norm.norm; + debug!( + operation = "matrix_mul", + k = %contraction, + lhs_norm = %self.poly_norm.norm, + rhs_norm = %rhs.poly_norm.norm, + out_norm = %norm, + lhs_deps = ?self.deps, + rhs_deps = ?rhs.deps, + deps_disjoint, + lhs_clt_ready = self.clt_ready, + rhs_clt_ready = rhs.clt_ready, + use_clt, + out_clt_ready, + rule = if use_clt { "CLT" } else { "WORST_CASE" }, + "simulator matrix norm multiplication" + ); + if out_clt_ready { + debug!(rule = "PRODUCT_CLOSURE", "simulator matrix norm product closure"); + } + PolyMatrixNorm { + nrow: self.nrow, + ncol: rhs.ncol, + ncol_sqrt: rhs.ncol_sqrt.clone(), + poly_norm: PolyNorm::new(self.clone_ctx(), norm), + zero_rows: None, + deps: self.deps.union(&rhs.deps), + clt_ready: out_clt_ready, + } }); impl MulAssign for PolyMatrixNorm { fn mul_assign(&mut self, rhs: Self) { - assert!(self.ncol == rhs.nrow, "inner dims must match for multiplication"); - let scale = if let Some(z) = rhs.zero_rows { - BigDecimal::from(self.ncol as u64 - z as u64).sqrt().expect("sqrt(ncol) to failed") - } else { - self.ncol_sqrt.clone() - }; - self.poly_norm = (self.poly_norm.clone() * rhs.poly_norm) * scale; - self.ncol = rhs.ncol; - self.ncol_sqrt = rhs.ncol_sqrt; - self.zero_rows = None; + let out = self.clone() * rhs; + *self = out; } } @@ -155,12 +279,15 @@ impl Mul<&PolyNorm> for PolyMatrixNorm { type Output = Self; fn mul(self, rhs: &PolyNorm) -> Self::Output { assert!(self.poly_norm.ctx == rhs.ctx, "ctx must match"); + let is_zero = rhs.norm.is_zero(); PolyMatrixNorm { nrow: self.nrow, ncol: self.ncol, ncol_sqrt: self.ncol_sqrt, poly_norm: self.poly_norm * rhs, zero_rows: None, + deps: if is_zero { DependencySet::empty() } else { self.deps }, + clt_ready: if is_zero { false } else { self.clt_ready }, } } } @@ -169,12 +296,15 @@ impl Mul<&PolyNorm> for &PolyMatrixNorm { type Output = PolyMatrixNorm; fn mul(self, rhs: &PolyNorm) -> Self::Output { assert!(self.poly_norm.ctx == rhs.ctx, "ctx must match"); + let is_zero = rhs.norm.is_zero(); PolyMatrixNorm { nrow: self.nrow, ncol: self.ncol, ncol_sqrt: self.ncol_sqrt.clone(), poly_norm: &self.poly_norm * rhs, zero_rows: None, + deps: if is_zero { DependencySet::empty() } else { self.deps.clone() }, + clt_ready: if is_zero { false } else { self.clt_ready }, } } } @@ -196,12 +326,15 @@ impl Mul for PolyMatrixNorm { impl Mul<&BigDecimal> for PolyMatrixNorm { type Output = Self; fn mul(self, rhs: &BigDecimal) -> Self::Output { + let is_zero = rhs.is_zero(); PolyMatrixNorm { nrow: self.nrow, ncol: self.ncol, ncol_sqrt: self.ncol_sqrt, poly_norm: self.poly_norm * rhs, zero_rows: None, + deps: if is_zero { DependencySet::empty() } else { self.deps }, + clt_ready: if is_zero { false } else { self.clt_ready }, } } } @@ -226,3 +359,137 @@ impl Mul for &PolyMatrixNorm { self.clone() * BigDecimal::from(rhs) } } + +#[cfg(test)] +mod tests { + use super::*; + use num_traits::Zero; + + fn test_ctx_with_base(base: u64) -> Arc { + Arc::new(SimulatorContext::new(BigDecimal::from(4u64), BigDecimal::from(base), 2, 6, 3)) + } + + fn balanced_digit_step(value: i64, base: i64) -> (i64, i64) { + let quotient = value.div_euclid(base); + let remainder = value.rem_euclid(base); + let half = base / 2; + if remainder < half { + (remainder, quotient) + } else if remainder > half { + (remainder - base, quotient + 1) + } else if quotient % 2 == 0 { + (half, quotient) + } else { + (half - base, quotient + 1) + } + } + + #[test] + fn gadget_decomposed_uses_balanced_digit_second_moment_sigma() { + let base = 8u64; + let half = i64::try_from(base / 2).expect("small test base fits i64"); + let mut weighted_digits = Vec::new(); + weighted_digits.push((-half, 1u64)); + for digit in (-half + 1)..half { + weighted_digits.push((digit, 2u64)); + } + weighted_digits.push((half, 1u64)); + + assert_eq!(weighted_digits.first().expect("support is nonempty").0, -half); + assert_eq!(weighted_digits.last().expect("support is nonempty").0, half); + + let weight_sum = weighted_digits.iter().map(|(_, weight)| *weight).sum::(); + assert_eq!(weight_sum, 2 * base); + let mean_numerator = weighted_digits + .iter() + .map(|(digit, weight)| i128::from(*digit) * i128::from(*weight)) + .sum::(); + assert_eq!(mean_numerator, 0); + + let second_moment_numerator = weighted_digits + .iter() + .map(|(digit, weight)| { + BigDecimal::from((digit * digit) as u64) * BigDecimal::from(*weight) + }) + .fold(BigDecimal::zero(), |acc, value| acc + value); + let second_moment = second_moment_numerator / BigDecimal::from(weight_sum); + let expected_variance = + (BigDecimal::from(base * base) + BigDecimal::from(2u64)) / BigDecimal::from(12u64); + assert_eq!(second_moment, expected_variance); + + assert_eq!(balanced_digit_step(4, base as i64), (4, 0)); + assert_eq!(balanced_digit_step(12, base as i64), (-4, 2)); + + let ctx = test_ctx_with_base(base); + let expected_sigma = expected_variance.sqrt().expect("variance sqrt should exist"); + let expected_norm = high_probability_envelope_from_sigma(&expected_sigma); + let decomposed = PolyMatrixNorm::gadget_decomposed(ctx.clone(), 3); + assert_eq!(decomposed.nrow, ctx.m_g); + assert_eq!(decomposed.ncol, 3); + assert_eq!(decomposed.poly_norm.norm, expected_norm); + + let secret_decomposed = + PolyMatrixNorm::gadget_decomposed_with_secret_size(ctx.clone(), 5, 2); + assert_eq!(secret_decomposed.nrow, 5 * ctx.log_base_q); + assert_eq!(secret_decomposed.ncol, 2); + assert_eq!(secret_decomposed.poly_norm.norm, expected_norm); + } + + #[test] + fn dependency_set_tracks_disjoint_clone_overlap_and_union() { + let ctx = test_ctx_with_base(8); + let a = DependencySet::singleton(ctx.fresh_source_id()); + let b = DependencySet::singleton(ctx.fresh_source_id()); + assert!(a.is_disjoint(&b)); + assert!(!a.is_disjoint(&a.clone())); + let union = b.union(&a); + assert_eq!(union, a.union(&b)); + assert!(!DependencySet::Unknown.is_disjoint(&a)); + } + + #[test] + fn matrix_mul_uses_clt_only_for_disjoint_ready_operands() { + let ctx = test_ctx_with_base(8); + let lhs = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, 3, BigDecimal::from(2u64)); + let rhs = PolyMatrixNorm::sample_gauss(ctx.clone(), 3, 1, BigDecimal::from(5u64)); + let out = lhs.clone() * &rhs; + let lhs_norm = high_probability_envelope_from_sigma(&BigDecimal::from(2u64)); + let rhs_norm = high_probability_envelope_from_sigma(&BigDecimal::from(5u64)); + let k = BigDecimal::from(3u64) * &ctx.ring_dim_sqrt * &ctx.ring_dim_sqrt; + assert_eq!(out.poly_norm.norm, k.sqrt().unwrap() * &lhs_norm * &rhs_norm); + assert!(out.clt_ready); + + let overlap = lhs.clone() * &lhs.clone().split_cols(1).0.transpose_like_for_test(3); + let k_overlap = BigDecimal::from(3u64) * &ctx.ring_dim_sqrt * &ctx.ring_dim_sqrt; + assert_eq!(overlap.poly_norm.norm, k_overlap * &lhs_norm * &lhs_norm); + assert!(!overlap.clt_ready); + } + + #[test] + fn matrix_mul_with_exactly_one_clt_ready_uses_clt_without_product_closure() { + let ctx = test_ctx_with_base(8); + let lhs = PolyMatrixNorm::sample_gauss(ctx.clone(), 1, 2, BigDecimal::from(2u64)); + let rhs = PolyMatrixNorm::new(ctx.clone(), 2, 1, BigDecimal::from(7u64), None) + .with_deps(DependencySet::singleton(ctx.fresh_source_id()), false); + let out = lhs * &rhs; + let k = BigDecimal::from(2u64) * &ctx.ring_dim_sqrt * &ctx.ring_dim_sqrt; + assert_eq!( + out.poly_norm.norm, + k.sqrt().unwrap() * BigDecimal::from(13u64) * BigDecimal::from(7u64) + ); + assert!(!out.clt_ready); + } + + trait TestTransposeShape { + fn transpose_like_for_test(self, nrow: usize) -> Self; + } + + impl TestTransposeShape for PolyMatrixNorm { + fn transpose_like_for_test(mut self, nrow: usize) -> Self { + self.nrow = nrow; + self.ncol = 1; + self.ncol_sqrt = BigDecimal::from(1u64); + self + } + } +} diff --git a/src/simulator/poly_norm.rs b/src/simulator/poly_norm.rs index 524e86e8..a0b293d3 100644 --- a/src/simulator/poly_norm.rs +++ b/src/simulator/poly_norm.rs @@ -1,29 +1,50 @@ use crate::{impl_binop_with_refs, simulator::SimulatorContext}; use bigdecimal::BigDecimal; -use num_traits::{FromPrimitive, One}; +use num_traits::{One, Zero}; use std::{ ops::{Add, AddAssign, Mul, MulAssign}, sync::Arc, }; +pub fn high_probability_envelope_from_sigma(sigma: &BigDecimal) -> BigDecimal { + assert!(*sigma >= BigDecimal::zero(), "sigma must be nonnegative"); + sigma * BigDecimal::from(13u64) / BigDecimal::from(2u64) +} + +pub fn maximum_coefficient_bound_from_sigma(sigma: &BigDecimal) -> BigDecimal { + high_probability_envelope_from_sigma(sigma) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct PolyNorm { pub ctx: Arc, pub norm: BigDecimal, - pub is_constant: bool, + pub sigma: BigDecimal, + pub is_const_poly: bool, + pub is_constant_poly: bool, } impl PolyNorm { + fn assert_nonnegative_norm(norm: &BigDecimal) { + assert!(*norm >= BigDecimal::zero(), "norm must be nonnegative"); + } + + fn from_norm(ctx: Arc, norm: BigDecimal, is_const_poly: bool) -> Self { + Self::assert_nonnegative_norm(&norm); + PolyNorm { ctx, sigma: norm.clone(), norm, is_const_poly, is_constant_poly: is_const_poly } + } + pub fn new(ctx: Arc, norm: BigDecimal) -> Self { - PolyNorm { ctx, norm, is_constant: false } + Self::from_norm(ctx, norm, false) } pub fn constant(ctx: Arc, norm: BigDecimal) -> Self { - PolyNorm { ctx, norm, is_constant: true } + Self::from_norm(ctx, norm, true) } - pub fn into_constant(mut self) -> Self { - self.is_constant = true; + pub fn into_constant_poly(mut self) -> Self { + self.is_const_poly = true; + self.is_constant_poly = true; self } @@ -32,51 +53,58 @@ impl PolyNorm { } pub fn sample_gauss(ctx: Arc, sigma: BigDecimal) -> Self { - let norm = sigma * BigDecimal::from_f32(6.5).unwrap(); - PolyNorm { ctx, norm, is_constant: false } + Self::from_norm(ctx, high_probability_envelope_from_sigma(&sigma), false) + } + + pub fn maximum_coefficient_bound(&self) -> BigDecimal { + self.norm.clone() + } + + pub fn max_coefficients_bound(&self) -> BigDecimal { + self.norm.clone() } } impl_binop_with_refs!(PolyNorm => Add::add(self, rhs: &PolyNorm) -> PolyNorm { assert!(self.ctx == rhs.ctx, "ctx must match"); - PolyNorm { - ctx: self.ctx.clone(), - norm: &self.norm + &rhs.norm, - is_constant: self.is_constant && rhs.is_constant, - } + PolyNorm::from_norm( + self.ctx.clone(), + &self.norm + &rhs.norm, + self.is_const_poly && rhs.is_const_poly, + ) }); impl AddAssign for PolyNorm { fn add_assign(&mut self, rhs: Self) { assert!(self.ctx == rhs.ctx, "ctx must match"); self.norm += rhs.norm; - self.is_constant = self.is_constant && rhs.is_constant; + self.sigma = self.norm.clone(); + self.is_const_poly = self.is_const_poly && rhs.is_const_poly; + self.is_constant_poly = self.is_const_poly; } } impl_binop_with_refs!(PolyNorm => Mul::mul(self, rhs: &PolyNorm) -> PolyNorm { assert!(self.ctx == rhs.ctx, "ctx must match"); let mut norm = &self.norm * &rhs.norm; - if !self.is_constant && !rhs.is_constant { + if !self.is_const_poly && !rhs.is_const_poly { norm *= &self.ctx.ring_dim_sqrt; } - PolyNorm { - ctx: self.ctx.clone(), - norm, - is_constant: self.is_constant && rhs.is_constant, - } + PolyNorm::from_norm(self.ctx.clone(), norm, self.is_const_poly && rhs.is_const_poly) }); impl MulAssign for PolyNorm { fn mul_assign(&mut self, rhs: Self) { assert!(self.ctx == rhs.ctx, "ctx must match"); - let lhs_is_constant = self.is_constant; - let rhs_is_constant = rhs.is_constant; + let lhs_is_const_poly = self.is_const_poly; + let rhs_is_const_poly = rhs.is_const_poly; self.norm = &self.norm * rhs.norm; - if !lhs_is_constant && !rhs_is_constant { + if !lhs_is_const_poly && !rhs_is_const_poly { self.norm *= &self.ctx.ring_dim_sqrt; } - self.is_constant = lhs_is_constant && rhs_is_constant; + self.sigma = self.norm.clone(); + self.is_const_poly = lhs_is_const_poly && rhs_is_const_poly; + self.is_constant_poly = self.is_const_poly; } } @@ -90,13 +118,15 @@ impl Mul for PolyNorm { impl Mul<&BigDecimal> for PolyNorm { type Output = Self; fn mul(self, rhs: &BigDecimal) -> Self::Output { - PolyNorm { ctx: self.ctx, norm: self.norm * rhs, is_constant: self.is_constant } + Self::assert_nonnegative_norm(rhs); + PolyNorm::from_norm(self.ctx, self.norm * rhs, self.is_const_poly) } } impl Mul for BigDecimal { type Output = PolyNorm; fn mul(self, rhs: PolyNorm) -> Self::Output { - PolyNorm { ctx: rhs.ctx, norm: rhs.norm * self, is_constant: rhs.is_constant } + PolyNorm::assert_nonnegative_norm(&self); + PolyNorm::from_norm(rhs.ctx, rhs.norm * self, rhs.is_const_poly) } } diff --git a/src/we/diamond_we.rs b/src/we/diamond_we.rs index 84ab95ab..d4066730 100644 --- a/src/we/diamond_we.rs +++ b/src/we/diamond_we.rs @@ -1,9 +1,10 @@ use std::{marker::PhantomData, path::PathBuf, sync::Arc}; use num_bigint::BigUint; +use tracing::info; use crate::{ - bgg::{encoding::BggEncoding, public_key::BggPublicKey, sampler::BGGPublicKeySampler}, + bgg::{encoding::BggEncoding, public_key::BggPublicKey}, circuit::{Evaluable, PolyCircuit}, func_enc::NoCircuitEvaluator, input_injector::{ @@ -160,6 +161,33 @@ where M::from_compact_bytes(&self.injector.params, &bytes) } + fn matrix_from_staging_bytes(&self, bytes: &[u8]) -> M { + M::from_cpu_staging_bytes(&self.injector.params, bytes) + } + + fn online_eval_state_bytes( + &self, + ct: &DiamondWECiphertext, + witness_digits: &[u32], + ) -> Vec> { + let states = + self.injector.online_eval(&self.artifact_dir, &ct.preprocess_out, witness_digits); + assert_eq!( + states.len(), + 1 + self.witness_size, + "DiamondWE final Diamond state count mismatch" + ); + states.into_iter().map(M::into_cpu_staging_bytes).collect() + } + + fn remove_matrix_if_exists(&self, id: &str) { + match std::fs::remove_file(self.matrix_path(id)) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => panic!("DiamondWE failed to remove stale matrix {id}: {err}"), + } + } + fn one_preimage_id() -> &'static str { "we_one_preimage" } @@ -180,105 +208,337 @@ where "we_enc_lookup_base_preimage" } - fn sample_bgg_public_keys( + fn preimage_chunk_id(id: &str, chunk_idx: usize) -> String { + format!("{id}_chunk{chunk_idx}") + } + + fn preimage_chunk_cols(total_cols: usize) -> Option { + let chunk_cols = crate::env::aux_sampling_chunk_width().min(total_cols); + (chunk_cols < total_cols).then_some(chunk_cols) + } + + fn preimage_chunk_count(total_cols: usize, chunk_cols: usize) -> usize { + assert!(total_cols > 0, "DiamondWE preimage total column count must be positive"); + total_cols.div_ceil(chunk_cols) + } + + fn preimage_chunk_bounds( + total_cols: usize, + chunk_cols: usize, + chunk_idx: usize, + ) -> (usize, usize) { + assert!(chunk_cols > 0, "DiamondWE preimage chunk column count must be positive"); + let col_start = + chunk_idx.checked_mul(chunk_cols).expect("DiamondWE preimage chunk start overflow"); + assert!( + col_start < total_cols, + "DiamondWE preimage chunk index out of range: total_cols={}, chunk_cols={}, chunk_idx={}", + total_cols, + chunk_cols, + chunk_idx + ); + let col_len = (total_cols - col_start).min(chunk_cols); + (col_start, col_len) + } + + fn sample_target_preimage( &self, - hash_key: [u8; 32], - ) -> (BggPublicKey, BggPublicKey, Vec>) { + preprocess_out: &DiamondInjectorPreprocessOut, + state_idx: usize, + target: &M, + ) -> M { + let trapdoor = TS::trapdoor_from_bytes( + &self.injector.params, + preprocess_out.final_trapdoor_bytes(state_idx), + ) + .expect("DiamondInjector final trapdoor checkpoint must decode"); + let public_matrix = preprocess_out.final_public_matrix(&self.injector.params, state_idx); + TS::new(&self.injector.params, self.injector.trapdoor_sigma).preimage( + &self.injector.params, + &trapdoor, + &public_matrix, + target, + ) + } + + fn sample_and_write_preimage_columns( + &self, + id: &str, + preprocess_out: &DiamondInjectorPreprocessOut, + state_idx: usize, + total_cols: usize, + mut target_columns: impl FnMut(usize, usize) -> M, + ) { + if let Some(chunk_cols) = Self::preimage_chunk_cols(total_cols) { + let chunk_count = Self::preimage_chunk_count(total_cols, chunk_cols); + self.remove_matrix_if_exists(id); + info!( + id, + total_cols, chunk_cols, chunk_count, "diamond we final preimage: sampling chunks" + ); + for chunk_idx in 0..chunk_count { + let (col_start, col_len) = + Self::preimage_chunk_bounds(total_cols, chunk_cols, chunk_idx); + info!( + id, + chunk_idx = chunk_idx + 1, + chunk_count, + col_start, + col_len, + "diamond we final preimage: sampling chunk" + ); + let target_chunk = target_columns(col_start, col_len); + let preimage = + self.sample_target_preimage(preprocess_out, state_idx, &target_chunk); + self.write_matrix(&Self::preimage_chunk_id(id, chunk_idx), &preimage); + } + } else { + let target = target_columns(0, total_cols); + let preimage = self.sample_target_preimage(preprocess_out, state_idx, &target); + self.write_matrix(id, &preimage); + } + } + + fn left_mul_preimage_columns( + &self, + lhs: &M, + id: &str, + total_cols: usize, + col_start: usize, + col_len: usize, + ) -> M { + assert!(col_len > 0, "DiamondWE preimage projection must request columns"); + let col_end = col_start + .checked_add(col_len) + .expect("DiamondWE preimage projection column range overflow"); + assert!( + col_end <= total_cols, + "DiamondWE preimage projection out of range: total_cols={total_cols}, col_start={col_start}, col_len={col_len}" + ); + + if self.matrix_path(id).exists() { + let preimage = self.read_matrix(id); + assert_eq!( + preimage.col_size(), + total_cols, + "DiamondWE full preimage artifact {id} has unexpected column count" + ); + let preimage = if col_start == 0 && col_len == total_cols { + preimage + } else { + preimage.slice_columns(col_start, col_end) + }; + return lhs.clone() * &preimage; + } + + let chunk_cols = Self::preimage_chunk_cols(total_cols).unwrap_or_else(|| { + panic!( + "DiamondWE missing full preimage artifact {id} and chunking is disabled for total_cols={total_cols}" + ) + }); + let first_chunk = col_start / chunk_cols; + let last_chunk = (col_end - 1) / chunk_cols; + let mut out = M::zero(&self.injector.params, lhs.row_size(), col_len); + for chunk_idx in first_chunk..=last_chunk { + let (chunk_col_start, expected_cols) = + Self::preimage_chunk_bounds(total_cols, chunk_cols, chunk_idx); + let chunk_col_end = chunk_col_start + expected_cols; + let overlap_start = col_start.max(chunk_col_start); + let overlap_end = col_end.min(chunk_col_end); + let overlap_len = overlap_end - overlap_start; + let preimage_chunk = self.read_matrix(&Self::preimage_chunk_id(id, chunk_idx)); + assert_eq!( + preimage_chunk.col_size(), + expected_cols, + "DiamondWE preimage chunk {chunk_idx} for {id} has unexpected column count" + ); + let local_start = overlap_start - chunk_col_start; + let preimage_chunk = if local_start == 0 && overlap_len == expected_cols { + preimage_chunk + } else { + preimage_chunk.slice_columns(local_start, local_start + overlap_len) + }; + let projected_chunk = lhs.clone() * &preimage_chunk; + out.copy_block_from( + &projected_chunk, + 0, + overlap_start - col_start, + 0, + 0, + lhs.row_size(), + overlap_len, + ); + } + out + } + + fn left_mul_preimage(&self, lhs: &M, id: &str, total_cols: usize) -> M { + self.left_mul_preimage_columns(lhs, id, total_cols, 0, total_cols) + } + + fn bgg_public_key_columns(&self) -> usize { + DIAMOND_SECRET_SIZE + .checked_mul(self.injector.params.modulus_digits()) + .expect("DiamondWE public-key column count overflow") + } + + fn sample_bgg_public_key(&self, hash_key: [u8; 32], idx: usize) -> BggPublicKey { let params = &self.injector.params; let mut tag = self.bgg_tag.clone(); tag.extend_from_slice(b":witness_public_keys"); - let reveal_plaintexts = vec![true; self.witness_size]; - let sampled = BGGPublicKeySampler::<[u8; 32], HS>::new(hash_key, DIAMOND_SECRET_SIZE) - .sample(params, &tag, &reveal_plaintexts); - assert_eq!( - sampled.len(), - self.witness_size + 1, - "DiamondWE public-key sampler must return one and all witness keys" + let columns = self.bgg_public_key_columns(); + let total_keys = + self.witness_size.checked_add(1).expect("DiamondWE public-key count overflow"); + assert!( + idx < total_keys, + "DiamondWE public-key index out of bounds: idx={}, total={}", + idx, + total_keys + ); + let total_cols = columns + .checked_mul(total_keys) + .expect("DiamondWE public-key total column count overflow"); + let col_start = + columns.checked_mul(idx).expect("DiamondWE public-key column start overflow"); + let matrix = HS::new().sample_hash_columns( + params, + hash_key, + &tag, + DIAMOND_SECRET_SIZE, + total_cols, + col_start, + columns, + DistType::FinRingDist, ); - let one = sampled[0].clone(); - let witness_pubkeys = sampled[1..].to_vec(); + BggPublicKey::new(matrix, true) + } + + fn k_public_key_columns(&self) -> usize { + self.bgg_public_key_columns() + } + fn sample_k_public_key(&self, hash_key: [u8; 32]) -> BggPublicKey { + let params = &self.injector.params; let mut k_tag = self.bgg_tag.clone(); k_tag.extend_from_slice(b":k_public_key"); - let k_matrix = HS::new().sample_hash(params, hash_key, &k_tag, 1, 1, DistType::FinRingDist); - let k_pubkey = BggPublicKey::new(k_matrix, false); + let k_matrix = HS::new().sample_hash( + params, + hash_key, + &k_tag, + 1, + self.k_public_key_columns(), + DistType::FinRingDist, + ); + BggPublicKey::new(k_matrix, false) + } + + #[cfg(test)] + fn sample_bgg_public_keys( + &self, + hash_key: [u8; 32], + ) -> (BggPublicKey, BggPublicKey, Vec>) { + let one = self.sample_bgg_public_key(hash_key, 0); + let witness_pubkeys = (0..self.witness_size) + .map(|bit_idx| self.sample_bgg_public_key(hash_key, bit_idx + 1)) + .collect(); + let k_pubkey = self.sample_k_public_key(hash_key); (one, k_pubkey, witness_pubkeys) } - fn sample_output_preimage( + fn gadget_row_columns(params: &::Params, col_start: usize, col_len: usize) -> M { + assert_eq!( + DIAMOND_SECRET_SIZE, 1, + "DiamondWE gadget row chunks assume the current one-row Diamond secret" + ); + let col_end = + col_start.checked_add(col_len).expect("DiamondWE gadget column range overflow"); + assert!( + col_end <= params.modulus_digits(), + "DiamondWE gadget column range out of bounds: col_start={col_start}, col_len={col_len}, modulus_digits={}", + params.modulus_digits() + ); + M::from_poly_vec_row( + params, + (col_start..col_end) + .map(|digit| M::P::from_power_of_base_to_constant(params, digit)) + .collect(), + ) + } + + fn output_preimage_target_columns( &self, - preprocess_out: &DiamondInjectorPreprocessOut, - state_idx: usize, pubkey: &BggPublicKey, top_gadget_plaintext: Option<&M::P>, bottom_gadget_plaintext: Option<&M::P>, + col_start: usize, + col_len: usize, ) -> M { let params = &self.injector.params; - let gadget = M::gadget_matrix(params, DIAMOND_SECRET_SIZE); - let cols = DIAMOND_SECRET_SIZE - .checked_mul(params.modulus_digits()) - .expect("DiamondWE final gadget column count overflow"); - let mut top = pubkey.matrix.clone(); + let col_end = + col_start.checked_add(col_len).expect("DiamondWE target column range overflow"); + let mut top = pubkey.matrix.slice_columns(col_start, col_end); if let Some(plaintext) = top_gadget_plaintext { - top = top - &(gadget.clone() * plaintext); + let gadget = Self::gadget_row_columns(params, col_start, col_len); + top = top - &(gadget * plaintext); } - let mut bottom = M::zero(params, DIAMOND_SECRET_SIZE, cols); + let mut bottom = M::zero(params, DIAMOND_SECRET_SIZE, col_len); if let Some(plaintext) = bottom_gadget_plaintext { + let gadget = Self::gadget_row_columns(params, col_start, col_len); bottom = bottom - &(gadget * plaintext); } - let target = top.concat_rows(&[&bottom]); - let (trapdoor, public_matrix) = preprocess_out.final_checkpoint(state_idx); - TS::new(params, self.injector.trapdoor_sigma).preimage( - params, - trapdoor, - public_matrix, - &target, - ) + top.concat_rows_owned(vec![bottom]) } - fn sample_k_preimage( + fn k_preimage_target_columns( &self, - preprocess_out: &DiamondInjectorPreprocessOut, k_pubkey: &BggPublicKey, + col_start: usize, + col_len: usize, ) -> M { let params = &self.injector.params; + let columns = self.k_public_key_columns(); assert_eq!( k_pubkey.matrix.size(), - (DIAMOND_SECRET_SIZE, DIAMOND_SECRET_SIZE), - "DiamondWE k public key must be a 1 x 1 public matrix" + (DIAMOND_SECRET_SIZE, columns), + "DiamondWE k public key must match k_public_key_columns" ); - let identity = M::identity(params, DIAMOND_SECRET_SIZE, None); - let target = k_pubkey.matrix.concat_rows(&[&identity]); - let (trapdoor, public_matrix) = preprocess_out.final_checkpoint(0); - TS::new(params, self.injector.trapdoor_sigma).preimage( + let col_end = + col_start.checked_add(col_len).expect("DiamondWE k target column range overflow"); + assert!( + col_end <= columns, + "DiamondWE k target column range out of bounds: col_start={col_start}, col_len={col_len}, columns={columns}" + ); + let q: Arc = params.modulus().into(); + let scale = M::P::from_biguint_to_constant(params, q.as_ref() / 2u32); + let zero = M::P::const_zero(params); + let selector = M::from_poly_vec_row( params, - trapdoor, - public_matrix, - &target, - ) + (col_start..col_end) + .map(|column| if column == 0 { scale.clone() } else { zero.clone() }) + .collect(), + ); + k_pubkey.matrix.slice_columns(col_start, col_end).concat_rows_owned(vec![selector]) } - fn sample_decoder_preimage( + fn decoder_preimage_target_columns( &self, - preprocess_out: &DiamondInjectorPreprocessOut, dec_pubkey_matrix: &M, + col_start: usize, + col_len: usize, ) -> M { let params = &self.injector.params; - let bottom = M::zero(params, DIAMOND_SECRET_SIZE, dec_pubkey_matrix.col_size()); - let target = dec_pubkey_matrix.concat_rows(&[&bottom]); - let (trapdoor, public_matrix) = preprocess_out.final_checkpoint(0); - TS::new(params, self.injector.trapdoor_sigma).preimage( - params, - trapdoor, - public_matrix, - &target, - ) + let col_end = + col_start.checked_add(col_len).expect("DiamondWE decoder target range overflow"); + let top = dec_pubkey_matrix.slice_columns(col_start, col_end); + let bottom = M::zero(params, DIAMOND_SECRET_SIZE, col_len); + top.concat_rows_owned(vec![bottom]) } - fn sample_lookup_base_preimage( + fn lookup_base_preimage_target_columns( &self, - preprocess_out: &DiamondInjectorPreprocessOut, lookup_base_matrix: &M, + col_start: usize, + col_len: usize, ) -> M { assert_eq!( lookup_base_matrix.row_size(), @@ -286,35 +546,30 @@ where "DiamondWE lookup base matrix row count must match the Diamond secret size" ); let params = &self.injector.params; - let lookup_bottom = M::zero(params, DIAMOND_SECRET_SIZE, lookup_base_matrix.col_size()); - let target = lookup_base_matrix.concat_rows(&[&lookup_bottom]); - let (trapdoor, public_matrix) = preprocess_out.final_checkpoint(0); - TS::new(params, self.injector.trapdoor_sigma).preimage( - params, - trapdoor, - public_matrix, - &target, - ) + let col_end = + col_start.checked_add(col_len).expect("DiamondWE lookup target range overflow"); + let top = lookup_base_matrix.slice_columns(col_start, col_end); + let bottom = M::zero(params, DIAMOND_SECRET_SIZE, col_len); + top.concat_rows_owned(vec![bottom]) } - fn sample_r(&self, hash_key: [u8; 32]) -> M { + fn sample_r_columns(&self, hash_key: [u8; 32], col_start: usize, col_len: usize) -> M { let mut tag = self.bgg_tag.clone(); tag.extend_from_slice(b":r"); - HS::new().sample_hash(&self.injector.params, hash_key, &tag, 1, 1, DistType::FinRingDist) - } - - fn instance_pubkeys(&self, one: &BggPublicKey, instance: &[bool]) -> Vec> { - instance - .iter() - .map(|bit| one.small_scalar_mul(&self.injector.params, &[*bit as u32])) - .collect() + HS::new().sample_hash_columns( + &self.injector.params, + hash_key, + &tag, + 1, + self.k_public_key_columns(), + col_start, + col_len, + DistType::FinRingDist, + ) } - fn instance_encodings(&self, one: &BggEncoding, instance: &[bool]) -> Vec> { - instance - .iter() - .map(|bit| one.small_scalar_mul(&self.injector.params, &[*bit as u32])) - .collect() + fn sample_r(&self, hash_key: [u8; 32]) -> M { + self.sample_r_columns(hash_key, 0, self.k_public_key_columns()) } fn pack_witness_digits(&self, witness: &[bool]) -> Vec { @@ -354,6 +609,64 @@ where let three_quarter_q = &quarter_q * 3u32; !(coeff < quarter_q || coeff > three_quarter_q) } + + fn centered_distance_to_zero( + value: &BigUint, + modulus: &BigUint, + half_modulus: &BigUint, + ) -> BigUint { + if value > half_modulus { modulus - value } else { value.clone() } + } + + fn log_noisy_plaintext_error_diagnostic(&self, noisy_plaintext: &M) { + let Some(expected_msg) = + std::env::var("DIAMOND_WE_GPU_BENCH_EXPECTED_MSG").ok().and_then(|raw| { + match raw.as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + } + }) + else { + return; + }; + let selected_bound_bits = + std::env::var("DIAMOND_WE_GPU_BENCH_SELECTED_NOISY_PLAINTEXT_ERROR_BITS") + .ok() + .and_then(|raw| raw.parse::().ok()); + let q = self.injector.params.modulus(); + let q: std::sync::Arc = q.into(); + let q = q.as_ref(); + let half_q = q / 2u32; + let coeffs = noisy_plaintext.entry(0, 0).coeffs_biguints(); + let mut max_error = BigUint::from(0u32); + let mut decode_coeff_error = BigUint::from(0u32); + for (coeff_idx, coeff) in coeffs.iter().enumerate() { + let error = if expected_msg && coeff_idx == 0 { + if coeff >= &half_q { coeff - &half_q } else { &half_q - coeff } + } else { + Self::centered_distance_to_zero(coeff, q, &half_q) + }; + if coeff_idx == 0 { + decode_coeff_error = error.clone(); + } + if error > max_error { + max_error = error; + } + } + let decode_coeff_error_bits = decode_coeff_error.bits(); + let max_poly_error_bits = max_error.bits(); + let within_selected_bound = + selected_bound_bits.map(|bound_bits| max_poly_error_bits <= bound_bits); + info!( + expected_msg, + decode_coeff_error_bits, + max_poly_error_bits, + selected_bound_bits, + within_selected_bound, + "diamond we dec: noisy plaintext error diagnostic" + ); + } } impl WitnessEnc @@ -393,24 +706,26 @@ where ); let params = &self.injector.params; - let k = if *msg { - let q: std::sync::Arc = params.modulus().into(); - M::P::from_biguint_to_constant(params, q.as_ref() / 2u32) - } else { - M::P::const_zero(params) - }; + let k = if *msg { M::P::const_one(params) } else { M::P::const_zero(params) }; let preprocess_out = self.injector.preprocess(&self.artifact_dir, &k); let hash_key = rand::random::<[u8; 32]>(); - let (one_pubkey, k_pubkey, witness_pubkeys) = self.sample_bgg_public_keys(hash_key); - let instance_pubkeys = self.instance_pubkeys(&one_pubkey, instance); - let mut input_pubkeys = witness_pubkeys.clone(); - input_pubkeys.extend(instance_pubkeys); + let one_pubkey = self.sample_bgg_public_key(hash_key, 0); + let one_pubkey_compact = one_pubkey.clone().to_compact(); + let zero_pubkey_compact = one_pubkey.small_scalar_mul(params, &[0]).to_compact(); + let mut input_pubkey_compacts = Vec::with_capacity(circuit.num_input()); + for bit_idx in 0..self.witness_size { + input_pubkey_compacts + .push(self.sample_bgg_public_key(hash_key, bit_idx + 1).to_compact()); + } + input_pubkey_compacts.extend(instance.iter().map(|bit| { + if *bit { one_pubkey_compact.clone() } else { zero_pubkey_compact.clone() } + })); let out_pubkey = circuit - .eval( + .eval_from_compacts( params, - one_pubkey.clone(), - input_pubkeys, + one_pubkey_compact, + input_pubkey_compacts, self.pk_lookup_evaluator.as_ref(), self.pk_slot_transfer_evaluator .as_ref() @@ -422,40 +737,74 @@ where .expect("DiamondWE circuit must produce one output public key"); let one_plaintext = M::P::const_one(params); - let one_preimage = self.sample_output_preimage( + self.sample_and_write_preimage_columns( + Self::one_preimage_id(), &preprocess_out, 0, - &one_pubkey, - Some(&one_plaintext), - None, + one_pubkey.matrix.col_size(), + |col_start, col_len| { + self.output_preimage_target_columns( + &one_pubkey, + Some(&one_plaintext), + None, + col_start, + col_len, + ) + }, ); - self.write_matrix(Self::one_preimage_id(), &one_preimage); - for (bit_idx, pubkey) in witness_pubkeys.iter().enumerate() { + for bit_idx in 0..self.witness_size { + let pubkey = self.sample_bgg_public_key(hash_key, bit_idx + 1); let digit_idx = bit_idx / self.injector.batch_bits(); let bit_in_digit = bit_idx % self.injector.batch_bits(); let state_idx = self.injector.bit_state_idx(digit_idx, bit_in_digit); - let preimage = self.sample_output_preimage( + self.sample_and_write_preimage_columns( + &Self::witness_preimage_id(bit_idx), &preprocess_out, state_idx, - pubkey, - None, - Some(&one_plaintext), + pubkey.matrix.col_size(), + |col_start, col_len| { + self.output_preimage_target_columns( + &pubkey, + None, + Some(&one_plaintext), + col_start, + col_len, + ) + }, ); - self.write_matrix(&Self::witness_preimage_id(bit_idx), &preimage); } - let k_preimage = self.sample_k_preimage(&preprocess_out, &k_pubkey); - self.write_matrix(Self::k_preimage_id(), &k_preimage); + let k_pubkey = self.sample_k_public_key(hash_key); + self.sample_and_write_preimage_columns( + Self::k_preimage_id(), + &preprocess_out, + 0, + k_pubkey.matrix.col_size(), + |col_start, col_len| self.k_preimage_target_columns(&k_pubkey, col_start, col_len), + ); if let Some(lookup_base_matrix) = self.enc_lookup_base_matrix.as_ref() { - let lookup_base_preimage = - self.sample_lookup_base_preimage(&preprocess_out, lookup_base_matrix); - self.write_matrix(Self::enc_lookup_base_preimage_id(), &lookup_base_preimage); + self.sample_and_write_preimage_columns( + Self::enc_lookup_base_preimage_id(), + &preprocess_out, + 0, + lookup_base_matrix.col_size(), + |col_start, col_len| { + self.lookup_base_preimage_target_columns(lookup_base_matrix, col_start, col_len) + }, + ); } let r = self.sample_r(hash_key); let dec_term = one_pubkey - &out_pubkey; let dec_pubkey_matrix = k_pubkey.matrix + &dec_term.matrix.mul_decompose(&r); - let decoder_preimage = self.sample_decoder_preimage(&preprocess_out, &dec_pubkey_matrix); - self.write_matrix(Self::decoder_preimage_id(), &decoder_preimage); + self.sample_and_write_preimage_columns( + Self::decoder_preimage_id(), + &preprocess_out, + 0, + dec_pubkey_matrix.col_size(), + |col_start, col_len| { + self.decoder_preimage_target_columns(&dec_pubkey_matrix, col_start, col_len) + }, + ); DiamondWECiphertext { circuit, instance: instance.clone(), hash_key, preprocess_out } } @@ -467,63 +816,256 @@ where ct.circuit.num_input(), "DiamondWE witness_size + instance length must match the circuit input size" ); + let started = std::time::Instant::now(); let params = &self.injector.params; let witness_digits = self.pack_witness_digits(witness); - let states = - self.injector.online_eval(&self.artifact_dir, &ct.preprocess_out, &witness_digits); - assert_eq!( - states.len(), - 1 + self.witness_size, - "DiamondWE final Diamond state count mismatch" + info!( + witness_size = self.witness_size, + instance_len = ct.instance.len(), + circuit_inputs = ct.circuit.num_input(), + "diamond we dec: begin online_eval" + ); + let mut state_bytes = self + .online_eval_state_bytes(ct, &witness_digits) + .into_iter() + .map(Some) + .collect::>(); + let root_state_bytes = + state_bytes[0].take().expect("DiamondWE root state bytes must be present"); + let state_byte_lens = state_bytes + .iter() + .enumerate() + .filter_map(|(idx, bytes)| bytes.as_ref().map(|bytes| (idx, bytes.len()))) + .collect::>(); + info!( + elapsed_ms = started.elapsed().as_millis(), + state_count = state_bytes.len(), + root_state_bytes = root_state_bytes.len(), + ?state_byte_lens, + bgg_public_key_columns = self.bgg_public_key_columns(), + k_public_key_columns = self.k_public_key_columns(), + "diamond we dec: online_eval states staged" + ); + + info!( + rows = DIAMOND_SECRET_SIZE, + cols = self.bgg_public_key_columns(), + "diamond we dec: sample one public key begin" + ); + let one_pubkey = self.sample_bgg_public_key(ct.hash_key, 0); + info!( + rows = one_pubkey.matrix.row_size(), + cols = one_pubkey.matrix.col_size(), + "diamond we dec: sample one public key done" + ); + let mut input_encoding_compacts = + Vec::with_capacity(self.witness_size.saturating_add(ct.instance.len())); + for bit_idx in 0..self.witness_size { + info!( + bit_idx, + rows = DIAMOND_SECRET_SIZE, + cols = self.bgg_public_key_columns(), + "diamond we dec: sample witness public key begin" + ); + let pubkey = self.sample_bgg_public_key(ct.hash_key, bit_idx + 1); + info!( + bit_idx, + rows = pubkey.matrix.row_size(), + cols = pubkey.matrix.col_size(), + "diamond we dec: sample witness public key done" + ); + let digit_idx = bit_idx / self.injector.batch_bits(); + let bit_in_digit = bit_idx % self.injector.batch_bits(); + let state_idx = self.injector.bit_state_idx(digit_idx, bit_in_digit); + let plaintext = M::P::from_usize_to_constant( + params, + self.injector.digit_bit_value(witness_digits[digit_idx] as usize, bit_in_digit), + ); + let staged = state_bytes[state_idx] + .take() + .expect("DiamondWE witness state bytes must be present exactly once"); + info!( + bit_idx, + state_idx, + staged_bytes = staged.len(), + "diamond we dec: reload witness state begin" + ); + let state = self.matrix_from_staging_bytes(staged.as_ref()); + info!( + bit_idx, + state_idx, + rows = state.row_size(), + cols = state.col_size(), + "diamond we dec: reload witness state done" + ); + info!( + bit_idx, + preimage_id = %Self::witness_preimage_id(bit_idx), + total_cols = pubkey.matrix.col_size(), + "diamond we dec: witness left_mul_preimage begin" + ); + let vector = self.left_mul_preimage( + &state, + &Self::witness_preimage_id(bit_idx), + pubkey.matrix.col_size(), + ); + info!( + bit_idx, + rows = vector.row_size(), + cols = vector.col_size(), + "diamond we dec: witness left_mul_preimage done" + ); + drop(state); + info!(bit_idx, "diamond we dec: build witness output encoding begin"); + let encoding = self.injector.build_output_encoding(vector, pubkey, Some(plaintext)); + info!(bit_idx, "diamond we dec: compact witness output encoding begin"); + input_encoding_compacts.push(encoding.to_compact()); + info!( + bit_idx, + input_encoding_count = input_encoding_compacts.len(), + "diamond we dec: build witness output encoding done" + ); + } + drop(state_bytes); + info!( + input_encoding_count = input_encoding_compacts.len(), + elapsed_ms = started.elapsed().as_millis(), + "diamond we dec: witness encodings complete" + ); + + info!(staged_bytes = root_state_bytes.len(), "diamond we dec: reload root state begin"); + let root_state = self.matrix_from_staging_bytes(&root_state_bytes); + info!( + rows = root_state.row_size(), + cols = root_state.col_size(), + "diamond we dec: reload root state done" ); + info!( + total_cols = one_pubkey.matrix.col_size(), + "diamond we dec: one left_mul_preimage begin" + ); + let one_vector = self.left_mul_preimage( + &root_state, + Self::one_preimage_id(), + one_pubkey.matrix.col_size(), + ); + info!( + rows = one_vector.row_size(), + cols = one_vector.col_size(), + "diamond we dec: one left_mul_preimage done" + ); + let owned_enc_lookup_evaluator = + if let Some(factory) = self.enc_lookup_evaluator_factory.as_ref() { + let lookup_base_cols = self + .enc_lookup_base_matrix + .as_ref() + .expect("DiamondWE encoding lookup-base preimage requires a lookup base matrix") + .col_size(); + info!(lookup_base_cols, "diamond we dec: lookup-base left_mul_preimage begin"); + let c_b0 = self.left_mul_preimage( + &root_state, + Self::enc_lookup_base_preimage_id(), + lookup_base_cols, + ); + info!( + rows = c_b0.row_size(), + cols = c_b0.col_size(), + "diamond we dec: lookup-base left_mul_preimage done" + ); + info!("diamond we dec: build owned encoding lookup evaluator begin"); + let evaluator = factory(c_b0); + info!("diamond we dec: build owned encoding lookup evaluator done"); + Some(evaluator) + } else { + info!("diamond we dec: no owned encoding lookup evaluator needed"); + None + }; + let k_public_key_columns = self.k_public_key_columns(); + info!( + total_cols = k_public_key_columns, + col_start = 0usize, + col_len = 1usize, + "diamond we dec: k left_mul_preimage_columns begin" + ); + let k_vector = self.left_mul_preimage_columns( + &root_state, + Self::k_preimage_id(), + k_public_key_columns, + 0, + 1, + ); + info!( + rows = k_vector.row_size(), + cols = k_vector.col_size(), + "diamond we dec: k left_mul_preimage_columns done" + ); + info!( + total_cols = k_public_key_columns, + col_start = 0usize, + col_len = 1usize, + "diamond we dec: decoder left_mul_preimage_columns begin" + ); + let decoder = self.left_mul_preimage_columns( + &root_state, + Self::decoder_preimage_id(), + k_public_key_columns, + 0, + 1, + ); + info!( + rows = decoder.row_size(), + cols = decoder.col_size(), + "diamond we dec: decoder left_mul_preimage_columns done" + ); + drop(root_state); + info!("diamond we dec: root state dropped"); - let (one_pubkey, k_pubkey, witness_pubkeys) = self.sample_bgg_public_keys(ct.hash_key); + info!("diamond we dec: build one output encoding begin"); let one_encoding = self.injector.build_output_encoding( - states[0].clone() * &self.read_matrix(Self::one_preimage_id()), + one_vector, one_pubkey, Some(M::P::const_one(params)), ); - let k_encoding = self.injector.build_output_encoding( - states[0].clone() * &self.read_matrix(Self::k_preimage_id()), - k_pubkey, - None, + info!( + vector_rows = one_encoding.vector.row_size(), + vector_cols = one_encoding.vector.col_size(), + pubkey_rows = one_encoding.pubkey.matrix.row_size(), + pubkey_cols = one_encoding.pubkey.matrix.col_size(), + "diamond we dec: build one output encoding done" + ); + info!( + instance_len = ct.instance.len(), + input_encoding_count = input_encoding_compacts.len(), + "diamond we dec: compact input encodings begin" + ); + let zero_encoding_compact = one_encoding.small_scalar_mul(params, &[0]).to_compact(); + let one_encoding_compact = one_encoding.to_compact(); + let witness_input_count = input_encoding_compacts.len(); + input_encoding_compacts.extend(ct.instance.iter().map(|bit| { + if *bit { one_encoding_compact.clone() } else { zero_encoding_compact.clone() } + })); + info!( + witness_input_count, + instance_len = ct.instance.len(), + input_encoding_count = input_encoding_compacts.len(), + "diamond we dec: compact input encodings done" ); - let mut input_encodings = witness_pubkeys - .into_iter() - .enumerate() - .map(|(bit_idx, pubkey)| { - let digit_idx = bit_idx / self.injector.batch_bits(); - let bit_in_digit = bit_idx % self.injector.batch_bits(); - let state_idx = self.injector.bit_state_idx(digit_idx, bit_in_digit); - let plaintext = M::P::from_usize_to_constant( - params, - self.injector.digit_bit_value(witness_digits[digit_idx] as usize, bit_in_digit), - ); - self.injector.build_output_encoding( - states[state_idx].clone() * - &self.read_matrix(&Self::witness_preimage_id(bit_idx)), - pubkey, - Some(plaintext), - ) - }) - .collect::>(); - input_encodings.extend(self.instance_encodings(&one_encoding, &ct.instance)); - let owned_enc_lookup_evaluator = - self.enc_lookup_evaluator_factory.as_ref().map(|factory| { - let lookup_base_preimage = self.read_matrix(Self::enc_lookup_base_preimage_id()); - let c_b0 = states[0].clone() * &lookup_base_preimage; - factory(c_b0) - }); let enc_lookup_evaluator = owned_enc_lookup_evaluator.as_ref().or(self.enc_lookup_evaluator.as_ref()); - + info!( + input_encoding_count = input_encoding_compacts.len(), + has_enc_lookup_evaluator = enc_lookup_evaluator.is_some(), + has_enc_slot_transfer_evaluator = self.enc_slot_transfer_evaluator.is_some(), + elapsed_ms = started.elapsed().as_millis(), + "diamond we dec: circuit eval begin" + ); let out_encoding = ct .circuit - .eval( + .eval_from_compacts( params, - one_encoding.clone(), - input_encodings, + one_encoding_compact.clone(), + input_encoding_compacts, enc_lookup_evaluator, self.enc_slot_transfer_evaluator .as_ref() @@ -533,22 +1075,92 @@ where .into_iter() .next() .expect("DiamondWE circuit must produce one output encoding"); - let r = self.sample_r(ct.hash_key); - let dec_term = one_encoding - &out_encoding; - let dec_encoding_vector = k_encoding.vector + &dec_term.vector.mul_decompose(&r); - let decoder = states[0].clone() * &self.read_matrix(Self::decoder_preimage_id()); - let noisy_plaintext = decoder - &dec_encoding_vector; - self.decode_noisy_bool(&noisy_plaintext) + let one_encoding = BggEncoding::::from_compact(params, &one_encoding_compact); + info!( + vector_rows = out_encoding.vector.row_size(), + vector_cols = out_encoding.vector.col_size(), + pubkey_rows = out_encoding.pubkey.matrix.row_size(), + pubkey_cols = out_encoding.pubkey.matrix.col_size(), + elapsed_ms = started.elapsed().as_millis(), + "diamond we dec: circuit eval done" + ); + drop(owned_enc_lookup_evaluator); + info!("diamond we dec: owned lookup evaluator dropped"); + + info!("diamond we dec: sample r decode column begin"); + let r_decode_col = self.sample_r_columns(ct.hash_key, 0, 1); + info!( + rows = r_decode_col.row_size(), + cols = r_decode_col.col_size(), + "diamond we dec: sample r decode column done" + ); + let BggEncoding { vector: mut dec_term_vector, .. } = one_encoding; + let BggEncoding { vector: out_vector, .. } = out_encoding; + info!( + dec_term_rows = dec_term_vector.row_size(), + dec_term_cols = dec_term_vector.col_size(), + out_rows = out_vector.row_size(), + out_cols = out_vector.col_size(), + "diamond we dec: subtract output vector begin" + ); + dec_term_vector.sub_in_place(&out_vector); + info!("diamond we dec: subtract output vector done"); + drop(out_vector); + info!("diamond we dec: dropped output vector"); + info!("diamond we dec: dec term mul_decompose begin"); + let dec_term_projection = dec_term_vector.mul_decompose(&r_decode_col); + info!( + rows = dec_term_projection.row_size(), + cols = dec_term_projection.col_size(), + "diamond we dec: dec term mul_decompose done" + ); + drop(dec_term_vector); + drop(r_decode_col); + info!("diamond we dec: dropped dec term vector and r decode column"); + let mut dec_encoding_vector = k_vector; + info!( + rows = dec_encoding_vector.row_size(), + cols = dec_encoding_vector.col_size(), + "diamond we dec: add dec term projection begin" + ); + dec_encoding_vector.add_in_place(&dec_term_projection); + info!("diamond we dec: add dec term projection done"); + drop(dec_term_projection); + let mut noisy_plaintext = decoder; + info!( + rows = noisy_plaintext.row_size(), + cols = noisy_plaintext.col_size(), + "diamond we dec: subtract dec encoding from decoder begin" + ); + noisy_plaintext.sub_in_place(&dec_encoding_vector); + info!("diamond we dec: subtract dec encoding from decoder done"); + drop(dec_encoding_vector); + info!( + elapsed_ms = started.elapsed().as_millis(), + "diamond we dec: decode noisy bool begin" + ); + self.log_noisy_plaintext_error_diagnostic(&noisy_plaintext); + let decoded = self.decode_noisy_bool(&noisy_plaintext); + info!( + decoded, + elapsed_ms = started.elapsed().as_millis(), + "diamond we dec: decode noisy bool done" + ); + decoded } } #[cfg(test)] mod tests { + use std::sync::{Mutex, OnceLock}; + use keccak_asm::Keccak256; use tempfile::tempdir; use super::*; use crate::{ + __PAIR, __TestState, + bgg::sampler::BGGPublicKeySampler, circuit::PolyCircuit, matrix::dcrt_poly::DCRTPolyMatrix, poly::dcrt::{params::DCRTPolyParams, poly::DCRTPoly}, @@ -558,8 +1170,127 @@ mod tests { }, }; + fn diamond_we_env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + struct EnvVarGuard { + key: &'static str, + old_value: Option, + } + + impl EnvVarGuard { + fn set(key: &'static str, value: &str) -> Self { + let old_value = std::env::var(key).ok(); + unsafe { std::env::set_var(key, value) }; + Self { key, old_value } + } + } + + impl Drop for EnvVarGuard { + fn drop(&mut self) { + if let Some(value) = &self.old_value { + unsafe { std::env::set_var(self.key, value) }; + } else { + unsafe { std::env::remove_var(self.key) }; + } + } + } + + type TestDiamondWE = DiamondWE< + DCRTPolyMatrix, + DCRTPolyUniformSampler, + DCRTPolyHashSampler, + DCRTPolyTrapdoorSampler, + >; + + #[test] + fn test_diamond_we_column_window_public_keys_match_contiguous_sampler() { + let params = DCRTPolyParams::default(); + let witness_size = 3; + let hash_key = [7u8; 32]; + let tag = b"diamond_we_public_key_columns_test".to_vec(); + + let injector = DiamondInjector::< + DCRTPolyMatrix, + DCRTPolyUniformSampler, + DCRTPolyHashSampler, + DCRTPolyTrapdoorSampler, + >::new(params.clone(), 1, 4, 2, 4.578, 0.0); + let dir = tempdir().expect("temporary DiamondWE artifact directory should be created"); + let we = DiamondWE::new(injector, witness_size, dir.path(), tag.clone()); + + let mut full_tag = tag; + full_tag.extend_from_slice(b":witness_public_keys"); + let expected = BGGPublicKeySampler::<[u8; 32], DCRTPolyHashSampler>::new( + hash_key, + DIAMOND_SECRET_SIZE, + ) + .sample(¶ms, &full_tag, &vec![true; witness_size]); + + let actual_one = we.sample_bgg_public_key(hash_key, 0); + assert_eq!(actual_one, expected[0]); + for bit_idx in 0..witness_size { + let actual = we.sample_bgg_public_key(hash_key, bit_idx + 1); + assert_eq!(actual, expected[bit_idx + 1]); + } + + let (one, k_pubkey, witness_pubkeys) = we.sample_bgg_public_keys(hash_key); + assert_eq!(one, expected[0]); + assert_eq!(witness_pubkeys, expected[1..].to_vec()); + + let mut k_tag = we.bgg_tag.clone(); + k_tag.extend_from_slice(b":k_public_key"); + let expected_k = DCRTPolyHashSampler::::new().sample_hash( + ¶ms, + hash_key, + &k_tag, + 1, + we.k_public_key_columns(), + DistType::FinRingDist, + ); + assert_eq!(k_pubkey, BggPublicKey::new(expected_k, false)); + } + + #[test] + fn test_diamond_we_k_preimage_target_uses_hidden_plaintext_selector() { + let params = DCRTPolyParams::default(); + let witness_size = 2; + let hash_key = [9u8; 32]; + + let injector = DiamondInjector::< + DCRTPolyMatrix, + DCRTPolyUniformSampler, + DCRTPolyHashSampler, + DCRTPolyTrapdoorSampler, + >::new(params.clone(), 1, 4, 2, 4.578, 0.0); + let dir = tempdir().expect("temporary DiamondWE artifact directory should be created"); + let we = DiamondWE::new( + injector, + witness_size, + dir.path(), + b"diamond_we_k_preimage_target_test".to_vec(), + ); + + let k_pubkey = we.sample_k_public_key(hash_key); + let columns = we.k_public_key_columns(); + let target = we.k_preimage_target_columns(&k_pubkey, 0, columns); + assert_eq!(target.size(), (2 * DIAMOND_SECRET_SIZE, columns)); + assert_eq!(target.slice_rows(0, DIAMOND_SECRET_SIZE), k_pubkey.matrix); + + let q: std::sync::Arc = params.modulus().into(); + let scale = DCRTPoly::from_biguint_to_constant(¶ms, q.as_ref() / 2u32); + let expected_bottom = DCRTPolyMatrix::unit_row_vector(¶ms, columns, 0) * &scale; + assert_eq!( + target.slice_rows(DIAMOND_SECRET_SIZE, 2 * DIAMOND_SECRET_SIZE), + expected_bottom + ); + } + #[test] fn test_diamond_we_constant_one_circuit_round_trip() { + let _env_lock = diamond_we_env_lock().lock().expect("DiamondWE env lock poisoned"); let params = DCRTPolyParams::default(); let witness_size = 2; let instance = vec![true]; @@ -585,8 +1316,66 @@ mod tests { } } + #[test] + #[sequential_test::sequential] + fn test_diamond_we_chunked_preimages_round_trip_and_artifacts() { + let _env_lock = diamond_we_env_lock().lock().expect("DiamondWE env lock poisoned"); + let _chunk_cols = EnvVarGuard::set("AUX_SAMPLING_CHUNK_WIDTH", "20"); + + let params = DCRTPolyParams::default(); + let witness_size = 2; + let instance = vec![true]; + let mut circuit = PolyCircuit::::new(); + circuit.input(witness_size + instance.len()); + let one = circuit.const_one_gate(); + circuit.output(vec![one]); + + let witness = vec![false, true]; + let injector = DiamondInjector::< + DCRTPolyMatrix, + DCRTPolyUniformSampler, + DCRTPolyHashSampler, + DCRTPolyTrapdoorSampler, + >::new(params.clone(), 1, 4, 2, 4.578, 0.0); + let dir = tempdir().expect("temporary DiamondWE artifact directory should be created"); + let we = DiamondWE::new( + injector, + witness_size, + dir.path(), + b"diamond_we_chunked_preimage_test".to_vec(), + ); + + let ct = we.enc(&true, circuit, &instance); + assert_eq!(we.dec(&ct, &witness), true); + + let total_cols = params.modulus_digits(); + let chunk_cols = crate::env::aux_sampling_chunk_width().min(total_cols); + let chunk_count = TestDiamondWE::preimage_chunk_count(total_cols, chunk_cols); + assert_eq!(chunk_cols, 20); + assert_eq!(chunk_count, 2); + assert!( + !we.matrix_path(TestDiamondWE::one_preimage_id()).exists(), + "chunked preimage mode should not write the legacy full one-preimage artifact" + ); + assert!( + !dir.path() + .join(format!("{}.preimage_chunks.json", TestDiamondWE::one_preimage_id())) + .exists(), + "chunked preimage mode should not write extra JSON metadata" + ); + for chunk_idx in 0..chunk_count { + let chunk_id = + TestDiamondWE::preimage_chunk_id(TestDiamondWE::one_preimage_id(), chunk_idx); + assert!( + we.matrix_path(&chunk_id).exists(), + "chunked one-preimage artifact {chunk_id} should exist" + ); + } + } + #[test] fn test_diamond_we_witness_dependent_circuit_round_trip() { + let _env_lock = diamond_we_env_lock().lock().expect("DiamondWE env lock poisoned"); let params = DCRTPolyParams::default(); let witness_size = 2; let instance = vec![false]; diff --git a/src/we/diamond_we/bench_estimator.rs b/src/we/diamond_we/bench_estimator.rs index 6814a43d..077240ed 100644 --- a/src/we/diamond_we/bench_estimator.rs +++ b/src/we/diamond_we/bench_estimator.rs @@ -908,7 +908,7 @@ mod tests { } fn estimate_large_scalar_mul(&self, _scalar: &[BigUint]) -> CircuitBenchEstimate { - CircuitBenchEstimate::new(6.5, 6.5) + CircuitBenchEstimate::new(6.6, 6.6) } fn estimate_slot_transfer( diff --git a/src/we/diamond_we/simulation.rs b/src/we/diamond_we/simulation.rs index f211ca9c..189640a0 100644 --- a/src/we/diamond_we/simulation.rs +++ b/src/we/diamond_we/simulation.rs @@ -18,9 +18,11 @@ use super::DiamondWE; /// Error-growth summary for the DiamondWE online decryption path. /// /// DiamondWE does not use the DiamondIO PRF/noise-refresh path, so this -/// simulation records the Diamond input-injection bounds, the per-input BGG -/// encoding bounds used to evaluate the witness circuit, the circuit output -/// bound, and the final decoder cancellation bound. +/// simulation records the Diamond input-injection sigma-mode norms, the +/// per-input BGG encoding norms used to evaluate the witness circuit, the +/// circuit output norm, and the final decoder cancellation norm. The final +/// noisy plaintext sigma is converted once to a maximum coefficient bound for +/// correctness checks and selected-parameter reporting. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiamondWEErrorSimulation { pub input_injection: DiamondInputErrorSimulation, @@ -32,6 +34,7 @@ pub struct DiamondWEErrorSimulation { pub dec_encoding_error: ErrorNorm, pub decoder_projection_error: PolyMatrixNorm, pub noisy_plaintext_error: PolyMatrixNorm, + pub noisy_plaintext_error_bound: BigDecimal, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -45,7 +48,7 @@ pub struct DiamondWECrtDepthSearchResult { } fn diamond_we_decode_margin(params: &::Params) -> BigDecimal { - let q: std::sync::Arc = params.modulus().into(); + let q: std::sync::Arc = params.modulus(); BigDecimal::from(BigInt::from(q.as_ref() / 4u32)) } @@ -53,7 +56,7 @@ fn diamond_we_correctness_margin_holds( simulation: &DiamondWEErrorSimulation, margin: &BigDecimal, ) -> bool { - simulation.noisy_plaintext_error.poly_norm.norm < *margin + simulation.noisy_plaintext_error_bound < *margin } pub fn diamond_we_find_crt_depth( @@ -99,7 +102,7 @@ where .checked_shl(min_log_ring_dim.try_into().expect("min_log_ring_dim must fit in u32")) .expect("minimum ring_dim shift overflow"); let probe_candidate = build_candidate(min_ring_dim, high); - let Some(ring_dim_search) = sim_utils::select_min_secure_ring_dim_gaussian_only( + let ring_dim_search = sim_utils::select_min_secure_ring_dim_gaussian_only( "DiamondWE", high, min_log_ring_dim, @@ -112,9 +115,7 @@ where let candidate = build_candidate(ring_dim, high); candidate.injector.params.clone() }, - ) else { - return None; - }; + )?; let candidate = build_candidate(ring_dim_search.ring_dim, high); let slot_transfer_evaluator = slot_transfer_evaluator .map(|evaluator| evaluator as &dyn SlotTransferEvaluator); @@ -125,7 +126,8 @@ where debug!( crt_depth = high, valid, - noisy_plaintext_error = %simulation.noisy_plaintext_error.poly_norm.norm, + noisy_plaintext_error_sigma = %simulation.noisy_plaintext_error.poly_norm.sigma, + noisy_plaintext_error_bound = %simulation.noisy_plaintext_error_bound, decode_margin = %margin, "DiamondWE CRT-depth upper-bound candidate evaluated" ); @@ -159,7 +161,7 @@ where .checked_shl(min_log_ring_dim.try_into().expect("min_log_ring_dim must fit in u32")) .expect("minimum ring_dim shift overflow"); let probe_candidate = build_candidate(min_ring_dim, crt_depth); - let Some(ring_dim_search) = sim_utils::select_min_secure_ring_dim_gaussian_only( + let ring_dim_search = sim_utils::select_min_secure_ring_dim_gaussian_only( "DiamondWE", crt_depth, min_log_ring_dim, @@ -172,9 +174,7 @@ where let candidate = build_candidate(ring_dim, crt_depth); candidate.injector.params.clone() }, - ) else { - return None; - }; + )?; let candidate = build_candidate(ring_dim_search.ring_dim, crt_depth); let slot_transfer_evaluator = slot_transfer_evaluator .map(|evaluator| evaluator as &dyn SlotTransferEvaluator); @@ -185,7 +185,8 @@ where debug!( crt_depth, valid, - noisy_plaintext_error = %simulation.noisy_plaintext_error.poly_norm.norm, + noisy_plaintext_error_sigma = %simulation.noisy_plaintext_error.poly_norm.sigma, + noisy_plaintext_error_bound = %simulation.noisy_plaintext_error_bound, decode_margin = %margin, "DiamondWE CRT-depth candidate evaluated" ); @@ -254,15 +255,15 @@ where let ctx = input_injection.output_preimage.clone_ctx(); let one_plaintext = PolyNorm::one(ctx.clone()); let q = self.injector.params.modulus(); - let q: std::sync::Arc = q.into(); let hidden_plaintext = PolyNorm::constant(ctx.clone(), BigDecimal::from(BigInt::from(q.as_ref() / 2u32))); - let k_preimage = PolyMatrixNorm::new( - ctx.clone(), + let k_preimage = PolyMatrixNorm::from_parts( input_injection.output_preimage.nrow, 1, - input_injection.output_preimage.poly_norm.norm.clone(), + input_injection.output_preimage.poly_norm.clone(), None, + input_injection.output_preimage.deps.clone(), + input_injection.output_preimage.clt_ready, ); let projected_state_error = |state_idx: usize| { @@ -308,6 +309,7 @@ where let dec_encoding_matrix = k_encoding_error.matrix_norm.clone() + &r_scaled_difference; let decoder_projection_error = input_injection.state_errors[0].clone() * &k_preimage; let noisy_plaintext_error = decoder_projection_error.clone() + &dec_encoding_matrix; + let noisy_plaintext_error_bound = noisy_plaintext_error.maximum_coefficient_bound(); DiamondWEErrorSimulation { input_injection, @@ -319,6 +321,7 @@ where dec_encoding_error: ErrorNorm::new(hidden_plaintext, dec_encoding_matrix), decoder_projection_error, noisy_plaintext_error, + noisy_plaintext_error_bound, } } } @@ -361,6 +364,12 @@ mod tests { DiamondWE::new(injector, witness_size, dir.path(), b"diamond_we_sim_test".to_vec()); let simulation = we.simulate_error_growth(&circuit, None::<&NoCircuitEvaluator>, None); + let q: std::sync::Arc = we.injector.params.modulus().into(); + let expected_hidden_plaintext = PolyNorm::constant( + simulation.k_encoding_error.clone_ctx(), + BigDecimal::from(BigInt::from(q.as_ref() / 2u32)), + ); + assert_eq!(simulation.k_encoding_error.plaintext_norm, expected_hidden_plaintext); assert_eq!(simulation.input_injection.state_errors.len(), 1 + witness_size); assert_eq!(simulation.witness_encoding_errors.len(), witness_size); assert_eq!(simulation.instance_encoding_errors.len(), 1); diff --git a/tests/test_ggh15_modp_chain.rs b/tests/test_ggh15_modp_chain.rs index 96ecbcc9..eac871ca 100644 --- a/tests/test_ggh15_modp_chain.rs +++ b/tests/test_ggh15_modp_chain.rs @@ -126,7 +126,7 @@ fn find_crt_depth_for_modp_chain() -> (usize, DCRTPolyParams, PolyCircuit (usize, DCRTPolyParams, PolyCircuit usize { .unwrap_or(default) } +fn env_or_default_f64(key: &str, default: f64) -> f64 { + env::var(key) + .map(|raw| raw.parse::().unwrap_or_else(|e| panic!("{key} must be a valid f64: {e}"))) + .unwrap_or(default) +} + +fn env_or_optional_usize(key: &str) -> Option { + env::var(key).ok().map(|raw| { + raw.parse::().unwrap_or_else(|e| panic!("{key} must be a valid usize: {e}")) + }) +} + +fn selected_error_point_from_env(crt_bits: usize) -> Option { + match ( + env_or_optional_usize("DIAMOND_INJECTOR_SELECTED_CRT_DEPTH"), + env_or_optional_usize("DIAMOND_INJECTOR_SELECTED_MAX_ERROR_BITS"), + ) { + (None, None) => None, + (Some(crt_depth), Some(max_error_bits)) => { + assert!(crt_depth > 0, "DIAMOND_INJECTOR_SELECTED_CRT_DEPTH must be positive"); + assert!( + max_error_bits > 0, + "DIAMOND_INJECTOR_SELECTED_MAX_ERROR_BITS must be positive" + ); + let q_bits = crt_bits + .checked_mul(crt_depth) + .expect("selected q_bits overflow in DiamondInjector plot test"); + let max_error_int = (BigUint::from(1u8) << max_error_bits) - BigUint::from(1u8); + let max_error = BigDecimal::from_biguint(max_error_int, 0); + let max_error_bits = bigdecimal_bits_ceil(&max_error); + Some(ErrorPoint { crt_depth, q_bits, max_error_bits, max_error }) + } + _ => panic!( + "DIAMOND_INJECTOR_SELECTED_CRT_DEPTH and \ + DIAMOND_INJECTOR_SELECTED_MAX_ERROR_BITS must be set together" + ), + } +} + fn output_path_from_env() -> PathBuf { env::var("DIAMOND_INJECTOR_ERROR_PLOT_PATH") .map(PathBuf::from) @@ -205,6 +244,8 @@ fn verify_gpu_online_eval_errors_below_simulation( digit_bits: u32, crossing_point: &ErrorPoint, gpu_ids: &[i32], + trapdoor_sigma: f64, + error_sigma: f64, ) { assert!( input_base <= u32::MAX as usize, @@ -220,8 +261,8 @@ fn verify_gpu_online_eval_errors_below_simulation( input_count, input_base, usize::try_from(digit_bits).expect("digit_bits must fit into usize"), - DIAMOND_INJECTOR_TRAPDOOR_SIGMA, - DIAMOND_INJECTOR_ERROR_SIGMA, + trapdoor_sigma, + error_sigma, ) .with_gpu_device_ids(gpu_ids.to_vec()); @@ -259,7 +300,7 @@ fn verify_gpu_online_eval_errors_below_simulation( ); let secret_product = reconstruct_secret_product(¶ms, dir.path(), &input_digits); - let base_public_matrix = preprocess_out.final_pub_matrices[0].clone(); + let base_public_matrix = preprocess_out.final_public_matrix(&injector.params, 0); let base_selector = GpuDCRTPolyMatrix::from_poly_vec_row(¶ms, vec![secret_product.entry(0, 0), k]); assert_state_residual_below_bound( @@ -280,7 +321,7 @@ fn verify_gpu_online_eval_errors_below_simulation( ¶ms, vec![secret_product.entry(0, 0), secret_product.entry(0, 0) * &bit_plaintext], ); - let bit_public_matrix = preprocess_out.final_pub_matrices[state_idx].clone(); + let bit_public_matrix = preprocess_out.final_public_matrix(&injector.params, state_idx); assert_state_residual_below_bound( &format!("bit_state_{state_idx}"), ¶ms, @@ -626,6 +667,12 @@ fn test_gpu_diamond_injector_q_bits_vs_max_error_plot_generates_svg() { let crt_bits = env_or_default_usize("DIAMOND_INJECTOR_CRT_BITS", DEFAULT_CRT_BITS); let input_count = env_or_default_usize("DIAMOND_INJECTOR_INPUT_COUNT", DEFAULT_INPUT_COUNT); let digit_bits = env_or_default_u32("DIAMOND_INJECTOR_DIGIT_BITS", DEFAULT_DIGIT_BITS); + let trapdoor_sigma = + env_or_default_f64("DIAMOND_INJECTOR_TRAPDOOR_SIGMA", DIAMOND_INJECTOR_TRAPDOOR_SIGMA); + let error_sigma = + env_or_default_f64("DIAMOND_INJECTOR_ERROR_SIGMA", DIAMOND_INJECTOR_ERROR_SIGMA); + assert!(trapdoor_sigma > 0.0, "DIAMOND_INJECTOR_TRAPDOOR_SIGMA must be positive"); + assert!(error_sigma >= 0.0, "DIAMOND_INJECTOR_ERROR_SIGMA must be nonnegative"); let min_crt_depth = env_or_default_usize("DIAMOND_INJECTOR_MIN_CRT_DEPTH", DEFAULT_MIN_CRT_DEPTH); let max_crt_depth = @@ -642,36 +689,51 @@ fn test_gpu_diamond_injector_q_bits_vs_max_error_plot_generates_svg() { .unwrap_or_else(|| panic!("DIAMOND_INJECTOR_DIGIT_BITS={} is too large", digit_bits)); let output_path = output_path_from_env(); - let mut points = Vec::with_capacity(max_crt_depth - min_crt_depth + 1); - for crt_depth in min_crt_depth..=max_crt_depth { - let params = gpu_params_for_crt_depth(ring_dim, crt_depth, crt_bits, base_bits, &gpu_ids); - let injector = TestInjector::new( - params, - input_count, - input_base, - usize::try_from(digit_bits).expect("digit_bits must fit into usize"), - DIAMOND_INJECTOR_TRAPDOOR_SIGMA, - DIAMOND_INJECTOR_ERROR_SIGMA, - ) - .with_gpu_device_ids(gpu_ids.clone()); - let simulated = injector.simulate_output_error_bounds(); - let max_error = simulated - .state_errors - .iter() - .map(|state_error| state_error.poly_norm.norm.clone()) - .max() - .expect("state error list must be non-empty"); - let max_error_bits = bigdecimal_bits_ceil(&max_error); - let q_bits = - crt_bits.checked_mul(crt_depth).expect("q_bits overflow in DiamondInjector plot test"); - + let selected_error_point = selected_error_point_from_env(crt_bits); + let used_selected_simulation = selected_error_point.is_some(); + let points = if let Some(point) = selected_error_point { info!( - "diamond injector plot point: crt_depth={crt_depth}, q_bits={q_bits}, max_projected_bits={}", - bigdecimal_bits_ceil(&max_error), + crt_depth = point.crt_depth, + q_bits = point.q_bits, + max_error_bits = point.max_error_bits, + "diamond injector selected simulation point provided; skipping error simulation" ); + vec![point] + } else { + let mut points = Vec::with_capacity(max_crt_depth - min_crt_depth + 1); + for crt_depth in min_crt_depth..=max_crt_depth { + let params = + gpu_params_for_crt_depth(ring_dim, crt_depth, crt_bits, base_bits, &gpu_ids); + let injector = TestInjector::new( + params, + input_count, + input_base, + usize::try_from(digit_bits).expect("digit_bits must fit into usize"), + trapdoor_sigma, + error_sigma, + ) + .with_gpu_device_ids(gpu_ids.clone()); + let simulated = injector.simulate_output_error_bounds(); + let max_error = simulated + .state_errors + .iter() + .map(|state_error| state_error.maximum_coefficient_bound()) + .max() + .expect("state error list must be non-empty"); + let max_error_bits = bigdecimal_bits_ceil(&max_error); + let q_bits = crt_bits + .checked_mul(crt_depth) + .expect("q_bits overflow in DiamondInjector plot test"); + + info!( + "diamond injector plot point: crt_depth={crt_depth}, q_bits={q_bits}, max_projected_bits={}", + bigdecimal_bits_ceil(&max_error), + ); - points.push(ErrorPoint { crt_depth, q_bits, max_error_bits, max_error }); - } + points.push(ErrorPoint { crt_depth, q_bits, max_error_bits, max_error }); + } + points + }; write_svg_plot(&points, &output_path, ring_dim, crt_bits, base_bits, input_count, digit_bits); @@ -681,9 +743,10 @@ fn test_gpu_diamond_injector_q_bits_vs_max_error_plot_generates_svg() { written.contains(", +} + +impl EnvVarGuard { + fn set(key: &'static str, value: &str) -> Self { + let old_value = env::var(key).ok(); + unsafe { env::set_var(key, value) }; + Self { key, old_value } + } +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + if let Some(value) = &self.old_value { + unsafe { env::set_var(self.key, value) }; + } else { + unsafe { env::remove_var(self.key) }; + } + } +} + impl DiamondWEGpuBenchConfig { fn from_env() -> Self { let ring_dim = env_or_parse_u32("DIAMOND_WE_GPU_BENCH_RING_DIM", DEFAULT_RING_DIM); @@ -275,13 +298,23 @@ impl DiamondWEGpuBenchConfig { env_or_parse_optional_u64("DIAMOND_WE_GPU_BENCH_SELECTED_ACHIEVED_SECPAR_FOR_GAUSS"); let achieved_secpar_for_cbd = env_or_parse_optional_u64("DIAMOND_WE_GPU_BENCH_SELECTED_ACHIEVED_SECPAR_FOR_CBD"); - let noisy_plaintext_error_bits = - env_or_parse_optional_usize("DIAMOND_WE_GPU_BENCH_SELECTED_NOISY_PLAINTEXT_ERROR_BITS") - .unwrap_or(0); - let input_injection_error_bits = - env_or_parse_optional_usize("DIAMOND_WE_GPU_BENCH_SELECTED_INPUT_INJECTION_ERROR_BITS") - .unwrap_or(0); + let noisy_plaintext_error_bits = env_or_parse_optional_usize( + "DIAMOND_WE_GPU_BENCH_SELECTED_NOISY_PLAINTEXT_ERROR_BITS", + ) + .expect("DIAMOND_WE_GPU_BENCH_SELECTED_NOISY_PLAINTEXT_ERROR_BITS must be set with selected CRT depth"); + let input_injection_error_bits = env_or_parse_optional_usize( + "DIAMOND_WE_GPU_BENCH_SELECTED_INPUT_INJECTION_ERROR_BITS", + ) + .expect("DIAMOND_WE_GPU_BENCH_SELECTED_INPUT_INJECTION_ERROR_BITS must be set with selected CRT depth"); assert!(crt_depth > 0, "DIAMOND_WE_GPU_BENCH_SELECTED_CRT_DEPTH must be positive"); + assert!( + noisy_plaintext_error_bits > 0, + "DIAMOND_WE_GPU_BENCH_SELECTED_NOISY_PLAINTEXT_ERROR_BITS must be positive" + ); + assert!( + input_injection_error_bits > 0, + "DIAMOND_WE_GPU_BENCH_SELECTED_INPUT_INJECTION_ERROR_BITS must be positive" + ); Some(DiamondWEGpuBenchSelectedSimulation { crt_depth, ring_dim, @@ -344,7 +377,7 @@ fn artifact_dir_from_env(default: PathBuf) -> PathBuf { fn gpu_params_for_crt_depth( cfg: &DiamondWEGpuBenchConfig, crt_depth: usize, - gpu_id: i32, + gpu_ids: Vec, ) -> (DCRTPolyParams, GpuDCRTPolyParams) { let cpu_params = DCRTPolyParams::new(cfg.ring_dim, crt_depth, cfg.crt_bits, cfg.base_bits); let (moduli, _, actual_depth) = cpu_params.to_crt(); @@ -353,7 +386,7 @@ fn gpu_params_for_crt_depth( cpu_params.ring_dimension(), moduli, cpu_params.base_bits(), - vec![gpu_id], + gpu_ids, Some(1), ); assert_eq!(gpu_params.modulus(), cpu_params.modulus()); @@ -398,7 +431,7 @@ fn build_cpu_diamond_we_for_search( fn build_gpu_diamond_we( cfg: &DiamondWEGpuBenchConfig, gpu_params: GpuDCRTPolyParams, - gpu_id: i32, + gpu_ids: Vec, dir_path: PathBuf, ) -> GpuDiamondWE { let injector = GpuInjector::new( @@ -409,7 +442,7 @@ fn build_gpu_diamond_we( cfg.trapdoor_sigma, cfg.error_sigma, ) - .with_gpu_device_ids(vec![gpu_id]); + .with_gpu_device_ids(gpu_ids); DiamondWE::new(injector, cfg.witness_size, dir_path, b"test_gpu_diamond_we".to_vec()) } @@ -613,17 +646,7 @@ async fn build_encoding_bench_estimator( #[tokio::test] #[sequential_test::sequential] async fn test_gpu_diamond_we_error_search_bench_estimate_and_round_trip() { - let log_filter = tracing_subscriber::filter::Targets::new() - .with_target("test_gpu_diamond_we", tracing_subscriber::filter::LevelFilter::INFO) - .with_target("mxx::we::diamond_we", tracing_subscriber::filter::LevelFilter::INFO) - .with_target("mxx::io::utils::simulation", tracing_subscriber::filter::LevelFilter::INFO) - .with_target( - "mxx::we::diamond_we::bench_estimator", - tracing_subscriber::filter::LevelFilter::DEBUG, - ) - .with_target("mxx::bench_estimator", tracing_subscriber::filter::LevelFilter::INFO) - .with_target("mxx::storage::write", tracing_subscriber::filter::LevelFilter::INFO) - .with_default(tracing_subscriber::filter::LevelFilter::WARN); + let log_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); let _ = tracing_subscriber::registry() .with(log_filter) .with(tracing_subscriber::fmt::layer()) @@ -632,8 +655,9 @@ async fn test_gpu_diamond_we_error_search_bench_estimate_and_round_trip() { let cfg = DiamondWEGpuBenchConfig::from_env(); info!("DiamondWE GPU bench config: {:?}", cfg); - let gpu_id = - *detected_gpu_device_ids().first().expect("test_gpu_diamond_we requires at least one GPU"); + let gpu_ids = detected_gpu_device_ids(); + assert!(!gpu_ids.is_empty(), "test_gpu_diamond_we requires at least one GPU"); + info!(?gpu_ids, gpu_count = gpu_ids.len(), "DiamondWE GPU devices selected"); let temp_dir = tempdir().expect("DiamondWE GPU bench test must create a tempdir"); init_storage_system(temp_dir.path().to_path_buf()); @@ -678,10 +702,10 @@ async fn test_gpu_diamond_we_error_search_bench_estimate_and_round_trip() { achieved_secpar_for_gauss: search.achieved_secpar_for_gauss, achieved_secpar_for_cbd: search.achieved_secpar_for_cbd, noisy_plaintext_error_bits: bigdecimal_bits_ceil( - &search.simulation.noisy_plaintext_error.poly_norm.norm, + &search.simulation.noisy_plaintext_error_bound, ) as usize, input_injection_error_bits: bigdecimal_bits_ceil( - &search.simulation.input_injection.state_errors[0].poly_norm.norm, + &search.simulation.input_injection.state_errors[0].maximum_coefficient_bound(), ) as usize, }; info!( @@ -699,7 +723,7 @@ async fn test_gpu_diamond_we_error_search_bench_estimate_and_round_trip() { let selected_cfg = DiamondWEGpuBenchConfig { ring_dim: selected.ring_dim, ..cfg.clone() }; let (_cpu_params, gpu_params) = - gpu_params_for_crt_depth(&selected_cfg, selected.crt_depth, gpu_id); + gpu_params_for_crt_depth(&selected_cfg, selected.crt_depth, gpu_ids.clone()); let final_dir = artifact_dir_from_env(temp_dir.path().join("final_estimate")); ensure_dir(&final_dir); info!( @@ -707,7 +731,8 @@ async fn test_gpu_diamond_we_error_search_bench_estimate_and_round_trip() { "DiamondWE GPU artifact directory selected" ); init_storage_system(final_dir.clone()); - let diamond = build_gpu_diamond_we(&cfg, gpu_params.clone(), gpu_id, final_dir.clone()); + let diamond = + build_gpu_diamond_we(&cfg, gpu_params.clone(), gpu_ids.clone(), final_dir.clone()); let gpu_circuit = build_circuit::(cfg.circuit_height); info!("starting DiamondWE GPU public-key bench estimator construction"); @@ -750,6 +775,8 @@ async fn test_gpu_diamond_we_error_search_bench_estimate_and_round_trip() { let ct = diamond.enc(&msg, gpu_circuit, &instance); gpu_device_sync(); info!("starting DiamondWE GPU dec"); + let _expected_msg_guard = + EnvVarGuard::set("DIAMOND_WE_GPU_BENCH_EXPECTED_MSG", if msg { "true" } else { "false" }); let decoded = diamond.dec(&ct, &witness); gpu_device_sync(); info!(msg, decoded, "DiamondWE GPU round trip finished"); diff --git a/tests/test_gpu_ggh15_goldreich_ring_gsw_bench.rs b/tests/test_gpu_ggh15_goldreich_ring_gsw_bench.rs index bc42b030..2945f97e 100644 --- a/tests/test_gpu_ggh15_goldreich_ring_gsw_bench.rs +++ b/tests/test_gpu_ggh15_goldreich_ring_gsw_bench.rs @@ -384,7 +384,7 @@ fn probe_crt_depth_for_goldreich_ring_gsw_bench( let max_eval_error = max_bigdecimal( out_errors .par_iter() - .map(|error| error.matrix_norm.poly_norm.norm.clone()) + .map(|error| error.matrix_norm.maximum_coefficient_bound()) .collect::>(), ); let corrected_max_eval_error = @@ -401,7 +401,9 @@ fn probe_crt_depth_for_goldreich_ring_gsw_bench( encrypted_outputs .par_iter() .map(|ciphertext| { - ciphertext.estimate_decryption_error_norm(cfg.error_sigma).poly_norm.norm + ciphertext + .estimate_decryption_error_norm(cfg.error_sigma) + .maximum_coefficient_bound() }) .collect::>(), ); @@ -489,7 +491,7 @@ fn find_crt_depth_for_goldreich_ring_gsw_bench( let ring_dim_sqrt = BigDecimal::from_u32(cfg.ring_dim).unwrap().sqrt().unwrap(); let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { @@ -671,7 +673,9 @@ async fn test_gpu_ggh15_goldreich_ring_gsw_bench() { encrypted_outputs .iter() .map(|ciphertext| { - ciphertext.estimate_decryption_error_norm(cfg.error_sigma).poly_norm.norm + ciphertext + .estimate_decryption_error_norm(cfg.error_sigma) + .maximum_coefficient_bound() }) .collect::>(), ); diff --git a/tests/test_gpu_ggh15_modp_chain.rs b/tests/test_gpu_ggh15_modp_chain.rs index a78c8d04..d6c87c54 100644 --- a/tests/test_gpu_ggh15_modp_chain.rs +++ b/tests/test_gpu_ggh15_modp_chain.rs @@ -139,7 +139,7 @@ fn find_crt_depth_for_modp_chain() -> (usize, DCRTPolyParams, BigUint) { )); let plt_evaluator = NormPltGGH15Evaluator::new(ctx.clone(), &error_sigma, &error_sigma, None); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); let input_bound = BigDecimal::from((P - 1) as u64); let out_errors = circuit.simulate_max_error_norm( @@ -153,7 +153,7 @@ fn find_crt_depth_for_modp_chain() -> (usize, DCRTPolyParams, BigUint) { let max_error = out_errors .into_iter() - .map(|e| e.matrix_norm.poly_norm.norm) + .map(|e| e.matrix_norm.maximum_coefficient_bound()) .max_by(|a, b| a.partial_cmp(b).expect("comparable BigDecimal")) .expect("non-empty output"); let q_over_2p_bd = BigDecimal::from_biguint(q_over_2p.clone(), 0); diff --git a/tests/test_gpu_ggh15_montgomery_modq_arith.rs b/tests/test_gpu_ggh15_montgomery_modq_arith.rs index 36d04439..dbd63d4e 100644 --- a/tests/test_gpu_ggh15_montgomery_modq_arith.rs +++ b/tests/test_gpu_ggh15_montgomery_modq_arith.rs @@ -200,7 +200,7 @@ fn find_crt_depth_for_modq_arith(cfg: &MontgomeryModqArithConfig) -> (usize, DCR let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); let input_bound = BigDecimal::from((1u64 << cfg.limb_bit_size) - 1); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); for crt_depth in 1..=cfg.max_crt_depth { let params = DCRTPolyParams::new(cfg.ring_dim, crt_depth, cfg.crt_bits, cfg.base_bits); @@ -233,7 +233,7 @@ fn find_crt_depth_for_modq_arith(cfg: &MontgomeryModqArithConfig) -> (usize, DCR assert_eq!(out_errors.len(), 1); let threshold = full_q.as_ref() / BigUint::from(2u64 * q_max); - let error = &out_errors[0].matrix_norm.poly_norm.norm; + let error = &out_errors[0].matrix_norm.maximum_coefficient_bound(); let max_error_bits = bigdecimal_bits_ceil(error); let all_ok = *error < BigDecimal::from_biguint(threshold.clone(), 0); diff --git a/tests/test_gpu_ggh15_negacyclic_conv_mul.rs b/tests/test_gpu_ggh15_negacyclic_conv_mul.rs index 13aa17dd..1723f547 100644 --- a/tests/test_gpu_ggh15_negacyclic_conv_mul.rs +++ b/tests/test_gpu_ggh15_negacyclic_conv_mul.rs @@ -386,7 +386,7 @@ fn find_crt_depth_for_negacyclic_conv_mul( let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); let input_bound = BigDecimal::from((1u64 << cfg.p_moduli_bits) - 1); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); for crt_depth in 1..=cfg.max_crt_depth { let params = DCRTPolyParams::new(cfg.ring_dim, crt_depth, cfg.crt_bits, cfg.base_bits); @@ -423,7 +423,7 @@ fn find_crt_depth_for_negacyclic_conv_mul( let threshold = full_q.as_ref() / BigUint::from(2u64 * q_max); let threshold_bd = BigDecimal::from_biguint(threshold.clone(), 0); - let error = &out_errors[0].matrix_norm.poly_norm.norm; + let error = &out_errors[0].matrix_norm.maximum_coefficient_bound(); let max_error_bits = bigdecimal_bits_ceil(error); let all_ok = *error < threshold_bd; diff --git a/tests/test_gpu_ggh15_nested_rns_modq_arith.rs b/tests/test_gpu_ggh15_nested_rns_modq_arith.rs index f2e9a020..aa1d1a60 100644 --- a/tests/test_gpu_ggh15_nested_rns_modq_arith.rs +++ b/tests/test_gpu_ggh15_nested_rns_modq_arith.rs @@ -189,7 +189,7 @@ fn find_crt_depth_for_modq_arith( let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); let input_bound = BigDecimal::from((1u64 << cfg.p_moduli_bits) - 1); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); for crt_depth in 1..=cfg.max_crt_depth { let params = DCRTPolyParams::new(cfg.ring_dim, crt_depth, cfg.crt_bits, cfg.base_bits); @@ -229,7 +229,7 @@ fn find_crt_depth_for_modq_arith( assert_eq!(out_errors.len(), 1); let threshold = full_q.as_ref() / BigUint::from(2u64 * q_max); - let error = &out_errors[0].matrix_norm.poly_norm.norm; + let error = &out_errors[0].matrix_norm.maximum_coefficient_bound(); let max_error_bits = bigdecimal_bits_ceil(error); let all_ok = *error < BigDecimal::from_biguint(threshold, 0); diff --git a/tests/test_gpu_ggh15_poly_modq_arith.rs b/tests/test_gpu_ggh15_poly_modq_arith.rs index f5d6a676..02f29497 100644 --- a/tests/test_gpu_ggh15_poly_modq_arith.rs +++ b/tests/test_gpu_ggh15_poly_modq_arith.rs @@ -256,7 +256,7 @@ fn find_crt_depth_for_modq_arith( let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); let input_bound = BigDecimal::from((1u64 << cfg.p_moduli_bits) - 1); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); for crt_depth in 1..=cfg.max_crt_depth { let params = DCRTPolyParams::new(cfg.ring_dim, crt_depth, cfg.crt_bits, cfg.base_bits); @@ -297,7 +297,7 @@ fn find_crt_depth_for_modq_arith( assert_eq!(out_errors.len(), 1); let threshold = full_q.as_ref() / BigUint::from(2u64 * q_max); - let error = &out_errors[0].matrix_norm.poly_norm.norm; + let error = &out_errors[0].matrix_norm.maximum_coefficient_bound(); let max_error_bits = bigdecimal_bits_ceil(error); let all_ok = *error < BigDecimal::from_biguint(threshold, 0); diff --git a/tests/test_gpu_ggh15_poly_slot_transfer.rs b/tests/test_gpu_ggh15_poly_slot_transfer.rs index 2aee9d98..e4c6702b 100644 --- a/tests/test_gpu_ggh15_poly_slot_transfer.rs +++ b/tests/test_gpu_ggh15_poly_slot_transfer.rs @@ -237,7 +237,7 @@ fn find_crt_depth_for_simple_slot_transfer(cfg: &SlotTransferConfig) -> DCRTPoly let ring_dim_sqrt = BigDecimal::from_u32(cfg.ring_dim).unwrap().sqrt().unwrap(); let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); let input_bound = BigDecimal::from(*INPUT_CONSTANTS.iter().max().expect("input constants must not be empty")); @@ -273,7 +273,7 @@ fn find_crt_depth_for_simple_slot_transfer(cfg: &SlotTransferConfig) -> DCRTPoly assert_eq!(out_errors.len(), 1); let threshold = params.modulus().as_ref() / BigUint::from(2u64 * q_max); - let error = &out_errors[0].matrix_norm.poly_norm.norm; + let error = &out_errors[0].matrix_norm.maximum_coefficient_bound(); info!( "crt_depth={} q_bits={} max_error_bits={} threshold_bits={}", crt_depth, diff --git a/tests/test_gpu_lwe_montgomery_goldreich_ring_gsw_bench.rs b/tests/test_gpu_lwe_montgomery_goldreich_ring_gsw_bench.rs index 4e5c21f5..c992f237 100644 --- a/tests/test_gpu_lwe_montgomery_goldreich_ring_gsw_bench.rs +++ b/tests/test_gpu_lwe_montgomery_goldreich_ring_gsw_bench.rs @@ -363,7 +363,7 @@ fn probe_crt_depth_for_goldreich_ring_gsw_bench( let max_eval_error = max_bigdecimal( out_errors .par_iter() - .map(|error| error.matrix_norm.poly_norm.norm.clone()) + .map(|error| error.matrix_norm.maximum_coefficient_bound()) .collect::>(), ); let corrected_max_eval_error = @@ -491,7 +491,7 @@ fn find_crt_depth_for_goldreich_ring_gsw_bench( let ring_dim_sqrt = BigDecimal::from_u32(cfg.ring_dim).unwrap().sqrt().unwrap(); let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { @@ -738,7 +738,9 @@ async fn test_gpu_lwe_montgomery_goldreich_ring_gsw_bench() { encrypted_outputs .iter() .map(|ciphertext| { - ciphertext.estimate_decryption_error_norm(cfg.error_sigma).poly_norm.norm + ciphertext + .estimate_decryption_error_norm(cfg.error_sigma) + .maximum_coefficient_bound() }) .collect::>(), ); diff --git a/tests/test_gpu_lwe_montgomery_modq_arith.rs b/tests/test_gpu_lwe_montgomery_modq_arith.rs index bccf2df5..47ebb0c5 100644 --- a/tests/test_gpu_lwe_montgomery_modq_arith.rs +++ b/tests/test_gpu_lwe_montgomery_modq_arith.rs @@ -413,7 +413,7 @@ fn probe_crt_depth_for_modq_arith( let threshold = full_q.as_ref() / BigUint::from(2u64 * q_max); let threshold_bd = BigDecimal::from_biguint(threshold.clone(), 0); - let max_eval_error = out_errors[0].matrix_norm.poly_norm.norm.clone(); + let max_eval_error = out_errors[0].matrix_norm.maximum_coefficient_bound(); let corrected_max_eval_error = corrected_max_eval_error_for_active_levels(&max_eval_error, active_levels); let eval_ok = corrected_max_eval_error < threshold_bd; @@ -444,7 +444,7 @@ fn find_crt_depth_for_modq_arith( let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); let input_bound = BigDecimal::from((1u64 << cfg.limb_bit_size) - 1); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { diff --git a/tests/test_gpu_lwe_montgomery_ring_gsw_mul_bench.rs b/tests/test_gpu_lwe_montgomery_ring_gsw_mul_bench.rs index f54b7bde..da5896d4 100644 --- a/tests/test_gpu_lwe_montgomery_ring_gsw_mul_bench.rs +++ b/tests/test_gpu_lwe_montgomery_ring_gsw_mul_bench.rs @@ -401,7 +401,7 @@ fn probe_crt_depth_for_ring_gsw_mul_bench( let max_eval_error = max_bigdecimal( out_errors .par_iter() - .map(|error| error.matrix_norm.poly_norm.norm.clone()) + .map(|error| error.matrix_norm.maximum_coefficient_bound()) .collect::>(), ); let corrected_max_eval_error = @@ -435,7 +435,7 @@ fn find_crt_depth_for_ring_gsw_mul_bench(cfg: &RingGswMulBenchConfig) -> (usize, let ring_dim_sqrt = BigDecimal::from_u32(cfg.ring_dim).unwrap().sqrt().unwrap(); let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { @@ -683,7 +683,9 @@ async fn test_gpu_lwe_montgomery_ring_gsw_mul_bench() { encrypted_outputs .iter() .map(|ciphertext| { - ciphertext.estimate_decryption_error_norm(cfg.error_sigma).poly_norm.norm + ciphertext + .estimate_decryption_error_norm(cfg.error_sigma) + .maximum_coefficient_bound() }) .collect::>(), ); diff --git a/tests/test_gpu_lwe_nested_rns_goldreich_ring_gsw_bench.rs b/tests/test_gpu_lwe_nested_rns_goldreich_ring_gsw_bench.rs index 6349d116..976f70b9 100644 --- a/tests/test_gpu_lwe_nested_rns_goldreich_ring_gsw_bench.rs +++ b/tests/test_gpu_lwe_nested_rns_goldreich_ring_gsw_bench.rs @@ -389,7 +389,7 @@ fn probe_crt_depth_for_goldreich_ring_gsw_bench( let max_eval_error = max_bigdecimal( out_errors .par_iter() - .map(|error| error.matrix_norm.poly_norm.norm.clone()) + .map(|error| error.matrix_norm.maximum_coefficient_bound()) .collect::>(), ); let corrected_max_eval_error = @@ -529,7 +529,7 @@ fn find_crt_depth_for_goldreich_ring_gsw_bench( let ring_dim_sqrt = BigDecimal::from_u32(cfg.ring_dim).unwrap().sqrt().unwrap(); let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { @@ -776,7 +776,9 @@ async fn test_gpu_lwe_nested_rns_goldreich_ring_gsw_bench() { encrypted_outputs .iter() .map(|ciphertext| { - ciphertext.estimate_decryption_error_norm(cfg.error_sigma).poly_norm.norm + ciphertext + .estimate_decryption_error_norm(cfg.error_sigma) + .maximum_coefficient_bound() }) .collect::>(), ); diff --git a/tests/test_gpu_lwe_nested_rns_modq_arith.rs b/tests/test_gpu_lwe_nested_rns_modq_arith.rs index 1d68b1f3..dc89fd62 100644 --- a/tests/test_gpu_lwe_nested_rns_modq_arith.rs +++ b/tests/test_gpu_lwe_nested_rns_modq_arith.rs @@ -401,7 +401,7 @@ fn probe_crt_depth_for_modq_arith( let threshold = full_q.as_ref() / BigUint::from(2u64 * q_max); let threshold_bd = BigDecimal::from_biguint(threshold.clone(), 0); - let max_eval_error = out_errors[0].matrix_norm.poly_norm.norm.clone(); + let max_eval_error = out_errors[0].matrix_norm.maximum_coefficient_bound(); let corrected_max_eval_error = corrected_max_eval_error_for_active_levels(&max_eval_error, active_levels); let eval_ok = corrected_max_eval_error < threshold_bd; @@ -432,7 +432,7 @@ fn find_crt_depth_for_modq_arith( let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); let input_bound = BigDecimal::from((1u64 << cfg.p_moduli_bits) - 1); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { diff --git a/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs b/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs index 6e88e363..27da3295 100644 --- a/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs +++ b/tests/test_gpu_lwe_nested_rns_ring_gsw_mul_bench.rs @@ -464,7 +464,7 @@ fn probe_crt_depth_for_ring_gsw_mul_bench( let max_eval_error = max_bigdecimal( out_errors .par_iter() - .map(|error| error.matrix_norm.poly_norm.norm.clone()) + .map(|error| error.matrix_norm.maximum_coefficient_bound()) .collect::>(), ); let corrected_max_eval_error = @@ -498,7 +498,7 @@ fn find_crt_depth_for_ring_gsw_mul_bench(cfg: &RingGswMulBenchConfig) -> (usize, let ring_dim_sqrt = BigDecimal::from_u32(cfg.ring_dim).unwrap().sqrt().unwrap(); let base = BigDecimal::from_biguint(BigUint::from(1u32) << cfg.base_bits, 0); let error_sigma = BigDecimal::from_f64(cfg.error_sigma).expect("valid error sigma"); - let e_init_norm = &error_sigma * BigDecimal::from_f32(6.5).unwrap(); + let e_init_norm = error_sigma.clone(); if let Some(raw_crt_depth) = env::var("CRT_DEPTH").ok().filter(|value| !value.trim().is_empty()) { @@ -874,12 +874,14 @@ async fn test_gpu_lwe_nested_rns_ring_gsw_mul_bench() { .expect("Ring-GSW multiplication circuit must produce a final product") .estimate_decryption_error_norm(cfg.error_sigma) .poly_norm - .norm; + .sigma; let max_selected_decryption_error = max_bigdecimal( encrypted_outputs .iter() .map(|ciphertext| { - ciphertext.estimate_decryption_error_norm(cfg.error_sigma).poly_norm.norm + ciphertext + .estimate_decryption_error_norm(cfg.error_sigma) + .maximum_coefficient_bound() }) .collect::>(), ); diff --git a/tests/test_lwe_modp_chain.rs b/tests/test_lwe_modp_chain.rs index d224d4f3..9b6a03be 100644 --- a/tests/test_lwe_modp_chain.rs +++ b/tests/test_lwe_modp_chain.rs @@ -125,7 +125,7 @@ fn find_crt_depth_for_modp_chain() -> (usize, DCRTPolyParams, PolyCircuit (usize, DCRTPolyParams, PolyCircuit